Skip to content

fix(26-03): release gates 8–11 — one real defect found and fixed - #124

Merged
dknauss merged 3 commits into
mainfrom
release/v1.5.0-gates
Aug 9, 2026
Merged

fix(26-03): release gates 8–11 — one real defect found and fixed#124
dknauss merged 3 commits into
mainfrom
release/v1.5.0-gates

Conversation

@dknauss

@dknauss dknauss commented Aug 9, 2026

Copy link
Copy Markdown
Owner

26-03 of Phase 26. Gate 10 found a genuine data-loss defect; gates 9 and 11 pass with notes; gate 8 needs an independent pass.

Gate 10 — the defect

Probed the authorization boundary with tests rather than reasoning about it, against a delegated Maestro editor (maestro_capability role without list_users):

Probe Result
Can they inject a per-user rule? ✅ No
Does saving the same item preserve an admin's rule? ✅ Yes
Does omitting the item preserve it? No — destroyed
Does the model leak display names? ✅ No

This wasn't an exotic crafted POST — it was ordinary use. get_menu_model() correctly withholds the user axes from such a saver, so diffItem() never flags them, so an item whose only override is a per-user rule is omitted from their full-replace autosave. A rule omitted from a full replace is deleted. Any edit a delegated editor made silently wiped every per-user rule they couldn't see.

Round 2's per-item preserve only fired for items present in the payload. The restore now runs over the stored items instead. My own comment had claimed the saver "can neither add nor destroy" — half was true.

Two guardrail tests then failed, which is the fix working: they removed their rule while acting as the editor, which now correctly no-ops.

Gate 9 — a11y

7 new checks over surfaces that had never had an independent pass: programmatic label, four distinct group names (v1.4.0's S1 here was two groups sharing one), keyboard-reachable results, per-person accessible names on remove controls, populated live region, focus returning after add/remove, focus trap intact.

Contrast computed, not eyeballed: text 6.83–10.03:1, focus ring 5.17:1. Two sub-3:1 borders are inherited core tokens (#c3c4c7 already appears 21× in this stylesheet; #dba617 is core's notice amber) and neither carries information alone — recorded as notes rather than "fixed" into an inconsistency with wp-admin.

Not covered: what a screen reader actually announces. The structure it depends on is proven; the announcement isn't.

Gate 11 — performance, measured cold

Name lookup, whole model 1 query
Round-2 bounded validation 1 query (was a full user-table scan)
Zero-override path 0 queries
Pathological all-items-targeted config +0.108 ms/request

Same order as the entire pre-existing replay cost, and lost in admin TTFB.

Gate 8 — needs you

@codex review — please look at v1.4.1..main, and particularly at Config::sanitize()'s per-user authorization path, which now has three interacting behaviours: reject-on-add, preserve-on-submit, and restore-on-omit. That is exactly the shape where a fourth case hides. Every pass so far has found something real.

Gate: unit 167/167 · integration 119/119 · JS 83/83 · e2e 46 passed/0 failed · WPCS clean · PHPStan 0.

🤖 Generated with Claude Code

Gate 10 of the v1.5.0 release. Probed the authorization boundary with tests
instead of reasoning about it, and the probe found a real defect.

A delegated Maestro editor (maestro_capability role without list_users) could
NOT inject a per-user rule, and could not read display names out of the model —
both correct. But they could DESTROY an admin's rules, and not via a crafted
POST: get_menu_model() withholds the user axes from them (correctly), so
diffItem() never flags those axes client-side, so an item whose only override is
a per-user rule is omitted from their full-replace autosave — and a rule omitted
from a full replace is a rule deleted. Any edit such an editor made silently
wiped every per-user rule they could not see. Ordinary data loss.

Round 2's per-item preserve only fired for items PRESENT in the payload, which
was never sufficient. The restore now runs over the STORED items instead,
re-attaching or re-creating any entry carrying an axis the saver could not touch.
My own comment claimed the saver "can neither add nor destroy"; half was true.

The probe is kept as PerUserAxisAuthorizationTest rather than deleted — the
symptom is a rule quietly disappearing rather than anything failing loudly,
which is how this returns unnoticed.

Two guardrail tests then failed, and that is the fix working: they removed their
rule while acting as the EDITOR, which now correctly no-ops. They author as
admin and must remove the same way.

Also adds tests/e2e/specs/person-picker-a11y.spec.ts for Gate 9 — 7 checks over
surfaces that had never had an independent pass: programmatic label on the
search field, four DISTINCT group names (v1.4.0's S1 in this same popover was
two groups sharing a name), keyboard-reachable results, per-person accessible
names on the chip remove controls, a populated polite live region, focus
returning to the field after add and remove, and the focus trap still holding
with the new controls.

Contrast computed rather than eyeballed: all text 6.83-10.03:1 and the focus
ring 5.17:1. Two sub-3:1 borders (#c3c4c7, #dba617) are inherited core tokens —
#c3c4c7 already appears 21x in this stylesheet — and neither carries information
alone. Recorded as notes rather than "fixed" into an inconsistency with wp-admin.

Gate 11 measured cold: name lookup is ONE query for the whole model, the
round-2 bounded validation is ONE query rather than a user-table scan, the
zero-override path costs 0 queries, and a pathological all-items-targeted config
adds +0.108 ms/request — the same order as the entire pre-existing replay cost.

Gate: unit 167/167 (223), integration 119/119 (275), JS 83/83, e2e 46 passed /
28 capture-skipped / 0 failed, WPCS clean, PHPStan 0 errors, doc-links clean.

Gate 8 (independent code review) is NOT done — requested on the PR.

Plan: .planning/phases/26-release-v1.5.0/26-03-PLAN.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

dknauss and others added 2 commits August 9, 2026 13:52
Third iteration on the same function, and the second hole in my own fix.

Re-attaching a preserved rule under its STORED key is wrong when the payload
names the same item in a different but equivalent form — `upload.php?ver=9` for
a rule stored under `upload.php`. That is not a contrived input: slug drift is
the exact problem Slug::normalize() exists to solve, so it is the expected state
after a plugin bumps a ver= string and the client emits the new form.

The config then held BOTH keys. They normalize to the same item, so the Axis-1
collision guard resolved to "apply nothing" — silently neutralising the admin's
rule AND the delegate's own edit, while the stored option still looked healthy.
A rule that is present but inert is worse than one that is missing, because
nothing looks wrong.

The restore now indexes what is about to be written by normalized key and merges
into the equivalent entry instead of adding a second one.

Lineage worth recording: round 2 fixed round 1's client-only gate; A3 fixed
round 2's payload-scoped preserve; A5 fixes A3's raw-key matching. Each fix was
right about the case in front of it and blind to the next. The per-user path now
carries four interacting behaviours — reject-on-add, preserve-on-submit,
restore-on-omit, merge-on-equivalent-key — which is precisely the shape that
wants an independent reviewer rather than another pass by the person who wrote it.

Gate: unit 167/167 (223), integration 120/120 (277), WPCS clean, PHPStan 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…obes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant