Skip to content

ID-118 {118.3} TECH — Zone-grouped platform navigation implementation plan

ID-118 {118.3} TECH — Platform navigation information architecture

Section titled “ID-118 {118.3} TECH — Platform navigation information architecture”

Kind: TECH ({118.3}) — translates the ratified {118.2} PRODUCT (30 BIs) into an implementation plan across the three navigation surfaces. Feeds {118.4} PLAN decomposition and the id-135 {135.10} /search entry. Authored: 09/07/2026 · Task Planner (fresh dispatch — not the {118.2} author, by design). Inputs read in full: PRODUCT.md (this dir); IA-MAP-s454.md ({118.1} RESEARCH); decision register DR-041/DR-040/DR-038/DR-034/DR-025/DR-013/DR-021. Controlling ruling: DR-041 (three-zone IA). Blocking OQs answered: BI-8 = reserved slot; BI-21 Procurement = all authenticated now, RBAC later. This spec honours both; does not reopen.

Empirical-verification note. This spec cites internal Canonical routes/components and the existing @/components/ui/dropdown-menu Radix wrapper (verified below by direct read of its exports) — no new external-library symbols are introduced. The pre-ratification import-and-call check (shared-discipline.md §Empirical verification) is therefore N/A.

The change re-organises the platform’s flat top-level nav into the DR-041 three-zone IA (Applications | Knowledge | Governance + utilities), surfacing /reference as the Knowledge-zone “External sources” entry and providing the Knowledge slot for id-135’s /search ({135.10}). Behaviour is fully specified in PRODUCT.md (BI-1..BI-30); this section grounds the plan in the code that must change.

Code-intelligence orientation (verbatim — cited per the Planner code-intel binding rule; guide refs .gitnexus/CLAUDE.md, .ast-dataflow/CLAUDE.md).

  • gitnexus_query({query: 'navigation shell reference IA zone grouped nav command palette', repo: 'canonical'})processes: [] for the nav surfaces themselves (nav is presentational, not a traced execution flow). Definitions surfaced the mutation targets + harness: Function:components/shell/site-header.tsx:SiteHeader (startLine 80, endLine 242, module Ui); Function:components/shell/command-palette.tsx:CommandPalette (22–347, module Shell) + runCommand (74–78); harness Function:e2e/helpers/responsive.ts:getVisibleNavLinks (125–141, module Cluster_118) and navigateViaHeader (30–48); the separate OKF-bundle nav surface Function:components/okf/bundle-nav.tsx:BundleNav (114–180) — out of scope (per-bundle side-nav, not top-level IA).
  • gitnexus_impact({target: 'SiteHeader', direction: 'upstream', repo: 'canonical'})risk LOW, impactedCount 2. d=1 Function:components/shell/auth-aware-chrome.tsx:AuthAwareChrome (sole caller, CALLS); d=2 Function:app/layout.tsx:RootLayout. Single affected module Shell.
  • gitnexus_impact({target: 'CommandPalette', direction: 'upstream', repo: 'canonical'})risk LOW, impactedCount 1. d=1 Function:app/layout.tsx:RootLayout (mounted globally, CALLS).

