diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff04905..8d37797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,15 @@ jobs: - name: Integration tests (PHPUnit in wp-env) run: npm run test:php + # ROLE-02's super-admin exemption is `is_multisite() && is_super_admin()`, + # so on a single-site run the second half never evaluates and the whole + # branch is uncovered. That gap shipped through the v1.5.0 gates as a known + # unknown; this lane closes it. Reuses the same containers — WP_MULTISITE=1 + # only changes how the PHPUnit bootstrap installs, so it costs one extra + # suite run rather than a second environment. + - name: Integration tests (multisite) + run: npm run test:php:multisite + - name: Install Playwright Chromium run: npx playwright install --with-deps chromium diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85d628d..0b51827 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,3 +38,20 @@ jobs: files: build/maestro-menu-editor.zip generate_release_notes: true fail_on_unmatched_files: true + + # Deploy to WordPress.org by CALLING the deploy workflow, not by publishing a + # Release and hoping the `release: published` event starts it. It does not: the + # step above authenticates as the default GITHUB_TOKEN, and GitHub does not start + # workflow runs from GITHUB_TOKEN-generated events. That dead trigger is why every + # deploy in this repo's history was manual. See wp-deploy.yml's trigger comment. + # + # `needs: release` means a failed build, a failed tag/version check, or a failed + # Release publish all stop the deploy — wp.org is never reached by a tag that did + # not pass the gate above. + deploy: + name: Deploy to WordPress.org + needs: release + uses: ./.github/workflows/wp-deploy.yml + with: + tag: ${{ github.ref_name }} + secrets: inherit diff --git a/.github/workflows/wp-deploy.yml b/.github/workflows/wp-deploy.yml index 2f08722..361162f 100644 --- a/.github/workflows/wp-deploy.yml +++ b/.github/workflows/wp-deploy.yml @@ -1,20 +1,44 @@ name: Deploy to WordPress.org +# TRIGGERS — read this before adding a third one. +# +# This workflow used to declare `release: types: [published]`. That trigger NEVER +# FIRED, in six consecutive releases: `release.yml` publishes the Release with the +# default GITHUB_TOKEN, and GitHub deliberately does not start workflow runs from +# GITHUB_TOKEN-generated events. Every deploy in this repo's history was a manual +# workflow_dispatch, and STATE.md carried "remember the manual step" as a standing +# lesson for four releases — scar tissue over a trigger that could not work. +# +# It is replaced by `workflow_call`: release.yml now invokes this workflow +# DIRECTLY as a dependent job, so there is no event in the middle to be swallowed. +# workflow_dispatch stays, deliberately — it is how you re-deploy a tag whose +# commit predates these workflows, and how you recover if a deploy half-fails. on: workflow_dispatch: inputs: tag: - description: "Git tag to deploy (e.g. v1.1.1). Leave blank when triggered by a release." + description: "Git tag to deploy (e.g. v1.1.1). Leave blank to use the ref this was dispatched from." required: false default: "" - release: - types: [published] + workflow_call: + inputs: + tag: + description: "Git tag to deploy (e.g. v1.1.1), passed by release.yml." + required: true + type: string + secrets: + WP_ORG_SVN_USERNAME: + required: true + WP_ORG_SVN_PASSWORD: + required: true permissions: contents: read concurrency: - group: wporg-deploy-${{ github.event.inputs.tag || github.ref_name }} + # `inputs` covers BOTH workflow_dispatch and workflow_call; `github.event.inputs` + # would be null on a call and silently collapse every release into one group. + group: wporg-deploy-${{ inputs.tag || github.ref_name }} cancel-in-progress: false jobs: @@ -22,11 +46,30 @@ jobs: name: Deploy to WordPress.org SVN runs-on: ubuntu-latest + # THE HUMAN GATE. Everything above this line is automatic — tag push, build, + # version check, Release publish — and then this job STOPS and waits for an + # approval on the `wordpress-org` environment before anything reaches wp.org. + # + # This is the deliberate answer to a real tension. Wiring the deploy directly + # (see the trigger comment above) fixes a step that was silently forgotten six + # times; but it would also mean a pushed tag publishes to users with no human + # in the loop. The environment gate keeps the automation — the run is sitting + # there, notified, impossible to forget — while keeping the final confirm. + # + # ⚠️ The gate is only real because the environment EXISTS with a required + # reviewer (created 2026-08-10, reviewer: dknauss). If a workflow names an + # environment that does not exist, GitHub AUTO-CREATES IT WITH NO PROTECTION + # RULES and the job sails straight through. Deleting or recreating this + # environment without reviewers silently converts this line into a no-op — + # there is no error, and the deploy simply stops asking. + environment: wordpress-org + steps: - # Resolve which ref/version to ship. On a release event the tag comes from - # github.ref_name; on manual dispatch it comes from the `tag` input. This - # lets us deploy a tag whose commit predates these workflows (e.g. v1.0.0), - # since the workflow file itself is read from the dispatched ref (main). + # Resolve which ref/version to ship. The `tag` input carries it on both a + # workflow_call (release.yml passes github.ref_name) and a manual dispatch; + # github.ref_name is the fallback when a dispatch leaves it blank. This lets + # us deploy a tag whose commit predates these workflows (e.g. v1.0.0), since + # the workflow file itself is read from the dispatched ref (main). - name: Resolve ref + version id: meta env: @@ -34,7 +77,12 @@ jobs: # of interpolating ${{ }} straight into the script body — GitHub sets # these as env values, so no dispatch input can break out of the string # and inject shell. - INPUT_TAG: ${{ github.event.inputs.tag }} + # + # `inputs.tag`, NOT `github.event.inputs.tag`: the latter is null on a + # workflow_call, which would silently fall through to github.ref_name. + # That happens to be correct today (the caller is a tag push) but only by + # accident, and it would break the moment anything else calls this. + INPUT_TAG: ${{ inputs.tag }} REF_NAME: ${{ github.ref_name }} run: | REF="$INPUT_TAG" diff --git a/.planning/DECISION-settings-surface.md b/.planning/DECISION-settings-surface.md new file mode 100644 index 0000000..a5017f8 --- /dev/null +++ b/.planning/DECISION-settings-surface.md @@ -0,0 +1,87 @@ +# Decision: where menu-wide features live + +**Decided 2026-08-10.** Unblocks Phase 27 (profiles) and Phase 28 (width), which +both stalled on the same question from different directions. + +## The decision + +**Two new icon buttons in the edit-mode toolbar's right zone, each opening a +modal dialog:** + +| Icon | Modal | Owns | +|---|---|---| +| Profiles | Manage hiding profiles | create / rename / delete a profile, edit its membership (Phase 27) | +| Settings | Menu-wide configuration | `menu_width` (Phase 28); later the declutter switch and config presets | + +**Per-item features stay exactly where they are** — the shared panel and its +icon/visibility popovers, acting on the selected row. + +**No wp-admin settings page. Not now, not later.** + +## Why this is consistent rather than a new invention + +Two things already exist that make this the path of least surprise: + +1. **The toolbar's right zone is already the home of menu-wide actions.** + `Reset All` lives there (`maestro.js` ~L646-660) and is global — it is the one + existing control that does not act on the selected item. Two more global + controls beside it is the established meaning of that zone, not a new one. +2. **The modal idiom already exists.** The icon picker, the visibility popover + and the coachmark are all `role="dialog"` + `aria-modal="true"` with focus + traps (`maestro.js` :758, :1009, :1942). These modals reuse that machinery + rather than introducing a second dialog pattern. + +And the thing it protects: a settings page would add an entry to the admin menu. +For a plugin whose purpose is decluttering the admin menu, that is +self-parodying — and it would break the core value's "operates on the menu +itself", which is about not having to go elsewhere to configure the menu. + +## Why a shared surface was worth deciding once + +Four queued features are menu-wide, not per-item, and none had a home: + +- `cloned-role-hiding-profiles` (Phase 27) +- `configurable-admin-menu-width` (Phase 28) +- `config-presets-export-import` (V2-06) +- `declutter-switch-non-core-menu-items` + +Answering this per-phase risked two inconsistent surfaces and then four — +precisely the drift the 2026-08-09 backlog reconciliation existed to clean up. + +## Guardrail: two icons is the budget + +A fifth menu-wide feature goes **inside the Settings modal**, not as a third +toolbar icon. Presets and the declutter switch are already earmarked for it. + +Without this rule the toolbar accretes one icon per feature and becomes the +settings screen we just said we would not build — arrived at by increments +instead of by decision. + +## Consequences for the two phases + +**Phase 27** — 27-01's authoring-UX question is answered: profiles are created +and managed in the Profiles modal; assigning one to an item stays in the existing +visibility popover as a fifth group. That split matters — assignment is per-item +and belongs with the other per-item axes; management is global and does not. + +**Phase 28** — 28-03's placement question is answered for the *control*, but with +a deliberate exception: + +> **Width should ALSO be directly draggable**, not only a field in the Settings +> modal. Dragging the menu edge and watching it resize is more in-place than any +> field, and 28-01 makes the width a CSS custom property, so live preview is +> nearly free. The Settings field is the precise/accessible path; the drag is the +> discoverable one. Neither alone is sufficient — a drag-only control is +> inaccessible, a field-only control is a settings screen in a costume. + +**Sequencing:** whichever phase runs first builds the modal shell; the second +reuses it. Phase 28 is the lighter lift and would prove the surface with a single +scalar before Phase 27 puts CRUD in one. + +## Still open, deliberately + +- Which dashicons. Cosmetic; pick at build time. +- Whether the Settings modal shows anything at all before Phase 28 lands (if 27 + goes first, it may ship with only the Profiles icon). +- Whether Reset All clears menu-wide settings — flagged in 28-03 and unchanged by + this decision. 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/PROJECT.md b/.planning/PROJECT.md index 8e6da9a..befffab 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -168,7 +168,7 @@ and `.planning/milestones/` for records. - **Release assets:** Phase 4 is complete. WordPress.org icon, banner, and screenshot graphics exist under `.wordpress-org/` and are referenced from the GitHub/wp.org readmes. User-facing documentation is published in the GitHub README, WordPress.org readme, and `docs/user-guide.md`. - **Localization:** The plugin is translation-ready with the `maestro` text domain and `Domain Path: /languages`. PHP strings use WordPress translation helpers, and JavaScript editor labels are passed through `maestroData.i18n` from PHP. The repo ships a POT template plus starter catalogs for `es_ES`, `de_DE`, `ja`, `fr_FR`, `pt_BR`, and `it_IT`; WordPress.org language packs can still override and extend them, and native-speaker/Polyglots review is welcome. - **Submit:** Phase 5 is complete. The runtime zip builds cleanly, WPCS passes, Plugin Check 2.0.0 reports no errors on the extracted build zip, npm audit reports 0 vulnerabilities after removing unused `@wordpress/scripts`, and local unit/integration/E2E tests pass. **The plugin has been submitted to WordPress.org** and is in the review queue; approval and SVN access are pending (external, out of our hands). On approval: commit to SVN `trunk`, tag `1.0.0`, and upload `.wordpress-org/` to the SVN `assets/` dir. -- **Future roadmap (post-1.0 backlog):** reparenting (top↔sub, highlighting minefield); separator management; keyboard-accessible reordering; per-item-reset UI affordance with a "modified" indicator; custom icon upload (SVG sanitization); import/export config as JSON; optional enforcement bridge (opt-in, clearly-labelled defense-in-depth); multisite/network defaults with per-site override; configurable admin-menu width (V2-09); admin-toolbar editing feasibility research (V2-10); UI/UX design polish for edit-mode hierarchy, responsive behavior, modified-state affordances, status clarity, and icon-picker scanability (V2-12); documentation link hygiene for prose references to project files (V2-13); deterministic banner source/regeneration with the "ADMIN MENU" leader line removed (V2-14); role cloning / per-user cosmetic hiding (V2-15); third-party menu compatibility research, WooCommerce-first (V2-16 — pulled forward to v1.2 Phase 10, 2026-06-19); and a single-site "super-admin equivalent" / privileged editor tier research item (V2-17, 2026-06-19) — note it edges toward the Out-of-Scope "page locking" line; an *enforced* tier is out of scope for Maestro (which never enforces and assumes no dependency on any other plugin), so it would be entirely separate work in a separate project, not Maestro core. +- **Future roadmap (post-1.0 backlog):** see `SPEC.md`'s Roadmap section for the narrative list and `.planning/todos/pending/` for what is actually queued — that directory is the system of record (declared in SPEC.md, 2026-08-09). **Shipped since this paragraph was written:** keyboard reordering (v1.1), per-item reset + modified indicator (v1.1), custom icon support incl. the heavier bundled set V2-11 (v1.1), doc-link hygiene V2-13 and the banner pipeline V2-14 (v1.1 Phase 8), third-party compat research V2-16 (v1.2 Phase 10), UI/UX design polish V2-12 (v1.3.1 Phase 23, with dark-toolbar follow-ups in v1.5.0 Phase 25), and per-user cosmetic hiding V2-15 / ROLE-02 (v1.5.0, per-user half only — cloned-role profiles still deferred). **Still open and now schedulable as todos:** configurable admin-menu width (V2-09), admin-toolbar editing research (V2-10), the remaining icon-picker scope, config presets / import-export (V2-06), reparenting, separator management, multisite defaults, and the privileged-editor-tier research item (V2-17). The "optional enforcement bridge" is flagged IN TENSION with the Out of Scope list — it needs a decision, not implementation. ## Constraints @@ -184,6 +184,7 @@ and `.planning/milestones/` for records. | Sparse delta, not a stored full menu | Trivial reset, resilience to plugin churn, upstream label changes show through | ✓ Good | | Debounced autosave, no Save button | In-place ethos; Save was implicated in early "doesn't persist" reports | ✓ Good | | Click-to-select, whole-row drag, no handles | Per-item clusters/handles were heavy and broke folded mode + hard to grab | ✓ Good | +| **Menu-wide features get a toolbar icon opening a modal; per-item features stay in the per-item panel. No wp-admin settings page, ever.** | The toolbar's right zone ALREADY hosts the only global action (Reset All), so placement itself distinguishes global from per-item — and the modal idiom already exists (`role=dialog` + `aria-modal` + focus trap) in the icon and visibility popovers. A settings page would add an admin menu item, which is self-parodying for a menu-decluttering plugin, and would break "operates on the menu itself". Decided 2026-08-10 for Phases 27/28, which both stalled on it. | Pending (Phases 27, 28) | | Unique slug, no `Update URI` header | Slug uniqueness is the .org collision protection; the header is disallowed by Plugin Check | ✓ Good | | Strip `menu-icon-*` for custom image icons | Core's `background-image:none !important` on its own items hid data-URI/URL icons | ✓ Good | | Visibility is cosmetic only | Authorization is a separate, mature concern; half-enforcement is the worst failure mode | ✓ Good | diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 17fa60c..7cd5d01 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,14 +60,18 @@ 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. --- ## Deferred (future milestone) - **COMPAT-05/06/08/09/11/12/13** — documented WordPress menu-model limitations from R1; docs-only, correct by design (carried as user-guidance, not code). -- **UX-11 follow-ups** beyond the screenshot recapture (the coachmark itself shipped in v1.3.0). + ## Out of Scope @@ -96,7 +100,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..d6b0a9a 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** make the cut, per the fallback. Phase 25 has since been completed post-release (2026-08-09, human-verified) and will ride the next release; Phase 22 remains open. ## Phases @@ -149,7 +149,9 @@ Full phase details, success criteria, and outcomes are archived in - [ ] **Phase 22: Slug-Resolution Showcase Demo** — Playground demo that visibly demonstrates the v1.3.0 slug-normalization fixes - [x] **Phase 23: Editor UX Polish** — native wp-admin restyle of all edit-mode surfaces (UX-13, added 2026-07-03), semantic-colour borders removed (UX-12 verdict), first-run banner centering (BUG-08) — complete 2026-07-05 - [x] **Phase 24: Release v1.4.0** — cut and shipped to WordPress.org 2026-08-04 (tag `v1.4.0` on `482510c`, PR #113); editor + directory screenshots recaptured. Patch **v1.4.1** followed 2026-08-05 (tag on `c6cdcbe`, PR #116) for the shared-slug propagation defect. Shipped WITHOUT Phases 21 and 22 — see the outcome note below -- [ ] **Phase 25: Edit-Mode Toolbar Dark-Surface Polish** — dark-toolbar icon/focus contrast (WCAG 1.4.11), save-indicator layout shift, rename commit feedback (added 2026-08-02; pre-existing v1.3.1 surfaces, non-blocking for v1.4.0) +- [x] **Phase 25: Edit-Mode Toolbar Dark-Surface Polish** — ✅ COMPLETE 2026-08-09 (2/2 plans; human-verified). Audit struck 1 of 5 criteria as already satisfied, downgraded 1 from fix to choice, absorbed 1. Shipped: reserved status slot, focus ring 3.07:1 → 6.74:1, a11y M2/M3. Original text follows — dark-toolbar icon/focus contrast (WCAG 1.4.11), save-indicator layout shift, rename commit feedback (added 2026-08-02; pre-existing v1.3.1 surfaces, non-blocking for v1.4.0) +- [ ] **Phase 27: Cloned-Role Hiding Profiles** — completes ROLE-02's deferred half: a named, Maestro-internal hiding profile that compiles onto an inline `hidden_profiles` axis and resolves through the seam slot Phase 21 reserved at `class-replay.php:510` (planned 2026-08-10) +- [ ] **Phase 28: Configurable Admin Menu Width** — a global `menu_width` so a renamed item no longer wraps at 160px, plus the fold decision it depends on: edit mode keeps forcing unfold but the collapse control stops pretending to work (planned 2026-08-10) ### Phase 19: Cosmetic Hiding Feasibility **Goal**: It is known, before any implementation, whether per-user and/or cloned-role menu hiding can be delivered without touching capabilities — and if so, how it should be stored and resolved @@ -272,22 +274,81 @@ ship Phase 21's per-user hiding. See "Phase Details (v1.5 — Per-User Visibilit **Goal**: The edit-mode bottom toolbar reads cleanly on its dark (`#1d2327`) background and meets the accessibility bar Phase 23 held — control icons and focus states clear WCAG non-text contrast, the save-status indicator no longer shifts the layout, and renames give adequate commit feedback — spot-checked on Default + Modern + Midnight admin colour schemes. **Depends on**: Phase 23 (builds on the shipped editor-UX toolbar) **Requirements**: (none formal — UX/a11y polish; surfaced during Phase 20 verification 2026-08-02. Seed: [.planning/todos/pending/2026-08-01-editor-toolbar-icon-contrast.md](todos/pending/2026-08-01-editor-toolbar-icon-contrast.md)) -**Success Criteria** (what must be TRUE): - 1. Control **icon glyphs** meet WCAG 1.4.11 (≥3:1) on the dark toolbar — recoloured from WP interactive-blue `#3858e9` (~2.8:1) to WP light gray `#c3c4c7` (~9:1), matching the button labels — confirmed by a contrast check. - 2. The **focus ring** on toolbar controls is a light/bright indicator (`#fff` or WP blue-30 `#72aee6`, ≥3:1 on `#1d2327`), replacing the dark `#2271b1` ring, mirroring WP's admin bar / Gutenberg dark surfaces — confirmed by a focus-visible contrast check. - 3. The **save-status indicator** (`.maestro-status`) occupies a persistent fixed-width slot so its appearance/disappearance no longer reflows the rename field — content fades in/out inside a reserved box — confirmed by before/after (no layout shift). - 4. **Rename commit feedback** is improved (the save-status fires on rename commit and/or an "Enter to apply" hint) so a rename no longer feels unsaved — WITHOUT introducing live as-you-type autosave (keeps the commit-on-Enter/blur + debounced-save model and the HARD-03 save-race behavior intact). - 5. Existing PHP unit, integration, JS, and Playwright e2e suites stay green; WPCS clean; PHPStan clean; Plugin Check 0 new errors; verified across Default/Modern/Midnight admin colour schemes. +**Success Criteria** — ⚠️ **RE-SCOPED 2026-08-09 by the 25-01 audit.** The original +five were written from a 2026-08-02 defect report; two had decayed. Measured +verdicts below; full evidence in `25-01-SUMMARY.md`. + 1. **The save-status indicator occupies a reserved slot** so it no longer displaces the rename field. *(CONFIRMED and measured: the status grows 4px → 24px and the rename field shifts 20px horizontally as a direct result. Absorbs original criterion 4 — see below.)* + 2. **The focus ring margin is widened** from `#2271b1` (3.07:1) to `#72aee6` (6.74:1) on `#1d2327`. *(A ROBUSTNESS CHOICE, not a compliance fix — the current ring PASSES the 3:1 bar. Recorded as a choice so it is not re-litigated as a defect.)* + 3. **A11y M2** — the derived-locked checkbox drops its redundant `aria-disabled`, moves the lock reason from the accessible NAME to `aria-describedby`, and makes that reason reachable in screen-reader focus mode (a natively-disabled control is skipped there today, so the reason is never heard). *(From `todos/pending/2026-08-02-a11y-locked-checkbox-refinements.md`; NOT covered by the v1.5.0 axe scanning.)* + 4. **A11y M3** — outside-click dismissal restores focus to the anchor button, matching what Escape already does (WCAG 2.4.3). *(Confirmed by reading `placePopover()`. The v1.5.0 a11y spec asserted the Escape path but never outside-click, which is why it passed.)* + 5. Existing PHP unit, integration (single-site AND multisite), JS, and Playwright e2e suites stay green; WPCS clean; PHPStan clean; Plugin Check 0 new errors; verified across Default/Modern/Midnight admin colour schemes. + +**Resolved before execution — struck by the 25-01 audit, kept visible so nobody wonders whether the original report was addressed:** + - ~~Recolour icon glyphs from `#3858e9` (~2.8:1) to `#c3c4c7`~~ — **ALREADY SATISFIED.** The glyphs measure **9.11:1**; `#3858e9` appears nowhere in `maestro.css`. v1.4.0's a11y gate found this independently and closed it as stale; this roadmap entry was simply never updated. + - ~~Make the save-status fire on rename commit~~ — **ALREADY IMPLEMENTED.** Enter-commit produces `Saving… → Saved`. The residue (the confirmation is transient) is folded into criterion 1, since reserving the slot is most of what "feels unsaved" was describing. A persistent post-save marker remains an open design question, deliberately not smuggled in under a layout fix. + **Sequencing note**: these are **pre-existing** Phase 23 (v1.3.1) toolbar surfaces, not v1.4 regressions, so they do **not** block the Phase 24 release. Placement is the user's call — ship it *before* Phase 24 (so v1.4.0 carries the fixes and Phase 24's editor-screenshot recapture reflects them), or defer to a v1.4.1 / later follow-up. Currently appended after Phase 24; resequence with `/gsd:insert-phase` if it should precede the release. **Resolved 2026-08-08:** the question is moot as posed — v1.4.0 shipped on 2026-08-04 without this phase, so it did not precede that release. It is now a candidate for the (not yet created) v1.5 release, alongside the deferred a11y M2/M3 items from `todos/pending/2026-08-02-a11y-locked-checkbox-refinements.md`. -**Plans**: TBD (run /gsd:plan-phase 25 to break down) +**Plans**: 25-01 audit + re-scope ✅ COMPLETE 2026-08-09 · 25-02 implement, prove, close (pending; `autonomous: false`) + --- +### Phase 27: Cloned-Role Hiding Profiles +**Goal**: An admin can name a reusable hiding profile ("Reduced view"), assign people to it, and apply it to menu items — cosmetically, with membership resolved live — completing ROLE-02 +**Depends on**: Phase 21 (built the seam slot, the field-parameterized resolver, and the bounded-axis sanitize shape this extends) +**Requirements**: ROLE-02 (completion — currently PARTIAL since v1.5.0) +**Success Criteria** (what must be TRUE): + 1. A `profiles` map is the AUTHORING structure and **compiles** onto `items[slug].hidden_profiles`; nothing ever resolves the hide decision from the map itself (feasibility note §7's "one lookup, one seam, one audit point") + 2. Profile membership is intersected LIVE each request, so adding or removing a person takes effect with no re-save and a deleted profile self-heals + 3. Term 3 lands in the slot Phase 21 reserved — the seam remains ONE drop path per menu level, not a parallel resolve + 4. The cosmetic-only invariant holds for the profiles axis exactly as for roles and users, proven by extending the existing §6 guardrail rather than a new one + 5. Proven in a targeted user's OWN rendered sidebar, with a bystander outside the profile keeping every row + 6. Zero regression: both integration lanes, unit, JS, e2e, WPCS, PHPStan, Plugin Check +**Plans**: 27-01 decisions · 27-02 storage + compile · 27-03 seam + live membership · 27-04 editor · 27-05 e2e, gate, close ROLE-02 + +> **27-01 is a decisions checkpoint, deliberately.** The feasibility note locked +> storage and resolution in detail but left **authoring UX as "the main open +> design question for a future discuss-phase"** — where profiles get created and +> managed has no home today, since Maestro has never had a settings screen. It +> also re-verifies the note's architecture against the code Phase 21 actually +> shipped: the note cites line numbers from before v1.4.1 and Phase 21 both moved +> things, and its assumption that term 3 is a list-intersect may not survive +> contact, since membership is a property of the USER rather than the item. + +### Phase 28: Configurable Admin Menu Width +**Goal**: A renamed menu item that used to wrap at WordPress's hardcoded 160px no longer has to — a global width, applied while browsing, with folding still working everywhere it should +**Depends on**: nothing (28-01 is independently shippable) +**Requirements**: (none formal — V2-09, extracted from SPEC.md item 9 during the 2026-08-09 backlog reconciliation) +**Success Criteria** (what must be TRUE): + 1. `#collapse-menu` is VISIBLY disabled during edit mode with a programmatic reason, not silently swallowed by a capture-phase handler + 2. The menu column width resolves from ONE source, not three hardcoded `160px` literals (`maestro.css` :22, :28, :523) + 3. `menu_width` is stored bounded and sparse — the default is never written, and reset means absent + 4. The width applies on ORDINARY admin pages, not only in edit mode — and a site that never set one loads nothing new and pays no new page cost + 5. `body.folded` still folds to core's 36px outside edit mode, and the `<782px` overlay is unaffected + 6. Zero regression across both integration lanes, JS, e2e, WPCS, PHPStan, Plugin Check +**Plans**: 28-01 fold honesty + de-hardcode · 28-02 storage + the always-loaded seam · 28-03 control, docs, close + +> **The fold story was DECIDED 2026-08-10 and folded into this phase.** Edit mode +> keeps forcing unfold — a 36px icon rail cannot show the labels being edited, and +> `docs/archive/FIXES.md` #4 records that the editor *broke* in folded mode +> historically. What changes is the honesty: the collapse control currently +> renders, takes focus and does nothing, which is the actual defect. +> +> The "fold versus width conflict" flagged on 2026-08-09 was **mis-framed and is +> withdrawn**. It is not a design conflict — `160px` is simply hardcoded in three +> places, one of which is the constant this phase makes configurable. Outside edit +> mode width applies to the expanded menu and folding works normally; inside edit +> mode folding is off, so width just applies. +> +> **This phase turns Maestro into something that loads outside edit mode** for the +> first time (28-02). "Costs nothing unless you are editing" is true today and +> stops being — hence the measured-footprint checkpoint rather than an assumption. + ## Phase Details (v1.5 — Per-User Visibility) **Milestone goal:** ship ROLE-02's per-user cosmetic hiding to WordPress.org. The @@ -295,7 +356,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 bea32c1..96c5e9c 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,9 +2,9 @@ gsd_state_version: 1.0 milestone: v1.5 milestone_name: Per-User Visibility -status: in-progress -stopped_at: Phase 21 complete and merged (per-user hiding); 21-05 Task 5 accepted on automated evidence, not a human browser pass -last_updated: "2026-08-09T00:00:00.000Z" +status: shipped +stopped_at: "v1.5.1 SHIPPED and verified from SVN — main carries no unreleased code. Phases 27 and 28 planned and unblocked; no milestone open. New CI todo: the release→deploy trigger is dead by construction (GITHUB_TOKEN), which is what the four-release 'remember the manual step' lesson was actually describing." +last_updated: "2026-08-10T00: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: total_phases: 6 @@ -26,11 +26,105 @@ 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 `