Skip to content

Commit 4c17607

Browse files
ifahimrezaclaude
andcommitted
feat: verify-page — the closed-loop engine
Pillar 2 of the closed-loop scope (#26): one scored answer to "did what I built actually land, and is it any good?". - class-saddle-verify.php: three passes over FRESHLY RE-READ persisted state (the re-read is the point - it proves the database, not a write call's claim): structural, echo (persisted attrs the builder silently ignores - the intended design never took effect), and lint (the judgment rules). Findings merge into one deduped, severity-then-document-ordered, capped list (40 + overflow count), every one keyed by the same dot address the page-read tools emit. The score is deterministic arithmetic: -25 structural, -10 ignored, -8 lint error, -3 lint warn, floored at 0, with A-F grade bands. - saddle/verify-page (read tier): native pages run all three passes in free (echo via Saddle_Blocks_Echo::check_attrs over every persisted node); builder pages plug structural+echo through the new saddle_verify_builder_findings filter and judgment through the existing saddle_lint_accessor filter - ready for Pro's Divi driver (P1). A builder page where nothing could run is a clean unsupported error, not an empty "all good" report. - Saddle_Lint::compare_addresses is now public - verify sorts merged findings with the same ordering lint uses. tests/verify-test.php (9 tests): clean page scores 100, echo+lint arithmetic (82 = 100-10-8) with echo outranking lint, THE LOOP CLOSES (fix the flagged addresses, score returns to 100), raw-HTML structural finding, cap+overflow with score reflecting all findings, builder refusal, builder filters with dedupe, tier meta. Free suite 300 green; Pro 119 green; phpcs clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ffc1534 commit 4c17607

5 files changed

Lines changed: 631 additions & 2 deletions

File tree

includes/abilities/verify.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
/**
3+
* The verify ability — the closed loop's checkpoint.
4+
*
5+
* saddle/verify-page re-reads a page's PERSISTED state and returns one
6+
* scored, loopable report: structural soundness, silently-ignored attrs
7+
* (echo), and design/accessibility judgments (lint). Addresses match the
8+
* page-read tools, so an agent fixes by address and re-verifies
9+
* (https://github.com/plugpressco/saddle/issues/26).
10+
*
11+
* @package Saddle
12+
*/
13+
14+
defined( 'ABSPATH' ) || exit;
15+
16+
/**
17+
* Register the verify ability. Hooked to `wp_abilities_api_init`.
18+
*/
19+
function saddle_register_verify_abilities() {
20+
21+
wp_register_ability(
22+
'saddle/verify-page',
23+
array(
24+
'label' => __( 'Verify a page', 'saddle' ),
25+
'description' => __( 'Re-reads a page\'s SAVED state and returns one scored report (0–100 + grade): structural problems, attributes the builder silently ignores (your styling never took effect), and design/accessibility violations — each finding at a node address with a fix hint. Run it after building or editing: fix structural and "ignored" findings first, then errors, then re-run until the score is acceptable. This is how you know your work actually landed, not just that the write calls returned.', 'saddle' ),
26+
'category' => 'saddle',
27+
'input_schema' => array(
28+
'type' => 'object',
29+
'required' => array( 'post_id' ),
30+
'properties' => array(
31+
'post_id' => array(
32+
'type' => 'integer',
33+
'description' => __( 'The post or page to verify.', 'saddle' ),
34+
),
35+
),
36+
),
37+
'execute_callback' => array( 'Saddle_Verify_Abilities', 'verify_page' ),
38+
'permission_callback' => Saddle_Capabilities::permission( 'read', 'read', 'verify-page' ),
39+
'meta' => saddle_ability_meta( true, false, true, 'read' ),
40+
)
41+
);
42+
}
43+
44+
/**
45+
* Execute callbacks for the verify ability.
46+
*/
47+
class Saddle_Verify_Abilities {
48+
49+
/**
50+
* saddle/verify-page.
51+
*
52+
* @param array $input Ability input.
53+
* @return array|WP_Error
54+
*/
55+
public static function verify_page( $input = null ) {
56+
$input = is_array( $input ) ? $input : array();
57+
$post = get_post( isset( $input['post_id'] ) ? (int) $input['post_id'] : 0 );
58+
if ( ! $post || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
59+
return new WP_Error( 'saddle_not_found', __( 'No post or page with that ID.', 'saddle' ), array( 'status' => 404 ) );
60+
}
61+
if ( ! current_user_can( 'read_post', $post->ID ) ) {
62+
return new WP_Error( 'saddle_forbidden', __( 'You cannot read this post.', 'saddle' ), array( 'status' => 403 ) );
63+
}
64+
65+
$builder = Saddle_Abilities::builder_signature( $post );
66+
$accessor = null === $builder ? new Saddle_Lint_Gutenberg_Accessor() : null;
67+
68+
/** This filter is documented in includes/abilities/lint.php */
69+
$accessor = apply_filters( 'saddle_lint_accessor', $accessor, $builder, $post );
70+
if ( ! $accessor instanceof Saddle_Lint_Accessor ) {
71+
$accessor = null;
72+
}
73+
74+
$report = Saddle_Verify::run( $post, $builder, $accessor );
75+
76+
// A builder page where NOTHING could run isn't a report, it's a gap.
77+
if ( count( $report['skipped'] ) >= 3 ) {
78+
return new WP_Error(
79+
'saddle_verify_unsupported',
80+
sprintf(
81+
/* translators: 1: post ID, 2: builder name. */
82+
__( 'Post #%1$d is built with %2$s, and no verifier for that builder is installed. Divi 5 pages need Saddle Pro.', 'saddle' ),
83+
$post->ID,
84+
(string) $builder
85+
),
86+
array( 'status' => 409 )
87+
);
88+
}
89+
90+
return array_merge(
91+
array(
92+
'id' => $post->ID,
93+
'builder' => null === $builder ? 'native' : $builder,
94+
),
95+
$report,
96+
array(
97+
'note' => $report['findings']
98+
? __( 'Fix in order: structural, then "ignored" (echo — that styling never took effect), then errors, then warnings. Addresses match get-blocks/divi-get-page; re-read after structural edits, then re-run verify-page until the score is acceptable.', 'saddle' )
99+
: __( 'Everything checked out: the persisted state is structurally sound, every attribute takes effect, and no design violations were found.', 'saddle' ),
100+
)
101+
);
102+
}
103+
}

includes/lint/class-saddle-lint.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,14 @@ private static function walk( array $blocks, $parent_address, $depth, array &$no
127127
}
128128

129129
/**
130-
* Document-order comparison of two violations by dot address.
130+
* Document-order comparison of two violations by dot address. Public:
131+
* the verify engine sorts its merged findings with the same ordering.
131132
*
132133
* @param array $a First violation.
133134
* @param array $b Second violation.
134135
* @return int
135136
*/
136-
private static function compare_addresses( array $a, array $b ) {
137+
public static function compare_addresses( array $a, array $b ) {
137138
$pa = '' === $a['address'] ? array() : array_map( 'intval', explode( '.', $a['address'] ) );
138139
$pb = '' === $b['address'] ? array() : array_map( 'intval', explode( '.', $b['address'] ) );
139140

0 commit comments

Comments
 (0)