Load-bearing findings (confirmed by direct read):

  • No shared nav registry. SiteHeader hardcodes a flat NAV_LINKS array (components/shell/site-header.tsx:34–77) feeding both the desktop bar (map at 112–136) and the mobile drawer (map at 191–216). CommandPalette hand-duplicates a separate Command.Group heading="Navigation" list (command-palette.tsx:130–300) that is already drifted — it still lists Workspaces (150–159) and Bids → /procurement (160–169), and lacks Search/Answers/External sources/Coverage/Intelligence/Activity. This is the core reason BI-18/BI-19 (lockstep) exist: any membership/label change is today a three-place manual edit that has already fallen out of sync.
  • Current NAV_LINKS membership (site-header.tsx:34–77): Search /search (requiresEdit:false, already added flat by {135.10}), Q&A Library /library (false), Coverage /coverage (true), Workspaces /workspaces (false), Change Reports /change-reports (false), Intelligence /intelligence (true), Review /review (true). Plus Settings icon, persistent SearchBar (compact), and an explicit mobile “Home”.
  • Gating model is a single boolean. Nav items carry only requiresEdit, resolved against useUserRole(){ canEdit, canAdmin } (hooks/use-user-role.ts). Provenance (BI-11, admin-only) is not expressible in the current shape — it exists today only inside the command palette behind canAdmin (command-palette.tsx:190,288–297). The config must grow a three-value visibility axis.
  • Active-state machinery is pathname === href || pathname?.startsWith(href + '/') with aria-current="page" on the active leaf (site-header.tsx:114–120, 193–199). BI-23/BI-24 reuse this.
  • Disclosure primitive is already in the tree. components/ui/dropdown-menu.tsx (Radix wrapper) exports DropdownMenu / DropdownMenuTrigger / DropdownMenuContent / DropdownMenuItem (verified by read: exports block at line 230). accordion.tsx and popover.tsx are also available. cmdk (^1.1.1) backs the palette.
  • Route reality (verified find app -name page.tsx): every zone destination exists — /search /library /reference /procurement /intelligence /review /coverage /change-reports /activity /provenanceexcept an OKF index/landing: only /okf/[bundleId] is routable, no /okf index. This is exactly the BI-8 reserved-slot trigger. /workspaces does still exist as a route (app/workspaces/page.tsx) — see the BI-10↔BI-28 decision below. There is no /browse route and no /item/[id] route (see Risk R4).
  • Stale e2e nav test (owned by this Task, routed via {118.2} journal / DR-021). e2e/tests/auth.spec.ts:196–230 (“navigation header is visible with all expected links”) asserts Browse (204), Q&A Library (206), Coverage (208), Workspaces (209) links. Browse never existed in the current NAV_LINKS (it holds Search), so line 204 already fails at --workers=2; the whole assertion set is stale against the target IA and is rewritten here (BI-16/BI-17/BI-28).

Governing rulings respected (not re-litigated): DR-041 (the three-zone frame), DR-038 (“Workspaces”/“Bids” retired as labels; instances live under their application), DR-025 (“Evidence” is backend-only, never user-facing), DR-034 (Coverage entry surfaces only the surviving template-completion + governance coverage), DR-040 (Streamdown — informational; nav renders no markdown).

C0 — New single source of truth: components/shell/nav-config.ts (the central decision)

Section titled “C0 — New single source of truth: components/shell/nav-config.ts (the central decision)”

The one architectural move that makes BI-18/BI-19 structurally true rather than manually maintained: extract the zone model into one typed module both consumers import. Co-located in components/shell/ beside its two consumers; direct file import (@/components/shell/nav-config), no barrel (CLAUDE.md convention). It is data + pure helpers only — no JSX, no 'use client'.

import type { LucideIcon } from 'lucide-react';
export type NavVisibility = 'all' | 'edit' | 'admin'; // replaces requiresEdit boolean
export interface NavEntry {
href: string;
label: string; // the BI-17 ratified user-facing string, single-sourced
icon: LucideIcon;
visibility: NavVisibility;
keywords?: string; // command-palette search terms (cmdk value=)
reserved?: boolean; // BI-8: defined in the IA but NOT rendered until its route lands
}
export type NavZoneId = 'applications' | 'knowledge' | 'governance';
export interface NavZone {
id: NavZoneId;
header: 'Applications' | 'Knowledge' | 'Governance'; // BI-2 exact strings
entries: readonly NavEntry[];
}
export const NAV_ZONES: readonly NavZone[] = [...] as const;
// Shared helpers so all three surfaces agree (BI-23/BI-24, BI-20/BI-21):
export function isEntryVisible(v: NavVisibility, r: { canEdit: boolean; canAdmin: boolean }): boolean;
export function isEntryActive(href: string, pathname: string | null): boolean; // === || startsWith(+ '/')
export function isZoneActive(zone: NavZone, pathname: string | null): boolean; // any member active

Ratified membership encoded once (order per BI-4/BI-9/BI-11; labels per BI-17; gating per BI-20/BI-21):

