Architectural Analysis: Exposure as a Persistent Entity
Date: 2026-02-18 Trigger: Founder feedback on W1 implementation Status: Draft for review
Founder's Feedback (Summary)
- Clicking a risk cluster should show auth paths with exposures — not an abridged list
- Auth Path is the durable object — a chain that may have executions, may change over time, but is still one path
- Exposure is a time-bound finding on a path — path created = zero exposure, owner leaves = exposure A, sensitive data added = exposure B
- Exposures group into Risk Clusters — exposure A + exposure B together form a cluster
- Exposures need a start/effective date — temporal tracking
- Desired UX flow: clusters (homepage) → auth paths with exposures → path detail
Current State vs. Desired State
| Concept | Current | Desired |
|---|---|---|
| Auth Path | Ephemeral: computed from entity.execution_paths[] at query time, rewritten every sync | Persistent: durable entity with identity, version history, first_seen/last_seen |
| Exposure | Ephemeral: derived from findings + entities per API request; no persistence | Persistent: time-bound finding on a specific path, with effective_at date |
| Risk Cluster | Ephemeral: computed from compound finding type combinations per request | Can remain computed (derived from persistent exposures) |
| Finding | Entity-bound: one finding per entity per rule, affected_resources[] lists all resources | Path-bound: one exposure per (path, condition) — "owner left this path" not "owner left the entity" |
Core Insight: Two-Layer Model
The founder's mental model separates infrastructure (auth paths) from assessments (exposures):
Layer 1 — Infrastructure (durable)
AuthPath = deterministic route from workload → identity → destination → data domain
Properties: composition_hash, first_seen_at, last_seen_at, status (active/removed)
Changes: path appears (new sync), path disappears (identity loses role), path mutates (new role added)
Layer 2 — Assessment (temporal)
Exposure = condition detected on a specific AuthPath at a point in time
Properties: exposure_type, effective_at, resolved_at, path_id, severity
Examples:
- "Path P1 has no owner" (effective_at = when owner was removed)
- "Path P1 reaches restricted data" (effective_at = when sensitivity was classified)
- "Path P1 has dormant identity" (effective_at = when activity gap exceeded threshold)
This differs from the current model where findings are entity-level ("entity E has orphaned ownership") not path-level ("path P through entity E has orphaned ownership").
Proposed Data Model
AuthPathDoc (new collection: authority_paths)
interface AuthPathDoc {
_id: string; // Deterministic: hash(tenant_id, workload_id, identity_id, resource_id)
tenant_id: string;
composition_hash: string; // Hash of full path composition for change detection
// Path nodes (the 4-node tuple)
workload_id: string; // Workload entity (entry point)
identity_id: string | null; // Identity via RUNS_AS (null = unbound)
destination_id: string; // Target resource
data_domain: string; // Business domain of destination
// Path metadata
sensitivity: string; // restricted, confidential, internal, public
via_roles: string[]; // Role chain
actions: string[]; // Permissions (read, write, delete)
source_system: string; // System where path originates
auth_chain_depth: number; // Cross-system hops
// Temporal
first_seen_at: Date; // When path first appeared
last_seen_at: Date; // When path was last confirmed by a sync
status: "active" | "removed"; // Removed = no longer found in latest sync
removed_at?: Date; // When path disappeared
// Sync tracking
sync_version: number;
created_at: Date;
updated_at: Date;
}
Indexes:
{ tenant_id: 1, workload_id: 1 }— all paths from a workload{ tenant_id: 1, identity_id: 1 }— all paths through an identity{ tenant_id: 1, data_domain: 1, sensitivity: 1 }— paths to sensitive domains{ tenant_id: 1, status: 1, sensitivity: 1 }— active high-sensitivity paths
ExposureDoc (new collection: exposures)
interface ExposureDoc {
_id: string; // Deterministic: hash(tenant_id, path_id, exposure_type)
tenant_id: string;
// Links
path_id: string; // References AuthPathDoc._id
entity_id: string; // Workload entity (denormalized for query speed)
// Exposure definition
exposure_type: ExposureType; // Maps to finding conditions
severity: ExposureSeverity; // critical, high, medium, low
explanation: string; // Deterministic human-readable explanation
// Temporal
effective_at: Date; // When this exposure condition became true
resolved_at?: Date; // When condition was resolved (null = active)
status: "active" | "resolved";
// Evidence
evidence_refs: Record<string, unknown>; // Rule-specific evidence metadata
evidence_completeness: EvidenceCompletenessSection;
// Grouping
cluster_ids: string[]; // Which risk clusters this exposure belongs to
// Sync tracking
detected_at: Date; // When we first detected this exposure
last_evaluated_at: Date; // Last evaluation timestamp
sync_version: number;
}
type ExposureType =
| "orphaned_path" // Path has no active owner
| "dormant_path" // Identity on path has no recent activity
| "sensitive_reach" // Path reaches restricted/confidential data
| "unbound_path" // Path has no deterministic identity binding
| "unproven_path" // Path's workload has no execution evidence
| "scope_expansion" // Path gained new roles/permissions
| "llm_egress" // Path's workload has LLM egress
| "external_egress" // Path's workload has external egress
| "ambiguous_ownership" // Path has only group owners
| "unknown_ownership"; // Path has insufficient ownership metadata
Indexes:
{ tenant_id: 1, path_id: 1 }— all exposures on a path{ tenant_id: 1, entity_id: 1 }— all exposures for a workload{ tenant_id: 1, status: 1, severity: 1 }— active exposures by severity{ tenant_id: 1, cluster_ids: 1 }— exposures by cluster
Risk Clusters (remain computed, or lightly cached)
Clusters can continue to be computed from exposure combinations. With persistent exposures, the computation becomes a simple aggregation query rather than a multi-collection join.
Migration Path from Current Model
Phase A: Persist Auth Paths (lowest risk)
During sync_ingestion, after computing execution_paths[] on entities:
- For each execution path, compute a deterministic
path_id = hash(tenant, workload, identity, resource) - Upsert into
authority_pathscollection - Mark paths not seen in this sync as
status: "removed" - Continue writing
execution_paths[]on entities (backward compat)
Impact: Additive — no existing behavior changes. Auth paths become queryable.
Phase B: Persist Exposures (replaces finding-to-entity binding)
During evaluate_findings, for each entity:
- Resolve entity's auth paths from
authority_pathscollection - Evaluate rules per-path (not per-entity)
- Produce ExposureDocs with
path_idreference - Set
effective_atto earliest point when condition became true (from entity version history) - Continue writing FindingDocs (backward compat) — but findings now reference exposures
Impact: Evaluator rules need refactoring from entity-level to path-level granularity.
Phase C: Update UX Flow
- Dashboard risk clusters link to cluster detail (showing auth paths)
- Auth path list page — filterable by domain, sensitivity, status
- Auth path detail page — shows all exposures on this path + timeline
- Exposure detail — shows the specific condition, evidence, remediation
Phase D: Deprecate Computed Exposures API
Once persistent exposures are reliable, remove the computed /api/v1/exposures endpoint and replace with queries against the exposures collection.
Key Design Decisions Needed
1. Path Granularity
Option A: One path per (workload, identity, resource) — current ExecutionPath grain
- Pro: Direct mapping from current data
- Con: Multiple paths to same domain show as separate
Option B: One path per (workload, identity, resource, domain) — adds domain rollup
- Pro: Matches the 4-node visual (workload → identity → destination → domain)
- Con: Same resource in multiple domains creates multiple paths
Recommendation: Option A. Keep 1:1 with current ExecutionPath. Domain is metadata on the path.
2. Exposure vs. Finding Relationship
Option A: Exposures replace findings entirely
- Pro: Single model, cleaner
- Con: Breaking change; evidence packs reference findings
Option B: Exposures extend findings — FindingDoc gains path_id and effective_at
- Pro: Backward compatible
- Con: Two overlapping concepts
Option C: Exposures are new, findings remain for backward compat, new UX uses exposures
- Pro: Non-breaking migration
- Con: Dual model during transition
Recommendation: Option C for implementation, converge to Option A over time.
3. effective_at Computation
How do we determine when an exposure "started"?
Option A: Use detected_at (when our evaluator first found it)
- Pro: Simple, always available
- Con: Not the actual start — may have existed before we first scanned
Option B: Use entity version history to find when condition became true
- Pro: More accurate temporal signal
- Con: Complex; requires replaying history; may not have full history
Option C: Use first_seen_at of the auth path as baseline, then entity events for condition changes
- Pro: Combines path appearance with condition detection
- Con: Moderate complexity
Recommendation: Option A initially (detected_at), evolve to Option C. Label it as detected_at not started_at to be honest about what we know.
4. Chains vs. Auth Paths
The current execution_chains collection overlaps with the proposed authority_paths.
Recommendation: Authority paths subsume execution chains. Deprecate execution_chains collection and the Chains nav item. The "Chains" page becomes the "Auth Paths" page.
Impact on Existing Code
Evaluator Rules (13 rules)
Current rules evaluate at entity level. With path-level exposures:
- Some rules naturally decompose:
reachable_sensitive_domain→ one exposure per sensitive path - Some rules are entity-level by nature:
unknown_identity_binding,unproven_execution→ apply to ALL paths of that workload - Some rules need both:
dormant_authorityis entity-level (identity is dormant) but maps to all paths through that identity
Approach: Rules continue to evaluate entities, but produce exposure-per-path output. A thin adapter maps entity-level conditions to all affected paths.
Evidence Packs
Evidence packs are currently finding-scoped. With exposures:
- Option: Evidence packs per-exposure (more granular)
- Option: Evidence packs per-path (captures all exposures on a path)
- Option: Keep per-finding, link to exposures
Recommendation: Keep evidence packs per finding for now. Add exposure references to evidence pack content.
UI Pages
| Current Page | Becomes |
|---|---|
| Dashboard (Overview) | Exposure Discovery — posture + clusters (no change to structure) |
| Exposures list | Auth Paths list — each row is an auth path with exposure count |
| Exposure detail | Auth Path detail — path visualization + exposure timeline |
| Chains | Deprecated (merged into Auth Paths) |
| Findings | Legacy — kept for backward compat, links to exposures |
Estimated Scope
| Phase | Work | Size |
|---|---|---|
| A: Auth Path persistence | New collection, path materializer update, new API | Medium (2-3 days) |
| B: Exposure persistence | New collection, evaluator refactor, new API | Large (4-5 days) |
| C: UX redesign | Auth path list/detail pages, cluster→path flow | Medium (3-4 days) |
| D: Cleanup | Deprecate chains, computed exposures, legacy findings | Small (1-2 days) |
Design Questions — With Real Examples and Recommendations
1. Path Identity Stability
Question: When a workload's RUNS_AS identity changes, is that a new auth path or a mutation of the existing one?
Real example from our data:
Right now, AzureGraphRouter (a ServiceNow business rule) has this auth path:
AzureGraphRouter ──RUNS_AS──▶ SP (61c1fe...) ──▶ Microsoft Graph (high, identity_platform)
Imagine next month, the admin rotates this to a new service principal. The workload, the destination, and the data domain are all the same — only the identity in the middle changed. Is this still "the same path" with a mutation, or a completely new path?
Why it matters: If it's a mutation, the path keeps its history — all prior exposures (orphaned_ownership, dormant_authority) stay attached to the same path object. If it's a new path, the old one gets marked "removed" and we lose the continuity of "this workload has had exposure problems for 6 months."
Recommendation: Same path (mutation). The auth path's identity comes from its endpoints — the workload and the destination resource. The identity in the middle is how the workload reaches the destination, not what it reaches. Think of it like a highway: if the bridge in the middle gets replaced, it's still the same route from A to B. The path ID should be hash(tenant, workload_id, resource_id), NOT hash(tenant, workload_id, identity_id, resource_id). The identity becomes a versioned property of the path, and swapping it creates a change event on the path timeline ("identity rotated from SP-old to SP-new"), which is exactly the kind of drift we want to track.
2. Exposure Lifecycle
Question: When should an exposure auto-resolve?
Real example from our data:
AzureGraphRouterNoOwner currently has 3 active exposures:
orphaned_ownership(critical) — no OWNED_BY relationshipunproven_execution(high) — has path to Microsoft Graph but no sign-in evidenceexternal_egress(medium) — outbound data flow to external endpoint
Scenario: An admin assigns maint as the owner next week. Our next sync picks up the new OWNED_BY relationship. What happens?
Current behavior: The evaluator re-runs, finds the entity now has an owner, and the orphaned_ownership finding gets auto-resolved (status changes to resolved_by_reevaluation). But the other two exposures remain active because they're independent conditions.
Recommendation: Exposures auto-resolve when the evaluator no longer produces them. This is already how findings work — the evaluator is re-entrant. Each sync, every rule re-evaluates every entity. If a rule no longer fires, the finding resolves. Exposures should work identically:
| Event | Exposure effect |
|---|---|
| Owner assigned to workload | orphaned_path exposure → resolved, resolved_at = sync timestamp |
| Execution evidence appears | unproven_path exposure → resolved |
| Identity loses role to resource | Entire auth path → status "removed", all exposures → resolved |
| Resource reclassified from restricted → internal | sensitive_reach exposure → resolved |
The key is that effective_at records when the condition started and resolved_at records when it ended. This gives us duration: "this path was orphaned for 47 days."
3. Cluster Persistence
Question: Should risk clusters be persistent (tracked over time) or remain computed?
Real example from our data:
Current cluster: "unbound-sensitive" = workloads with unknown_identity_binding + reachable_sensitive_domain. Today this might match 2 workloads. Next month after remediation it drops to 0. The month after, a new workload appears and it goes back to 1.
If clusters are persistent, we'd have a RiskClusterDoc with its own timeline: "this cluster had 2 members on Feb 18, 0 on Mar 15, 1 on Apr 2." If computed, we only know the current count.
Recommendation: Keep clusters computed. Clusters are compound conditions — they're essentially saved queries, not entities. Persisting them adds complexity without much value because:
- Clusters don't have identity — they're not "a thing that exists," they're "a filter that matches." You don't remediate a cluster, you remediate its member exposures.
- History is derivable — if exposures are persistent with
effective_at/resolved_at, you can reconstruct "how many exposures matched this cluster condition on date X" from the exposure history. - Cluster definitions change — we may add new cluster types, change compound conditions, adjust severities. Persistent cluster docs would need migration.
The dashboard can show delta counts ("3 fewer than last scan") by comparing current exposure matches against the previous sync's results — no persistence needed.
4. Path Removal and Historical Visibility
Question: When a path disappears (identity loses role, workload deleted), should historical exposures remain visible?
Real example from our data:
Auto-route identity tickets via Entra currently has a path to Microsoft Graph. Suppose next month, the service principal's Directory.ReadWrite.All permission is revoked. The path disappears — the workload can no longer reach Microsoft Graph.
Should we:
- (A) Delete the path and its exposures — "clean slate"
- (B) Mark the path as
status: "removed"and keep all historical exposures — "audit trail"
Recommendation: Option B — soft-delete with full history. This aligns with all three design constraints:
- Evidence-grade: Immutable, timestamped records. An auditor asking "what could this workload reach 3 months ago?" should get an answer.
- Temporal: The path existed, exposures were detected on it, and then it was removed. That's a lifecycle worth tracking. "This workload had unauthorized access to Microsoft Graph for 47 days before remediation" is a finding that compliance teams need.
- Deterministic: We're not guessing — we observed the path, we observed its removal.
The UX handles this naturally:
- Active paths show by default (current risk surface)
- Removed paths visible via a "Show history" toggle or timeline view
- Removed paths appear grayed out with a
removed_attimestamp
The exposure on a removed path also gets resolved_at set (the path no longer exists, so the exposure condition is no longer true). This means duration = resolved_at - effective_at gives the exact exposure window.
5. Terminology
Question: "Auth Path" vs "Authority Path" vs "Execution Path" — which user-facing term?
Current usage in the codebase:
ExecutionPath— the TypeScript interface indomain/entities/types.ts(describes resource reachability)- "Authority path" — used in UI component
AuthorityPathDiagram.tsxand exposure detail API - "Execution chain" — the
ExecutionChainDoctype (workload-centric chain) - "Auth chain" — used in cross-system-paths API (
auth_chainsresponse field) - "Blast radius" — used in the paths API (
/blast-radiusendpoint)
The problem: Five different terms for overlapping concepts.
Recommendation: "Authority Path" as the single user-facing term. Reasoning:
| Term | Problem |
|---|---|
| "Execution Path" | Implies the path executed — but many paths are dormant (never used). The path represents potential, not activity. |
| "Auth Path" | Too abbreviated, sounds like authentication only. It's about authorization (what you're allowed to do), not authentication (who you are). |
| "Execution Chain" | Overloaded — "chain" suggests sequential steps, but the path is really about reachability, not a call chain. |
| "Blast Radius" | Describes the effect of a compromise, not the structure of the path. Good for a feature name, bad for an entity name. |
| "Authority Path" | Captures the essence: the path through which authority (permissions) flows from a workload to a resource. It's about standing authority — what the workload is authorized to do, whether or not it actually does it. |
In the UI: "Authority Paths" as the nav item, "authority path" in explanatory text. Internally, the type can be AuthorityPathDoc and the collection authority_paths.
Cross-Analysis: This Document vs. Codex Review (Doc 13)
Both analyses converge on the same fundamental insight. Here's where they agree, disagree, and how we compose the best of both.
Full Agreement (take directly)
| Topic | Both say | Confidence |
|---|---|---|
| Authority Path is the durable object | Yes — primary investigation anchor, not exposure | High |
| Exposure is persistent and time-bound | Yes — with effective_from/effective_at and resolved_at | High |
| Risk Clusters remain computed | Yes — derived from active exposures, not persisted | High |
| Soft-delete for path removal | Yes — status: "removed" + removed_at, history queryable | High |
| Findings coexist during transition | Yes — additive model, findings stay for backward compat | High |
effective_from = detected_at initially | Yes — honest labeling, upgrade to reconstructed dates later | High |
Exposure status includes acknowledged/false_positive | Doc 13 adds these; our doc only has active/resolved | Take from Doc 13 |
| UX flow: cluster → authority paths → exposure timeline | Yes — identical recommendation | High |
| Terminology: "Authority Path" | Yes — both recommend this | High |
| W1.1 framing | Doc 13 recommends this explicitly; our doc implies it | Take from Doc 13 |
Disagreement 1: Path Granularity (identity in the key)
Our doc: Path ID = hash(tenant, workload_id, resource_id). Identity is a versioned property, not part of the key. Rationale: "same highway, bridge replaced."
Doc 13: Path ID includes identity_id: grain is (workload_id, identity_id, destination_id). Rationale: identity is structurally part of the path.
Resolution: Our doc is right, but needs nuance.
The founder said: "auth path is the durable object... it may change over time, but it's still one path." If identity rotation creates a new path, exposures fragment across path objects. A CISO asking "how long has this workload had problems reaching Microsoft Graph?" gets two separate 3-week entries instead of one 6-week entry.
However, there's an edge case Doc 13 implicitly handles: what if the same workload reaches the same resource through two different identities simultaneously? Example:
AzureGraphRouter ──RUNS_AS──▶ SP-A ──▶ Microsoft Graph
AzureGraphRouter ──RUNS_AS──▶ SP-B ──▶ Microsoft Graph
With our key hash(workload, resource), these collapse into one path. With Doc 13's key hash(workload, identity, resource), they're two distinct paths — which is correct because they represent two independent authorization channels, either of which could have independent exposures (SP-A might be dormant while SP-B is active).
Composed answer: Use hash(tenant, workload_id, resource_id) as the path identity for continuity tracking, but include identity_id as a required field with versioning. When multiple identities reach the same resource simultaneously, create separate path records with the same logical path lineage but different identity bindings. Expose this as "2 routes to Microsoft Graph" in the UI, both under the same path lineage. This gives us the best of both:
- Identity rotation = mutation event on path timeline (continuity preserved)
- Multiple simultaneous identities = separate authorization channels (precision preserved)
Practically: _id = hash(tenant, workload_id, identity_id, resource_id) for storage uniqueness (Doc 13), but add a path_lineage_id = hash(tenant, workload_id, resource_id) for grouping and continuity tracking (our doc).
Disagreement 2: Reuse execution_chains or New Collection
Our doc: New authority_paths collection. Deprecate execution_chains.
Doc 13: "Use existing execution_chains foundation, but product-surface as Authority Paths." (Section 4.1)
Resolution: Our doc is right. New collection.
The current ExecutionChainDoc has a fundamentally different grain — it's workload-centric (one chain per workload, with entity_refs[] listing all entities in the chain). Authority paths are path-centric (one record per workload-to-resource route). These are structurally incompatible:
- Chain:
AzureGraphRouter → [SP, credential, connection, resource]= 1 record - Authority Path:
AzureGraphRouter → SP → Microsoft Graph= 1 record per reachable resource
A workload with 5 reachable resources has 1 chain but 5 authority paths. Trying to retrofit execution_chains would require either denormalizing it (breaking the chain concept) or adding sub-records (making it a hybrid). Cleaner to create authority_paths and deprecate execution_chains once the migration is complete.
Disagreement 3: current_state Snapshot on Path
Doc 13 adds (our doc doesn't have):
current_state: {
execution_30d: number,
ownership_status: string,
egress_category: string,
max_sensitivity: string
}
Resolution: Take from Doc 13. This is a valuable denormalization. The cluster → authority-path drill-down needs to show execution magnitude and ownership status in the row without joining to entities or exposures. A current_state snapshot updated each sync gives O(1) reads for the list view.
Disagreement 4: Exposure Status Values
Our doc: status: "active" | "resolved"
Doc 13: status: "active" | "resolved" | "acknowledged" | "false_positive"
Resolution: Take from Doc 13. Acknowledged and false_positive are essential for the CISO workflow. A security team marks an exposure as acknowledged ("we know about this, working on it") or false_positive ("this is intentional/expected"). These are the same status transitions currently on FindingDoc — exposures should inherit them.
Disagreement 5: cluster_ids on Exposure
Our doc: ExposureDoc has cluster_ids: string[] — precomputed cluster membership.
Doc 13: No cluster_ids on exposure. Clusters are computed at query time from exposure types.
Resolution: Doc 13 is right. Drop cluster_ids. Cluster membership is derivable from exposure_type + compound conditions. Storing cluster_ids on exposures creates a maintenance burden — every time cluster definitions change, all exposures need updating. Since clusters are computed, the computation should happen at query time, not at write time.
Disagreement 6: resolution_reason Field
Doc 13 adds: resolution_reason: string | null on ExposureDoc.
Resolution: Take from Doc 13. When an exposure resolves, knowing why is critical: "owner assigned", "path removed", "evidence appeared", "manually marked false positive". This is a simple nullable string that adds significant auditability.
Composed Unified Model
Taking the best from both analyses:
AuthorityPathDoc (new collection: authority_paths)
interface AuthorityPathDoc {
_id: string; // Deterministic: hash(tenant, workload_id, identity_id, resource_id)
tenant_id: string;
// Path lineage (for continuity across identity rotations)
path_lineage_id: string; // hash(tenant, workload_id, resource_id) — groups routes to same destination
// Path nodes
workload_id: string;
identity_id: string | null; // null = unbound
destination_id: string; // Target resource entity
data_domain: string; // Business domain of destination
// Path metadata
sensitivity: string; // restricted, confidential, internal, public
via_roles: string[];
actions: string[];
source_system: string;
auth_chain_depth: number;
// Denormalized state snapshot (updated each sync for fast list queries)
current_state: {
execution_30d: number; // Execution evidence count in last 30 days
ownership_status: string; // valid, orphaned, ambiguous, unknown
egress_category: string; // external, llm, internal, none
active_exposure_count: number; // Count of active exposures on this path
max_exposure_severity: string | null; // Highest active exposure severity
};
// Composition fingerprint (for mutation detection)
composition_hash: string; // hash(workload_id, identity_id, resource_id, via_roles, actions)
// Temporal
first_seen_at: Date;
last_seen_at: Date;
status: "active" | "removed";
removed_at?: Date;
// Sync tracking
sync_version: number;
created_at: Date;
updated_at: Date;
}
ExposureDoc (new collection: exposures)
interface ExposureDoc {
_id: string; // Deterministic: hash(tenant, path_id, exposure_type, active_interval_key)
tenant_id: string;
// Links
path_id: string; // References AuthorityPathDoc._id
entity_id: string; // Workload entity (denormalized)
// Exposure definition
exposure_type: ExposureType;
severity: ExposureSeverity;
explanation: string;
// Temporal
effective_from: Date; // When condition became true (initially = detected_at)
resolved_at?: Date; // When condition ended (null = active)
detected_at: Date; // When our system first observed it
last_evaluated_at: Date;
// Status (matches finding workflow)
status: "active" | "resolved" | "acknowledged" | "false_positive";
resolution_reason?: string; // Why it resolved: "owner_assigned", "path_removed", "evidence_appeared", "manual"
// Evidence
evidence_refs: Record<string, unknown>;
evidence_completeness: EvidenceCompletenessSection;
// Sync tracking
sync_version: number;
}
Implementation Phases (Composed)
| Phase | Scope | Days |
|---|---|---|
| A: Decision lock | ADR, terminology, approve composed model | 1 |
| B: Storage + lifecycle | authority_paths collection, exposures collection, path materialization during sync, exposure upsert/open/resolve lifecycle, fingerprint stability tests | 3 |
| C: API transition | GET /authority-paths, GET /authority-paths/:id, GET /authority-paths/:id/exposures, GET /risk-clusters/:key/authority-paths, preserve old route aliases | 2-3 |
| D: UI transition | Cluster → authority path rows, path detail + exposure timeline, relabel Chains → Authority Paths | 2-3 |
| E: Validation + migration | Backfill exposures from active findings, validate open/close across 2 syncs, founder walkthrough | 1-2 |
Acceptance Criteria (from Doc 13, fully endorsed)
- Clicking a Risk Cluster shows Authority Paths as primary rows
- Each row shows execution magnitude and ownership status before detail click
- Path detail displays exposure timeline with
effective_fromand status changes - Exposure can be opened/resolved on the same path across time without losing history
- Cluster aggregation is computed from active exposures and current path state
- Removed paths and resolved exposures remain historically queryable