> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# UX redesign — Domain Owner IA spec

> Final navigation/screen structure for the web UI redesign, Data Domain Owner first: object-first minimal nav, /d/:domainId path scope, capability-composed sidebar, seven-module orient home.

# Find My Data — Final IA Spec (Data Domain Owner first)

**Basis.** Aggregate judge ranking (3/2/1 points across the three verdicts): Object-first minimal nav **8**, Posture-first **6**, Worklist-first **4**. This spec starts from **Object-first minimal nav** (five nouns + Ask, `/d/:domainId` path scope, ≤8 sidebar items, capability-composed nav) and grafts the judge-endorsed ideas from the other two: the coverage-movement subline, chromatic tenant lens, switcher badges, route-preserving switch, batch-drawer excerpt peek, attest side-panel, and unified approvals (from Posture-first); the hero Waiting-on-you module, declarative module registry, mutation-gate lint convention, stub-expiry rule, error-copy standard, blocked-first sorting, and last-used-domain redirects (from Worklist-first). Judge disagreements are resolved inline; only the three genuinely contested calls appear in §7.

***

## 1. Nav model

Nav is composed **only** from `/api/auth/me` capabilities — union across roles, never role-switched, no role labels anywhere. A group renders only if a capability inside it does. Max sidebar size for any single persona: 6 items; worst realistic combo (owner+approver): 8.