ZoneEntry (label)hrefvisibilityreservedicon (suggested)
ApplicationsProcurement/procurementallBriefcase
ApplicationsIntelligence/intelligenceeditNewspaper
KnowledgeSearch/searchallSearch
KnowledgeAnswers/libraryallLibrary
KnowledgeExternal sources/referenceallLink2
KnowledgeConcepts/okfallyesWaypoints
GovernanceReview/revieweditShieldCheck
GovernanceCoverage/coverageeditBarChart3
GovernanceChange reports/change-reportsallFileBarChart
GovernanceActivity/activityallActivity
GovernanceProvenance/provenanceadminHistory

Notes on the encoded values (each traceable to a BI or ruling):

  • Procurement all, Intelligence edit — deliberate asymmetry ratified by BI-21 (Procurement OQ answered: all authenticated now, RBAC later; Intelligence keeps its existing requiresEdit:true).
  • Change reports all — preserves the current requiresEdit:false (BI-21 “no silent audience change”). Label lowercases the “r” per BI-17 (“Change reports”).
  • Provenance admin — the new axis value; today only reachable via the palette’s canAdmin block.
  • Activity all/activity is homeless today (no current gate); all-authenticated is ratified (OQ-T1, S457 owner ruling — as encoded, no behavioural change).
  • Concepts reserved:true — BI-8: /okf has no landing route, so the entry is defined in the IA and this config but not rendered by any surface until id-132/id-138 ships the landing (owner ratified reserved-slot). Icon/label pre-declared so enabling it later is a one-line reserved flip.
  • Utilities (Home via BrandLogo, the persistent SearchBar, Settings) are not in NAV_ZONES — they stay bespoke per surface (BI-12/BI-13/BI-14).

C1 — components/shell/site-header.tsx desktop bar → zone disclosures (BI-1/2/3, 22, 23/24/25, 27)

Section titled “C1 — components/shell/site-header.tsx desktop bar → zone disclosures (BI-1/2/3, 22, 23/24/25, 27)”

Replace the flat NAV_LINKS.map (112–136) with a map over NAV_ZONES, each zone rendered as a DropdownMenu from @/components/ui/dropdown-menu:

  • DropdownMenuTrigger shows the zone header string (BI-2) and carries the BI-24 active affordance (a non-colour signal — weight/underline — when isZoneActive(zone, pathname)), with aria-expanded supplied by Radix (BI-25).
  • DropdownMenuContent lists the zone’s visible, non-reserved entries; each DropdownMenuItem asChild wraps a Next <Link> carrying aria-current="page" when isEntryActive (BI-23). Radix supplies arrow-key traversal, Escape-to-close-with-focus-return, and outside-click/blur close (BI-25) — this is why the existing wrapper is chosen over hand-rolled disclosure.
  • Filter with isEntryVisible(entry.visibility, { canEdit, canAdmin }) (BI-20/BI-21); skip entry.reserved (BI-8).
  • All chrome uses Warm Meridian semantic tokens only, WCAG AA, visible focus ring, UK English (BI-27) — reuse the existing token classes already in this file (text-foreground / text-muted-foreground / bg-accent), no raw Tailwind colour.

Trade-off (recorded). DropdownMenu vs Radix NavigationMenu vs hand-rolled disclosure: NavigationMenu is semantically closest to a menubar but is not yet wrapped in components/ui/ (new dependency surface + new a11y wiring). Hand-rolling re-implements exactly the keyboard/focus/aria machinery BI-25 enumerates. DropdownMenu is already wrapped, already used elsewhere, and satisfies every BI-25 clause out of the box — chosen. Consequence for the harness: Radix marks the leaf <a> as role="menuitem", not link, so navigateViaHeader’s desktop getByRole('link') path must open the owning zone disclosure first, then select the leaf by menuitem/accessible-name (see C5).

C2 — components/shell/site-header.tsx mobile drawer → labelled zone sections (BI-26, 27)

Section titled “C2 — components/shell/site-header.tsx mobile drawer → labelled zone sections (BI-26, 27)”

The Sheet drawer (164–240) keeps its explicit “Home” at top (BI-12) and Settings + Sign-out at foot (BI-14/BI-26). Replace the flat NAV_LINKS.map (191–216) with, per zone, a non-interactive section heading (header string) followed by its visible, non-reserved members as the existing full-width <Link> rows (no disclosure needed on mobile — sections are always-expanded, matching the settings-sidebar section pattern). Same isEntryVisible / isEntryActive / reserved handling; selecting a row closes the drawer and navigates (existing onClick={() => setMobileMenuOpen(false)}).

C3 — components/shell/command-palette.tsx → three zone groups, de-drifted (BI-18/19, 16/17, 10)

Section titled “C3 — components/shell/command-palette.tsx → three zone groups, de-drifted (BI-18/19, 16/17, 10)”

Replace the single Command.Group heading="Navigation" (130–300) with three Command.Groups (heading="Applications" | "Knowledge" | "Governance", BI-2) generated from NAV_ZONES, each item a Command.Item routing via runCommand(() => router.push(entry.href)) with value={entry.keywords ?? entry.label}. Apply the same visibility filter (canEdit/canAdmin — the palette already reads canAdmin; add canEdit from useUserRole) and skip reserved. Net effect (BI-19): the palette gains Search / Answers / External sources / Coverage / Intelligence / Activity; loses Workspaces (150–159); Bids → /procurement (160–169) becomes Procurement under Applications (BI-18). Keep Home, the theme/keyboard Actions group, and the admin Settings › … fan-out (190–298) as-is except moving Provenance under the Governance group. Every palette destination now equals its site-header counterpart (BI-18).

Add the lucide imports the config needs that aren’t already imported: Link2 (External sources), Waypoints (Concepts — declared even though reserved, so the flip is one line), Activity, History (Provenance). Briefcase is freed by the Workspaces removal and reused for Procurement. Icons are suggestions, not load-bearing — the executor may pick a clearer lucide glyph provided it is distinct from siblings and imported in both consuming surfaces.

C5 — e2e nav harness + stale test (BI-28, and the {118.2}-journal-routed stale test)

Section titled “C5 — e2e nav harness + stale test (BI-28, and the {118.2}-journal-routed stale test)”
  • e2e/tests/auth.spec.ts:196–230 — rewrite the assertion set to the target IA: assert the three zone headers (Applications/Knowledge/Governance) and that each retained destination is reachable (Search, Answers, External sources, Procurement, Intelligence, Review, Coverage, Change reports, Activity) for an editor, with viewer-role variants honouring BI-20 (all Knowledge entries visible) and BI-21 (edit/admin entries hidden appropriately). Remove the Browse/Workspaces/Q&A Library literals (BI-16/BI-17). This is the DR-021-routed stale test; it lands in the same wave.
  • e2e/helpers/responsive.ts:navigateViaHeader (30–48) — desktop path must open the target leaf’s zone disclosure before clicking (C1 makes leaves role="menuitem"). Add a zone argument or resolve the zone from a leaf→zone lookup exported by nav-config. 2 call sites in 2 specs (1 each)qa-library.spec.ts:598 (Answers, Knowledge) and governance-review.spec.ts:261 (Review, Governance) — update them. getVisibleNavLinks (125–141, used only by the rewritten auth.spec) needs no signature change but its callers assert the new labels.
  • searchFromHeader (98–118) — the persistent search box is BI-13 “unchanged” and out of id-118 scope; do not touch it here. Its stale mobile /browse expectation (Risk R4) is an id-135 search-box concern, flagged not fixed.

C6 — Existing component tests (lockstep with C1–C3)

Section titled “C6 — Existing component tests (lockstep with C1–C3)”

__tests__/components/site-header.test.tsx and __tests__/components/command-palette.test.tsx assert the old labels (Q&A Library, Workspaces, Bids, Change Reports) and flat structure; update them in the same commit(s) as C1–C3 to the zone structure and BI-17 labels, using the existing vi.hoisted() mockUserRole pattern to exercise BI-20/BI-21 role variants. Keep the {135.10} /search assertions (site-header.test.tsx:274–299) green — Search simply moves into the Knowledge zone. Respect the __tests__/docs/ antipattern guards (behaviour-first titles, no state-token coupling).

C7 — Retire the /workspaces launcher route outright (S457 owner ruling — production-code deletion)

Section titled “C7 — Retire the /workspaces launcher route outright (S457 owner ruling — production-code deletion)”

OQ-T2 is ratified but supersedes the earlier de-link-only recommendation: the owner chose full retirement of the legacy launcher (“cheapest now — app not live, no bookmarks exist, any data found is deletable across all four DBs”). Its function is fully superseded by the Applications zone. Three parts:

  • (a) De-link everywhere — four sites, not three. Beyond the three nav surfaces (C1/C2 drop the /workspaces NAV_LINKS entry; C3 drops the palette Workspaces item), a fourth linker exists: app/not-found.tsx:34 (href="/workspaces"). Repoint it to / (or remove the row) so the 404 page does not link a deleted route. (Confirmed by grep: exactly these four source linkers; /workspaces is not in proxy.ts/lib/routes.ts.)
  • (b) Delete the route tree + the launcher-private hook + all their tests. Remove app/workspaces/ entirely (page.tsx, workspaces-content.tsx, error.tsx, loading.tsx), the launcher-private hook hooks/workspaces/use-application-types.ts, and their tests (__tests__/app/workspaces-page.test.tsx, __tests__/app/workspaces/workspaces-content.test.tsx, __tests__/app/workspaces/workspaces-launcher.test.tsx, __tests__/app/workspaces-boundaries.test.tsx, __tests__/hooks/workspaces/use-application-types.test.ts) plus the launcher e2e e2e/tests/workspaces.spec.ts. Deletion boundary verified — the hook is launcher-private, NOT shared. app/api/application-types/route.ts only names the hook in a prose comment (line 10) and defines its own ApplicationTypeRowSchema Zod schema — it does not import from it; a repo-wide sweep finds real import … from '@/hooks/workspaces/use-application-types' statements ONLY in the two deleted launcher pages and the hook’s own test. gitnexus_impact upstream (09/07/2026, index @409fdb2 — current for these unchanged files): useLauncherTypesLOW, sole callers WorkspacesContentWorkspacesPage (both inside the deleted tree, module Workspaces, no processes); useApplicationTypesimpactedCount 0. Deleting the launcher pages without the hook would therefore strand it as orphaned dead code. Re-run gitnexus_impact on the deleted symbols pre-edit and gitnexus_detect_changes pre-commit (production-code deletion).
  • (c) Data-cleanup audit across all four DBs — but scoped precisely. The launcher owns no private data table: page.tsx only reads workspaces (row counts) + application_types (grid), and writes nothing. So the audit’s expected finding is zero launcher-private rows to purge across the four DBs (Platform prod/staging + client prod/staging). The owner’s blanket “any data found is deletable” authorisation is recorded and applies to any genuinely-orphaned launcher artefact the audit surfaces. Explicitly NOT in this step: dropping/emptying the workspaces table itself — it is the shared live store still read by the Intelligence app (/intelligence/[workspaceId]) and procurement instances; its rework is the DR-038 activity-carried-ids migration owned by ID-130, not this nav Task. OQ-T3 RESOLVED (S457 owner ruling): launcher orphans only — the shared workspaces-table purge is out of scope (a separate DR-038/ID-130-coordinated migration), exactly the conservative scope this step encodes.

What this spec deliberately does NOT change

Section titled “What this spec deliberately does NOT change”

Per PRODUCT Non-goals and the decision register: proxy.ts publicRoutes (BI-30 — no entry is public); what /library renders (id-71 family); the workspaces-table → activity-carried-ids data-model migration for live Intelligence/procurement instances (DR-038/ID-130 — the /workspaces launcher route is deleted here per C7, but the shared workspaces table and its live readers are not touched); the OKF editing surface (id-132/138); /item/new* retirement (backlog). No RPC/seam change; no schema/migration; no sb()/tryQuery() surface (nav is presentational).

BI → change mapping (one-to-one, for Checker verification)

