wordpress-agent-kit 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.github/agents/wp-architect.agent.md +1 -0
  2. package/.github/skills/blueprint/SKILL.md +418 -0
  3. package/.github/skills/wordpress-router/SKILL.md +1 -0
  4. package/.github/skills/wp-abilities-api/SKILL.md +13 -0
  5. package/.github/skills/wp-abilities-api/references/delegate-helper-pattern.md +241 -0
  6. package/.github/skills/wp-abilities-api/references/domain-vs-projection.md +113 -0
  7. package/.github/skills/wp-abilities-api/references/error-code-vocabulary.md +123 -0
  8. package/.github/skills/wp-abilities-api/references/grouping-heuristic.md +89 -0
  9. package/.github/skills/wp-abilities-api/references/input-schema-gotchas.md +265 -0
  10. package/.github/skills/wp-abilities-api/references/php-registration.md +47 -20
  11. package/.github/skills/wp-abilities-api/references/plugin-family-patterns.md +233 -0
  12. package/.github/skills/wp-abilities-api/references/shared-core-service.md +184 -0
  13. package/.github/skills/wp-abilities-audit/SKILL.md +199 -0
  14. package/.github/skills/wp-abilities-audit/references/audit-schema.md +300 -0
  15. package/.github/skills/wp-abilities-audit/references/capability-gate-tracing.md +197 -0
  16. package/.github/skills/wp-abilities-audit/references/controller-enumeration.md +116 -0
  17. package/.github/skills/wp-abilities-verify/SKILL.md +215 -0
  18. package/.github/skills/wp-abilities-verify/references/annotation-correctness.md +154 -0
  19. package/.github/skills/wp-abilities-verify/references/audit-schema-validation.md +131 -0
  20. package/.github/skills/wp-abilities-verify/references/permission-roundtrip.md +190 -0
  21. package/.github/skills/wp-abilities-verify/references/runtime-harness.md +462 -0
  22. package/.github/skills/wp-abilities-verify/references/schema-lints.md +118 -0
  23. package/.github/skills/wp-abilities-verify/references/static-enumeration.md +126 -0
  24. package/.github/skills/wp-block-development/SKILL.md +1 -0
  25. package/.github/skills/wp-block-themes/SKILL.md +1 -0
  26. package/.github/skills/wp-interactivity-api/SKILL.md +1 -0
  27. package/.github/skills/wp-performance/SKILL.md +1 -0
  28. package/.github/skills/wp-phpstan/SKILL.md +1 -0
  29. package/.github/skills/wp-playground/SKILL.md +1 -0
  30. package/.github/skills/wp-plugin-development/SKILL.md +1 -0
  31. package/.github/skills/wp-plugin-directory-guidelines/SKILL.md +133 -0
  32. package/.github/skills/wp-plugin-directory-guidelines/references/gpl-compliance.md +217 -0
  33. package/.github/skills/wp-plugin-directory-guidelines/references/guideline-review-checklist.md +592 -0
  34. package/.github/skills/wp-plugin-directory-guidelines/references/naming-rules.md +121 -0
  35. package/.github/skills/wp-project-triage/SKILL.md +1 -0
  36. package/.github/skills/wp-project-triage/scripts/detect_wp_project.mjs +22 -4
  37. package/.github/skills/wp-rest-api/SKILL.md +1 -0
  38. package/.github/skills/wp-wpcli-and-ops/SKILL.md +1 -0
  39. package/.github/skills/wpds/SKILL.md +1 -0
  40. package/AGENTS.md +33 -10
  41. package/AGENTS.template.md +63 -18
  42. package/README.md +226 -124
  43. package/biome.json +1 -1
  44. package/dist/commands/install.js +47 -6
  45. package/dist/commands/upgrade.js +34 -5
  46. package/dist/lib/api.js +93 -27
  47. package/dist/lib/installer.js +113 -7
  48. package/dist/lib/updater.js +260 -0
  49. package/extensions/wp-agent-kit/index.ts +452 -0
  50. package/package.json +21 -3
  51. package/kit-learnings.md +0 -192
