# StellaOps Triage UI Reducer Spec (Pure State + Explicit Commands) ## 0. Purpose Define a deterministic, testable UI state machine for triage UI surfaces: - State transitions are pure functions. - Side effects are emitted as explicit commands. - Enables UI replay for debugging (aligns with StellaOps determinism and replay ethos). Target stack: Angular v17 + TypeScript. ## 1. Core Concepts - **Action:** user/system event (route change, button click, HTTP success). - **State:** all data required to render triage surfaces. - **Command:** side-effect request (HTTP, download, navigation). Reducer signature: ```ts type ReduceResult = { state: TriageState; cmd: Command }; function reduce(state: TriageState, action: Action): ReduceResult; ``` ## 2. State Model ```ts export type Lane = | "ACTIVE" | "BLOCKED" | "NEEDS_EXCEPTION" | "MUTED_REACH" | "MUTED_VEX" | "COMPENSATED"; export type Verdict = "SHIP" | "BLOCK" | "EXCEPTION"; export interface MutedCounts { reach: number; vex: number; compensated: number; } export interface FindingRow { id: string; // caseId == findingId lane: Lane; verdict: Verdict; score: number; reachable: "YES" | "NO" | "UNKNOWN"; vex: "affected" | "not_affected" | "under_investigation" | "unknown"; exploit: "YES" | "NO" | "UNKNOWN"; asset: string; updatedAt: string; // ISO-8601 UTC } export interface CaseHeader { id: string; verdict: Verdict; lane: Lane; score: number; policyId: string; policyVersion: string; inputsHash: string; why: string; // short narrative chips: Array<{ key: string; label: string; value: string; evidenceIds?: string[] }>; } export type EvidenceType = | "SBOM_SLICE" | "VEX_DOC" | "PROVENANCE" | "CALLSTACK_SLICE" | "REACHABILITY_PROOF" | "REPLAY_MANIFEST" | "POLICY" | "SCAN_LOG" | "OTHER"; export interface EvidenceItem { id: string; type: EvidenceType; title: string; issuer?: string; signed: boolean; signedBy?: string; contentHash: string; createdAt: string; previewUrl?: string; rawUrl: string; } export type DecisionKind = "MUTE_REACH" | "MUTE_VEX" | "ACK" | "EXCEPTION"; export interface DecisionItem { id: string; kind: DecisionKind; reasonCode: string; note?: string; ttl?: string; actor: { subject: string; display?: string }; createdAt: string; revokedAt?: string; signatureRef?: string; } export type SnapshotTrigger = | "FEED_UPDATE" | "VEX_UPDATE" | "SBOM_UPDATE" | "RUNTIME_TRACE" | "POLICY_UPDATE" | "DECISION" | "RESCAN"; export interface SnapshotItem { id: string; trigger: SnapshotTrigger; changedAt: string; fromInputsHash: string; toInputsHash: string; summary: string; } export interface SmartDiff { fromInputsHash: string; toInputsHash: string; inputsChanged: Array<{ key: string; before?: string; after?: string; evidenceIds?: string[] }>; outputsChanged: Array<{ key: string; before?: string; after?: string; evidenceIds?: string[] }>; } export interface TriageState { route: { page: "TABLE" | "CASE"; caseId?: string }; filters: { showMuted: boolean; lane?: Lane; search?: string; page: number; pageSize: number; }; table: { loading: boolean; rows: FindingRow[]; mutedCounts?: MutedCounts; error?: string; etag?: string; }; caseView: { loading: boolean; header?: CaseHeader; evidenceLoading: boolean; evidence?: EvidenceItem[]; decisionsLoading: boolean; decisions?: DecisionItem[]; snapshotsLoading: boolean; snapshots?: SnapshotItem[]; diffLoading: boolean; activeDiff?: SmartDiff; error?: string; etag?: string; }; ui: { decisionDrawerOpen: boolean; diffPanelOpen: boolean; toast?: { kind: "success" | "error" | "info"; message: string }; }; } ``` ## 3. Commands ```ts export type Command = | { type: "NONE" } | { type: "HTTP_GET"; url: string; headers?: Record; onSuccess: Action; onError: Action } | { type: "HTTP_POST"; url: string; body: unknown; headers?: Record; onSuccess: Action; onError: Action } | { type: "HTTP_DELETE"; url: string; headers?: Record; onSuccess: Action; onError: Action } | { type: "DOWNLOAD"; url: string } | { type: "NAVIGATE"; route: TriageState["route"] }; ``` ## 4. Actions (Minimum Set) Action unions will evolve, but should include: - routing actions (`ROUTE_TABLE`, `ROUTE_CASE`) - table load and filter actions (`TABLE_LOAD`, `TABLE_LOAD_OK/ERR`, filter updates) - case load and nested resource actions (header/evidence/decisions/snapshots/diff) - decision drawer open/close and decision create/revoke actions - bundle export actions ## 5. Determinism Requirements - Reducer must be pure (no global mutation, time access, randomness). - All derived URLs must be deterministic for a given state. - ETag/If-None-Match should be supported to reduce payload churn and improve sealed-mode behavior. ## 6. Unit Testing Requirements Minimum tests: - Reducer purity: no external effects. - `TABLE_LOAD` produces correct URL for filters. - `ROUTE_CASE` triggers case header load. - `CASE_LOAD_OK` triggers evidence load (and the integration layer triggers other nested loads deterministically). - `DECISION_CREATE_OK` closes drawer and refreshes case header. - `BUNDLE_EXPORT_OK` emits `DOWNLOAD`. Recommended: - Golden state snapshots to ensure backwards compatibility when the state model evolves. --- **Document Version**: 1.0 **Target Platform**: Angular v17 + TypeScript