Console feature contracts
Verified against commit
942a18727c(2026-09-26, DOC-2 of SPRINT_20260926_001). Re-verify by reading the evidence paths listed in the Evidence section.
The canonical Console architecture (mission, technology stack, source layout, navigation, authentication, runtime configuration, API prefix registry, serving and deployment) is ../ui/architecture.md. This file holds the per-surface contracts that refine it. It keeps its path and section numbers because Console source comments cite them (for example features/integrations/advisory-vex-sources/advisory-vex-sources-retired.component.ts and features/console-admin/connector-credentials/connector-credentials-retired.component.ts cite §8.2 and §8.3).
Each section names the source files and specs that back it. DOC-2 verified that every cited source path exists and removed sections whose implementation is gone (the Signals runtime dashboard and the pipeline run-centric view); the behavioral detail inside each section was not line-verified.
Findings match coverage and source freshness
Verified against source commit fb62d5bb63 (PCD-824, 2026-10-13).
The Console reads the served X-StellaOps-Match-State, X-StellaOps-Match-Incomplete-Versions, X-StellaOps-Source-State, X-StellaOps-Source-As-Of and X-StellaOps-Corpus-Digest headers through the shared core/api/findings-read-state.ts response mapper. HTTP sends remain in their clients for route ownership verification. Response transformations retain readState; the list compatibility method retains it on its returned array. Missing or malformed provenance is unknown. Completeness requires an explicit complete state and an explicit zero incomplete-version count.
Findings lists, dashboard and risk counts, exposure, artifact triage and bundle selection, image findings and SBOM components, vulnerability/finding details, developer service summaries, release/environment posture and evidence-thread component selectors show the shared Findings status beside their content. The navigation count has a compact status. Incomplete results qualify counts as evaluable findings and state how many versions could not be evaluated. A zero count means no evaluable findings; it does not establish absence in the unevaluated versions. Stale advisory sources are labelled stale independently of match completeness. The source timestamp and corpus digest remain visible when provided. An image receives a green zero-finding verdict only with current, complete response provenance and the existing component match-coverage checks. Rescan completion messages and notifications carry the same match/source qualifiers; a completed Scanner job never certifies that unevaluated findings are absent. The image findings empty panel uses the same served-state verdict as its summary; empty list and filter counts say evaluable findings.
Each surface tracks its own reads. Multiple responses retain their conservative combined match/source state; starting a replacement read returns that read to unknown, and superseded responses cannot certify its replacement. This state is independent of request failure and bounded-list/sample indicators. Parallel scoped/tenant lists and inventory/baseline comparisons use distinct logical read channels. Starting one does not cancel the other’s data or provenance.
Verification: run npx vitest run --config vitest.codex.config.ts src/app/core/api/findings-read-state.spec.ts from the Console directory, plus the adjacent feature specs. Source and rendered regression tests cover complete, incomplete, stale and missing headers. Console deployment remains an operator window; source tests do not establish which bundle the gateway serves.
Scheduler retained-run history
Verified against 282a76c5ac50db93216c8f391a4edbc6900ac0d6 on 2026-10-06.
The schedules panel’s desktop table and mobile cards use the owner’s lastRunAt first, then a fallback join of up to 200 retained runs by schedule ID. Both reads use the current tenant through SchedulerHttpClient. Neither absence nor failure establishes that a schedule has never executed; see the JobEngine run model and retention contract.
For missing timestamps, an enabled schedule shows No retained run only after a successful history read. A disabled schedule keeps its dash and a retained-history tooltip. A pending read shows Loading history; a failed read shows History unavailable, the read error and Retry history. Supplied owner timestamps remain visible even when the fallback fails. Retrying clears old fallback timestamps and cancels the previous history request; destroying the panel also cancels that read.
Re-verify with scheduler-schedules-panel.component.spec.ts and scheduler.client.spec.ts through vitest.codex.config.ts, then run the Angular production build. These are rendered component and HTTP-adapter controls; they do not establish deployment to an operator’s running Console.
Dashboard history notice
Verified against 80987018163df73f95526b372a836335cdd77766 on 2026-10-06.
The JobEngine dashboard reads schedules and a run-list page with limit: 1 for the current tenant. After both reads succeed, it shows No retained execution history only when at least one schedule is enabled and neither a schedule timestamp nor the run list supplies retained execution evidence. The notice uses information tokens and role="status"; it states that history may have expired and does not infer a lifetime execution absence or an outage. Its Diagnostics link preserves the current query context.
A pending or failed read hides that notice. The existing loading indicator and read-error Retry action remain separate. Refresh clears prior evidence and cancels the preceding read; destroying the dashboard cancels pending reads. Schedule and run failures remain failures rather than successful empty results. Disabled schedules and returned timestamps keep their existing interpretation.
Re-verify with jobengine-dashboard.component.spec.ts, scheduler.client.spec.ts and scheduler-runtime-local.e2e.spec.ts. Browser-harness sessions and responses are explicit fixtures; source/browser acceptance does not establish shared-estate deployment or an Authority-issued JWT boundary.
0-2) Mission, layout and stack
Moved to ../ui/architecture.md§0-§2.
3) Key features
Deployment comparison findings
The deploy-diff surface presents VEX status transitions and reachability transitions as change findings, with their CVE identifiers and before/after values. The browser does not turn these facts into promotion-gate passes, failures, warnings or severities. The summary, finding descriptions and component annotations use the same descriptive contract. DeployDiffService reads the lineage compare owner; its response supplies no policy evaluation for the compared pair, so the action bar states No policy evaluated. Rendered regressions in deploy-diff-panel.component.spec.ts cover affected and investigation VEX changes and both reachability directions.
Re-verify from src/Web/StellaOps.Web with npx vitest run --config vitest.codex.config.ts src/app/features/deploy-diff.
The canary HTTP wire and dispatcher are retired: the Console does not post to an unowned deployment route or emit a fabricated deployment-action response. The DTC-14 canary control remains a disabled placeholder explaining the missing traffic adapter. Driving a deployment action directly reports that it is unavailable and sends no request. The page no longer logs or navigates from a canary response.
Top-level route shells, guards and the route access model are described in ../ui/architecture.md§3-§4. src/app/core/auth/route-access.ts (NAV_ACCESS, ROUTE_FAMILY_ACCESS) is the contract; composite families such as operations and evidence union several scope sets, so read the model rather than a table.
The Setup family is only a coarse entry gate; each sensitive child has an exact owner-aligned contract. Identity Providers requires both ui.admin and authority:idp.read; mutations additionally require authority:idp.write and fresh authentication. Certificates & Trust deliberately joins two services without conflating their permissions: Platform trust inventory pages require trust:read (with trust:write for create/register/rotate and trust:admin for revoke/block actions), while Attestor Transparency requires attest:read or attest:create to view and exactly attest:create to mutate. Navigation, parent tabs, and child canMatch guards use the same rules from route-access.ts.
Environment read and operate boundary
The environment estate is readable independently of environment mutation. The readiness page and /releases/environments list/detail remain available to the applicable read personas, but create, delete, settings, target health/config, freeze-window, and protection-class actions require the exact orch:operate claim enforced by Release Orchestrator. The :environmentId/settings deep link is guarded and read-only sessions are returned to the environment list; a ?tab=settings query is normalized to Overview. Read-only detail keeps Targets, Freeze Windows, and Protection visible as inspection surfaces without rendering or executing their mutation handlers.
Estate-list enrichment is intentionally bounded in ReleaseEnvironmentHttpClient: at most two environments (four target/freeze-window reads) are enriched at once, and the backend list order is restored before the result is emitted. This keeps a large dashboard estate from consuming the shared PostgreSQL client budget while preserving the existing response contract; an unknown environment still follows the owning API’s 404 environment_not_found behavior.
Release authoring and promotion boundary
Release discovery and history remain readable under the applicable release or orchestrator read scopes. The Console’s release/version/hotfix authoring workflows publish sealed bundle content and therefore require orch:operate before their routes match; a release:write reader is not offered the Genesis authoring checklist and cannot enter /releases/new, /releases/versions/new, the legacy version creator, or /releases/hotfixes/new. Promotion creation is separate and requires release:publish; promotion list/detail reads remain available to the broader promotion-read contract. Route guards repeat the template and handler checks so copied deep links cannot defer authorization failure until submit time.
Global environment context is serialized as one comma-delimited set on the wire for either the environment or environments parameter declared by an endpoint. ReleaseOrchestrator approval/release list routes split that value and match any selected environment. A zero-row Promotions response while this global scope is active is labelled as no promotions in the selected environments and names the active scope; only an unscoped zero-row response may say No promotions yet or show first-use prerequisites.
Decision views: owner states stay separate
The release detail, approval detail and Estate pages keep content lifecycle, policy outcome, human decision, deployment, observation, evidence and replay as separate owner states. Web adds no state machine for them; each value is the owner’s read, and an unknown value renders as unknown, never as 0, “No” or an all-clear.
- Release detail: draft and ready are the content lifecycle. The header badge reads “Content ready” and its help states that ready content does not mean that policy allows a promotion, that target environments are ready or that evidence is verified. With no evaluated gate, the Gates card shows “Not evaluated” for the blocking and pending counts; approvals that carry no per-gate results leave the blocking count “Unknown” (the pending count is measured); an unknown risk tier shows “Unknown” for the reachable counts; Replay Mismatch shows “Not recorded” unless a mismatch is recorded. Last Actor is the actor of the latest recorded release activity; the release-truth read carries none, so it reads “Not recorded” (never the creator or a ‘system’ default). “Request Deployment” opens the promotion request.
- Approval detail:
ApprovalDetailStore.failedReadsnames the gate, security snapshot, evidence, ops and replay reads that failed. The security section is the Release Orchestrator approval security snapshot, the top findings (at most 10) derived from recorded gate evidence; no version-to-version delta exists. An empty snapshot reads “No snapshot” and a failed read “read failed”; neither renders counts. The decision banner states an approved request’s deployment state and an expired, superseded or cancelled request as closed before any gate claim, and a failed gate or snapshot read as “Review data unavailable”, naming only the state that is unknown (a failed gate read whose gates came from the approval detail read says so). The Security tab note on critical findings states what the returned gate results show (a blocking gate, no blocking gate, the skipped gates by name) and offers Approve only when it is available. The cross-environment matrix records no verdict for the source environment. A returned decision digest reads “Recorded”; the page does not verify it. The replay read reports whether the signed decision record (digest, envelope and signing key) is recorded (verified,not_available); it runs no replay, so the page and the verdict timeline state “not attempted” with the reason; only an owner that returnsmatchormismatchshows a reproduction result. - Estate: unproven-cell owner subjects and the recorded facts’ “Initiated by” resolve through the actor-identity projection; team IDs are labelled as IDs because no team name projection exists; the owner fact names its stored source.
- Actor identity on these pages: a machine subject (a GUID or a hex identifier of at least 16 characters) resolves through
stella-actor-identity-badge(GET /api/v1/platform/actor-identity/{actorRef}) and the recorded identifier stays beside the name; a recorded name or service actor renders as recorded. The badge renders an unknown subject and a failed lookup alike as “Unknown actor”.
Dashboard bootstrap action boundary
The empty-estate Dashboard derives both its bootstrap cards and its activation checklist from the current session’s effective scopes. Registry and advisory connection actions require the Setup-owning ui.admin scope plus an integration write/operate grant; topology and release-authoring handoffs require orch:operate; reachable-risk setup requires both a Security read scope and an operative scan/investigate/simulate scope; signing-key setup requires ui.admin plus signer rotate/admin. The synthetic admin scope retains the complete path. Unknown or read-only sessions fail closed: they receive setup-ownership guidance instead of links to routes or actions they cannot complete.
Dashboard bootstrap is also bounded. PlatformContextStore gives the complete regions -> preferences -> environments hydration chain 15 seconds, cancels the active request when that bound expires, and completes into an explicit degraded error state. Reload starts a new generation so late responses from an older attempt cannot overwrite current context. The Dashboard presents its existing custom skeleton through stella-bounded-loading: after the shared 10-second UI bound it exposes Retry, and a transport timeout becomes an honest context error instead of an indefinite full-page shimmer or a fabricated empty estate.
The Promotion Readiness lens has an independent 10-second deadline around its complete dashboard-plus-environment enrichment snapshot. The deadline cancels non-settling target or freeze-window reads as one operation, presents an honest unavailable state with Retry, and uses load generations so a late result cannot overwrite a newer attempt. The bound is not multiplied by environment count.
The Dashboard findings feed is a severity-ranked Priority findings slice, not a recency feed: Findings.Security supports deterministic severity or actionable ordering, while the projection exposes its update time through the compatibility FindingDto.firstSeen field. The card therefore labels row times as updates and renders “showing N of total” from the V2 page envelope instead of calling the fetched page length a tenant-wide count. The V1 summaries route is used only when the V2 route is absent (404); a present-but-unhealthy V2 service must reach the card’s explicit error and Retry state rather than being hidden by legacy data.
The Findings Explorer and Exposure action lens deliberately apply different scope semantics over the same Findings.Security projection. Exposure is tenant-wide so BYOS and other findings outside the current Console selectors remain actionable. The Explorer issues one read for the complete active region/environment selection and a second tenant-wide read; when only the scoped read is empty, it says that the selected scope hid existing findings and offers View all findings. The client serializes every selected region and environment as the API’s comma-delimited set rather than silently using the first selection. /security/findings?scope=tenant opens the tenant-wide list directly (CCP-021): the Exposure group drill uses it, so the header selection does not narrow the destination, and the Explorer says that the header selection does not filter the list. Only an environment group is an exact filter there (env=). A CVE or release group opens the Findings text search (q=), a case-insensitive substring match over the vulnerability id, package, component and artifact reference, so the list can include more findings than the group counts (CVE-2024-1234 also matches CVE-2024-12345). A failed tenant-wide read renders unavailable, never an empty list or a zero count. Back to selected scope shows the scoped list and removes the parameter. scope is read by the Explorer only: it is not a Console context key, so it never changes the header selection, and no read carries it.
The Explorer’s findings list sends no score request when it opens (PCD-980). Findings serves evidence-weighted scores only under its ledger-plane scope, vuln:operate by default, which no base role holds, and its batch route POST /api/findings/v1/ledger/scores is an audited computation: each uncached finding is computed, cached and recorded in the score history. The list therefore checks the session’s scope (FINDINGS_SCORING_SCOPE; the Console’s scope check treats admin as satisfying it, as every check in core/auth/scopes.ts, while Findings itself authorizes vuln:operate). Without it, the list states that scores are not available to this session and why, names the operator path (an administrator grants vuln:operate through a role in Setup > Identity & Access, /setup/identity-access), each score cell reads “Not available”, and no scoring request is sent. With it, the list offers “Compute scores for this page”, a labelled action marked data-state-changing that posts the ids of the visible page’s unscored findings (at most 50, under the 100-id batch limit). A score cell then reads “Not computed”, “Computing…”, the score, or “No score returned” when Findings answers without one; a failed request stays visible as an error with a retry. The bucket summary appears only once a score exists and states how many loaded findings its counts cover, so unscored findings never read as zero counts. The flag filters stay disabled, with the reason as their title, until a computed score exists, and a new findings set clears the bucket and flag filters with the scores, so a missing permission or computation never reads as “no findings match”. The cached read GET /api/findings/v1/ledger/{findingId}/score holds only scores still in the Findings process cache and has no batch form, so the list does not use it.
Metric scope inventory: Dashboard, Exposure and Estate
Each metric on these three surfaces names its owner scope, counted unit, source set and observation time. “Header scope” means the Console’s region and environment selection; the component serializes the complete selection into the owner read itself (environment=<csv> or environments=<csv>) rather than relying on the global context interceptor. The release-list client applies the same rule to every release list whose caller passes no environment, for example /releases (CCP-021): it sends the complete selection as environment=<csv>, never only the first selected environment, and no region parameter, which the owner read does not take. When the selected regions’ environments are not known, it sends no read and the list renders unavailable. While the Console context is still loading the saved selection on a cold load, the release store sends no list read and shows loading; it reads once that load settles, so an unfiltered first read cannot leave its rows on /releases. The ReleaseOrchestrator deployments and releases reads take an environment set and no region, so a region-only selection (“1 region · all environments”) is serialized as the platform environments of the selected regions. When no environment of the selected regions is known (the region’s environment list has not loaded, or the region defines none), the scoped estate reads are not sent and their values render unavailable: never an unfiltered read across every region, and never zero. “All regions” is no region selected or every known region selected; the topbar labels both “All regions”, and both are stored states (the Platform default preference lists every region, and clearing the region picker selects every region). An environment without a region belongs to All regions (CCP-022): Release Orchestrator can record deployments in an environment that the platform region model does not place, so under All regions with no environment selected the estate reads carry no environment filter, and those deployments are read and shown in the Estate Unmapped column. A specific region selection never includes an environment without a region, and an explicit environment selection reads only the selected environments. “Tenant-wide” means the owner read takes no region or environment parameter, and the surface says so where the metric is shown (“Tenant-wide: the header region and environment do not filter …”).
| Surface | Metric | Unit | Owner scope | Source set | Observation time |
|---|---|---|---|---|---|
| Dashboard custody strip | Source, Build, Scan, Verdict, Decision, Deploy, Watch | deployed digest with the stage verified or recorded, out of the deployed digests (“N of M”); gaps (missing or unverifiable) and unreadable digests named apart | Tenant-wide (a developer principal: its owned services and environments) | every placement of the deployed estate; Evidence stages read for the newest 50 digests, the rest stated unreadable | chain projection evaluatedAt |
| Dashboard custody strip | Watch drift | running container on a digest never deployed through Release Orchestrator | Tenant-wide (agent-keyed) | the tenant’s agent drift records, inside the chain read | chain projection evaluatedAt |
| Dashboard estate summary | rows × environment columns | service (recorded serviceId), or release with no recorded service identity | Header scope (region and environment) | the Deploy page above; a coverage clause when partial | deployments read time |
| Dashboard Overview “Open items” | Pending approvals / Agents reporting drift | approval / observing agent | Tenant-wide (stated in the item) | complete | as above |
| Dashboard Overview “Open items” | Critical findings / Tampered evidence / Updates available | finding / deployed cell / owned release | Header scope (stated in the item) | complete / the Deploy page above / first 20 owned releases; the item states “counted over the first N of M deployments” or “checked the first N of M owned releases” when partial | as above |
| Dashboard role queue (Operations) | Pending approvals; failed deployments; agents reporting drift | approval; estate cell whose latest deployment failed (the Estate page’s Failed door); observing agent | Tenant-wide; header scope; tenant-wide (each stated on the group) | complete; the Deploy page above with its coverage clause; complete | the read time of each group’s owner read |
| Dashboard role queue (Security) | Promotions blocked by reachable risk; critical findings | pending approval with a failed gate naming a reachable vulnerability; finding | Tenant-wide; header scope | complete approvals read, first 5 rows, no destination lists the rest (stated on the group); complete aggregate | approvals read time; aggregate read time |
| Dashboard role queue (My work) | Releases you own with a newer component version | owned release | Owned releases, header scope | first 20 owned releases, “checked the first N of M owned releases” when partial; a failed per-release check is counted as not checked (“N of M owned releases could not be checked”), never as no update, and every check failed is unavailable; first 5 rows, no destination lists the rest | owned-release read time |
| Dashboard role queue (Compliance) | Running digests with an outstanding deviation; running digests with a chain gap; running digests with proofs not verified; active exceptions | running placement whose Watch stage names an outstanding deviation (the owner code estate-deviation, as the stage code or as the note on a landing mismatch), each row showing the owner reason; running placement (digest in an environment) with a missing or unverifiable stage; running placement without a gap with a stage recorded but not verified, or unreadable; active exception | Tenant-wide (a developer principal: its owned services and environments); tenant-wide Policy count, bound to no digest | the chain read; owner order (most protected environment first), first 5 rows; a deviation count is incomplete (named, never the empty state) while a running placement’s current risk is not established (estate-unevaluated, estate-unbaselined, estate-outcome-unknown, estate-verdicts-unreadable, or no estate code at all), unavailable when no placement carries an estate code or none could be read, and no Estate filter lists only the deviations, so their rest is not linked; a deviating placement is also a chain gap and is counted there too; “Showing N of M” with the Estate broken-chain filter for the rest of the gaps; no Estate filter lists the proofs not verified, so their rest is not linked; complete owner count | chain projection evaluatedAt; counts read time |
| Dashboard detail | Priority findings; security queue | finding; VEX decision under investigation and active exception | Tenant-wide (stated on the card; Priority findings also says the findings explorer it links to applies the header scope) | “showing N of total”; owner counts | lens read time |
| Dashboard detail | Promotion readiness; Why is my release blocked?; Releases this week | environment × gate; blocked or warned release; shipped and blocked release | Tenant-wide (stated on the card) | complete environment list; first 50 with the total (“Showing the first N”); complete weekly totals | lens read time |
| Dashboard detail (IT) | Assurance posture; Evidence & attestation | framework readiness; evidence packet | Tenant-wide (stated on the card) | complete readiness; first page of packets, sealed and unsealed counted over the packets read, “counted over the first N of M” when partial | lens read time |
| Dashboard detail (Developer) | Your builds & SBOM scans | image of an owned service’s latest release (component digest) with Scanner’s latest attempt and last completed scan | Header scope for the owned services, built from the first 100 releases of the Release Orchestrator release list alone (no deployment or finding read, so neither owner’s failure hides the card); each Scanner read is tenant-scoped by the token | every image that the latest release of each owned service records, in a fixed service/component order, five per page with “page N of M · K owned images”; images of earlier releases are not read; a latest release without components, components without a digest and refused or failed reads are named | lens read time |
| Exposure | Aggregate rail | finding by severity, overall score | Tenant-wide | complete server aggregate | server computedAt |
| Exposure | What matters | finding | Tenant-wide | top 200 by severity, paged client-side; “top N of total” | dataset read time (UTC) |
| Exposure | By CVE / release / environment | CVE, release group, environment group (each row carries finding counts) | Tenant-wide; a group opens the Findings Explorer’s tenant-wide view (?scope=tenant) | complete server aggregate; the finding total is stated beside the group count | dataset read time (UTC) |
| Exposure | Vulnerability lookup | verdict for one identifier | Tenant-wide (skipScope) | bounded affected-findings sample (stated when truncated) | server asOf |
| Estate | Matrix, Services/Releases tile, Unproven, Tampered, Failed | service (recorded serviceId) or release with no recorded service identity (rows); environment (columns, Unmapped named apart); current deployment per cell, other recorded deployments as cell history | Header scope (region and environment) | page walk of up to 10 × 100 with the owner total; “built from the first N of M” when partial | deployments read time |
| Estate | Cell chain of custody, broken-chain filter | the seven stages of the cell’s deployment, joined by the deployment that established the placement (else environment and digest) | Tenant-wide chain read, shown on the header-scoped cells; the filter states how many matching placements are outside the view | every placement of the deployed estate | chain projection evaluatedAt |
| Estate | Deviations | drifting container / reporting agent | Tenant-wide (agent-keyed, no environment) | complete | drift read time |
| Estate | Unproven cells (named list) | unproven deployed cell | Tenant-wide ledger read | first page with total; can differ from the scoped Unproven tile | ledger read time |
| Chain of Custody | Watch stage | the placements of the subject digest (environment × digest), worst Watch state first | Tenant-wide chain read (a developer principal: its owned services and environments) | every placement of the deployed estate | chain projection evaluatedAt; the stage’s observedAt |
Scope changes: a tenant change re-runs every read on the surface; a region or environment change re-runs the header-scoped reads only. Lens cards that read their own data once (Promotion readiness, release blockers, releases this week, the IT assurance and evidence cards, Developer builds) are keyed by the scope their read depends on, so the board re-creates them and their read runs again: the tenant-wide ones on a tenant change, Developer builds on any header change. Each read replaces its predecessor’s subscription, so a late response from the previous tenant, scope, lens or lookup is never applied. While a header-scoped read re-runs, its value returns to loading; a failed read renders unavailable with Retry, never the previous scope’s value and never zero.
Chain of custody per deployed digest (CCP-056): the Home strip, the Estate cells, the Compliance view’s assurance queue and the Watch stage of the Chain of Custody page render one Release Orchestrator read, GET /api/v1/release-orchestrator/estate/chain-completeness, read by core/api/chain-completeness.client.ts (CHAIN_COMPLETENESS_API, scope release:read). The Console derives no chain state: each stage is the owner’s verified, recorded-signed, recorded-unsigned, missing, unverifiable, unavailable or not-applicable, with its observer, observation time and reason, and the stage ids are the custody page’s step ids. The strip counts deployed digests with a stage verified or recorded (“N of M”); a digest takes its worst placement state, so a digest with a missing Decision or Build in any environment never adds to a positive count. A stage is green only when every deployed digest has it verified; a gap is red and links to /evidence/estate?chain=<stage>; unreadable digests are named apart and never counted as missing. Drift is a Watch-stage state, not a separate stage, and so is an outstanding estate deviation (CCP-071): the Watch stage names the latest Release Orchestrator re-evaluation of the running release, unverifiable when it regressed against the decision that authorized it and unavailable while its current risk is not established; core/api/estate.client.ts keeps an absent /estate/status deviation field null, never zero. An empty estate, a failed read and a viewer without release:read render no-signal, em-dash with Retry and the missing scope respectively, never a zero. Each deployed Estate cell shows seven stage marks and one gap line (“Missing: Source, Build, Decision”); a loading, failed or out-of-scope read reads “Chain: reading…”, “Chain: unavailable” or “Chain: not read”, and a deployment that is not the estate’s current one reads “Chain: not evaluated”; the cell details list every stage with its observer and link the custody page. The Estate “Chain” filter (?chain=broken or a stage id) keeps rows with a matching cell, states how many matching placements this view shows, and on a failed read confirms no row instead of reporting none. The Estate verdict ranks a broken chain after a tampered cell and a failed deployment, and an unreadable chain after missing release evidence; its drill opens the broken-chain filter for broken cells and the ?chain=unread filter (a stage whose owner read failed or was not made, or a cell the deployed estate does not record as running) for unreadable ones. The Home estate embed shows the same gap line. The State filter’s evidence options are the release evidence posture and are named so (“Release evidence recorded”). A recorded Decision stage whose owner note classifies the bound decision as approved-without-basis or basis-not-recorded (Release Orchestrator’s ReleaseTruthGuard.AssessRecordedDecision) is qualified, never re-classified: the gap line, the queue row and the embed line add “Decision approved without basis”, the cell details show it beside the Decision state, and the strip’s Decision stage names how many deployed digests carry it; it is not counted as a gap. The Estate route also admits orch:read; a viewer without release:read makes no chain read, and the Estate cells and the Home embed read “Chain: not readable with your access” instead of a pending or failed read.
The Chain of Custody page’s Watch stage (CCP-056, 2026-10-20) takes its state from the same read, so it agrees with the Estate cell and the Home strip. The page first reads where the digest runs (GET /api/v1/release-orchestrator/estate/digest/{digest}). A digest that runs nowhere is Missing, and no chain read is made. For a running digest, the page reads the chain projection and shows the Watch stage of the digest’s placements in the projection’s words (“Unverifiable”, “Recorded, unsigned”, “Unavailable”), with the owner’s reason, observer and verdict reference. With several placements it shows the worst one, by the owner’s roll-up order, and names the environments where it holds; the details list every placement. “Running now in N environment(s)” is detail text, never the state. An outstanding estate deviation therefore reads Unverifiable, never Recorded. A failed chain read, a read the owner did not make, and a running digest without a placement in the read are Unavailable. A chain read that does not answer within 12 seconds, the Estate page’s bound for the same call, is a failed read. The digest join is tenant-wide, while a developer-owned chain read omits placements the principal may not read. An environment the join reports running the digest that the read does not evaluate therefore counts as Unavailable in the roll-up, and the details name it (“Not evaluated in this read”), so the worst state is never taken over the evaluated placements alone. A session without release:read makes no chain read, and Watch reads “Watch state not readable with your access”. The page never mints the projection state as a verified badge.
Units: a row keyed by a release because the deployment recorded no service identity is counted and labelled as a release (“3 releases with no recorded service identity”), never as a service. Exposure group views name their group key and state the finding total separately; the working-set view counts findings.
Estate row identity and history: the canonical service identity is the deployment’s recorded serviceId. Every release and version recorded under one serviceId is one service row, so historical versions never raise the service count. A display name is not an identity: a ReleaseOrchestrator deployment record stores serviceName verbatim from its create request, and deployments started from a release record neither a service id nor a name, so a deployment without a serviceId stays in a row of its own release (split, never merged, by the recorded name) and counts as a release. The release count is over distinct releases: one release whose deployments recorded different service names spans several rows and counts once. Row keys are namespaced (service:, release:, deployment:), so a release id equal to a service id or name cannot merge two rows; a row with neither a service nor a release id is keyed by its deployment and its details name it as a deployment. Different releases that share a display name and record no service identity stay separate rows, and the page says so (“3 releases shown as …: no service identity was recorded for them”). A shared display name also never attributes observed drift: a row whose label another row carries joins drift by its owner id only. Each cell shows the current deployment (the latest completed one, else the latest attempt when none completed); the cell details head it by that rule (“Current deployment (latest completed)” or “(latest attempt; none completed here)”) and say when a newer attempt that did not complete is in the history. Every other recorded deployment of that row and column is kept as history: the cell states “+N more recorded”, and the cell details list them under “Other recorded deployments here (not current)” with version, release, status, time and deployment link. When an environment leaves the platform topology, its records stay in the Unmapped column with their history and recorded environment name; under All regions the Unmapped column also holds the deployments of an environment without a region. The cell details name the stored digest “Deployed digest”: it is the deployment record’s value, not an agent observation.
Target readiness explanations (Environments and the Estate environment dock): both surfaces read the ReleaseOrchestrator readiness gates through one classifier (features/topology/target-readiness-explanation.ts). A target whose owner health is Healthy but that is not ready reads “Healthy (Docker daemon is Healthy) but not ready: …”. The owner also lets Degraded health through to readiness; such a target, or one whose health word the owner did not state, reads “Not ready; health allows readiness (Docker daemon is Degraded): …” and is never called healthy. A pending gate is “not measured”: for docker_version_ok, whose owner check currently always returns pending (“Docker version check requires agent execution”), the Docker version is unknown, not incompatible, and no upgrade is recommended. Only a measured failure (fail) carries a repair step; “Upgrade Docker on the host to 20.10+.” appears only for a measured incompatible version. The connectivity_ok meta-gate is not listed as its own failure; when it names target_health:<status>, the target is reported as unhealthy. The Environments “Failed Gates” counter counts measured failures only: the meta-gate, which the owner fails whenever another gate is merely pending, counts only when it names target health.
Target validation failures (Environments, CCP-033): Validate All and Re-check post POST /api/v1/targets/{id}/validate, which Release Orchestrator serves only under its operate policy (orch:operate); a session without that scope is not offered either control (integration:write alone is refused by the owner and does not enable them). A validation that returns no report is reported by class with the owner’s HTTP status: refused (401/403: states that Release Orchestrator refused validation for the session, that the endpoint requires orch:operate for the selected tenant and that re-checking with the same session and tenant will not change the answer; it does not claim a missing scope as the cause, because the controls are offered only to a session that holds orch:operate), target no longer held (404, points to Refresh) and rejected (other 4xx) are structural; no response, 408, 429 and 5xx are temporary and point to Re-check or Validate All. Refresh and the 30-second auto-refresh only re-read; they never repeat a validation.
Residual limits: the Estate evidence posture join reads releases by current or target environment, so a deployment whose release has since moved outside the selection shows evidence unknown, not a guessed posture. Deployments started from a release (ReleaseDeploymentInitiator) record no serviceId, and the deployment component subject’s componentId falls back to the release-local component row id when the release component has no registry component, so neither can serve as a service identity; such releases stay separate release rows until the owner records one.
Findings runtime help links
The runtime panel’s disabled state opens the enable guide; its enabled state without traces opens the runtime API reference. RuntimeEmptyStateComponent emits these as /docs-content/modules/findings/ledger/<page>.md links.
The Console’s angular.json copies the documentation tree into the docs-content build assets while preserving its directory structure. The Angular development server serves those assets; the gateway serves the built Console from its configured static-file root. The adjacent runtime-empty-state.component.spec.ts renders each state and checks the full emitted URL.
These runtime documentation links were verified against 81c0628c15 (PCD-259). Recheck from src/Web/StellaOps.Web with npx vitest run --config vitest.codex.config.ts src/app/features/findings/detail/runtime/runtime-empty-state.component.spec.ts.
Attestation verification presentation (CCP-086)
Chain of Custody and the Transparency entry drawer use the shared attestor-verification-verdict.ts classifier for Evidence verification responses. An owner ok:true renders Proven. A required missing witness (witness_missing) or an unendorsed witness with a nonempty witness_status_ suffix renders Unverifiable for ok:false, with the original reason retained. Decisive signature, root and bundle mismatches remain Tampered even beside a missing-material reason. Witness absence alone cannot demote a walkable leaf mismatch; the existing missing-orientation result remains Unverifiable. Policy refusal alone and unknown codes do not receive a general exception. A transport failure remains unavailable and retains the previous state; custody receipt and coverage context remain owner-derived. The dashboard/estate evidence posture continues to follow its separate owner read.
Source verified against b824a1b3b1ba5496e72b2e1a1bb515b9826efe87; recheck from src/Web/StellaOps.Web with npx vitest run --config vitest.codex.config.ts src/app/core/api/attestor-verification-verdict.spec.ts src/app/features/evidence-custody/evidence-custody.component.spec.ts src/app/features/evidence-transparency/evidence-transparency.component.spec.ts. A qualified Console deployment and sanctioned CCP-010/011 live acceptance remain separate.
Evidence capsule dual-store resolution
/evidence/capsules/:capsuleId is a dual-use detail route. The viewer first asks AdvisoryAI for an evidence pack. pack-* identifiers resolve there; a decision- capsule UUID is outside that namespace and receives an expected 404, after which the viewer reads EvidenceLocker’s decision-capsule store. A valid capsule there renders normally without a server error. If both stores return 404, the page reports that both tenant-scoped stores were checked. Genuine evidence-pack 5xx failures still remain visible if the capsule fallback cannot recover the record; the fallback is retained for resilience but a 5xx is not the ordinary capsule-resolution path. The EvidenceLocker read is attempted only when the session has evidence:read.
3.1 VEX Gate Inline Actions (Sprint 20260208_073)
Quiet-triage promote actions now support inline VEX gating with deterministic evidence tiers:
src/Web/StellaOps.Web/src/app/features/vex_gate/vex-gate-button.directive.ts- Morphs action buttons into tier-aware gate states:
- Tier 1 -> green (
allow) - Tier 2 -> amber (
review) - Tier 3 -> red (
block)
- Tier 1 -> green (
- Emits a
gateBlockedevent on tier-3 actions.
- Morphs action buttons into tier-aware gate states:
src/Web/StellaOps.Web/src/app/features/vex_gate/vex-evidence-sheet.component.ts- Renders inline evidence details with tier/verdict metadata and optional DSSE verification hints.
- Integrated in quiet-triage lane promote actions:
src/Web/StellaOps.Web/src/app/features/triage/components/quiet-lane/quiet-lane-container.component.tssrc/Web/StellaOps.Web/src/app/features/triage/components/quiet-lane/parked-item-card.component.ts
The UI behavior remains offline-first and deterministic:
- Tier mapping is derived from local finding attributes only.
- No additional network dependency is required to render gate/evidence states.
3.3 Audit Trail Reason Capsule (Sprint 20260208_067)
Findings and triage views now expose a per-row “Why am I seeing this?” reason capsule:
- Audit reasons client contract:
src/Web/StellaOps.Web/src/app/core/api/audit-reasons.client.ts- Uses
/api/audit/reasons/:verdictIdwith deterministic fallback records for offline/unavailable API conditions.
- Reusable capsule component:
src/Web/StellaOps.Web/src/app/features/triage/components/reason-capsule/reason-capsule.component.ts- Displays policy name, rule ID, graph revision ID, and inputs digest.
- UI integration points:
src/Web/StellaOps.Web/src/app/features/findings/findings-list.component.tssrc/Web/StellaOps.Web/src/app/features/findings/findings-list.component.htmlsrc/Web/StellaOps.Web/src/app/features/triage/components/triage-list/triage-list.component.ts
Verification coverage:
src/Web/StellaOps.Web/src/tests/audit_reason_capsule/audit-reasons.client.spec.tssrc/Web/StellaOps.Web/src/tests/audit_reason_capsule/reason-capsule.component.spec.tssrc/Web/StellaOps.Web/src/tests/audit_reason_capsule/findings-list.reason-capsule.spec.ts
3.3.1 Artifact Triage Related Links And Advisory AI (Sprint 20260531_057)
The artifact triage workspace exposes related links from the selected finding, but those links must always resolve to real Web routes or live API contracts:
- Policy gate detail links route to
/ops/policy/packsfor gate definitions and/security/findingsfor recent evaluations. Query parameters carryartifactId,findingId,cveId, gate, subject type, policy version/digest when present, andreturnToback to the triage workspace. - The Advisory AI rail calls
/api/advisoryai/v1/advisory-ai/pipeline/summaryfor analysis plans and/api/advisoryai/v1/advisory-ai/explainfor explanations. The route segment owns the pipeline task type; the request body does not send a duplicatetaskType. Explanation requests use the canonical Scanner finding UUID fromAnalysisContext.findingIdwhen it is available, while preserving the visible vulnerability id for display context. - PCD-456 source review at
ad9edbcef5(2026-10-08) traces those rail paths throughAPI_PATH_PREFIXESandAdvisoryAiServiceto the composed owner. Explanation bodies useExplainRequest; returned citation/authority/hash metadata does not prove a signature or deterministic replay. Provider availability and deployed route acceptance remain separate.
Historical 2026-10-08 withdrawal boundary: the former /api/v1/advisory/** recommendation, task and similar-vulnerability paths are absent from the current triage workspace; the old paragraph is not an alias or provider contract.
- Similar-vulnerability cards remain wired through the panel output to local triage selection or the findings explorer, but the optional list stays empty until a live Advisory AI clustering route exists.
- SBOM and VEX evidence actions keep explicit link labels and safe
rel="noopener noreferrer"attributes.rekor://receipt URIs fall back to the in-workspace Rekor evidence tab instead of opening an opaque browser protocol prompt.
Verification coverage:
src/Web/StellaOps.Web/src/app/features/triage/__tests__/triage.integration.spec.tssrc/Web/StellaOps.Web/src/app/features/triage/components/evidence-pills/evidence-pills.component.spec.ts
3.3.2 Open Work Queue Declared Refusal (SPRINT_20261006_141 PCD-981)
The Open Work queue (TriageArtifactsComponent, route triage/artifacts) reads the Findings artifact-triage list once when it opens. Findings refuses that read by design until it holds local Release Orchestrator deployment and Scanner scan projections: the answer is a 503 problem whose code (and title) is security_artifact_triage_projection_unavailable, because an empty queue would claim that no artifact needs triage. The page maps only a 503 that carries that code to a declared unavailable state: it states that artifact triage is not available on this installation yet, names both missing owner projections, and keeps the owner code and detail under Technical details. It offers no Retry or Refresh, and it hides the read-only workflow notice, which describes rows that cannot exist. Every other failure, including other 503s, keeps the generic retryable error. Home’s links to the queue are unchanged (D-03).
Verification coverage:
src/Web/StellaOps.Web/src/app/features/triage/triage-artifacts.component.spec.ts
3.4 Pack Registry Browser (Sprint 20260208_068)
TaskRunner pack operations now include a dedicated registry browser route:
- Route:
ops/packs - Route registration:
src/Web/StellaOps.Web/src/app/app.routes.tssrc/Web/StellaOps.Web/src/app/features/pack-registry/pack-registry.routes.ts
- Feature implementation:
src/Web/StellaOps.Web/src/app/features/pack-registry/pack-registry-browser.component.tssrc/Web/StellaOps.Web/src/app/features/pack-registry/services/pack-registry-browser.service.tssrc/Web/StellaOps.Web/src/app/features/pack-registry/models/pack-registry-browser.models.ts
Browser behavior:
- Lists available and installed packs using
PackRegistryClient, with deterministic ordering and capability filters. - Displays DSSE signature status per pack and per version history entry (
verified,present,unsigned) and signer metadata when available. - Executes install/upgrade actions only after compatibility evaluation; incompatible packs are blocked with explicit operator feedback.
- Supports version-history drill-down per pack without introducing additional external dependencies.
Verification coverage:
src/Web/StellaOps.Web/src/tests/pack_registry_browser/pack-registry-browser.service.spec.tssrc/Web/StellaOps.Web/src/tests/pack_registry_browser/pack-registry-browser.component.spec.ts
3.6 Reachability witness surface
Source verified against f26be185ea504d5253b7719eb17ab40a2a917748 on 2026-10-15. The witness UI dossier names the HTTP-client/rendered-consumer re-verification command and live boundary.
Reachability Center exposes only the Scanner-backed witness list and the proof-of-exposure projection derived from those witnesses:
- Feature implementation:
src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts
- Verification coverage:
src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.spec.ts
Surface behavior:
- The Console requires a trimmed, nonblank opaque
scanIdbefore it callsGET /api/v1/witnesses; it rejects control characters without imposing a UUID or hex-only grammar. A missing id is an input-required state; a rejected id or malformed list response never becomes a successful empty list. - A successful zero-row response is labelled as empty for that scan, not as fleet-wide absence of reachability evidence.
- Witness detail and list reads validate the persisted
WitnessResponseDtowrapper and its canonicalstellaops.witness.v1payload. WrapperwitnessId, payloadwitness_id, opaquescanIdandrunIdretain their separate meanings. The mapper reads nestedartifactandvuln, orderedpathpositions with exactsymbol_idvalues, evidence/build context, numeric observation types, runtime observations and symbolization. A malformed path node invalidates the whole displayed path; dropping it would invent an edge. Separate entrypoint and sink metadata never replace its ends. Repeated positions remain visible. - This payload records no public reachability verdict or overall confidence. The Console displays those values as unknown, excludes them from both reachable and unreachable filters, and does not derive verification from payload contents. A shaped DSSE envelope means a signature was recorded; only a coherent, identity-bound owner verification response can report success. Unsupported schemas and malformed fields preserve independently valid wrapper/subject data without inventing proof, timestamps, image-custody relations or Rekor inclusion.
- Coverage and Sensor Gaps are not shipped tabs because no production endpoint populates their former row models. Their legacy routes redirect to Witnesses.
- Runtime drift is owned by
/evidence/estate?focus=drift; the former/ops/operations/driftheading-only page is a compatibility redirect.
3.7 SBOM Graph Reachability Overlay with Time Slider (Sprint 20260208_071)
Graph explorer overlay behavior now supports deterministic lattice-state reachability halos with temporal snapshot exploration:
- Feature implementation:
src/Web/StellaOps.Web/src/app/features/graph/graph-overlays.component.tssrc/Web/StellaOps.Web/src/app/features/graph/graph-canvas.component.ts
Behavior details:
- Overlay controls only expose the live shipped overlay families
policy,vex, andaoc. - The explorer route consumes
GET /graphs/{graphId}/tiles?includeOverlays=truethroughGraphPlatformHttpClient. - Canvas halo colors and summaries derive from live overlay payloads, with policy taking precedence over vex and aoc when multiple overlays exist for the same node.
- When a graph has no overlays, the shipped route shows explicit empty-state messaging rather than inventing reachability or simulation data.
Verification coverage:
src/Web/StellaOps.Web/src/tests/graph_reachability_overlay/graph-overlays.component.spec.tssrc/Web/StellaOps.Web/src/tests/graph_reachability_overlay/graph-canvas.component.spec.ts
3.8 Active-Surface Verification Lane and Setup Wizard Styling (Sprint 20260405_002 / 005)
Focused verification and bundle-polish notes for the shipped surfaces:
- The repo now carries a dedicated active-surface test lane:
ng run stellaops-web:test-active-surfaces(also exposed asnpm run test:active-surfaces). - The lane intentionally covers the currently shipped Graph, Evidence, deployment creation, vulnerability detail, and environment-detail flows instead of the broader legacy spec backlog.
- The setup wizard and step-content styling moved from oversized inline component styles into global SCSS under
src/Web/StellaOps.Web/src/styles/so the production build clearsanyComponentStylebudgets without raising those budgets. - Touched shipped routes continue to use explicit live empty/error/unavailable states rather than mock action fallbacks.
- Shipped runtime DI stays live-only: app bootstrap and exported provider helpers must resolve HTTP/live clients, while any remaining mock implementations are restricted to test-local wiring or tracked retirement slices.
- When a shipped Web surface does not yet have a live catalog, verifier, or stream endpoint, it must fail closed with an operator-visible unavailable state instead of fabricating successful data or simulated event traffic.
Real-read model and absence-state boundary
- The Approvals queue and the approval decision cockpit classify, count and filter requests by the
statusRelease Orchestrator returns; the owner reconciles expiry on every read.expiresAtis shown only as the deadline of a pending request: an absent expiry rendersNo expiryand is never replaced by another timestamp, a decided request is never labelled expired, and each queue tab badge counts exactly the rows that tab lists (Pending, Approved, Rejected, Expiring, Expired). The shared vocabulary lives insrc/app/features/approvals/approval-expiry.ts(SPRINT_20261007_004 CCP-008). - The approval decision cockpit presents each gate as Release Orchestrator recorded it (SPRINT_20261007_004 CCP-009,
src/app/features/approvals/approval-gate-recovery.ts): for a waiting, blocked, skipped, advisory, unknown or invalid-exception gate it names the missing input from the owner reason codes or gate details (decision-basismissingInputs, the Policy.EnginereasonCode, SBOM readiness state, reachability witness read failure, attestation failures), or states that the owner recorded none; it shows the owner observation time (evaluatedAt, or “observation time not recorded”) and the owner repair, and states the re-evaluation path: gates are evaluated only when a promotion is requested, a repeated request returns the pending one, so the request is rejected or cancelled before a new promotion is requested. No repair text asks for a broader grant. The Evidence column lists owner references (a Scanner scan links to/security/scans/{scanId}; the approval policy that the approval-policy gate applied links to/releases/policies?policyId=<id>for a session holdingrelease.policy.readand is shown without a link otherwise, SPRINT_20261007_021 CCP-047) or states that none was recorded; a gate row recorded under the service-configuration fallback names no approval policy. An active signed exception reads as an exception with its expiry; an expired or invalid one reads as blocking approval again. Decisions are posted without an actor field; Release Orchestrator attributes them to the authenticated principal. When the signed-in identity matches the recorded requester, the cockpit’s self-request note states that Release Orchestrator refuses the requester’s own approval where the target environment requires separation of duties, that another approver must decide and that the requester can still reject (SPRINT_20261007_004 CCP-067 and CCP-074). The cockpit does not read the environment setting, so the note does not say whether this request is affected. The cockpit maps the owner refusal on its error code, not on the message text:separation_of_duties_self_approval(403) states that the requester’s own approval is refused and that another operator with release approval permission decides;separation_of_duties_requester_unrecorded(409) states that the request records no requester, so the approval is refused, and names reject or cancel and a new promotion request as the recovery. Either refusal turns Approve off until the request is loaded again; Reject stays available. The promotion detail page presents every typed approve or reject refusal (a 403 or 409 whose body carries the ownererrorcode ormessage) with the owner message and without retry advice; the gateway authorization refusal (a 403 whose body namesrequiredClaimType) and any other failure keep the retryable error text. The Deployments page lists pending requests read-only and offers no approve action. The note and the refusal mapping describe a Release Orchestrator that carries the separation-of-duties check of CCP-067; a Console bundle with them is deployed with that Release Orchestrator revision or after it. CCP-084 withdrew the unusedReleasesActivityComponentand its owning spec on 2026-10-15; their original bytes are frozen undersrc/__Obsoleted/Web/StellaOps.Web/src/app/features/releases/. The dormant activity/approve surface has no supported replacement or alias. The real Releases routes, read-only Deployments list and deployment monitor remain unchanged, as do the cockpit and promotion detail typed owner refusals. The source withdrawal receipt binds the reference/compiler checks and preservation tests; Console deployment verification remains a separate operator acceptance input. Component-test results alone do not establish deployed approval acceptance. - Promotion creation and request flows derive target identity, target counts, approval quorum, and guarded posture from
ReleaseEnvironmentApi. Deployment readiness comes fromPreflightApi; policy Gates remain explicitly unevaluated before launch and become authoritative only on the release decision/approval record. - Integration detail reads event history from
AuditLogClientand describes credentials from the integration record’scredentialBackendandcredentialKind; it does not synthesize events, scopes, rules, or AuthRef ownership. - Release detail reads evidence packets through
ReleaseEvidenceApi. Dashboard, image-security, inventory, Reachability, custody, signing-key, and Images surfaces preserve missing-producer or scope boundaries as unknown/unavailable facts rather than converting them into clean, healthy, empty-estate, verified, or zero-percent claims. - First-delivery progress reads Release Orchestrator owner facts through
FirstDeliveryApi(src/app/core/api/first-delivery.client.ts; SPRINT_20261007_018 CCP-040). Connect your CI shows the first CI evidence step from the CI receipt (done only for a provenance attestation whose statement records repository, commit and pipeline run; a scan, recorded lineage or a selected image never counts; receipt is not verification). The release catalog states the tenant’s first observed delivery (succeeded on every one of at least one target) apart from the per-actor checklist, and the checklist’s deploy step counts only owned releases with an observed deployment, never adeployedrelease status. Below the delivery history the release catalog renders the decision evidence proof (Release Orchestrator decision seal, SPRINT_20261007_018 CCP-064) of the first delivery, of the latest delivery when it differs, and, for a tenant-scope caller, of the newest decision captured for sealing, from the fields of the same read:completedas sealed only with the Evidence bundle id, root hash, decision revision and receipt time (acompletedanswer without them reads unknown);pendingandfailedwith attempts, last error and next attempt;disabledandunavailablewith the owner’s reason. Chain of Custody renders Source and Build from the provenance read of the subject digest; component lineage is shown only as not attested. An unavailable provenance read renders both stages UNKNOWN with the owner’s reason (for example provenance discovery switched off), and a developer principal’s digest outside its ownership scope is refused with403and also renders UNKNOWN. A failed read is unavailable, never an empty history or a pending step. - UI aggregation must preserve authoritative totals and identity. A fetched subset is not a fleet total, duplicate findings may be grouped only with visible multiplicity, and generic or unversioned package identity cannot support a clean vulnerability verdict.
- Advisory Source
Check Allruns individual connectivity checks through a deterministic queue with at most two requests in flight. A typed unhealthy connector result remains distinct from a transport failure such as a routed5xx; neither stops later checks, and the completed run names every affected source with its plain-language outcome plus reachable technical detail.
3.9 Live Graph Asset Inventory (Sprint 20260430_072)
The Console now exposes a tenant-scoped Asset Registry v1 inventory surface:
- Route:
/inventory - Navigation: Operations sidebar entry for operators with
graph:read - Runtime client:
src/Web/StellaOps.Web/src/app/core/api/asset-inventory.client.ts- Uses
POST /api/graph/assets/queryfor list/query data. - Uses
GET /api/graph/assets/{assetId}?includeLineage=truefor selected asset detail.
- Page implementation:
src/Web/StellaOps.Web/src/app/features/inventory/inventory-page.component.ts
Inventory behavior:
- Sends the active tenant through the canonical Stella Ops tenant header and never uses an in-memory provider in runtime DI.
- Sends supported Graph filters for asset type, exact criticality, search, and tombstone inclusion.
- Treats age filters as page-local views derived from
lastSeenorfirstSeen, because the current Graph contract exposes timestamps but no dedicated age filter field. - Never substitutes local data for Graph. Network failure, 404, 5xx, authorization and validation failures all render the error pane with the failure and the statement that asset records are unknown, not empty; the header shows no asset count until Graph answers (CCP-026).
- Requires Graph list/detail responses to declare
asset-registry.v1; schema drift is treated as a validation error. - Shows each asset’s Graph-recorded
source(table column and detail field), its asset ID and its observation time; an absent source or timestamp readsNot reported/No timestamp. - Keeps ownership and credential data as opaque refs and filters sensitive attribute expansions from the detail panel.
- When the live Graph endpoint answers with zero asset records for the tenant, the empty state reads the Graph
producerInputsstate: “automated asset ingestion is not configured” (no configured input), the configured input name with the statement that a configured input is not a delivery, or “Graph did not report its asset producer inputs” (field absent). The primary action opens the Graph asset producer documentation (/docs/modules/graph/asset-registry.md). Declared topology (/environments/overview) and registry integrations, or the observed estate for read-only users, are linked as related context that does not fill the inventory. The truthful0 assetscount is preserved. - When assets exist but current filters hide them, the page shows a filter-recovery state with clear-filter and refresh actions.
Verification coverage:
src/Web/StellaOps.Web/src/app/core/api/asset-inventory.client.spec.tssrc/Web/StellaOps.Web/src/app/features/inventory/inventory-page.component.spec.tssrc/Web/StellaOps.Web/src/app/layout/app-sidebar/app-sidebar.component.spec.tssrc/Web/StellaOps.Web/src/app/routes/route-surface-ownership.spec.ts
3.10b Compliance Profile Selector (Sprint 20260503_007)
Console panel under Setup -> Crypto Providers (/setup/crypto-providers) lets a tenant administrator change the active Cryptography:Compliance:Profile at runtime. Implementation:
- Page:
src/Web/StellaOps.Web/src/app/features/settings/crypto-providers/crypto-provider-panel.component.ts(the new section sits above the provider table). - Client:
src/Web/StellaOps.Web/src/app/core/api/crypto-provider.client.ts(getComplianceProfile,setComplianceProfile). - Models:
src/Web/StellaOps.Web/src/app/core/api/crypto-provider.models.ts(six built-inCOMPLIANCE_PROFILESdescriptors mirroringsrc/__Libraries/StellaOps.Cryptography/ComplianceProfiles.cs). - Friction step: the operator must type the target profile id into a confirmation input before the submit button enables — region-changing algorithm rotation warrants intentional friction.
- Algorithm delta: dialog shows a per-purpose
from -> totable; rows with actual changes are highlighted. - Compatibility hint: when the active provider id is not in the target profile’s
compatibleProviderslist (sprint risk #2) the dialog surfaces an explicit warning. - Scope gate: requires
crypto:profile:admin(distinct fromcrypto:admin); cards render disabled when the session lacks the scope. - Backend:
PUT /api/v1/admin/crypto-providers/compliance-profileon Platform; persists toplatform.tenant_compliance_profile; emitsplatform.update_compliance_profileaudit with before/after.
Verification coverage:
src/Web/StellaOps.Web/src/app/features/settings/crypto-providers/crypto-provider-panel.component.spec.tssrc/Web/StellaOps.Web/e2e/crypto-compliance-profile.e2e.spec.ts
See docs/modules/cryptography/architecture.md §11 for the full schema/endpoint/audit contract.
3.10 Live NIS2 Incident Reporting Dashboard (Sprint 20260430_053)
The Console NIS2 incident dashboard is bound to Notify-owned persisted regulatory timeline state:
- Route:
/compliance/nis2/incidents - Runtime client:
src/Web/StellaOps.Web/src/app/core/api/nis2-incident-dashboard.client.ts- Uses
GET /api/v1/regulatory/reporting-timelines?regime=nis2&active=truethrough the configured gateway base. - Passes optional
evidenceRefquery filtering through to Notify when a caller needs the timeline linked to a concrete evidence reference.
- Page implementation:
src/Web/StellaOps.Web/src/app/features/compliance/nis2-incident-dashboard/nis2-incident-dashboard-page.component.ts
Dashboard behavior:
- Sends the selected tenant through the canonical Stella Ops tenant header and relies on Gateway/Notify tenant-claim scoping instead of a
tenantIdquery parameter. - Requires the Notify response schema
eu-reporting-timeline.v1; schema drift, authorization failures, and endpoint failures fail closed with an operator-visible unavailable state. - Does not fall back to the local incident contract fixture for normal runtime operation.
- Receives operational timelines only: Notify excludes imported demonstration timelines (
fixture-ledger://provenance) from this collection read by default, so the active-incident, SEV-1, missed-deadline and reports-sent counts never include them. A demonstration record stays readable through Notify’s explicitprovenance=demonstrationview or anincidentIdlookup, each item labelled with itsprovenance(CCP-029). - Redacts incident PII by ignoring free-text incident titles/descriptions and rendering opaque incident IDs plus evidence, ledger, signer, payload, approval, and replay references.
Verification coverage:
src/Web/StellaOps.Web/src/app/core/api/nis2-incident-dashboard.client.spec.tssrc/Web/StellaOps.Web/src/app/features/compliance/nis2-incident-dashboard/nis2-incident-dashboard-page.component.spec.tssrc/Web/StellaOps.Web/src/app/routes/compliance.routes.spec.ts
3.10c Assurance framework overview and cockpit routes (Sprint 20260525_007)
The /compliance landing page (/assurance/* redirects to it) is the all-frameworks portfolio overview: readiness cards, the priority queue, operator configuration and the pack comparison matrix. Each framework has a distinct cockpit route instead of an in-page tab (corrected 2026-10-09; the earlier <stella-page-tabs> and ?framework= URL state were replaced by the COMP-1 split):
/compliance/nis2: control register, incident timelines, effectiveness./compliance/dora: the DORA review workspace (/compliance/dora/review)./compliance/cra: the product-security and technical-documentation review workspaces and the EU Declaration of Conformity (/compliance/cra/technical-documentation/declaration-of-conformity).- An unknown framework segment falls through to the 404 page. Legacy compatibility links are retired.
The overview and the framework cockpits must not imply legal certification. Disabled packs still mean optional tenant setup choices, not failed compliance. Readiness statuses continue to come from Authority, ExportCenter, and Notify APIs.
Verification coverage:
src/Web/StellaOps.Web/src/app/features/compliance/assurance-frameworks/assurance-frameworks-page.component.spec.ts
3.10d Assurance review workspaces: readiness counts come from owners only (CCP-029)
The DORA and CRA review workspaces (features/compliance/assurance-review-workspace/) list each pack profile with the readiness its owner returned (ExportCenter export readiness, Notify reporting readiness):
- A
liverow carries the owner result and counts toward Ready or Blocked. - A profile absent from owner discovery (
Not published by owner) or whose owner read failed (Readiness unavailable) carries no readiness, no reason code and no coverage line. It is excluded from the Ready and Blocked totals and shows the owner error text instead. - Ready and Blocked render an em-dash while loading or when no owner readiness was read, never a zero. When nothing was read because an owner read failed, the cards offer Retry (metric-card
errorstate), which re-reads owner readiness; when the owners publish none of the profiles there is nothing to retry (unavailable). The status chip readsReadiness unavailable(nothing read) orReadiness incomplete(some rows not read) unless a live row is blocked. - There is no contract-fixture fallback and the Web synthesises no readiness reason codes.
Verification coverage:
src/Web/StellaOps.Web/src/app/features/compliance/assurance-review-workspace/assurance-review-workspace.component.spec.ts
3.10e Compliance handoffs keep framework and record context; production follows owner readiness (CCP-031)
Framework identity (features/compliance/shared/framework-context.ts):
- A compliance page belongs to the framework named by its
/compliance/<nis2|dora|cra>/...route. Handoffs carry their origin asreturnTo: the portfolio overview sends/compliance, a framework cockpit sends/compliance/<framework>. The overview never substitutes a default framework. - The DORA/CRA review workspaces and the EU Declaration of Conformity honour a
returnToin their own framework or outside any single framework (the overview, another console surface). AreturnTointo a different framework, and a missing one, fall back to the page’s own framework: the review workspace to/compliance/<framework>, the declaration to the CRA technical-documentation review. The review’s link to the declaration carries the review URL (with its ownreturnTo) as the declaration’sreturnTo.
Incident handoffs:
- Notify’s countdown item
detailsHrefis an owner API query (/api/v1/regulatory/reporting-timelines?...), not a console route, and is not used as a link. A live reporting clock routes by the item’sregimeandincidentId: NIS2 to/compliance/nis2/incidents?incidentId=…, DORA to/compliance/dora/review?incidentId=…, CRA to/compliance/cra/product-security/review?incidentId=…. A regime without a console workflow renders as plain text. - The NIS2 incident page shows only the named incident from Notify’s active operational read, or states that the read did not return it (closed incidents and imported demonstration timelines are not listed); it never shows another incident in its place. The DORA/CRA review workspace names the incident and states that the console has no per-incident view for that framework.
- An incident card’s Proof trail links to the Chain of Custody only for an artifact digest carried by an affected-service ref. Reachability refs carry graph digests, not the affected artifact, and are not used. Without a digest the trail says so; there is no unfiltered Chain or Estate link.
- Demonstration timelines do not reach these links: the countdown is operational-only and the incident list reads Notify’s operational default (CCP-029). No console read in these pages requests
provenance=all.
Production controls:
- A review-workspace produce card, the declaration’s produce actions and the NIS2 effectiveness page’s monthly bundle action render only when the owner’s export readiness was read and reports the profile ready (
signedExportReadyand notblocked), which is the owner’s own produce gate for every request the console sends. Otherwise the page states the actual missing prerequisite: the unread readiness with the owner error, or the owner’s blocking reason codes and summaries. A CLI-produced DORA profile also shows itsstella …command as the supported path. The owner gate and theexport.operatorscope remain enforced server-side. - The technical-documentation review’s link to the EU Declaration of Conformity does not depend on the
cra.technical-filereadiness; the declaration page states its own production prerequisites.
Verification coverage:
src/Web/StellaOps.Web/src/app/features/compliance/shared/framework-context.spec.tssrc/Web/StellaOps.Web/src/app/features/compliance/assurance-frameworks/assurance-frameworks-page.component.spec.tssrc/Web/StellaOps.Web/src/app/features/compliance/assurance-review-workspace/assurance-review-workspace.component.spec.tssrc/Web/StellaOps.Web/src/app/features/compliance/cra-declaration-of-conformity/cra-declaration-of-conformity-page.component.spec.tssrc/Web/StellaOps.Web/src/app/features/compliance/nis2-incident-dashboard/nis2-incident-dashboard-page.component.spec.tssrc/Web/StellaOps.Web/src/app/features/compliance/nis2-effectiveness-dashboard/nis2-effectiveness-dashboard-page.component.spec.tssrc/Web/StellaOps.Web/src/app/routes/compliance.routes.spec.ts
3.11 Actor Identity Consumer (Sprint 20260519_079)
The console renders actor identity through the redaction-aware ActorIdentityProjection instead of reading actor PII (name/email) directly from per-service audit rows. This guarantees the GDPR Art. 17 erasure shape can never be silently missed in the UI.
- Models:
src/Web/StellaOps.Web/src/app/core/api/actor-identity.models.tsActorIdentityProjection(active / erased / unknown),ActorIdentityErasurediscriminator, and theactorIdentityShape()helper.
- Client:
src/Web/StellaOps.Web/src/app/core/api/actor-identity.client.tsActorIdentityHttpClientcallsGET /api/v1/platform/actor-identity/{actorRef}(Platform), sending the canonical tenant header. A404(cold cache) narrows tonull; the unknown projection defaults the region tounspecified.
- Reusable badge:
src/Web/StellaOps.Web/src/app/shared/components/actor-identity-badge.component.ts<stella-actor-identity-badge>renders the three canonical shapes. Active actors show name + email; erased actors show the required discriminator chip plus the erasure timestamp and never render PII; unknown actors show an opaque truncatedactorRef. Accepts a pre-resolved[projection]or an[actorRef](which it lazily resolves via the client).
- Consumer integration: the unified audit log table (
src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.ts) renders the actor cell through the badge, projectingpiiRedactedAtinto the erased shape.
Backend read endpoint: src/Platform/StellaOps.Platform.WebService/Endpoints/ActorIdentityEndpoints.cs projects a tenant-scoped actor ref via ActorIdentityRedactor. It authorizes on the any-of ActorIdentityRead policy — ui.read (any authenticated console user) OR platform:sar:read (a SAR-privileged caller). It was previously gated on the GDPR platform.subject_access.read scope alone, which the seeded admin does not hold, so every actor rendered as an unattributed raw GUID (Fable audit i5 #17).
Verification coverage:
src/Web/StellaOps.Web/src/app/core/api/actor-identity.client.spec.tssrc/Web/StellaOps.Web/src/app/shared/components/actor-identity-badge.component.spec.tssrc/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.spec.ts
See docs/modules/policy/data-handling.md for the read-path redaction shape and the SAR Ed25519 DSSE signature contract.
3.11A Module audit reads
The Console reads module-filtered events from Timeline at GET /api/v1/audit/events?modules=<module>. Authority’s administration audit and Policy’s governance audit retain their directly served read contracts. Failed reads propagate to the audit surface’s error state; they do not become empty successful pages.
The Risk Engine filter remains available as Risk Engine (historical) for records written before that host was withdrawn. It queries Timeline’s retained riskengine facet. The unhosted integrations audit prototype and unused per-service fan-out and instance-audit helpers were withdrawn in PCD-325; module audit reads use the owner route above.
PCD-413 removes BinaryIndex from the client filter contract because Timeline has no supported binaryindex query facet and no BinaryIndex audit producer was verified. The client reports unsupported runtime module inputs as errors without issuing HTTP requests. This does not remove owner data: unfiltered event module names and module-statistic keys remain raw strings, and unknown names display through the existing text fallback. No alias or historical-data claim is added.
Verified against e8c66e5967 (2026-10-06). Re-verify the client and raw-row contract from src/Web/StellaOps.Web:
npx vitest run --config vitest.codex.config.ts src/app/core/api/audit-log.client.spec.ts src/app/features/audit-log/audit-log-table.component.spec.ts src/app/shared/components/audit-module-events/audit-module-events.component.spec.ts
npx tsc --noEmit -p tsconfig.app.json
npx tsc --noEmit -p tsconfig.spec.json
Verification: audit-log.client.spec.ts exercises the module routes and a 503 response, with the adjacent audit surface and shared module-reader specs.
Deployment run audit evidence
Verified against f8605955b2 (2026-10-06).
The run shell’s evidence tab uses AuditLogClient to read Timeline at GET /api/v1/audit/events, filtered by modules=release,jobengine, resourceType=deployment, and the run GUID, with an initial limit of 50. These are the deployment emitter facets; the topology facet is different. The shell preserves owner event IDs, actions, actor IDs, timestamps, ordering, and correlation IDs. Missing owner correlation IDs remain absent.
The tab reports the number of entries loaded, preserves the owner cursor, hasMore and optional totalCount, and reports when more entries exist. It displays the initial bounded page with no continuation control. Failed reads display an unavailable state with technical details and a Retry audit control; they do not display a successful zero count or empty table. The retry subscription uses the component’s injected DestroyRef.
Re-verify from src/Web/StellaOps.Web:
node node_modules/vitest/vitest.mjs run --config vitest.codex.config.ts src/app/features/workflow-visualization/services/run-visualization-shell.service.spec.ts src/tests/workflow_visualization/run-graph-replay-page.behavior.spec.ts src/app/core/api/audit-log.client.spec.ts
node node_modules/typescript/bin/tsc --noEmit -p tsconfig.app.json
node node_modules/typescript/bin/tsc --noEmit -p tsconfig.spec.json
The assurance operator fixture uses a canonical run GUID and actual normalized deployment audit rows. With a locally served production bundle, set PLAYWRIGHT_BASE_URL to that server and PLAYWRIGHT_USE_RUNNING_SERVER=1, then run node node_modules/@playwright/test/cli.js test tests/e2e/assurance-operator-scenarios.spec.ts --workers=1 --reporter=line. The fixture validates every owner audit request and records a per-scenario request census. Unknown API requests and external network fallback fail the scenario; explicitly unavailable supporting reads return 503. This is local fixture acceptance, not a deployed Console or real session/grant claim.
Lineage owner API boundary
Verified against 683d6efaa8 (2026-10-06).
LineageGraphService reads SbomService’s lineage, compare and artifact-link contracts. The unused CGS proof, replay and verdict-build helpers were removed by PCD-412. CGS fields are display metadata only: Evidence reads verdicts by verdict ID and does not map those replay/build operations. Badge replay remains disabled unless a future owner-backed workflow is accepted. The adjacent ExplainerService only formats supplied explanation data for clipboard export; its unused explanation-fetch and replay operations were removed after a semantic reference audit found only test callers.
Re-verify from src/Web/StellaOps.Web:
npx vitest run --config vitest.codex.config.ts src/app/features/lineage/services/lineage-graph.service.spec.ts src/app/features/lineage/services/explainer.service.spec.ts
npx tsc --noEmit -p tsconfig.app.json
npx tsc --noEmit -p tsconfig.spec.json
Local production-bundle accessibility harness
Verified against helper source 617dca0cf3 (2026-10-06). The production-bundle browser workflow uses an explicit-root loopback server, one immutable bundle and owned lifecycle receipts. Missing JavaScript and owner API paths are refused rather than served as index HTML. Document fallback is restricted to explicit UI route prefixes.
PCD-415’s current production-bundle UIF-6 run passes all 32 cases with the existing 15-second splash deadline, Axe severity threshold and both-theme keyboard/focus assertions. Original and current releases harnesses also pass on the same bundle. Controlled queue saturation and a deliberately stopped server are separate negative controls; the exact cause of the retained PCD-321 browser refusal remains unproven. This is local fixture proof, not a deployed capability or real grant/session claim.
Lineage graph keyboard inspection
Each lineage node is a keyboard button. Focus opens its details and links the node to the current tooltip with aria-describedby; the tooltip announces changes and contains read-only date, digest, counts and diff details. Escape or focus loss closes the description. Counts remain owner-reported fields, with absence preserved.
Arrow keys move to the nearest node in the requested direction. Focus pans the graph when its target would be clipped, preserving zoom and converting the screen movement to SVG translation units. Nodes, edges and view bounds use the service’s assigned layout coordinates, so a horizontal chain remains separately navigable.
The timeline track has slider semantics and keyboard stepping. Its playback controls are siblings, and the speed selector has an accessible name. The lineage route joins the UIF-6 Playwright gate for headings, Axe findings and named visible keyboard focus in both themes. The crawl includes SVG node controls, and a retained scenario inspects nine nodes through a zoomed graph on the production build with local API fixtures.
3.12 Audit-bundle content-verification boundary
The evidence-bundle wizard treats Completed as job lifecycle state, not proof that every requested evidence producer emitted an artifact. Its status read preserves ExportCenter’s typed producerGaps and renders each gap as attributable missing evidence. The Console never synthesizes placeholder support for VEX, policy-evaluation, or attestation producers that the backend reports as unimplemented or unavailable. A producer gap also overrides a contradictory positive path-presence result, keeping the combined state fail closed.
On download, the Console reads GET /v1/audit-bundles/{bundleId}/index and cross-checks every indexed artifact, VEX-decision, and attestation path against the exact ZIP central directory. A positive category-presence result requires a typed index reference with a sha256 digest and a matching non-directory, non-empty ZIP entry. Missing or incomplete index metadata, bundle-id drift, invalid EOCD/central-directory bounds, and missing/empty indexed paths produce an explicit unverified state. Filename patterns are not evidence. Selecting zero categories produces a neutral “No evidence categories were selected” state, never a verification success.
This browser check verifies indexed presence only; it does not inflate entries or recompute their content digests. The archive remains downloadable for manual or offline cryptographic verification, with that limitation stated beside the result.
Verification coverage:
src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.spec.tssrc/Web/StellaOps.Web/src/app/features/triage/zip-inspect.spec.tssrc/Web/StellaOps.Web/src/app/features/triage/triage-audit-bundle-new.component.spec.ts
4) Authentication: operator decision signing
Sign-in, session, scopes, route guards and interceptors are described in ../ui/architecture.md§4. This section keeps the decision-signing contract.
- Operator decision-signing device keys (WS7 / ADR-025):
core/auth/decision-signing/DecisionSigningKeyServicegenerates browser-held non-extractable WebCrypto keys for NIST algorithms (ES256,ES384,ES512, plus RSA variants) and persists theCryptoKeyhandles in a dedicated IndexedDB database. Only the SPKI public key PEM and sha256 fingerprint are exportable for Authority enrollment; private key material is never exported or sent to the server. Browser signing is permanently NIST-only: SM/GOST/eIDAS-QES operators must be routed to the CLI/smartcard flow. Live decision signing must sign server-providedoperator-decision@v1PAE bytes verbatim; the Web client must not canonicalize the predicate itself.
Operator Decision-Signing UX
The Console exposes operator decision-signing in two places:
/administration/profilecontains the in-browser enrollment panel for NIST device keys. It routes to the realConsoleProfileComponent— which hostsDecisionSigningEnrollmentPanelComponent— in theadministrationblock ofapp.routes.ts. Do not send operators to/console/profile: that path loadsInsufficientPermissionsComponent(title “Insufficient Permissions”,app.routes.ts) and is the failure target thatrequireScopesGuard/requireAnyScopeGuardredirect to — an operator following it lands on an access-denied page instead of the enrollment panel. (The rest of theadministration/*block is legacyredirectToshims; this child is a real, non-redirect route.) On that page the operator chooses an issuer namespace and a browser-supported NIST algorithm, completesFreshAuthService.requireFreshAuth('Enroll a decision-signing key'), then uploads only Authority-compatible public DSSE key material. The private key is non-custodial: it stays as a non-extractable IndexedDBCryptoKey. If the device is lost, the operator enrolls a new key; Stella cannot recover the old private key. WhenrequireAuthGuardforced the operator here (pending enrollment and theauthority:signing-keys.enrollscope), the URL carriesissuer,providerand the interrupted page asreturnUrl. After an enrollment or rotation returns anActivekey, the panel navigates tosanitizeReturnUrl(returnUrl); the guard then re-reads Policy’s operator-key status and lets the operator through. APendingApprovalkey, an off-originreturnUrlor areturnUrlthat points back at Profile leaves the operator on Profile.- Release approvals and Policy exception approve/reject actions open
DecisionSigningModalComponentwhen the API metadata says signing is required. The modal starts with the fresh-auth step-up, signs the server-prepared PAE bytes throughOperatorDecisionSigningService, submits the DSSE envelope, and renders the verifiedkeyid, algorithm, envelope digest, and “signed by” identity returned by the flow.
The browser algorithm matrix is fixed by WebCrypto capability, not by server verification support:
| Region / algorithm family | In-browser enrollment/signing | Routing |
|---|---|---|
NIST ECDSA ES256 / ES384 / ES512 | Supported when the browser exposes the curve. ES256 is the default. | Console enrollment and signing modal. |
RSA RS* / PS* | Supported only where WebCrypto exposes the required key generation/signing algorithm. | Console enrollment where feature detection passes; otherwise CLI. |
ED25519 / EdDSA | Not reliable across WebCrypto implementations. | CLI. |
| SM2 / SM3 | Not supported by WebCrypto. | CLI or smartcard/plugin. |
| GOST12-256 / GOST12-512 | Not supported by WebCrypto. | CLI or smartcard/plugin. |
| eIDAS QES / qualified certificates | Not supported by WebCrypto. | CLI plus QSCD/smartcard path. |
For non-NIST metadata (providerHint = sm, gost, or eidas), the modal does not call WebCrypto or submit a browser-produced envelope. It renders the exact CLI command shape (stella release approve <id> --sign ... or stella exception approve|reject <id> --sign ...) so the operator follows the regional signing path.
The Console must never build the live operator-decision@v1 payload or PAE by canonicalizing JSON locally. The backend is the canonicalization authority because its CanonJson output is not browser-reproducible. The only FE-side PAE construction is the offline conformance regression that feeds the frozen WS3 canonical payload bytes into dssePreAuthEncode and compares them with the fixture’s expected PAE bytes.
5) Configuration
Runtime configuration is described in ../ui/architecture.md§5. Build-time flags in src/environments/environment.ts / environment.prod.ts carry no hostnames or secrets.
5.1 Standalone console image
devops/docker/Dockerfile.console builds the SPA and also configures Nginx with same-origin API proxy locations (service-owned routes such as /api/v1/release-orchestrator/ before the generic /api/ Platform catch-all, and enlarged upstream header buffers because the Authority authorize response for the full Console scope set exceeds Nginx’s default). Neither the dev compose estate nor the release bundle runs this Nginx: the dev estate uses the image only through the console-builder init container, and the release bundle bakes the dist into the router gateway. Keep the proxy rules consistent with the gateway routes if the image is run standalone.
6) Signed Score + Vulnerability Detail Contracts (Sprint 20260304_309)
Delivered contracts:
src/Web/StellaOps.Web/src/app/features/security/vulnerability-detail.facade.ts- Single API-backed facade for vulnerability detail loading and signed-score verification.
- Consolidates route/malformed/not-found handling for both Security and Security-Risk route trees.
src/Web/StellaOps.Web/src/app/features/security/vulnerability-detail-page.component.ts- No static CVE payloads. Reads route id and renders deterministic loading/error/not-found states.
- Uses API-backed fields for CVSS/EPSS/KEV, environment impact, gate impact, and witness path.
src/Web/StellaOps.Web/src/app/features/security-risk/vulnerability-detail-page.component.ts- Uses the shared Security vulnerability detail view; no placeholder text-only implementation remains.
src/Web/StellaOps.Web/src/app/shared/components/score/signed-score-ribbon.component.ts- Reusable signed-score ribbon for vulnerability and triage detail contexts.
- Supports collapsed/expanded factor breakdown, provenance links, verify action, and policy gate badge (
pass|warn|block). - Reuses existing shared score primitives (
ScorePillComponent,ScoreBadgeComponent) instead of duplicating score visuals.
Scanner replay route contract (Web client):
- Implemented by
src/Web/StellaOps.Web/src/app/core/api/proof.client.ts(ScoreReplayClient). - Canonical paths:
POST /api/v1/scans/{scanId}/score/replayGET /api/v1/scans/{scanId}/score/bundlePOST /api/v1/scans/{scanId}/score/verifyGET /api/v1/scans/{scanId}/score/history
- Compatibility aliases remain backend-side (
/api/v1/score/{scanId}/...) while clients migrate, but Web now uses canonical scanner routes.
Coverage:
src/Web/StellaOps.Web/src/app/core/api/proof.client.spec.tssrc/Web/StellaOps.Web/src/tests/sprint309/signed-score-ribbon.component.spec.tssrc/Web/StellaOps.Web/src/tests/sprint309/security-vulnerability-detail-page.component.spec.tssrc/Web/StellaOps.Web/src/tests/sprint309/security-risk-vulnerability-detail-page.component.spec.ts
Remaining planned FE capability (explicitly still planned):
- Signed-score ribbon integration into additional triage detail canvases beyond vulnerability detail routes (not in sprint 309 scope).
7) Self-Evident UI source contracts (SPRINT_20260713_011, 2026-07-22)
The 2026-09-09 AAI-6 acceptance repair keeps each dashboard security-queue read behind its own owner permission. VEX pending decisions require vex:read; active Policy exception totals require policy:read and come from GET /api/policy/exceptions/counts. The dashboard does not fetch Findings summaries to calculate that total. A failed or malformed count remains unavailable.
Health metadata, retries, and quick/full diagnostic actions use the current Doctor source’s scope (ops.health for the registry source, doctor:run for the Doctor source). Missing permission disables initial runs and retained-result reruns with a reason and prevents the request. The Feeds & Vuln Data landing preserves query/fragment context and selects source freshness for advisory readers or Scanner operations for Scanner-only readers, without widening either destination’s permissions.
The Scanner audit tab separately requires timeline:read before mounting the shared Timeline event reader. Scanner read or administration scope alone leaves the audit stream unavailable with an explicit explanation, while other Scanner tabs remain available according to their own permissions.
The full four-persona gate waits for each isolated action page to finish loading, bounds reads of volatile controls, and retains partial phase/action evidence if a route reaches its deadline. Partial evidence always leaves that route failed.
Dynamic glossary annotation preserves unchanged visible controls when asynchronous copy elsewhere in the route adds or changes glossary vocabulary. Their DOM identity, keyboard focus and open definition remain stable; a removed, changed, hidden or out-of-scope annotation releases its listeners before replacement. New duplicate mentions do not move an existing control.
Glossary decoration never replaces or freezes the task text (CCP-041). The directive keeps the text node the owning template binds, shortens it to the copy before the first term, and inserts the term controls and the remaining copy after it. Decoration follows the copy the owner currently renders, in static and dynamic mode alike: when the owner writes new copy into a bound text node, the inserted nodes of its previous copy are released and the new copy is decorated; when the owner moves a decorated node, its decoration is rebuilt; when the owner removes it, the inserted nodes are removed with it. One limit applies to static mode: a text node the owner adds after the first render stays undecorated; dynamic mode (observeMutations) decorates it. A page header therefore shows the selected dashboard persona’s title and subtitle together, with one control per term. Glossary hosts can nest (the AppShellComponent scan covers routed pages that carry their own hosts): an instance decorates only text that no other instance has decorated or inserted, so the shell scan never shortens a nested host’s copy. A term control is named by its visible text, so a heading or sentence that contains a term reads as written; “Show glossary definition” is its description while closed (one shared hidden element) and the definition is its description while open. data-glossary-term records the glossary entry for specs and the browser audit harness.
Tab help agrees with tab state (CCP-041): stella-page-tabs and app-tabbed-nav expose “This tab is unavailable for the current record or permission set.” only on a disabled tab. Active and enabled tabs carry no such help, disabled tabs are not keyboard stops, and a route tab of app-tabbed-nav reports the active route as its selected state (aria-current="page" in the submenu variant). The button tabs of app-tabbed-nav form one roving tab stop (the active enabled tab, or the first enabled tab), ArrowRight/ArrowLeft/Home/End move focus and selection over the enabled tabs, and the active button of the submenu variant carries aria-current="true". The component renders no tab panels, so aria-controls is left to the consuming page; its route-tab variant (no current consumer) keeps the natural tab order.
Expired approval rows use the paired error-surface text token for linked identity and metadata, so all supported themes retain readable text on the row’s error background.
Standing, spec-enforced contracts added by the W0–W7 source waves. Each is manifest-driven — the enforcement re-derives its scope from the live nav config / route tables on every run, so counts never freeze in prose.
- Canonical feed freshness (W0.5) — superseded 2026-08-11 (
SPRINT_20260804_002FCR-1); kept as the record of the rule, not of the code.src/app/core/api/feed-freshness.models.tswas the one client-side model of Concelier’sFeedFreshnessProjection, and the surfaces named here (Intelligence Advisory Sources, the Advisory & VEX catalog, Security overview, VulnCatalog coverage) rendered it verbatim. Concelier’s projection died with the G1 cutover and none of those surfaces reads it today: the model andderiveSourceIngestionStateare still on disk but no longer reachable from any route. The rule survives unchanged and is now enforced further upstream — the successor vocabulary (src/app/core/api/vuln-source-generations.models.ts, stateshealthy | ingesting | empty | stalled | never-completed) is computed by the Vulnerabilities hub and rendered verbatim by the Advisory & VEX sources page and the dashboard feed lens, so a competing state vocabulary is not merely discouraged, it has nowhere to be derived. - State contract (W1, D4) —
src/app/core/navigation/state-contract.tsclassifies every unique-route nav destination (mechanical / bespoke-reviewed / static / missing);src/app/core/navigation/empty-state-copy-contract.tsgives every current destination reviewed teaching-empty copy plus a real fill-path CTA. The route-aware page-help loader supplies it only when a legacy caller omits or uses generic copy, so explicit page-authored empty states and actions remain authoritative.src/tests/navigation/state-contract.spec.tsresolves each route to its live rendered component tree viasrc/app/testing/route-component-resolver.ts(lazy thunks awaited, redirects followed, guards not executed) and proves a sanctioned mechanism (stella-bounded-loading/stella-empty-state/stella-error-panel/ adata-state-panestamp) in the transitive closure. It also rejects missing/orphan/duplicate or filler copy entries, non-manifest and retired Workers CTA targets, and CTA routes that do not resolve. Ratchets both ways; a non-resolving destination fails as the blank-route class. - Acronym first use (W2, D5) —
src/tests/navigation/acronym-first-use.spec.tsscans each destination’s compiled template corpus for registry acronyms (plain-language.service.tsentries with an abbreviation); coverage = page-wiredGlossaryTooltipDirective, a shell-annotatedpage-headerblock, or inline expansion. The report is empty and enforced to stay so. - Disabled-reason contract (W3, D8) —
src/tests/ux/disabled-reason-contract.spec.tsscans all templates for interactive disabled bindings without an on-element reason (stellaDisabledReason, boundtitle,aria-describedby); transient busy/validity disables are excluded by documented heuristic. The pinned per-file debt ledger only shrinks. The sealed-mode override Create control explains that Policy override writes are unavailable. Symbol catalog pagination explains the first-page and exhausted-catalog boundaries on its disabled buttons. The adjacent component specs verify these explanations reach the native controls (PCD-286, 2026-10-05). - Naming contract (W4.1, D7) — the alias table in
src/app/core/navigation/naming-contract.tscarries zero owed renames; every remaining alias is a declared, reviewed divergence. - W7 persona consumers —
security-findings.client.ts#getCveExposure(Exposure CVE verdict banner),estate.client.ts#getUnprovenCells(Estate named-gap panel), andrelease-blockers.client.ts(dashboardreleaseBlockerslens) render their producers’ bounded projections verbatim, including explicit absence states (not_recorded,unassigned, “no owner assigned”); nothing is inferred client-side. Estate renders the unproven-cell producer’s stored release/environment owner ids and teams. When that projection is empty, the row statesno owner assignedand points to the release/environment Owner-grant fix instead of hiding the gap.
8) Vulnerability-plane cutover: what came back and what did not (SPRINT_20260804_002, 2026-08-11)
The G1 cutover withdrew the Concelier/Excititor plane and eight Console capabilities with it. Three were ruled rebuild-later and are settled here; the rest died with successors named in the G1 evidence table (docs/implplan/_evidence/2026-08-04-sprint-008-vuln-g1-window.md §4.3).
8.1 Source freshness — REBUILT on the hub (FCR-1)
/ops/integrations/advisory-vex-sources and its /setup twin render SourceFreshnessComponent against GET /api/vulnerabilities/v1/sources (advisory:read, the same scope the route already guarded). One card per source, ordered worst-first, carrying the hub’s own health word. The dashboard Feed Status lens reads the same endpoint through rollupHubSources, so the two surfaces cannot report different numbers for the same estate.
Two states exist because a “last run succeeded” view hides them, and both are live on the lab estate as of 2026-08-11:
| State | Meaning | Rendered |
|---|---|---|
never-completed | no generation has ever completed | red; “none in N runs”, never “0 documents” |
empty | a run closed CLEANLY having ingested nothing | amber, not green |
empty is the dangerous one. osv sat at 26 completed generations with docs_seen = 0 for days because its ecosystem list was unset (SPRINT_20260804_001 CRED-2) — a red source gets noticed, a green source that ingests nothing does not, and under D4 a completed Full generation is what may authorize absence-tombstones.
8.2 Source administration — STAYS CONFIG-DRIVEN (FCR-2)
Decision: no operator-facing source-administration surface returns. The retired catalog’s write half (enable/disable, per-source endpoint editing, VEX provider registration, OCI trust policy, OpenVEX configuration, mirror management) is not rebuilt. Measured basis:
- A UI could not add a source anyway. Connectors are typed and dispatch through a compiled registry (
NormalizationPump.KnownSources); a source with no connector in the binary cannot be created from a browser. The only writes a UI could offer are toggling an existing connector and editing its endpoints. - Those two writes are already the deployment contract.
Vulnerabilities:Ingestion:Scheduler:EnabledSourcesplus per-source endpoint keys, applied at worker start. Moving them into a mutable store buys a second source of truth that can disagree with the compose file, and a new write scope, in exchange for nothing the config file does not already do reviewably and reproducibly. - Air-gap posture argues the same way. A self-hosted operator must be able to change source configuration without a browser session; config is that path, and a Console-only editor would make the reproducible path the second-class one.
- The observability need is met without writes. Every source failure the retired catalog surfaced — dead upstream, misconfiguration, silent no-op — is visible in §8.1’s read surface, which is what operators actually act on.
Reversible if a concrete operator need appears that config cannot serve; the argument above is what such a proposal has to defeat.
8.3 Connector credentials — NO INDICATOR (FCR-3)
Decision: no “which sources have credentials configured” indicator. /console-admin/connector-credentials keeps its retired state and renders no credential value, reference, or metadata (§2.12).
Measured against src/Vulnerabilities/ on 2026-08-11: the hub’s entire ingestion tree contains one credential-bearing setting, NvdSourceOptions.ApiKey, and it is a rate-limit raiser (5→50 req/30 s), not an access requirement. NVD ingests without it — 27 completed generations and 377,256 documents on the lab estate with no key configured. Zero of the eight configured sources require a credential: SPRINT_20260804_001 removed the last two, repointing cve at the credential-free CVE List v5 bulk distribution and retiring ubuntu-csaf onto OSV’s Ubuntu ecosystem.
So the indicator would render “0 of 8 sources have credentials configured” and stay that way — a badge nobody can act on, on a page whose only remaining risk is leaking the thing it is describing. If a missing NVD key ever matters it shows up as throttled ingest in §8.1’s generation timings, which is the signal an operator can actually use.
Revisit only if a connector is added whose source genuinely cannot be read without a credential.
Evidence
| Claim | Source |
|---|---|
| Section numbers cited from source | src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-sources-retired.component.ts (§8.2), src/Web/StellaOps.Web/src/app/features/console-admin/connector-credentials/connector-credentials-retired.component.ts (§8.3) |
| Route access contract | src/Web/StellaOps.Web/src/app/core/auth/route-access.ts |
| Findings runtime help links | src/Web/StellaOps.Web/src/app/features/findings/detail/runtime/runtime-empty-state.component.ts and its adjacent spec; asset mapping in src/Web/StellaOps.Web/angular.json; static serving in src/Router/StellaOps.Gateway.WebService/Middleware/RouteDispatchMiddleware.cs |
| VEX gate inline actions | src/Web/StellaOps.Web/src/app/features/vex_gate/ |
| Audit reason capsule | src/Web/StellaOps.Web/src/app/core/api/audit-reasons.client.ts, src/app/features/triage/components/reason-capsule/ |
| Pack registry browser | src/Web/StellaOps.Web/src/app/features/pack-registry/ |
| Reachability witness surface | src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts |
| Graph overlays | src/Web/StellaOps.Web/src/app/features/graph/graph-overlays.component.ts, graph-canvas.component.ts |
| Asset inventory | src/Web/StellaOps.Web/src/app/core/api/asset-inventory.client.ts, src/app/features/inventory/inventory-page.component.ts |
| Compliance profile selector | src/Web/StellaOps.Web/src/app/features/settings/crypto-providers/crypto-provider-panel.component.ts, src/app/core/api/crypto-provider.client.ts |
| NIS2 incident dashboard | src/Web/StellaOps.Web/src/app/core/api/nis2-incident-dashboard.client.ts |
| Actor identity | src/Web/StellaOps.Web/src/app/core/api/actor-identity.client.ts, src/Platform/StellaOps.Platform.WebService/Endpoints/ActorIdentityEndpoints.cs |
| Operator decision signing | src/Web/StellaOps.Web/src/app/core/auth/decision-signing/, administration/profile route in src/app/app.routes.ts |
| Standalone console image | devops/docker/Dockerfile.console; estate use in devops/compose/docker-compose.stella-services.yml (console-builder) and devops/release/bundle/tools/build-and-publish.sh |
| Signed score / vulnerability detail | src/Web/StellaOps.Web/src/app/features/security/vulnerability-detail.facade.ts, src/app/core/api/proof.client.ts |
| Self-evident UI contracts | src/Web/StellaOps.Web/src/app/core/navigation/state-contract.ts, empty-state-copy-contract.ts, naming-contract.ts, src/tests/navigation/ |
| Source freshness (hub) | src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/source-freshness.component.ts, src/app/features/integration-hub/integration-hub.routes.ts |
AdvisoryAI SBOM object navigation (PCD-309, 2026-10-05)
An SBOM reference carries an artifact digest and component package URL. Following it opens /security/images/sbom?digest=<digest>&purl=<package-url>. The image view reads the digest’s inventory from Findings and displays exact package-URL matches; an absent reference is reported explicitly. Removing purl shows the complete returned image inventory. Query-only navigation updates the selected component. Loading and failed reads remain distinct from an absent component or stored SBOM. The general supply-chain explorer retains its service filter.
Current Findings reads accept the SBOM document digest. OCI-image to document identity requires the separately tracked Findings projection work; missing owner/reachability projections remain explicit refusals. A displayed component is inventory evidence, not a claim that vulnerability matching or reachability is complete.