| Group                                                                                                                                        | Item           | Route                                                                                                                   | Capability gate                                                                                                                     | Serves (clusters / personas)                                      |
| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| —                                                                                                                                            | Home           | `/home` (redirects — see §2)                                                                                            | any authenticated user                                                                                                              | A; all five personas via capability-composed modules              |
| **My Domain** — group header IS the domain switcher; label = current domain name (e.g. "HR")                                                 | Landscape      | `/d/:domainId/landscape` (non-nav children: `/d/:domainId/assets` filtered list, `/d/:domainId/assets/:assetId` detail) | domain-scoped `asset.metadata.read`                                                                                                 | B, D; owner                                                       |
|                                                                                                                                              | Review         | `/d/:domainId/review`, `/d/:domainId/review/:candidateId`                                                               | domain-scoped `review.read` (decide/batch/contribute controls gated on `review.decide`, `review.batch`, `model.dataset.contribute`) | C, F; owner                                                       |
|                                                                                                                                              | Findings       | `/d/:domainId/findings`, `…/:findingId`                                                                                 | domain-scoped `finding.read`                                                                                                        | B, E-entry; owner                                                 |
|                                                                                                                                              | Actions        | `/d/:domainId/actions`, `…/:actionId`                                                                                   | domain-scoped `action.draft` or `action.submit`                                                                                     | E; owner                                                          |
|                                                                                                                                              | Analyst        | `/d/:domainId/analyst`                                                                                                  | domain-scoped `analyst.query`                                                                                                       | D; owner                                                          |
| **Tenant**                                                                                                                                   | Approvals      | `/approvals` with capability-rendered tabs: `/approvals/actions`, `/approvals/taxonomy`, `/approvals/releases`          | `action.approve` OR `governance.taxonomy.approve` OR `model.release.approve`                                                        | E-downstream; remediation\_approver, governance\_admin            |
|                                                                                                                                              | All findings   | `/findings`                                                                                                             | **tenant-scoped** `finding.read` (owners hold domain scope and do not see this)                                                     | oversight; governance\_admin, remediation\_approver, auditor (ro) |
|                                                                                                                                              | Governance     | `/governance` — tabs: Program, Taxonomy, Ownership & attestation, Policies                                              | any tenant-scoped `governance.*`                                                                                                    | tenant stewardship, B-at-tenant; governance\_admin, auditor (ro)  |
|                                                                                                                                              | Model releases | `/models`                                                                                                               | `model.dataset.curate` OR `model.release.propose` OR `model.release.approve`                                                        | F tenant half; governance\_admin                                  |
| **Platform** (gate excludes an owner's *domain-scoped* `scan.job.read` — owners see freshness inside their own surfaces, never Platform nav) | Connections    | `/setup` (route unchanged, relabeled)                                                                                   | `connector.configure`                                                                                                               | platform\_admin                                                   |
|                                                                                                                                              | Operations     | `/operations`                                                                                                           | `operations.read` OR tenant-scoped `scan.job.*`                                                                                     | platform\_admin; auditor (ro)                                     |
| —                                                                                                                                            | Audit trail    | `/audit`                                                                                                                | `audit.read`                                                                                                                        | auditor                                                           |

Non-nav routes: `/assets/:assetId` = tenant **metadata-only** asset detail for tenant-capability users (approver, auditor); owners are redirected to the scoped `/d/:domainId/assets/:assetId`. Evidence/excerpt panels are separate components that require `asset.evidence.*` — they never mount without it, on any page.

**Per-persona sidebars (union, never role-picked):**

* **Data Domain Owner:** Home · \[domain] Landscape, Review, Findings, Actions, Analyst
* **Remediation Approver:** Home · Approvals · All findings (+ metadata-only asset detail)
* **Governance Admin:** Home · Approvals (taxonomy/releases tabs) · All findings · Governance · Model releases
* **Platform Admin:** Home · Connections · Operations
* **Auditor:** Home · All findings (ro) · Governance (ro) · Operations (ro) · Audit trail
* Combos are pure union (owner+approver = My Domain group + Approvals + All findings, distinguished by chrome — §3).

**Two implementation conventions (grafted, non-optional):**

1. Nav groups and Home modules come from one **declarative capability→surface registry** — no conditional role logic in components (prevents "the new App.tsx role soup").
2. **Lint/test rule:** every mutation control on a shared page must declare its capability gate at the component level; a missed gate is treated as a compliance bug, not a UI bug (required because auditor/approver reuse shared surfaces read-only).

***

## 2. Orient landing spec

**Routing:** `/home` → single-domain owner: redirect to `/d/:theirDomain/home`. Multi-domain owner: portfolio view at `/home`. Tenant-only personas: tenant composition at `/home`. All modules render inside a shared **Panel** component with mandatory `loading / empty / error` variants; error copy standard: "Couldn't load reviews — Retry". An API failure must never render as emptiness.

**Owner domain home — `/d/:domainId/home`, modules in motivation order (exposed → waiting → prove → fix):**

**1. Headline strip** — four stat tiles, every number a link.

* Content: Open findings (critical/high sub-chips) · Reviews waiting · Actions blocked/awaiting approval · Attestation age + state (ok/due/overdue).
* Data: `/api/findings?status=open` + `/api/home` (pendingReview, attestation dates) + `/api/actions` — **all existing**.
* Links: `/d/:x/findings?status=open&severity=critical,high` · `/d/:x/review` · `/d/:x/actions?status=blocked,submitted` · opens attest side-panel (module 4c).
* Empty: tiles render "0" and stay linked (target lists own the true-empty state); attestation tile shows "Never attested".

**2. Waiting on you (hero — largest module, primary CTA of the page).**

* Content: top-3 queue preview cards (info type, priority, uncertainty, exposureSummary, `family · N` chip); CTA **"Start reviewing (N)"**; subline (grafted, all three judges endorsed): *"Clearing this queue moves confirmed coverage 62% → 71%"*, computed client-side from queue size vs `/api/landscape` provenance counts.
* Data: `/api/review/queue?domainId=` top 3 + `/api/home` pendingReview — **existing**; subline **computed**.
* Links: cards → `/d/:x/review/:candidateId`; family chip → same candidate **with batch drawer already open**; CTA → `/d/:x/review`.
* Empty: "Queue clear — nothing waiting on you" + link to the provenance gap ("12% still model-inferred →" landscape).

**3. Exposure.**

* Content: top-5 open findings by severity (badge, info type, asset, first\_seen; "recent" chip = first\_seen ≤ 7d, explicitly labeled a recency proxy). Inline **"Draft fix"** per row (grafted: fewest-hops bridge) — POSTs `/api/actions` with `targetAssetId`+`findingId`, opens the prefilled draft.
* Data: `/api/findings?status=open` + existing `POST /api/actions` — **existing**. True "new since last visit" **NEEDS-NEW-ENDPOINT** (seen-state).
* Links: rows → `/d/:x/findings/:id`; footer "All N open findings →" list.
* Empty: "No open findings in HR."

**4. Ground truth (prove control).**

* Content: (a) provenance mix bar — owner-confirmed / model-inferred / unreviewed, each segment clickable; (b) label coverage % + unlabeled count; (c) **attestation card** opening a side-panel that snapshots current posture (coverage %, provenance mix, open findings) with an **"Attest this domain"** button — disabled with "endpoint coming" note until the POST exists; **stub-expiry rule:** if the endpoint slips more than two releases, pull the button. Trend sparkline omitted until a series endpoint exists — never faked.
* Data: `/api/home` assetCounts + unlabeledCount + attestation dates, `/api/landscape` cells — **existing/computed**. Attest POST + trend series **NEEDS-NEW-ENDPOINT**.
* Links: segments → `/d/:x/assets?provenance=…`; coverage → `/d/:x/assets?labelState=unlabeled`.
* Empty: zero-asset domain: "No assets discovered yet — check source coverage."

**5. Actions in flight.**

* Content: status board (draft / submitted / executing / blocked / done+verification) with `blocked_reason` surfaced; **blocked and rejected sort first** (grafted). Labeled **"In my domains"** — not "Mine" — until a requester filter exists (honest for multi-owner domains).
* Data: `/api/actions` client-filtered to current domain — **existing**; `mine=true` **NEEDS-NEW-ENDPOINT**.
* Links: counts → `/d/:x/actions?status=…`.
* Empty: "No remediation in flight" (+ link to open findings if any exist).

**6. What changed (7d).**

* Content: "N assets changed" — rendered as **text, deliberately not a link** (the one documented exception to every-number-is-a-link; both judge-criticized alternatives — inventing a filter, or a mislabeled findings proxy — are rejected). Below it: per-source freshness from `/api/sources` ("SharePoint-HR scanned 2h ago"), stale sources called out; plus an honestly labeled secondary link **"Recently active findings →"** `/d/:x/findings?sort=lastSeen`.
* Data: `/api/home` changedLast7Days + `/api/sources` — **existing**. Item-level feed **NEEDS-NEW-ENDPOINT** (`/api/changes`) — the count becomes a link the day it ships.
* Links: freshness line links to `/operations` **only if** the viewer holds `connector.configure`/`operations.read`; informational otherwise (grafted pattern).
* Empty: "No changes in the last 7 days · sources current."

**7. Recent questions (footer rail).**

* Content: last 3 analyst queries with one-click re-run.
* Data: `/api/analyst/history` — **existing, currently fetched-and-dropped**.
* Links: → `/d/:x/analyst`.
* Empty: collapses to a single "Ask about your data →" link.

**Multi-domain portfolio (`/home`):** one compact card per owned domain — name + color, the four strip numbers, attestation age, blocked-actions count. Counts are per-domain rows, **never merged**; each card → that domain's `/d/:x/home`; per-card error states.

**Tenant personas' `/home`** (same registry): approver — "Awaiting your approval (N)" + 3 oldest → `/approvals`; governance\_admin — attestation coverage/overdue, ownership gaps, pending taxonomy/release approvals, tenant findings rollup; platform\_admin — connector health, running/failed scans, freshness; auditor — audit volume/last event, open findings by severity (ro), scan coverage.

***

## 3. Domain scope mechanism

**URL scheme.** Every domain-scoped surface lives under `/d/:domainId/…` — **including asset detail** (`/d/:domainId/assets/:assetId`), closing the scope-dropping drill hop that today's `/assets/:id` causes and that sank the Worklist-first proposal. Tenant surfaces (`/approvals`, `/findings`, `/governance`, `/models`, `/setup`, `/operations`, `/audit`) never carry the prefix. A pasted `/d/finance/review/123` opens exactly that item for the authorized, and renders a clean "Not your domain" screen for anyone else.

**Enforcement machinery (not just a URL convention):**

* A `DomainScope` route boundary validates `:domainId ∈ me.ownedDomains` **before any data fetch**; mismatch renders "Not your domain" with zero requests fired (server `domainScopeFilter` remains the authoritative backstop).
* `scopedLink(path)` is the only sanctioned way to build in-scope links — scope survives every hop by construction, and the rule is lintable.
* All API calls inside the boundary automatically append `?domainId=` from the route param — never from component state.

**Single-domain owner.** Bare legacy routes (`/review`, `/landscape`, `/findings`, `/actions`, `/analyst`) redirect into `/d/:theirDomain/…`. The switcher renders as a static domain badge (name + stable per-domain accent color) — scope feels innate, no dropdown.

**Multi-domain owner.** The My Domain group header is a dropdown listing `ownedDomains`, each entry carrying a **pending-review badge** (grafted) so the switcher itself answers "which domain needs me". Switching **rewrites the `/d/:id` prefix of the current route** — you stay on Findings when swapping HR→Finance (`review/:candidateId` falls back to the queue). "All domains" routes **only** to the `/home` portfolio — a mixed-domain content screen structurally cannot exist. Bare routes go to the last-used domain (localStorage), with a picker on first visit.

**Owner+approver (and any tenant-capability combo).** Chromatic tenant lens (grafted): tenant pages swap the domain badge for an **"All domains · Tenant"** pill **and drop the domain accent color** — crossing from `/d/hr/actions` to `/approvals` is a visible chrome change, making the SoD boundary spatial and chromatic, not a hidden permission check. In `/approvals`, items the viewer submitted **or targeting a domain they own** carry a "Yours — awaiting another approver" badge with approve disabled (client mirror; server enforcement is a verification gate, §6). `/d/:x/actions` never renders approve controls, regardless of capabilities held.

***

## 4. Cluster A–F → screen map, and the 10 problems

**Clusters:**

* **A Orient** → `/home` (redirect/portfolio) + `/d/:x/home` modules 1–7.
* **B Understand** → `/d/:x/landscape` grid + facets → `/d/:x/assets` filtered lists; summary via home modules 4 and 6; trend deferred pending series endpoint.
* **C Review & confirm** → `/d/:x/review` two-pane workbench: persistent scannable queue pane (sort/filter/jump, **position indicator "7 of 43"**, collapsible, degrades to list→detail on narrow viewports), focus pane (evidence, redacted excerpts, open-source link, 6-way decide), **batch drawer** with sampled-member excerpt peek before "Apply to all N", propagation toast ("confirmed; 12 byte-identical copies updated").
* **D Investigate** → `/d/:x/analyst` (scope banner + history rail) + `/d/:x/assets/:assetId` (versions, permissions, duplicates, risk, findings, Draft-remediation shortcut).
* **E Act** → "Draft fix"/"Draft remediation" (home module 3, finding detail, asset detail) → prefilled draft → `/d/:x/actions` (submit/track) → `/approvals` (approver side, tenant chrome).
* **F Improve the model** → "contribute as confirmed example" checkbox inside C's decide control (deliberately no owner-facing screen); curation at `/models` for governance.

**The 10 problems, resolved:**

1. **Dead-end numbers** — IA-wide invariant: every count/chip/cell is a `scopedLink` to a filtered list; `/d/:x/assets` is the universal drill target. One documented temporary exception (changed-count, §2.6) with stub expiry.
2. **Dropped domain scope** — path prefix + `DomainScope` boundary + `scopedLink` + automatic `domainId` injection; `Me.ownedDomains` finally consumed; asset detail scoped.
3. **Review resets / invisible queue** — persistent queue pane, cursor preserved on decide (auto-advance to next), position indicator, stable `/review/:candidateId` URLs, jumpable.
4. **Inert batch chips** — `family · N` chip opens the batch drawer wired to `review.batch`, with per-member excerpt peek before bulk commit. Endpoint existence is a **verification gate** before the layout is committed (§6); fallback: client-side sequential decide with progress.
5. **Severed finding→action bridge** — every Draft button POSTs `targetAssetId`+`findingId` (server accepts today); the hand-typed asset-ID field is deleted.
6. **SoD blur** — approver inbox exists only at `/approvals` (gated `action.approve`, tenant chrome); owner Actions is drafts/submissions only, Submit only on own drafts; Home composes per capability so a pure owner never sees an approvals module; self-items badged and disabled.
7. **Attestation affordance** — home tile + attest side-panel snapshotting posture at signing time; POST flagged as new server work; disabled-with-note + stub-expiry until then; oversight table in Governance.
8. **Swallowed errors** — shared Panel with mandatory loading/empty/error variants on every module and list; written error-copy standard; error visually distinct from empty.
9. **Dropped data rendered** — provenance mix (home 4a + landscape), clusterContext (queue cards, family chips, batch drawer), scope.domains (switcher + boundary), analyst history (rail + home 7).
10. **No triage controls** — filter/sort/pagination live in query params on `/d/:x/assets`, both findings mounts, actions, approvals, audit — so triage state and drill links are the **same currency** (one mechanism holds problems 1 and 10 closed together).

***

## 5. Screen-by-screen deltas from today

Ship the regroup with a one-release "moved here" affordance on renamed/moved items.

| Current page                   | Verdict                     | Delta                                                                                                                                                                                                                                                                                        |
| ------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Home                           | **Reshape**                 | Becomes capability-composed orient (§2). Approver-inbox module removed from owner view (extracted to `/approvals`); per-domain redirect/portfolio logic added; all modules from the declarative registry.                                                                                    |
| My Data Landscape `/landscape` | **Reshape**                 | Keeps its name; moves to `/d/:x/landscape`. Every cell becomes a link to `/d/:x/assets?infoType=&source=`; adds provenance/label-coverage/risk side panels; new non-nav child `/d/:x/assets` filtered-sortable-paginated list is the drill target for the whole product.                     |
| Review `/review`               | **Reshape (biggest)**       | Two-pane workbench at `/d/:x/review`: visible queue pane (sort/filter/jump, position indicator, collapsible), stable candidate URLs, decide auto-advances preserving cursor, batch drawer with sampled peek, contribute checkbox, propagation toast.                                         |
| Findings `/findings`           | **Reshape + second mount**  | Owner view moves to `/d/:x/findings` with query-param triage; detail gains "Draft remediation" (prefilled POST); hand-typed asset-ID form dies. Same component mounts tenant-wide at `/findings` (adds domain column/facet) for tenant `finding.read` holders; excerpts component-gated off. |
| Analyst `/analyst`             | **Reshape lightly**         | Keeps the name (Ask rename rejected as churn without payoff). Moves to `/d/:x/analyst` with scope banner; renders the already-fetched history rail with re-run; answers link entities into scoped asset routes.                                                                              |
| Actions `/actions`             | **Reshape**                 | Moves to `/d/:x/actions`; status-grouped with blocked\_reason, blocked/rejected first; Submit only on own drafts; approve controls never render here; composer only ever opens prefilled.                                                                                                    |
| Governance                     | **Keep as one tabbed page** | Tabs: Program (tenant rollup — degraded client-side aggregation until a rollup endpoint, §6), Taxonomy, Ownership & attestation (oversight table), Policies. Approve verbs move out to `/approvals` tabs.                                                                                    |
| Model releases `/models`       | **Keep**                    | Curation queue (owner contributions land here) + release proposals; release approval moves to `/approvals/releases`.                                                                                                                                                                         |
| Setup `/setup`                 | **Keep route, relabel**     | Nav label "Connections" under Platform group; connectors configure/test, tenant profile, directory as tabs. No content surfaces — platform\_admin holds no evidence capabilities.                                                                                                            |
| Operations `/operations`       | **Keep**                    | Adds read-only mount for auditor; scan job controls per `scan.job.*` capability; real error surfacing.                                                                                                                                                                                       |
| Audit trail `/audit`           | **Keep, upgrade**           | Adds the shared triage shell (filter/sort/pagination, export where `analyst.export`); events link to their objects, which 403 cleanly if unreadable.                                                                                                                                         |
| Asset detail `/assets/:id`     | **Reshape / split**         | Owner variant at `/d/:x/assets/:assetId` (scope preserved on the drill); tenant metadata-only variant stays at `/assets/:id` for approver/auditor; evidence panels are capability-gated components.                                                                                          |
| —                              | **New screens**             | `/approvals` (unified, capability-rendered tabs); `/d/:x/assets` drill list; `/home` multi-domain portfolio.                                                                                                                                                                                 |
| —                              | **Retired**                 | Role-flavored nav groups ("Find & Confirm", "Connect & Govern", "Administer", "Operate"); the approver inbox on Home; the free-text asset-ID action form; unscoped owner asset links.                                                                                                        |

***

## 6. New-endpoint shortlist

Minimal set, in recommended build order. Every one has a shipped degraded mode, so none blocks the redesign.

1. **`POST /api/domains/:id/attest`** (record attestation + posture snapshot). Closes problem 7's loop — attestation dates are already read; the button exists. *Degraded:* side-panel with snapshot + disabled button + "coming" note; pulled per stub-expiry if it slips >2 releases.
2. **`mine=true` (requester filter) on `GET /api/actions`.** Makes "my drafts" true when a domain has two owners — the current client-side domain filter shows colleagues' drafts under your name. *Degraded:* tabs honestly labeled "In my domains", never "Mine".
3. **`GET /api/changes?domainId=&since=`** (item-level changed feed). Turns the last dead number (changed-count) into a link and gives module 6 a real click-through. *Degraded:* text count + source freshness + honestly-labeled "Recently active findings" link.
4. **Coverage/trend series** (e.g. `GET /api/landscape/history?domainId=`). Powers the sparkline and "prove control over time"; point-in-time numbers ship first. *Degraded:* no trend UI at all — never faked.
5. **Seen-state for findings** (per-user last-visit watermark). Enables true "new since your last visit". *Degraded:* "recent (≤7d)" chip explicitly labeled a proxy.

**Verification gates (server work only if the check fails):**

* **Batch decide endpoint behind `review.batch`** — verify before committing the batch-drawer layout (the brief only confirms the chip renders). *Fallback:* drawer applies the decision client-side as N sequential single decides with progress.
* **Server-side SoD on approve** — confirm the server rejects approving own submissions and own-domain actions; the UI badge is a courtesy mirror, not the enforcement. If absent, this is required server work **before** `/approvals` ships.

**Deferred (explicitly not v1):** tenant posture rollup endpoint for Governance→Program — until then the tab aggregates per-domain calls client-side, acceptable at current domain counts, flagged as temporary.

***

## 7. Open decisions for the user

1. **Approvals: one inbox or in-place?** Recommended (and specced above): a single `/approvals` with capability-rendered tabs (actions / taxonomy / releases) — one place the approve verb lives, cleanest for owner+approver and governance combos. The judges split: two favored unification, one flagged the three-object junk-drawer risk as volumes grow. Reversal trigger: if any tab's review UI diverges materially, split that tab back into its home surface.
2. **SoD blast radius for owner+approver:** block approving **own submissions only**, or (recommended, specced) **anything targeting a domain you own**? The proposals split; the stricter read matches "Owner NEVER approves" but can deadlock small orgs where the only approver also owns a domain — whichever you choose must be server-enforced, so this decision gates the SoD verification work.
3. **Launch coupling to server work:** ship the redesign UI-only with all degraded modes (fastest, but the Attest button and "In my domains" labeling run on honesty + stub-expiry), or hold launch for the small pair (attest POST + `mine=true`) that removes the two most trust-sensitive degradations? Judges weighted degradation cost very differently (the feasibility judge tolerated it; another warned stubs "curdle into distrust"). Recommended: UI-only launch with the pair committed for the immediately following release.