@@ -0,0 +1,215 @@
1
+ ---
2
+ name: wp-abilities-verify
3
+ description: "Verify a WordPress plugin's Abilities API registrations: enumerate abilities, check that callback behavior matches each annotation's claim (the adversarial readonly-but-writes detection), validate permissions and schemas, and validate audit documents produced by wp-abilities-audit."
4
+ license: GPL-2.0-or-later
5
+ compatibility: "Targets WordPress 6.9+ plugins (PHP 7.2.24+). Requires a runnable environment (wp-env, docker-based dev stack, or equivalent) for runtime mode; static mode runs entirely from the plugin checkout with no env. Filesystem-based agent with bash + node."
6
+ ---
7
+
8
+ # WP Abilities Verify
9
+
10
+ Verify a WordPress plugin's Abilities API registrations. The
11
+ centerpiece is the **adversarial annotation correctness check**: a
12
+ `readonly: true` ability that actually writes (via `$wpdb->update`,
13
+ `update_option`, a non-GET delegate, etc.) is a security and UX
14
+ disaster because agents plan actions on the basis of the annotations
15
+ they introspect. This skill catches those lies by reading the callback
16
+ body and comparing what it does against what the annotation claims.
17
+
18
+ The skill also validates audit docs produced by `wp-abilities-audit`,
19
+ checks permission gates and schema hygiene, and optionally executes
20
+ each ability against a live environment.
21
+
22
+ ## When to use
23
+
24
+ - After abilities have been registered in a plugin but before a PR
25
+ lands.
26
+ - As a health-check on an already-shipped plugin (catch regressions
27
+ where a refactor turned a readonly ability into a writing one).
28
+ - To validate an audit document before handing it to an implementer.
29
+
30
+ ## Two modes
31
+
32
+ - **Static mode** — runs from the plugin checkout. No env. Enumerates
33
+ via source inspection, runs the adversarial correctness check, runs
34
+ schema and permission lints, and validates audit docs.
35
+ - **Runtime mode** — requires a running env. Does everything static
36
+ does PLUS: `wp_get_abilities()` for authoritative enumeration,
37
+ executes each ability with curated inputs, confirms permission
38
+ roundtrip against real users, and runs a twin-invocation heuristic
39
+ on `idempotent: true` abilities to flag candidates for review
40
+ (return-value equality is a signal, not a verdict — core defines
41
+ idempotent as "no additional effect on the environment").
42
+
43
+ Both modes produce the same structured report format.
44
+
45
+ A static-mode PASS means "no obvious-shape violations," not "verified
46
+ write-free." For high-stakes plugins, run runtime mode before landing
47
+ — it catches bootstrap-order, permission-roundtrip, and idempotency
48
+ issues that static can't. See `references/annotation-correctness.md`
49
+ for the static blind spots.
50
+
51
+ ## Inputs required
52
+
53
+ 1. **Plugin checkout path** — working tree to verify.
54
+ 2. **Mode** — `static` or `runtime`. Default to static if unspecified.
55
+ 3. **(Runtime only) Env-up command** — read the plugin's `AGENTS.md`.
56
+ Common patterns: `npm run wp-env start`, `npx wp-env start`, or a
57
+ composer-based bring-up. Plugin families with their own dev tooling
58
+ will document their own command. Do NOT assume `npm run wp-env`
59
+ works.
60
+ 4. **(Optional) Audit doc path** — enables cross-checks between the
61
+ audit and the registered abilities, and validates the audit itself.
62
+ 5. **Report output path** — explicit path, typically the user's vault.
63
+
64
+ ## Prerequisites
65
+
66
+ - `wp-project-triage` has been run on the plugin.
67
+ - The plugin has at least one registered ability in source. Zero hits
68
+ on `wp_register_ability(` → return a clear "no abilities registered"
69
+ report, not an empty PASS.
70
+
71
+ ## Procedure
72
+
73
+ ### 1. (If audit provided) Validate the audit doc
74
+
75
+ Read `references/audit-schema-validation.md`. Validate the audit
76
+ against the canonical schema owned by `wp-abilities-audit`. Surface
77
+ missing required fields, multiple `reference_ability: true`, and
78
+ `backing: null` entries that aren't paired with a `surfaced_gaps`
79
+ entry. `backing: null` alone is WARN (intentional gap output), not
80
+ FAIL.
81
+
82
+ ### 2. Enumerate abilities statically
83
+
84
+ Read `references/static-enumeration.md`. Find each
85
+ `wp_register_ability(` call, extract the name, the annotation block,
86
+ and the execute-callback location. Use a multi-line tool (`rg
87
+ --multiline --pcre2`) — the canonical formatting splits the call
88
+ across lines. Record each ability's source-file + line + annotations +
89
+ callback byte range.
90
+
91
+ ### 3. (Runtime only) Enumerate via REST + wp-cli
92
+
93
+ Read `references/runtime-harness.md`. Bring the env up using the
94
+ command from `AGENTS.md`, then enumerate via `wp_get_abilities()` over
95
+ wp-cli and cross-check against the static inventory. Source-only →
96
+ FAIL (registration not firing). Runtime-only → WARN (dynamic
97
+ registration path).
98
+
99
+ ### 4. Annotation correctness (the adversarial core)
100
+
101
+ Read `references/annotation-correctness.md`. Read each callback body
102
+ and verify it matches the annotation claim:
103
+
104
+ - `readonly: true` → callback must not write to the database, the
105
+ options table, post / user / term / comment data, the filesystem,
106
+ cron, or via non-GET HTTP / REST delegates.
107
+ - `destructive: false` → callback must not delete, refund, void,
108
+ cancel, or trash.
109
+ - `idempotent: true` → repeated calls with the same input have no
110
+ additional effect on the environment (per the `idempotent`
111
+ annotation's docblock in `class-wp-ability.php`). Static catches
112
+ counter writes and per-call cron schedules; runtime adds a
113
+ twin-invocation heuristic for visible state changes.
114
+
115
+ The reference lists common write patterns as a starting set, not a
116
+ checklist — plugin vocabularies vary, and the agent extends with verbs
117
+ specific to the plugin under verification.
118
+
119
+ False positives get suppressed via an inline `// verify-ignore:
120
+ <annotation> -- <reason>` comment.
121
+
122
+ ### 5. Permission roundtrip
123
+
124
+ Read `references/permission-roundtrip.md`. Static: classify each
125
+ `permission_callback` against the six shapes (preferred Shape A
126
+ `current_user_can(...)`; FAIL on Shape B-bad `WP_REST_Request`
127
+ patterns or Shape E literal `true`). Runtime: anon and subscriber
128
+ denied; admin allowed (unless deliberately public). When an audit was
129
+ provided, cross-check the registered cap against the audit's declared
130
+ gate.
131
+
132
+ ### 6. Schema lints
133
+
134
+ Read `references/schema-lints.md`. Six small principles applied to
135
+ each ability's `input_schema`: object schemas declare
136
+ `additionalProperties`; required fields have descriptions; enums
137
+ non-empty; no `$ref`; defaults are statically constant (including
138
+ `(object) array()`); reference abilities have no required inputs.
139
+
140
+ Cross-reference `../wp-abilities-api/references/input-schema-gotchas.md`
141
+ for the four runtime gotchas (defaults not injected on the
142
+ property-level path, pagination key drift, `empty()` on string IDs,
143
+ direct vs indirect invocation strictness).
144
+
145
+ ### 7. Error-code vocabulary
146
+
147
+ Cross-reference `../wp-abilities-api/references/error-code-vocabulary.md`.
148
+ Inspect each callback's `WP_Error` returns; non-vocabulary codes →
149
+ WARN.
150
+
151
+ ## Verification
152
+
153
+ The run produces a structured markdown report at the user-specified
154
+ path:
155
+
156
+ ```
157
+ ---
158
+ Last updated: <YYYY-MM-DD HH:MM>
159
+ ---
160
+
161
+ # <Plugin> Abilities Verification — <Static|Runtime> Mode
162
+
163
+ ## Status: <PASS|WARN|FAIL>
164
+
165
+ ## Audit doc validation (if provided)
166
+
167
+ ## Static inventory
168
+
169
+ ## Annotation correctness
170
+ | Ability | Claim | Result | Evidence |
171
+ |---|---|---|---|
172
+
173
+ ## Permission gates
174
+
175
+ ## Schema lints
176
+
177
+ ## Error-code vocabulary
178
+ ```
179
+
180
+ Every ability is OK, WARN, or FAIL. A single FAIL → top-line FAIL;
181
+ WARNs without FAILs → WARN; otherwise PASS.
182
+
183
+ ## Failure modes / debugging
184
+
185
+ - **Env not reachable (runtime)** — env-up failed or Docker isn't
186
+ running. Re-run `wp-project-triage`, then fix the env. Don't fall
187
+ back silently to static without noting it in the report.
188
+ - **No abilities in source** — return a clear "nothing to verify"
189
+ report.
190
+ - **Audit schema mismatch** — point at
191
+ `references/audit-schema-validation.md`; don't auto-fix the audit.
192
+ - **False positive on readonly-writes** — see the `// verify-ignore`
193
+ mechanism in `references/annotation-correctness.md`. Document why
194
+ each suppression is legitimate.
195
+ - **Runtime enumeration smaller than static** — registration hook
196
+ isn't firing. Check init hook timing, activation state, autoloader
197
+ order.
198
+
199
+ ## Escalation
200
+
201
+ - Recurring legitimate pattern that trips the adversarial check across
202
+ multiple plugins → propose adding it to the suppression guidance in
203
+ `annotation-correctness.md`. Don't broaden the candidate-pattern
204
+ list speculatively.
205
+ - Audit-schema validator rejects a legitimate audit → the canonical
206
+ schema in `../wp-abilities-audit/references/audit-schema.md` has
207
+ evolved. Update `references/audit-schema-validation.md` to match.
208
+
209
+ ## Out of scope
210
+
211
+ Token-budget measurement is a separate verification axis — an
212
+ annotation-clean, schema-clean, runtime-passing ability set can still
213
+ be unshippable if its `tools/list` form burns through an agent's
214
+ context budget. That axis is tracked separately. Do not aggregate
215
+ manual or external measurement into this skill's PASS / FAIL verdict.
@@ -0,0 +1,154 @@
1
+ # Annotation Correctness
2
+
3
+ The adversarial core of this skill: verify what the annotation claims
4
+ by reading the callback. A `readonly: true` ability that actually
5
+ writes is a security and UX disaster, and unit tests don't catch it
6
+ because the mock looks just like the real writer.
7
+
8
+ ## Why this matters
9
+
10
+ Agents plan actions on the basis of the annotations they introspect.
11
+ If an ability is annotated `readonly: true`, an orchestrator will
12
+ confidently invoke it in a dry-run, speculative exploration, or
13
+ multi-agent fan-out without thinking twice — because `readonly` means
14
+ "can't break anything".
15
+
16
+ A `readonly: true` ability that actually writes is therefore:
17
+
18
+ 1. **A security hazard** — agents will invoke it in contexts where
19
+ side effects are forbidden.
20
+ 2. **A UX disaster** — the agent's mental model of what happened
21
+ diverges silently from reality.
22
+ 3. **Undetectable at the annotation layer** — the annotation says
23
+ `readonly: true`; nothing in the registration forces it to be true.
24
+
25
+ Unit tests won't catch this class of bug because the mock the test
26
+ constructs looks just like the real writer. What catches it is reading
27
+ the execute callback body and comparing what it does against what the
28
+ annotation says it does.
29
+
30
+ ## What each annotation promises
31
+
32
+ | Annotation | What it promises (from core) |
33
+ |---|---|
34
+ | `readonly: true` | No durable writes to user / business state. GET-style side-effect-free. |
35
+ | `destructive: false` | Won't irreversibly destroy data or forfeit money. |
36
+ | `idempotent: true` | Repeated calls with the same arguments produce no additional effect on the environment (per the `idempotent` annotation's docblock in `class-wp-ability.php`). |
37
+
38
+ `readonly: true` prohibits durable writes to user or business state.
39
+ Read-through cache writes (e.g. `set_transient`) and observability
40
+ timestamps (e.g. `last_read_at`) are acceptable when explicitly
41
+ annotated with `verify-ignore` — see the "Suppressing legitimate
42
+ exceptions" section below. The static check treats unannotated writes
43
+ as FAILs; annotated ones pass with the reason recorded as evidence.
44
+
45
+ These overlap but are not redundant: `readonly` is the strictest;
46
+ `destructive: false` is weaker (updates that don't destroy are OK);
47
+ `idempotent` is orthogonal (a POST that writes the same row twice is
48
+ both "writes" and "idempotent").
49
+
50
+ The Abilities REST run controller operationalizes annotations into
51
+ HTTP method routing (`readonly: true` → GET, `destructive && idempotent`
52
+ → DELETE, otherwise POST — see
53
+ `WP_REST_Abilities_V1_Run_Controller::validate_request_method()`). That
54
+ mapping is the load-bearing semantic; verify checks that each
55
+ callback's behavior is consistent with how the routing will treat it.
56
+
57
+ ## How to verify
58
+
59
+ For each ability, locate the `execute_callback` body (see
60
+ `static-enumeration.md` step 4), then:
61
+
62
+ 1. **Read the callback end-to-end.** Form a model of what it actually
63
+ does. Don't rely on pattern-matching alone.
64
+ 2. **Compare to the claim.** A `readonly: true` callback that writes
65
+ anywhere — the database via `$wpdb`, options / post / user / term /
66
+ comment writes, filesystem, cron schedules, or non-GET HTTP/REST
67
+ delegates — FAILs readonly. A `destructive: false` callback that
68
+ deletes, refunds, voids, cancels, or trashes FAILs destructive. An
69
+ `idempotent: true` callback whose environmental effect *accumulates*
70
+ per call (counters, append-only logs, per-call cron schedules) FAILs
71
+ idempotent.
72
+ 3. **Record evidence.** Cite file + line of the offending pattern so a
73
+ reviewer can jump straight to it.
74
+
75
+ Use grep or ripgrep to surface *candidates*. Common writes worth
76
+ looking for:
77
+
78
+ ```text
79
+ $wpdb->update / insert / delete / replace
80
+ update_option / add_option / delete_option
81
+ wp_insert_post / wp_update_post / wp_delete_post
82
+ update_post_meta / update_user_meta / update_term_meta
83
+ ->save / ->delete / ->set_status / ->add_*
84
+ wp_remote_post / wp_remote_delete
85
+ file_put_contents / wp_upload_bits / unlink / rename
86
+ wp_schedule_event / wp_schedule_single_event
87
+ ```
88
+
89
+ Treat the list as a starting set, not a checklist. Plugin vocabularies
90
+ vary — domain-specific verbs (`->markAsPaid`, `->commit`, `->refund`)
91
+ and framework patterns (Doctrine `->persist`, queue `->dispatch`) won't
92
+ appear above. Once you've grepped for candidates, read the callback to
93
+ confirm whether each hit is actually a write and whether it
94
+ contradicts the annotation in context.
95
+
96
+ ## Known blind spots
97
+
98
+ Static reading + grep can't reach every write. A static-mode PASS
99
+ means "no obvious-shape violations," not "verified write-free."
100
+
101
+ | Blind spot | Why static misses it | Mitigation |
102
+ |---|---|---|
103
+ | Indirected service writes — `$repo->persist()`, `$service->commit()`, custom verbs. | Any finite verb list drifts; domain vocabulary varies. | Inspect callbacks that touch custom services or repositories. |
104
+ | `do_action()` whose listeners write. | Provenance ambiguity: ability looks clean; system mutates state in a listener. | Audit listeners on the action. If any writes, downgrade or split. |
105
+ | Implicit core hooks fired by WP API calls — `wp_insert_post()` fires `save_post`; `update_option()` fires `updated_option`; `wp_create_user()` fires `user_register`; etc. | The WP API call IS the write; the hooks fire automatically as a side effect. Agents looking for `do_action()` won't see this. | Treat any WP write-API call as a write regardless of whether the callback also calls `do_action()`. |
106
+ | Action Scheduler / deferred writes — `as_schedule_single_action()`, `WC()->queue()->schedule_single()`, custom job dispatchers. | The callback returns cleanly with no immediately visible DB mutation; the durable write lands later in the AS tables. A static grep for `$wpdb->insert` won't catch it. | Treat scheduler dispatches as writes. The "no additional effect on the environment" promise of `idempotent: true` is violated by accumulating queued jobs even if the immediate return value is constant. |
107
+ | Variable-built HTTP methods on delegate helpers. | Static can't follow runtime values. | Treat callers of helpers whose default method isn't `GET` as suspect. |
108
+ | Tautological capability gates — `current_user_can('read')` on a "private" ability. | The cap looks valid; subscribers happen to hold it. | Cross-reference the permission roundtrip — subscribers should be denied. |
109
+
110
+ For high-stakes plugins, run runtime mode (see `runtime-harness.md`)
111
+ before landing — it catches some blind spots via twin-invocation diff
112
+ and live state inspection.
113
+
114
+ ## Suppressing legitimate exceptions
115
+
116
+ When a pattern that looks like a write is semantically a read (e.g.
117
+ populating a read-through cache via `set_transient`, updating a
118
+ `last_read_at` timestamp for tracking, diagnostic logging), suppress
119
+ with an inline comment on the offending line:
120
+
121
+ ```php
122
+ // verify-ignore: readonly -- writes to read-through cache; semantically a read.
123
+ set_transient( $cache_key, $data, HOUR_IN_SECONDS );
124
+ ```
125
+
126
+ Format: `// verify-ignore: <annotation> -- <reason>`. Legal annotation
127
+ names: `readonly`, `destructive`, `idempotent`, `all`. Narrower is
128
+ better than `all`.
129
+
130
+ ## Runtime check complement
131
+
132
+ For `idempotent: true` abilities, runtime mode adds a heuristic: invoke
133
+ twice with the same input and compare. See `runtime-harness.md`
134
+ Check 6. Differing returns are a *signal* to inspect, not a verdict —
135
+ under core's definition, the question is whether the *environment*
136
+ changed, not whether the *return value* matches. A response that
137
+ embeds a per-call timestamp / nonce / random ID is fine; a response
138
+ that reflects a counter that grew between calls is not.
139
+
140
+ ## Report format
141
+
142
+ Each finding gets one row in the run's "Annotation correctness" table:
143
+
144
+ ```markdown
145
+ | Ability | Claim | Result | Evidence |
146
+ |---|---|---|---|
147
+ | myplugin/get-things | readonly=true | OK | callback reads only |
148
+ | myplugin/get-things-with-counts | readonly=true | FAIL | `src/Abilities/Things.php:142`: `$wpdb->update( $table, ... )` |
149
+ | myplugin/submit-thing | destructive=false | OK | no destructive patterns |
150
+ | myplugin/submit-thing | idempotent=false | OK | check only applies when idempotent=true; false annotation acknowledged |
151
+ ```
152
+
153
+ The evidence column MUST cite file + line so a reviewer can jump
154
+ straight to the issue.
@@ -0,0 +1,131 @@
1
+ # Audit Schema Validation
2
+
3
+ How `wp-abilities-verify` validates an audit document produced by
4
+ `wp-abilities-audit`. The canonical schema (field tables, types,
5
+ invariants, known limitations) lives in
6
+ `../../wp-abilities-audit/references/audit-schema.md` — this reference
7
+ covers only the validation procedure: how to extract the YAML, what
8
+ checks to run in what order, and how to report results.
9
+
10
+ If a field type or shape question is not answered here, look in the
11
+ canonical schema. Do NOT duplicate field tables in this file — the
12
+ canonical is the single source of truth.
13
+
14
+ ## Why verify owns the validator
15
+
16
+ Verify fails fast on a malformed audit so the rest of its procedure can
17
+ assume well-formed input. Audit produces; verify validates the
18
+ production. Co-locating the validator with verify keeps the
19
+ "validate audit" step in the same procedure as "validate registered
20
+ abilities" and lets a single run produce one consolidated report.
21
+
22
+ ## Step 1 — extract the YAML
23
+
24
+ The audit doc is a markdown file with a single fenced ` ```yaml ` block
25
+ containing the structured fields:
26
+
27
+ ```bash
28
+ # Scan for the ```yaml fence and capture until the closing ``` fence.
29
+ awk '/^```yaml$/{f=1;next} /^```$/{f=0} f' <audit-doc.md> > /tmp/audit.yaml
30
+ ```
31
+
32
+ If the audit has multiple YAML blocks (it shouldn't, but defensively),
33
+ take the first one with `proposed_abilities` as a top-level key.
34
+
35
+ Parse with any YAML library — `js-yaml` from Node, `yaml` (Python), or
36
+ `yq` from the command line. None of the canonical fields require
37
+ non-standard YAML features (no anchors, no aliases), so a plain
38
+ `yaml.load` is sufficient.
39
+
40
+ ## Step 2 — validate against the canonical schema
41
+
42
+ Apply the field-shape rules defined in
43
+ `../../wp-abilities-audit/references/audit-schema.md`. Specifically:
44
+
45
+ 1. Every required top-level field is present and non-empty (see
46
+ "Top-level fields" in the canonical).
47
+ 2. `capability_gate` matches one of the legal shapes (single string,
48
+ `{read, write}` object, or — with WARN per the canonical's "Known
49
+ limitations" — the legacy slash-separated string).
50
+ 3. Every entry in `proposed_abilities` has every required per-ability
51
+ field with the right type (see "`proposed_abilities`" in the
52
+ canonical).
53
+ 4. Each ability's `annotations` block has all three booleans
54
+ (`readonly`, `destructive`, `idempotent`) as actual booleans —
55
+ string `"true"` / `"false"` is FAIL (indicates a quoting bug).
56
+ 5. Each ability's `backing` is either an object with the canonical
57
+ fields or `null`; `null` is WARN, not FAIL (it's intentional gap
58
+ output).
59
+
60
+ Missing required field → FAIL. Wrong type → FAIL. Legacy
61
+ `capability_gate` slash-string → WARN.
62
+
63
+ ## Step 3 — whole-audit invariants
64
+
65
+ Run these after per-field validation passes:
66
+
67
+ ### Exactly 0 or 1 abilities with `reference_ability: true`
68
+
69
+ Count abilities where `reference_ability` is `true`. More than 1 → FAIL
70
+ (the schema permits at most one reference; multiple are ambiguous for
71
+ implementers picking a starting point).
72
+
73
+ ```js
74
+ const refCount = audit.proposed_abilities.filter(a => a.reference_ability === true).length;
75
+ if (refCount > 1) fail("multiple abilities claim reference_ability: true");
76
+ ```
77
+
78
+ ### Every `backing: null` ability appears in `surfaced_gaps`
79
+
80
+ Per the canonical's "Known limitations": a `null` backing is intentional
81
+ gap output and MUST be paired with a matching `surfaced_gaps` entry.
82
+
83
+ ```js
84
+ const gapNames = new Set((audit.surfaced_gaps || []).map(g => g.name));
85
+ for (const ability of audit.proposed_abilities) {
86
+ if (ability.backing === null && !gapNames.has(ability.name)) {
87
+ fail(`ability ${ability.name} has backing: null but is missing from surfaced_gaps`);
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### `excluded_from_mvp` and `surfaced_gaps` may be empty
93
+
94
+ Both are optional; empty arrays are legal. Missing entirely → WARN
95
+ (schema expects them, even if empty).
96
+
97
+ ## Step 4 — emit the report section
98
+
99
+ Each check goes into the "Audit doc validation" section of the run's
100
+ final report:
101
+
102
+ ```markdown
103
+ ## Audit doc validation
104
+
105
+ | Check | Result | Detail |
106
+ |---|---|---|
107
+ | Top-level required fields | OK | All 7 required fields present |
108
+ | `capability_gate` shape | OK | string (single-cap) |
109
+ | Per-ability fields | WARN | 1 ability has `backing: null` (intentional) |
110
+ | `reference_ability` uniqueness | OK | 1 ability marked |
111
+ | `surfaced_gaps` consistency | OK | all `backing: null` entries present |
112
+ ```
113
+
114
+ A single FAIL in this section makes the whole run FAIL; verify cannot
115
+ meaningfully continue without a trustworthy audit. WARN entries don't
116
+ block the rest of the procedure.
117
+
118
+ The procedure is manual-but-deterministic: follow the steps above in
119
+ order, emit the report section, and fail fast on any missing required
120
+ field. A future contribution may add a deterministic CLI helper that
121
+ extracts the YAML fence and applies the rules end-to-end; until that
122
+ exists, the steps above are the contract.
123
+
124
+ ## Escalation
125
+
126
+ If the validator rejects an audit that's actually well-formed, the
127
+ canonical schema in
128
+ `../../wp-abilities-audit/references/audit-schema.md` has evolved.
129
+ Update this file's procedure to match (likely adding a new invariant
130
+ or relaxing a field rule). Don't loosen the validation in isolation —
131
+ the canonical schema is the contract; this file is the enforcer.