Plan: Authority-First UX Refactor (2026-02-22 Feedback)
Context
The founder over-rotated on execution telemetry metrics. The CTO pushed back: "We collect CONFIGURATION (authority paths) first. Execution proof is best-effort. We can't guarantee execution counts like Datadog can." The founder agreed and updated Stitch designs. Three new UX feedback docs reflect this corrected direction. This plan implements those changes.
Part 1: Critical Review
Alignment Assessment
All three docs align with the "authority-first, execution-secondary" correction:
| Doc | Key Signal | Verdict |
|---|---|---|
| Overview Page | Hero KPI = "AUTONOMOUS AUTHORITY PATHS" (config discovery), not "Total Executions" | Correct pivot |
| Authority Path Dashboard | Remove 30D Trend column, rename "30D Runs" → "Observed Executions (30d)" — execution is secondary, labeled as "observed" (not guaranteed) | Correct |
| Authority Path Details | Graph (authority chain) becomes first card; RuntimeActivity block removed; execution demoted to 2 compact lines in risk strip | Correct |
Contradictions Between Documents
None found. Consistent direction: remove all deltas/trends, de-emphasize execution counts, promote authority paths as primary.
One naming consistency note: sidebar should become "Risk Clusters" (Doc 1), Overview section becomes "Top Risk Clusters" (Doc 1), page title stays "Risk Clusters" — all consistent.
Backend Data Gaps
Gap 1: Overview Card B — "EXECUTED PATHS WITH INVALID OWNERSHIP"
Current backend has ownership_invalid_count (all invalid paths) and total_executions_30d (all executions). Neither gives "count of paths where execution_30d > 0 AND ownership is invalid."
- Fix: Add counter in existing
activePathsloop inposture-service.ts(line 64). - Semantic precision: "Invalid ownership" means orphaned + ambiguous + connector-vocabulary "invalid" — but NOT "unknown". Unknown means insufficient metadata, which is a data gap, not a governance failure. The UX brief's Card B label ("INVALID OWNERSHIP") implies a definitive status, so
unknownshould be excluded. Filter:ownership_status IN ('orphaned', 'ambiguous', 'invalid'). - Truncation risk: The
activePathsloop is fed by a capped query (PATHS_CAP = 5000, posture-service.ts line 40). If the tenant exceeds 5000 active paths, the counter undercounts silently. Mitigation: whentruncated === true, append"~"prefix to the count in the UI (e.g., "~38") and addtruncatedtoPathPostureSummarytype so the UI can surface it. This is already flagged byconsole.warnon line 58 — just need to propagate the signal to the API response.
Gap 2: "Export Report" button No export infra exists. Stub as disabled button with "Coming soon" tooltip.
Gap 3: Detail page ownership decomposition labels
The feedback wants "Automation owner" + "Runtime identity" rows. Current code already has "Automation / Agent" + "Service Principal" rows reading owner_name from entity properties. This is a label-only rename — data is already there.
Delta Feature Tension — Critical Decision
The problem: We just implemented a full delta system (PostureSnapshotDoc, prior_execution_30d, DeltaBadge, DeltaTrendCell). Now all 3 docs say "remove all deltas."
Decision: KEEP backend infrastructure, REMOVE from UI rendering.
Rationale:
- Backend delta infra (snapshots, prior counts) is cheap to maintain, architecturally clean, and may serve future needs (admin views, PDF reports, API consumers)
- UI component files (
DeltaBadge.tsx,delta.ts) are small and well-tested — keep files, remove imports from these 3 pages - API response shapes stay additive (delta fields remain, UI ignores them)
- Seed script posture_snapshots remain for dev/testing
Part 2: Implementation Plan
Stream A: Overview Page
Files: ui/src/pages/OverviewPage.tsx, ui/src/components/PathRiskClusterCard.tsx, ui/src/components/Layout.tsx, src/services/posture-service.ts, ui/src/api/api-types.ts
A1. Backend: add executed_invalid_ownership_count
src/services/posture-service.ts — inside existing activePaths loop (line 64):
const INVALID_STATUSES = new Set(["orphaned", "ambiguous", "invalid"]);
let executed_invalid_ownership_count = 0;
for (const path of activePaths) {
// ...existing code...
if (path.current_state.execution_30d > 0 && INVALID_STATUSES.has(path.current_state.ownership_status)) {
executed_invalid_ownership_count++;
}
}
Excludes unknown — "unknown" is a data gap, not an ownership violation. Only orphaned/ambiguous/invalid (connector vocab) count as "invalid ownership" per the UX brief's intent.
Add executed_invalid_ownership_count and truncated: boolean to return object and PostureSummaryResult type.
Add both to PathPostureSummary in ui/src/api/api-types.ts. UI renders "~{N}" when truncated.
A2. OverviewPage.tsx
| Change | Before | After |
|---|---|---|
| Title (line 73) | "Autonomous Authority Posture" | "Autonomous Authority Surface" |
| Subtitle | None | "Deterministic mapping of standing autonomous authority derived from observed execution." (muted text below title) |
| Section header (line 86) | "Observed Autonomous Execution (30d)" | Remove entirely |
| KPI Card 1 label | "Total Executions" | "AUTONOMOUS AUTHORITY PATHS" |
| KPI Card 1 value | totalExecutions | posture.active_paths |
| KPI Card 1 subtext | DeltaBadge + "vs prior 30d" | "Deterministically mapped" (muted) |
| KPI Card 2 label | "Executed Autonomous Paths" | "EXECUTED PATHS WITH INVALID OWNERSHIP" |
| KPI Card 2 value | totalPaths | posture.executed_invalid_ownership_count |
| KPI Card 2 subtext | DeltaBadge + "vs prior 30d" | "Observed runtime activity (30d)" (muted) |
| Section heading (line 123) | "Priority Exposures" | "Top Risk Clusters" |
| Section right side | "View all" link only | "Export Report" button (disabled stub) + "View all" link |
| Cluster card props | totalExecutions={totalExecutions} totalPaths={totalPaths} | Remove both props |
Remove imports: DeltaBadge. Remove totalExecutions/totalPaths variables.
A3. PathRiskClusterCard.tsx — complete restructure
Before:
Title
{N} Executions + DeltaBadge
X% of total autonomous executions
{Y} Paths (Z% of total paths)
Oldest finding: Nd | +N New Findings
After:
Title
{Y} Paths ← primary metric, large
{X} Observed runtime executions (30d) ← secondary, smaller muted
+N New ← green pill, bottom-right
- Remove props:
totalExecutions,totalPaths - Remove imports:
DeltaBadge,computeDeltaPct - Remove: runtimeSharePct/pathSharePct calculations, "% of total" lines, "Oldest finding" row
- Primary:
cluster.path_countPaths (text-3xl font-bold) - Secondary:
cluster.total_execution_30dObserved runtime executions (30d) (text-sm text-gray-500) - Pill: "+N New" (green, if
new_findings_30d > 0) or "0 New" (gray muted)
A4. Sidebar rename
ui/src/components/Layout.tsx line 24: "Clusters" → "Risk Clusters"
A5. ClustersListPage.tsx
Remove totalExecutions/totalPaths props from PathRiskClusterCard usage. Remove usePostureSummary hook if no longer needed on this page.
Stream B: Authority Path Dashboard (table)
Files: ui/src/pages/AuthorityPathsListPage.tsx, ui/src/components/badges.tsx
B1. Remove "30D Trend" column
- Remove column header
<th>for "30d Trend" - Remove
<DeltaTrendCell>cell in row - Remove
DeltaTrendCellimport - Update
colSpanin expanded row (11 → 10)
B2. Rename "30d Runs" → "Observed Executions (30d)"
Column header text change only.
B3. Fix OwnershipBadge — make all "Invalid" statuses red
ui/src/components/badges.tsx — update the colors map:
Before:
orphaned: "bg-red-100 text-red-800" → "Invalid"
ambiguous: "bg-yellow-100 text-yellow-800" → "Invalid"
unknown: "bg-gray-100 text-gray-500" → "Unknown"
After:
orphaned: "bg-red-100 text-red-800" → "Invalid" (unchanged)
ambiguous: "bg-red-100 text-red-800" → "Invalid" (yellow → red)
invalid: "bg-red-100 text-red-800" → "Invalid" (NEW — connector vocab)
unknown: "bg-gray-100 text-gray-500" → "Unknown" (unchanged)
The backend uses "invalid" as a raw status from connector vocabulary (posture-service.ts line 79 queries for it explicitly). Without a mapping, it falls through to the default gray — making an actual invalid status look neutral. Add it to both colors and labels maps.
Stream C: Authority Path Detail Page
Files: ui/src/pages/AuthorityPathDetailPage.tsx
C1. Move graph to position 3 (after header, before risk strip)
Reorder JSX: move the Execution Path <div> block (currently lines 669-677) to right after <PathHeader>.
Add small label above graph: "Execution-derived authority path" (muted, small text).
New section order:
- Breadcrumb
- PathHeader
- Execution Path (graph) ← moved up
- Active Risk Conditions strip ← merged component
- Ownership ← simplified (side-by-side with Authority State per mockup)
- Authority State (AutonomousExecutionModel) ← VISIBLE (secondary to Ownership, not collapsed)
- Deterministic Identity Linkage ← collapsed by default
- Automation Metadata ← collapsed by default (rename from "Autonomous Execution Model" details if separate)
- Audit Metadata ← already collapsed
Note: The UX brief (line 111-115) explicitly says "Keep Authority state card... ensure it is secondary to Ownership... No extra telemetry language inside it." It does NOT say collapse it. Only "deeper sections" below Ownership + Authority State get collapsed (line 118): "Deterministic linkage proof, Automation metadata, Audit metadata, Any identity linkage details."
C2. Remove RuntimeActivityBar, merge runtime data into RiskConditionsStrip
Delete RuntimeActivityBar function entirely (lines 156-215).
Modify RiskConditionsStrip to become "Active risk conditions" with a split layout:
┌─────────────────────────────────────────────────────────────────┐
│ ACTIVE RISK CONDITIONS │
│ │
│ [Invalid owner ·Since 12d] [Sensitive: fin ·Since 12d] ... │ ← left: finding tiles
│ │
│ Last execution: 3d ago │ ← right: compact runtime
│ Observed executions (30d): 47 │
└─────────────────────────────────────────────────────────────────┘
Changes to RiskConditionsStrip:
- Add
detail: AuthorityPathDetailprop alongsidepathId - Rename heading: "Risk Conditions" → "Active risk conditions"
- Wrap in a single card container (
rounded-lg border bg-white p-5) - Inside: flex row — left side is finding tiles, right side is compact execution stats from
detail.current_state(last_execution_at + execution_30d) - NO delta, NO "vs prior 30d"
- When no active findings, still show the right-side runtime stats (don't return null)
- CRITICAL: Remove the
orphaned_ownershipexclusion filter (currently line 311:f.finding_type !== "orphaned_ownership"). The UX brief explicitly requires "Invalid owner" as a risk condition tile (ux-feedback line 77). Show ALL active findings including orphaned_ownership.
C2a. Risk tile label normalization
Current code renders finding.finding_type.replace(/_/g, " ") (line 250), producing "orphaned ownership", "reachable sensitive domain", etc. The UX brief requires specific labels: "Invalid owner", "Sensitive: fin", "LLM egress".
Add a label map:
const RISK_TILE_LABELS: Record<string, string> = {
orphaned_ownership: "Invalid owner",
reachable_sensitive_domain: "Sensitive: fin",
llm_egress: "LLM egress",
external_egress: "External egress",
dormant_authority: "Dormant",
scope_drift: "Scope drift",
unknown_identity_binding: "Unbound identity",
ownership_ambiguous: "Ambiguous owner",
};
Fallback: finding_type.replace(/_/g, " ") for unmapped types.
C3. Simplify Ownership to 2 rows
Remove the "Path Ownership" aggregate first row (lines 385-399). Keep only:
| Row | Label (before) | Label (after) | Value |
|---|---|---|---|
| 1 | "Automation / Agent" + "Business owner" | "Automation owner" | ownerName ?? "Not assigned" |
| 2 | "Service Principal" + "Runtime identity" | "Runtime identity" | identityOwner ?? "Not assigned" (styled invalid if missing) |
Remove: "Aggregate status across all objects in this path" text, top-level badge.
For "Not assigned" on runtime identity when ownership is invalid: add text-red-600 styling + optional reason like "Service principal owner departed" if available from entity properties.
C4. Collapse deep sections by default (NOT Authority State)
Per UX brief (line 118): collapse "Deterministic linkage proof, Automation metadata, Audit metadata, Any identity linkage details" by default. Authority State stays visible.
Sections to collapse:
IdentityLinkageSection— collapsed by default (currently always open)AuditMetadata— already collapsed by default (no change)
AutonomousExecutionModel (Authority State) stays always visible — it is secondary to Ownership but not hidden.
Collapsible pattern — same as existing AuditMetadata (line 539):
const [open, setOpen] = useState(false);
return (
<div className="rounded-lg border border-gray-200 bg-white">
<button onClick={() => setOpen(!open)} className="flex w-full items-center gap-2 px-5 py-3 text-left ...">
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
Section Title
</button>
{open && <div className="border-t border-gray-100 px-5 py-4">...content...</div>}
</div>
);
Part 3: Files Changed Summary
| File | Stream | Change |
|---|---|---|
src/services/posture-service.ts | A1 | Add executed_invalid_ownership_count in existing loop |
ui/src/api/api-types.ts | A1 | Add field to PathPostureSummary |
ui/src/pages/OverviewPage.tsx | A2 | Title, subtitle, KPI cards, section heading, remove deltas |
ui/src/components/PathRiskClusterCard.tsx | A3 | Restructure: paths primary, executions secondary, remove deltas |
ui/src/components/Layout.tsx | A4 | Sidebar: "Clusters" → "Risk Clusters" |
ui/src/pages/ClustersListPage.tsx | A5 | Remove posture props from card |
ui/src/pages/AuthorityPathsListPage.tsx | B1-B2 | Remove 30D Trend column, rename 30d Runs |
ui/src/components/badges.tsx | B3 | OwnershipBadge ambiguous: yellow → red |
ui/src/pages/AuthorityPathDetailPage.tsx | C1-C4 | Reorder graph, remove RuntimeActivityBar, merge risk strip (include orphaned_ownership + label map), simplify ownership, Authority State stays visible, collapse IdentityLinkage |
test/services/posture-service.test.ts | A1 | Add tests for executed_invalid_ownership_count semantics + truncation |
NOT changed (delta infra kept):
src/domain/posture/types.ts,src/storage/mongo/adapters/posture-snapshot-adapter.tsui/src/components/DeltaBadge.tsx,ui/src/utils/delta.tsscripts/seed-demo-w1.ts- All backend delta computation in posture/risk-cluster services
Part 4: Verification
Build & Type Safety
cd sv0-platform && npm run typecheck # zero errors
cd ui && npm run build # production build succeeds
npm test # all unit tests pass
API Contract Verification
# Seed demo data and check the new field
npx tsx scripts/seed-demo-w1.ts --reset
curl -s http://localhost:3000/api/v1/posture/summary -H "X-Tenant-Id: demo-w1" | jq '.data.executed_invalid_ownership_count, .data.truncated'
# Should return a number (not null) and false
Posture Service Unit Test Updates
Add test cases to test/services/posture-service.test.ts:
executed_invalid_ownership_countexcludes paths withownership_status: "unknown"and paths withexecution_30d: 0executed_invalid_ownership_countincludes paths withownership_status: "orphaned"ANDexecution_30d > 0truncatedistruewhen path count hits PATHS_CAP
Visual Verification (with seed-demo-w1 data)
- Overview: title = "Autonomous Authority Surface" with subtitle; Card A = path count + "Deterministically mapped"; Card B = executed invalid ownership count + "Observed runtime activity (30d)"; NO deltas anywhere; "Top Risk Clusters" heading; cluster cards show "{N} Paths" primary
- Authority Paths table: 10 columns (not 11); "Observed Executions (30d)" header; no 30D Trend column; orphaned AND ambiguous AND raw "invalid" all render as red "Invalid" badge
- Authority Path Detail: graph is first card after header; no RuntimeActivityBar; "Active risk conditions" strip shows "Invalid owner" tile (NOT filtered out) + other tiles with human labels ("Sensitive: fin", "LLM egress"); compact execution summary on right; ownership = 2 rows only; Authority State visible (not collapsed); IdentityLinkage collapsed by default
- Sidebar: "Risk Clusters" (not "Clusters")
Regression
- ClustersListPage still renders without posture data
- Cluster drill-down (click card → filtered authority paths) still works
- Expanded row in table uses correct colSpan (10)
- Graph renders correctly in new position
OwnershipBadgewith raw"invalid"status (from connector vocab) renders red "Invalid", not gray fallback