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.

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 …”).

SurfaceMetricUnitOwner scopeSource setObservation time
Dashboard custody stripSource, Build, Scan, Verdict, Decision, Deploy, Watchdeployed digest with the stage verified or recorded, out of the deployed digests (“N of M”); gaps (missing or unverifiable) and unreadable digests named apartTenant-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 unreadablechain projection evaluatedAt
Dashboard custody stripWatch driftrunning container on a digest never deployed through Release OrchestratorTenant-wide (agent-keyed)the tenant’s agent drift records, inside the chain readchain projection evaluatedAt
Dashboard estate summaryrows × environment columnsservice (recorded serviceId), or release with no recorded service identityHeader scope (region and environment)the Deploy page above; a coverage clause when partialdeployments read time
Dashboard Overview “Open items”Pending approvals / Agents reporting driftapproval / observing agentTenant-wide (stated in the item)completeas above
Dashboard Overview “Open items”Critical findings / Tampered evidence / Updates availablefinding / deployed cell / owned releaseHeader 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 partialas above
Dashboard role queue (Operations)Pending approvals; failed deployments; agents reporting driftapproval; estate cell whose latest deployment failed (the Estate page’s Failed door); observing agentTenant-wide; header scope; tenant-wide (each stated on the group)complete; the Deploy page above with its coverage clause; completethe read time of each group’s owner read
Dashboard role queue (Security)Promotions blocked by reachable risk; critical findingspending approval with a failed gate naming a reachable vulnerability; findingTenant-wide; header scopecomplete approvals read, first 5 rows, no destination lists the rest (stated on the group); complete aggregateapprovals read time; aggregate read time
Dashboard role queue (My work)Releases you own with a newer component versionowned releaseOwned releases, header scopefirst 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 restowned-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 exceptionsrunning 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 exceptionTenant-wide (a developer principal: its owned services and environments); tenant-wide Policy count, bound to no digestthe 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 countchain projection evaluatedAt; counts read time
Dashboard detailPriority findings; security queuefinding; VEX decision under investigation and active exceptionTenant-wide (stated on the card; Priority findings also says the findings explorer it links to applies the header scope)“showing N of total”; owner countslens read time
Dashboard detailPromotion readiness; Why is my release blocked?; Releases this weekenvironment × gate; blocked or warned release; shipped and blocked releaseTenant-wide (stated on the card)complete environment list; first 50 with the total (“Showing the first N”); complete weekly totalslens read time
Dashboard detail (IT)Assurance posture; Evidence & attestationframework readiness; evidence packetTenant-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 partiallens read time
Dashboard detail (Developer)Your builds & SBOM scansimage of an owned service’s latest release (component digest) with Scanner’s latest attempt and last completed scanHeader 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 tokenevery 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 namedlens read time
ExposureAggregate railfinding by severity, overall scoreTenant-widecomplete server aggregateserver computedAt
ExposureWhat mattersfindingTenant-widetop 200 by severity, paged client-side; “top N of total”dataset read time (UTC)
ExposureBy CVE / release / environmentCVE, 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 countdataset read time (UTC)
ExposureVulnerability lookupverdict for one identifierTenant-wide (skipScope)bounded affected-findings sample (stated when truncated)server asOf
EstateMatrix, Services/Releases tile, Unproven, Tampered, Failedservice (recorded serviceId) or release with no recorded service identity (rows); environment (columns, Unmapped named apart); current deployment per cell, other recorded deployments as cell historyHeader scope (region and environment)page walk of up to 10 × 100 with the owner total; “built from the first N of M” when partialdeployments read time
EstateCell chain of custody, broken-chain filterthe 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 viewevery placement of the deployed estatechain projection evaluatedAt
EstateDeviationsdrifting container / reporting agentTenant-wide (agent-keyed, no environment)completedrift read time
EstateUnproven cells (named list)unproven deployed cellTenant-wide ledger readfirst page with total; can differ from the scoped Unproven tileledger read time
Chain of CustodyWatch stagethe placements of the subject digest (environment × digest), worst Watch state firstTenant-wide chain read (a developer principal: its owned services and environments)every placement of the deployed estatechain 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.

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:

The UI behavior remains offline-first and deterministic:

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:

Verification coverage:

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:

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.

Verification coverage:

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:

3.4 Pack Registry Browser (Sprint 20260208_068)

TaskRunner pack operations now include a dedicated registry browser route:

Browser behavior:

Verification coverage:

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:

Surface behavior:

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:

Behavior details:

Verification coverage:

3.8 Active-Surface Verification Lane and Setup Wizard Styling (Sprint 20260405_002 / 005)

Focused verification and bundle-polish notes for the shipped surfaces:

Real-read model and absence-state boundary

3.9 Live Graph Asset Inventory (Sprint 20260430_072)

The Console now exposes a tenant-scoped Asset Registry v1 inventory surface:

Inventory behavior:

Verification coverage:

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:

Verification coverage:

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:

Dashboard behavior:

Verification coverage:

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):

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:

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):

Verification coverage:

3.10e Compliance handoffs keep framework and record context; production follows owner readiness (CCP-031)

Framework identity (features/compliance/shared/framework-context.ts):

Incident handoffs:

Production controls:

Verification coverage:

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.

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:

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:


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 UX

The Console exposes operator decision-signing in two places:

  1. /administration/profilecontains the in-browser enrollment panel for NIST device keys. It routes to the real ConsoleProfileComponent — which hosts DecisionSigningEnrollmentPanelComponent — in the administration block of app.routes.ts. Do not send operators to /console/profile: that path loads InsufficientPermissionsComponent (title “Insufficient Permissions”, app.routes.ts) and is the failure target that requireScopesGuard / requireAnyScopeGuard redirect to — an operator following it lands on an access-denied page instead of the enrollment panel. (The rest of the administration/* block is legacy redirectTo shims; this child is a real, non-redirect route.) On that page the operator chooses an issuer namespace and a browser-supported NIST algorithm, completes FreshAuthService.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 IndexedDB CryptoKey. If the device is lost, the operator enrolls a new key; Stella cannot recover the old private key. When requireAuthGuard forced the operator here (pending enrollment and the authority:signing-keys.enroll scope), the URL carries issuer, provider and the interrupted page as returnUrl. After an enrollment or rotation returns an Active key, the panel navigates to sanitizeReturnUrl(returnUrl); the guard then re-reads Policy’s operator-key status and lets the operator through. A PendingApproval key, an off-origin returnUrl or a returnUrl that points back at Profile leaves the operator on Profile.
  2. Release approvals and Policy exception approve/reject actions open DecisionSigningModalComponent when the API metadata says signing is required. The modal starts with the fresh-auth step-up, signs the server-prepared PAE bytes through OperatorDecisionSigningService, submits the DSSE envelope, and renders the verified keyid, 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 familyIn-browser enrollment/signingRouting
NIST ECDSA ES256 / ES384 / ES512Supported 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 / EdDSANot reliable across WebCrypto implementations.CLI.
SM2 / SM3Not supported by WebCrypto.CLI or smartcard/plugin.
GOST12-256 / GOST12-512Not supported by WebCrypto.CLI or smartcard/plugin.
eIDAS QES / qualified certificatesNot 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:

Scanner replay route contract (Web client):

Coverage:

Remaining planned FE capability (explicitly still planned):

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.

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:

StateMeaningRendered
never-completedno generation has ever completedred; “none in N runs”, never “0 documents”
emptya run closed CLEANLY having ingested nothingamber, 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:

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

ClaimSource
Section numbers cited from sourcesrc/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 contractsrc/Web/StellaOps.Web/src/app/core/auth/route-access.ts
Findings runtime help linkssrc/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 actionssrc/Web/StellaOps.Web/src/app/features/vex_gate/
Audit reason capsulesrc/Web/StellaOps.Web/src/app/core/api/audit-reasons.client.ts, src/app/features/triage/components/reason-capsule/
Pack registry browsersrc/Web/StellaOps.Web/src/app/features/pack-registry/
Reachability witness surfacesrc/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts
Graph overlayssrc/Web/StellaOps.Web/src/app/features/graph/graph-overlays.component.ts, graph-canvas.component.ts
Asset inventorysrc/Web/StellaOps.Web/src/app/core/api/asset-inventory.client.ts, src/app/features/inventory/inventory-page.component.ts
Compliance profile selectorsrc/Web/StellaOps.Web/src/app/features/settings/crypto-providers/crypto-provider-panel.component.ts, src/app/core/api/crypto-provider.client.ts
NIS2 incident dashboardsrc/Web/StellaOps.Web/src/app/core/api/nis2-incident-dashboard.client.ts
Actor identitysrc/Web/StellaOps.Web/src/app/core/api/actor-identity.client.ts, src/Platform/StellaOps.Platform.WebService/Endpoints/ActorIdentityEndpoints.cs
Operator decision signingsrc/Web/StellaOps.Web/src/app/core/auth/decision-signing/, administration/profile route in src/app/app.routes.ts
Standalone console imagedevops/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 detailsrc/Web/StellaOps.Web/src/app/features/security/vulnerability-detail.facade.ts, src/app/core/api/proof.client.ts
Self-evident UI contractssrc/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.