Section titled “BI → change mapping (one-to-one, for Checker verification)”
BIPRODUCT invariant (short)Proposed change
BI-1Three zones + utilities, nothing outsideC0 NAV_ZONES; C1/C2 render only zones + bespoke utilities
BI-2Exact zone header strings on every grouping surfaceC0 header union type; C1 trigger, C2 section heading, C3 group heading
BI-3Membership closed, no route in two zonesC0 single-sourced membership (a route appears once)
BI-4Knowledge = Search/Answers/External sources/Concepts, no content_items surfaceC0 Knowledge rows; no /browse//item/new present
BI-5External sources → /reference, id-111 UI homeC0 row /reference; C1/C2/C3 render it
BI-6Search → /search, id-135 Surface A, requiresEdit:falseC0 Search all; regroups the existing flat {135.10} entry into Knowledge
BI-7Answers → /library as-is (reserved slot)C0 Answers row; no change to /library content
BI-8Concepts landing-conditional (reserved)C0 reserved:true; C1/C2/C3 skip reserved entries
BI-9Applications = live application_types (Procurement, Intelligence)C0 Applications rows; extension seam = new row only
BI-10”Workspaces” retired as a label everywhereC1/C2 remove /workspaces entry; C3 removes palette Workspaces; C7 retires the route outright (S457) — de-link (incl. not-found.tsx) + delete app/workspaces/ + data-audit
BI-11Governance = Review/Coverage/Change reports/Activity/Provenance(admin)C0 Governance rows incl. Provenance admin
BI-12Home via logo (all) + explicit mobile HomeC2 keeps mobile Home; logo unchanged
BI-13Persistent search box unchangedNo change (explicitly excluded in C5)
BI-14Settings a utility outside zonesUnchanged (Settings icon + mobile foot entry)
BI-15/documents/[id] + /admin/* no top-level slotC0 omits them (contextual-only)
BI-16No Browse/Workspaces/Bids/Evidence labelsC0 label set; C1/C2/C3 apply it; C5/C6 drop the literals
BI-17Exact ratified label set incl. “External sources”C0 label strings single-sourced
BI-18Lockstep across the three surfacesC0 shared config imported by C1/C2/C3 — structurally enforced
BI-19Palette de-drift completeness (gains Knowledge entries)C3 generates groups from NAV_ZONES
BI-20Knowledge zone role-uniform (all)C0 Knowledge visibility all; isEntryVisible
BI-21No silent audience change elsewhereC0 preserves edit/admin gating; Procurement all per OQ
BI-22Full discovery set (desktop, mobile, Cmd+K)C1/C2/C3 cover all three; box + logo utilities remain
BI-23Leaf active-state + aria-currentC0 isEntryActive; C1/C2 apply on leaf
BI-24Grouped-zone active affordance on headerC0 isZoneActive; C1 trigger affordance (non-colour)
BI-25Grouped-menu keyboard + a11yC1 Radix DropdownMenu supplies aria-expanded/arrow/Escape/outside-click
BI-26Mobile drawer three labelled sectionsC2 always-expanded sections
BI-27Warm Meridian tokens, WCAG AA, non-colour signal, UK EnglishC1/C2/C3 semantic tokens + focus ring + weight/underline
BI-28Destinations preserved (harness resolves each)C0 retains every destination except the deliberately-retired /workspaces launcher (C7, S457 owner ruling); C5 harness rewrite
BI-29Corpus surfaces unchanged in contentNo corpus route/page body touched — nav only (the /workspaces launcher deletion in C7 is not a corpus surface)
BI-30Authentication unchanged (publicRoutes untouched)Explicitly excluded

Vitest (bun run test, never bun test), behaviour-first per test-philosophy.md. Component tests own BI verification at the unit level; e2e owns the cross-surface + role reachability.

  • BI-1/2/3/4/9/11 (structure + membership)site-header.test.tsx: render, assert the three zone headers present and each zone’s members (by label) rendered under it; assert no content_items-era label appears. command-palette.test.tsx: assert three heading groups with matching members.
  • BI-5/6/7 (Knowledge destinations) — assert External sources → /reference, Search → /search, Answers → /library (href on the leaf <a>), all three surfaces.
  • BI-8 (Concepts reserved) — assert no “Concepts” entry renders while /okf landing is absent (reserved:true); add a unit test on nav-config proving a reserved entry is filtered by the render predicate (guards the future flip).
  • BI-10/16/17 (labels/de-drift) — assert Workspaces/Bids/Browse/Q&A Library/Evidence are absent as labels on all three surfaces; assert External sources, Answers, Change reports, Procurement present. (This subsumes the rewritten auth.spec.ts assertion.)
  • BI-18/19 (lockstep) — a nav-config unit test is the anchor: both consumers import the same NAV_ZONES, so a table-driven test asserting every zone member’s {label, href, zone} gives the Checker a single lockstep proof; palette test asserts each destination equals the config href.
  • BI-20/21 (role-gating) — drive mockUserRole variants (viewer / editor / admin): viewer sees all Knowledge entries + Change reports + Procurement + Activity, but not Review/Coverage/Intelligence (edit) or Provenance (admin); admin additionally sees Provenance. Assert on both header and palette.
  • BI-23/24 (active-state) — set mockPathname to /reference/abc, assert aria-current="page" on External sources and the Knowledge zone trigger shows its active affordance; /documents/xyz → no top-level aria-current (BI-15); /search → Search active (keep existing {135.10} test green).
  • BI-25 (keyboard a11y)command-palette.test.tsx already exercises focus-trap/Escape; add a header disclosure test: Enter/Space opens (aria-expanded toggles), arrow moves between menuitems, Escape closes and returns focus to the trigger. (Radix-backed, but assert the contract holds.)
  • BI-27 (non-colour signal + contrast + tokens) — assert the active leaf and active zone trigger carry a non-colour marker in addition to aria-current: the active <a>/trigger class list includes the weight/underline token (e.g. font-semibold) so meaning is not colour-only, and the focus ring is present on :focus-visible. Contrast (WCAG AA) is checked by an axe/pa11y scan of the header (desktop bar + open disclosure + mobile drawer) in the e2e pass; the semantic-token usage (no raw Tailwind colour) is additionally guarded by the __tests__/docs/ token-coupling check.
  • BI-26 (mobile drawer) — open the Sheet (existing pattern), assert three section headings with members beneath, Home at top, Settings + Sign-out at foot; selecting a row closes the drawer.
  • BI-28 (non-regression reachability)bun run test:e2e nav specs: the rewritten auth.spec.ts plus the updated navigateViaHeader path (open zone → click leaf) resolving Answers and Review; every retained destination reachable.
  • BI-29/30 — assert-by-omission: no route/page/publicRoutes file in the diff; a browser pass (agent-browser, editor + viewer) confirming /reference /search /library /procurement /intelligence render unchanged and each is reachable from the new nav. Capture screenshots (desktop + mobile).
  • Full regression gatebun run test green after the wave (nav tests + the __tests__/docs/ antipattern guards).
  • R1 — Three-surface drift re-opens (the exact problem this Task fixes). Mitigation: C0 makes NAV_ZONES the single source; the BI-18 table-driven nav-config test fails if any consumer diverges. Do not let an executor re-inline a per-surface list “for now”.
  • R2 — Harness breakage from role="menuitem". Radix DropdownMenu leaves are menuitems, not links; navigateViaHeader and any spec using getByRole('link', {name}) against the header break silently (test-time, not prod). Mitigation: C5 updates the helper + its 2 call sites in the same wave; land header + harness together (never split across waves) or e2e goes red.
  • R3 — BI-10 (retire Workspaces) vs BI-28 (preserve every destination): RESOLVED (OQ-T2, S457 owner ruling). The owner chose full retirement — delete the /workspaces launcher route outright (superseding the earlier de-link-only recommendation), because the app is not live, no bookmarks exist, and any orphaned data is deletable across all four DBs. BI-28 now carries an explicit carve-out for the one deliberately-retired launcher; every other destination is preserved. Implementation + the precise data scope (route deleted, shared workspaces table not touched) is C7 / slice 6. OQ-T3 RESOLVED (S457): launcher orphans only — the shared workspaces-table purge is out of scope (a separate DR-038/ID-130 migration, not this Task).
  • R4 — Adjacent stale search-box target. components/browse/search-bar.tsx routes queries to /browse?q= and results to /item/[id] (171/183/197/212), neither of which is a live route; PRODUCT BI-29 also names /browse as an “unchanged” corpus surface that does not exist. Mitigation: out of id-118 scope (BI-13 search box unchanged); flag to the Orchestrator/Curator as an id-135 search-box / backlog item, not fixed here. Noting it prevents an executor “fixing” the box mid-nav-wave.
  • R5 — Activity audience: RESOLVED (OQ-T1, S457 owner ruling) = all-authenticated, as encoded. No residual risk; the all value stands.

No new binding ruling is warranted: DR-041 already records the three-zone IA; DR-038 the Workspaces label retirement; BI-8/BI-21 OQ answers are in the {118.1}/{118.2} journals; OQ-T1 (Activity = all-authenticated) and OQ-T2 (full /workspaces route retirement) are S457 owner rulings folded into this spec (C7, R3, R5). These are task-scoped decisions implementing the existing DR-038 direction, not new cross-cutting rulings — no DR-intent. The R4 search-box drift is a Curator finding (curated as ID-135.23). OQ-T3 is Resolved (S457 owner ruling): launcher orphans only; the shared workspaces table is out of scope (DR-038/ID-130) — the conservative scope this spec already encodes.

Decomposition-readiness ({118.4} PLAN candidate slices)

Section titled “Decomposition-readiness ({118.4} PLAN candidate slices)”

Sibling-only dependencies (all within Task 118). The config module is the spine; the three consumer surfaces then fan out and CAN parallelise (disjoint files), but each consumer + its test move together, and the header + harness must land in the same wave (R2). Candidate slices:

  1. nav-config.ts + its unit test (C0) — the NAV_ZONES data, the three helpers, and the table-driven lockstep/reserved/visibility tests. Depends on: none. Blocks all others.
  2. Site-header desktop zone disclosures (C1) — DropdownMenu per zone, active affordance, a11y; update site-header.test.tsx desktop assertions. Depends on: (1).
  3. Site-header mobile drawer sections (C2) — labelled zone sections; mobile assertions in site-header.test.tsx. Depends on: (1). Parallel with (2) but same file — group (2)+(3) into one executor to avoid a site-header.tsx conflict.
  4. Command-palette zone groups + de-drift (C3) + command-palette.test.tsx. Depends on: (1). Parallel with (2)+(3) — disjoint file.
  5. e2e harness + stale-test rewrite (C5) — navigateViaHeader zone-aware update + its 2 call sites (1 each: qa-library.spec.ts:598, governance-review.spec.ts:261) + auth.spec.ts:196 rewrite. Depends on: (2)+(3) (needs the shipped desktop disclosure DOM).
  6. Retire the /workspaces launcher route (C7) — repoint/remove the 4th linker (app/not-found.tsx:34); delete the app/workspaces/ tree + the launcher-private hook hooks/workspaces/use-application-types.ts + all 5 unit tests (4 launcher + the hook’s own) + the launcher e2e workspaces.spec.ts; run the four-DB data-audit (expected finding: zero launcher-private rows). Production-code deletion — dispatch with an independent-checker note (blast-radius review + gitnexus_detect_changes, not self-verified). Depends on: (2)+(3) (its de-link overlaps the site-header/palette edits — same-file coordination) — group the not-found.tsx/nav de-link portion into slices (2)+(3)/(4); the app/workspaces/ deletion + audit is the independently-checkable remainder. (Optional icons slice C4 folds into (2)/(4); no own subtask.)

Effort re-estimate for the Orchestrator: the pre-reframe effort_estimate “0.5–1d” is understated. This is a config-module extraction + net-new desktop a11y disclosure machinery + three coordinated surfaces + a harness/test rewrite (config module, both shell components, their unit tests, the e2e harness + specs) + the C7 launcher-route retirement (production-code deletion + four-DB data-audit). Realistic 2–3d; the {118.2}–{118.4} pre-reframe impl subtasks (single-link add) are superseded and must be replaced by slices 1–6 above.