diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md index ecbb4ad..4425a47 100644 --- a/.planning/MILESTONES.md +++ b/.planning/MILESTONES.md @@ -1,5 +1,37 @@ # Milestones: Maestro +## v1.5.0 Per-User Visibility (Shipped: 2026-08-09) + +**Phases:** 21 (built under the v1.4 roadmap) + 26 (release). **Release tag:** `v1.5.0` (commit `694b1bf`) · GitHub Release + WordPress.org SVN `trunk`/`tags/1.5.0`, verified from SVN. + +**Delivered:** ROLE-02's **per-user half**. An admin can hide a menu item from named individuals as well as whole roles, on four independent axes (role × person, item × sub-items). Cosmetic only, like every other hide in Maestro: a hidden page still opens by URL for anyone authorized, and no capability changes. + +**Key accomplishments:** +- `items[slug].hidden_users` / `child_hidden_users` storage, sparse and capped at `MAX_HIDDEN_USERS` +- `is_hidden_for_current_user()` widened to independent OR'd terms, with the third reserved for the deferred `hidden_profiles` half — the seam stays a pure identity intersect and never touches a capability +- `resolved_hidden_roles()` **generalized** into a field-parameterized resolver rather than duplicated, so the user axis inherits the qualified-key, schema-v2 and Axis-1 guards from one implementation +- The feasibility note's §6 cosmetic-invariant guardrail made **enforcing** (`CosmeticInvariantUsersTest`) +- Four-group visibility popover with an async person picker on core's `wp/v2/users`, gated by `list_users` on both sides +- A multisite CI lane, closing the super-admin exemption's coverage gap + +**NOT in this milestone:** the cloned-role **"profiles"** half of ROLE-02, still a backlog item (`todos/pending/2026-08-02-cloned-role-hiding-profiles.md`). ROLE-02 remains **partially delivered**. Phase 22 (demo) and Phase 25 (toolbar polish) were optional inclusions that did not land. + +### What this milestone should be remembered for + +**Nine defects were found by verification and review; almost none by the test suite as first written.** Three surfaced during Phase 21's own browser checks — including a guardrail that could not detect a broken seam, because `current_user_can()` answers from a cached allcaps array. Two more came from Codex rounds on the plans, both of which I then hit as live bugs anyway. The adversarial security gate found two. The final ultrareview found three. + +Most instructive: **four consecutive holes in `Config::sanitize()`'s per-user authorization path**, each fix correct about the case in front of it and blind to the next — client-only gate → payload-scoped preserve → raw-key matching → item-cap starvation, plus a DELETE endpoint that bypassed all of them. After each fix the path looked settled. It was only resolved by *collapsing* three mechanisms into one normalized-key map with reserved capacity, rather than adding a fifth guard. + +The transferable lesson: when a fix keeps needing another fix, the shape is wrong, not the coverage. + +**Known limitations, carried deliberately rather than dropped at the finish line:** +- The final round of fixes (#128) is itself unreviewed — the ultrareview ran against the prior commit +- No human screen-reader pass on the person picker; axe is clean across empty and populated states, which is not the same claim +- 21-05 Task 5 (human browser verification) was never performed; the phase was accepted on automated evidence +- On multisite, super admins are exempt from the person axis only — deliberate, now tested both ways + +--- + ## v1.3.0 Slug-Resolution Hardening (Shipped: 2026-06-30) **Phases completed:** 2 phases (17–18), 6 plans. **Release tag:** `v1.3.0` (commit `884c6df`) · GitHub Release + WordPress.org SVN `trunk`/`tags/1.3.0`. diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 17fa60c..2e89bd1 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -28,7 +28,7 @@ backlog IDs without renumbering. ### Roles (cosmetic only) - [x] **ROLE-01**: A feasibility note determines whether per-user and/or cloned-role cosmetic menu hiding can be delivered **without** touching capabilities (stays cosmetic per the core value) within WordPress's role/user model, and specifies the storage shape + resolution seam. **Gates ROLE-02** — if it can't stay cosmetic, ROLE-02 defers. *(Complete 2026-07-05 — Phase 19. Verdict: **partial-go**; both branches clear the cosmetic-only bar. Storage: inline `items[slug].hidden_users` axis + a `profiles` registry compiling onto `items[slug].hidden_profiles`; seam: widen `is_hidden_for_current_user()`. Phase 21 unblocked, per-user first. See `phases/19-cosmetic-hiding-feasibility/19-FEASIBILITY-NOTE.md`.)* -- [~] **ROLE-02**: An admin can apply cosmetic menu-hiding rules scoped to a **specific user** (or a cloned role), intersected against that user's live roles. The rules never grant or remove a capability; a hidden page still loads by URL for a user who has the capability. *(conditional on ROLE-01)* — **PARTIALLY DELIVERED (Phase 21, v1.5).** The **per-user** half is complete: `items[slug].hidden_users` + `child_hidden_users`, resolved through the same single `is_hidden_for_current_user()` seam as the role axes, with the cosmetic invariant enforced by `tests/integration/CosmeticInvariantUsersTest.php` and the effect proven in a targeted user's real sidebar by `tests/e2e/specs/hidden-users.spec.ts`. The **cloned-role "profiles"** half is NOT delivered and remains a v1.5 backlog item — see `todos/pending/2026-08-02-cloned-role-hiding-profiles.md`; the seam is built as an OR of independent terms so `hidden_profiles` lands as a third term without rework. Known limitation: on multisite, network super admins are exempt from the per-user axis only (the role axes keep their v1.4.1 behaviour); that exempt branch is not covered by the single-site test suite. +- [~] **ROLE-02**: An admin can apply cosmetic menu-hiding rules scoped to a **specific user** (or a cloned role), intersected against that user's live roles. The rules never grant or remove a capability; a hidden page still loads by URL for a user who has the capability. *(conditional on ROLE-01)* — **PARTIALLY DELIVERED (Phase 21, v1.5).** The **per-user** half is complete: `items[slug].hidden_users` + `child_hidden_users`, resolved through the same single `is_hidden_for_current_user()` seam as the role axes, with the cosmetic invariant enforced by `tests/integration/CosmeticInvariantUsersTest.php` and the effect proven in a targeted user's real sidebar by `tests/e2e/specs/hidden-users.spec.ts`. The **cloned-role "profiles"** half is NOT delivered and remains a v1.5 backlog item — see `todos/pending/2026-08-02-cloned-role-hiding-profiles.md`; the seam is built as an OR of independent terms so `hidden_profiles` lands as a third term without rework. Known limitation: on multisite, network super admins are exempt from the per-user axis only (the role axes keep their v1.4.1 behaviour) — an intentional asymmetry, now covered by a dedicated multisite CI lane (`npm run test:php:multisite`). SHIPPED in v1.5.0 (tag `v1.5.0` on `694b1bf`, 2026-08-09). ### Editor UX @@ -60,7 +60,7 @@ first; neither blocks the cut. ### Release -- [ ] **REL-11**: v1.5 is cut and shipped — runtime zip builds clean, Plugin Check 0 errors, full PHP/JS/e2e suites green, tagged `v1.5.0`, deployed to WordPress.org SVN `trunk` following the v1.2/v1.3/v1.4 pipeline (**including the manual `wp-deploy.yml` dispatch**, which has never been automatic); changelog verified against the `v1.4.1..main` diff rather than the phase list; **screenshot 3 recaptured** to show the visibility popover's four groups instead of two; and the multisite super-admin exemption stated as a known limitation. +- [x] **REL-11**: v1.5 is cut and shipped — runtime zip builds clean, Plugin Check 0 errors, full PHP/JS/e2e suites green, tagged `v1.5.0`, deployed to WordPress.org SVN `trunk` following the v1.2/v1.3/v1.4 pipeline (**including the manual `wp-deploy.yml` dispatch**, which has never been automatic); changelog verified against the `v1.4.1..main` diff rather than the phase list; **screenshot 3 recaptured** to show the visibility popover's four groups instead of two; and the multisite super-admin exemption stated as a known limitation. --- @@ -96,7 +96,7 @@ Which phases cover which requirements. Populated during roadmap creation. | UX-13 | Phase 23 | ✅ Complete 2026-07-05 | | BUG-08 | Phase 23 | ✅ Complete 2026-07-05 | | REL-10 | Phase 24 | ✅ Complete 2026-08-04 (v1.4.0; patch v1.4.1 2026-08-05) | -| REL-11 | Phase 26 (v1.5) | Pending — ships Phase 21's per-user hiding | +| REL-11 | Phase 26 (v1.5) | ✅ Complete 2026-08-09 (tag v1.5.0 on 694b1bf; SVN verified) | **Coverage:** - v1.4 requirements: 11 total diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 592ba6d..6d5415a 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -10,7 +10,7 @@ - ✅ **R1 Third-Party Compatibility Research** — Phases 13–16 (completed 2026-06-29; non-versioned research — no plugin code, no release tag, no SVN deploy) → [archive](milestones/R1-ROADMAP.md) - ✅ **v1.3.0 Slug-Resolution Hardening** — Phases 17–18 (shipped 2026-06-30; release tag `v1.3.0`) → [archive](milestones/v1.3.0-ROADMAP.md) - ✅ **v1.4 Compatibility, Roles & Showcase** — Phases 19–24 (shipped 2026-08-04; release tag `v1.4.0`, patch `v1.4.1` 2026-08-05). Shipped **without** Phase 21 (ROLE-02, deferred to v1.5 under the Release Binding fallback) and Phase 22 (not reached; still open). -- 🚧 **v1.5 Per-User Visibility** — Phase 21 (built under the v1.4 roadmap, ships here) + Phase 26 (release). Phases 22 and 25 are **optional inclusions**: they ship in v1.5 if they land before the cut, and otherwise slip without blocking it. +- ✅ **v1.5 Per-User Visibility** — Phase 21 + Phase 26 (shipped 2026-08-09; release tag `v1.5.0` on `694b1bf`). Delivered ROLE-02's **per-user half**; the cloned-role "profiles" half remains a backlog item. Phases 22 and 25 were optional inclusions and did **not** land, per the fallback. ## Phases @@ -295,7 +295,7 @@ feature is built (Phase 21, 5/5 plans, 2026-08-08) but unreleased — v1.4.0 was cut before it landed. This milestone exists to get it to users. - [~] **Phase 21: Cosmetic Per-User Hiding** — built under the v1.4 roadmap (details in the v1.4 section above); ships here. Per-user half complete; cloned-role profiles remain a backlog item. -- [ ] **Phase 26: Release v1.5.0** — cut and ship to WordPress.org +- [x] **Phase 26: Release v1.5.0** — cut and shipped to WordPress.org 2026-08-09 (tag `v1.5.0` on `694b1bf`; GitHub Release + SVN `trunk`/`tags/1.5.0` verified from SVN) - [ ] *(optional)* **Phase 22: Slug-Resolution Showcase Demo** — ships in v1.5 if it lands before the cut - [ ] *(optional)* **Phase 25: Edit-Mode Toolbar Dark-Surface Polish** — ships in v1.5 if it lands before the cut diff --git a/.planning/STATE.md b/.planning/STATE.md index c855902..e8da43a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,8 +2,8 @@ gsd_state_version: 1.0 milestone: v1.5 milestone_name: Per-User Visibility -status: in-progress -stopped_at: "v1.5.0 hold cleared — Gate 8 satisfied via ultrareview (3 findings, fixed in #128); ready to tag" +status: shipped +stopped_at: "v1.5.0 SHIPPED to WordPress.org (tag v1.5.0 on 694b1bf, SVN verified). Milestone recorded; next milestone not yet opened." last_updated: "2026-08-09T00:00:00.000Z" last_activity: "2026-08-08 — **Phase 21 (ROLE-02 per-user hiding) executed, 5/5 plans, awaiting the human-verify checkpoint.** Branch `phase/21-cosmetic-per-user-hiding`, PR #120, nothing merged. Delivered: `hidden_users` / `child_hidden_users` storage; the `is_hidden_for_current_user()` seam widened to independent OR'd terms (3rd term reserved for the deferred `hidden_profiles`); `resolved_hidden_roles()` GENERALIZED to a field-parameterized resolver rather than duplicated, so the user axis inherits the qualified-key, schema-v2 (#115) and Axis-1 guards from one implementation; a shared `Cascade` union; the §6 cosmetic-invariant guardrail made enforcing; editor model exposure as id+name pairs via one batched query; four-group visibility popover with an async person picker on core's `wp/v2/users`. Gate: unit 165/165 (218), integration 109/109 (257), JS 83/83, e2e 39 passed/28 capture-skipped/0 failed, WPCS clean, PHPStan 0, Plugin Check 0 errors on the ZIP (1 pre-existing readme warning). THREE bugs found by verification that no unit test would have caught: (1) the guardrail initially could NOT detect a broken seam — `current_user_can()` answers from a cached allcaps array, so `snapshot_caps()` now drops `$GLOBALS['current_user']` to force re-derivation; (2) the picker URL appended `?` unconditionally, 404ing on every PLAIN-PERMALINK site since `rest_url()` already carries a query string; (3) clicking a search result or chip closed the whole popover, because re-rendering detached the node before `placePopover()`'s outside-click handler ran. Ruling recorded 2026-08-08: the super-admin exemption covers the NEW user axis only, multisite-scoped (unscoped would make administrators un-hideable on single-site and contradict the locked self-target decision). Prior: 2026-08-05 — **v1.4.1 SHIPPED** (PR #116, tag on c6cdcbe; wp.org API confirms 1.4.1). Patch for the shared-slug propagation defect (#115): a bare top-level key no longer applies to a submenu row whose slug names a rendered top-level item. Two Codex P2 rounds on #115 — the first cut of the gate tested `$nk === $norm_parent` and missed submenus parked under an unrelated parent; widened to `isset( $top_rendered_matches[ $nk ] )`. Prior: 2026-08-04 — **v1.4.0 SHIPPED** (PR #113, tag on 482510c, GitHub Release + wp.org SVN trunk/tags/1.4.0/assets confirmed). Phase 24 release gates 8–11 run consolidated over the full v1.3.1..main diff; one defect found and fixed (multibyte truncation blanked labels) and one changelog overclaim corrected (shared-slug isolation is submenu-direction only). ROLE-02 (Phase 21) deferred to v1.5." progress: @@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-07-03) ## Current Position Milestone: **v1.5 Per-User Visibility** (active) — Phase 21 ✅ MERGED 2026-08-09 + Phase 26 (release, created 2026-08-09); Phases 22/25 optional inclusions. v1.4 SHIPPED (v1.4.0 + v1.4.1). -Phase: Phase 19 ✅, Phase 20 ✅, Phase 23 ✅ (shipped as v1.3.1). **Phase 21 (Cosmetic Per-User Hiding) ✅ COMPLETE — 5/5 plans, merged to `main` 2026-08-09 via PR #120 (merge commit, history preserved). Task 5 accepted on automated evidence rather than a human browser pass — see the note below.** +Phase: Phase 19 ✅, Phase 20 ✅, Phase 23 ✅ (shipped as v1.3.1), **Phase 26 (Release v1.5.0) ✅ SHIPPED 2026-08-09**. **Phase 21 (Cosmetic Per-User Hiding) ✅ COMPLETE — 5/5 plans, merged to `main` 2026-08-09 via PR #120 (merge commit, history preserved). Task 5 accepted on automated evidence rather than a human browser pass — see the note below.** Plan: Phase 21 delivered ROLE-02's **per-user half**: 21-01 storage → 21-02 seam + cascade → 21-03 guardrail → 21-04 editor + picker → 21-05 e2e + gate + docs, plus two rounds of Codex review fixes. The cloned-role **profiles** half stays deferred to the backlog todo. Next: **Phase 26 (Release v1.5.0)** — per-user hiding is on `main` but unreleased; Phase 22 (demo) and Phase 25 (toolbar polish) are optional inclusions that do not block the cut. Status: Phase 20's COMPAT-10 was reworked mid-checkpoint 2026-08-02: the boolean `cascade_hide` + "rides the parent hide" model built in 20-05/20-06 was found **inert** — WordPress core's `_wp_menu_output()` never renders a hidden parent's `