component_architecture_web.md - Stella Ops Web (2025Q4)
Angular 21 frontend SPA for StellaOps console.
Scope. Frontend architecture for Web: the Angular 21 single-page application providing the StellaOps console interface.
Note: For the comprehensive Web UI architecture including all features, see
../ui/architecture.md. This file provides a condensed overview.
0) Mission & boundaries
Mission. Provide an intuitive, responsive web interface for StellaOps operators to manage scans, review findings, configure policies, and monitor system health.
Boundaries.
- Web is a frontend-only application. All data comes from backend APIs.
- Web does not store sensitive data locally beyond session tokens — tokens and PKCE login state live in
sessionStorage(transient, auto-pruned byAuthStorageService), neverlocalStorage. - Supports offline-first patterns for air-gapped console access.
1) Solution & project layout
src/Web/StellaOps.Web/
├─ src/
│ ├─ app/
│ │ ├─ core/ # Cross-cutting singletons
│ │ │ ├─ api/ # Typed HTTP clients (one per backend surface)
│ │ │ ├─ auth/ # OIDC/PKCE/DPoP, session store, guards, interceptors
│ │ │ ├─ config/ # Runtime AppConfigService + config/backend-reachable guards
│ │ │ ├─ context/ # Global tenant/context interceptor + param map
│ │ │ ├─ i18n/ # Locale catalog, translate pipe, date formatting
│ │ │ ├─ navigation/ # Page-title strategy, breadcrumbs
│ │ │ ├─ orchestrator/ # Operator-metadata interceptor
│ │ │ ├─ analytics/ aoc/ branding/ console/ doctor/ guards/ http/
│ │ │ ├─ models/ policy/ services/ telemetry/ testing/
│ │ ├─ shared/ # Reusable UI building blocks
│ │ │ ├─ components/ directives/ pipes/ services/ ui/ utils/
│ │ │ ├─ accessibility/ domain/ overlays/
│ │ ├─ layout/ # App shell, sidebar, topbar, breadcrumb, global search
│ │ ├─ routes/ # Top-level lazy route aggregators (ops, security, releases, …)
│ │ ├─ features/ # 100+ feature areas (dashboard-v3, security, triage,
│ │ │ # evidence, compliance, inventory, policy, graph, …)
│ │ ├─ app.config.ts # ApplicationConfig: router, HTTP, interceptors, DI providers
│ │ └─ app.routes.ts
│ ├─ assets/
│ ├─ environments/ # environment.ts / environment.prod.ts (build-time flags)
│ ├─ styles/
│ └─ tests/ # Vitest spec batches grouped by feature/sprint
├─ e2e/ # Playwright end-to-end specs
├─ scripts/ # Live/e2e harness + guard scripts
├─ angular.json
├─ package.json
└─ tsconfig.json
The
features/tree is far larger than a handful of modules — there are 100+ feature directories. Routing is composed through lazyroutes/*.routes.tsaggregators that import feature components on demand (see §3).
2) Technology stack
- Framework: Angular 21 (
@angular/core ^21.1.2) with standalone components and the signals API - State management: Angular signals + bespoke per-feature stores (e.g.
AuthSessionStore). The app does not depend on@ngrx/*; the only reactive runtime dependency is RxJS (rxjs ~7.8). - UI components: Angular Material (
@angular/material ^21.1.2); Markdown rendered viangx-markdown. - HTTP: Angular
HttpClientwith four DI-registered global interceptors (auth, operator-metadata, tenant header, global context — see §4). - Routing: Angular Router with
loadComponent/loadChildrenlazy loading andcanMatchguard chains;withComponentInputBinding()is enabled. - Build:
@angular/build:application(esbuild) builder; output is emitted todist/stellaops-web/(browser/sub-folder). - Tests: unit/component specs run on the
@angular/build:unit-testVitest runner (not Karma/Jasmine); end-to-end coverage uses Playwright (@playwright/test). - i18n: 8 non-default locales are registered at bootstrap (
bg-BG,de-DE,ru-RU,es-ES,fr-FR,uk-UA,zh-TW,zh-CN) andLOCALE_IDis wired to the user’s selection viaI18nService.
3) Key features
Top-level routes are declared in src/app/app.routes.ts. Each non-trivial branch is gated by the standard canMatch chain [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, <scope guard>] and redirects to /console/profile (Insufficient Permissions) when the scope guard fails.
| Route | Title | Scope guard (any-of unless noted) |
|---|---|---|
'' (root) | Dashboard | auth only — lazy-loads DashboardV3Component |
mission-control | Mission Control | ui.read, release:read, scanner:read, sbom:read |
environments | Environments | ops guard (ui.admin, orch:read, orch:operate, ops.health, notify.viewer, policy:*) — plus merged sub-surfaces, see note |
releases | Releases | release:read, release:write, release:publish, orch:read, orch:operate, policy:read (NAV_ACCESS.promotions) |
developer | Developer Services | all-of release:read, release:write, scanner:read, sbom:read, findings:read |
security | Security | scanner:read, sbom:read, advisory:read, vex:read, exceptions:read, findings:read, vuln:view |
triage | Vulnerabilities | auth only |
evidence | Evidence | evidence:read, release:read, policy:audit, authority:audit.read, signer:read, attest:read, vex:read — plus merged sub-surfaces, see note |
assurance / compliance | Assurance | policy:read, policy:audit, notify.viewer, findings:read, ui.admin |
inventory | Asset Inventory | graph:read |
ops | Operations | ops guard (as environments) |
setup | Setup | ui.admin, orch:read, orch:operate, release:read, trust:read, attest:read, attest:create |
settings | Settings | auth only |
console-admin | Console Admin | ui.admin |
notifications | Notifications | auth only |
docs / welcome | Documentation / Mission | unguarded |
auth/callback, auth/silent-refresh | — | OIDC redirect endpoints (unguarded) |
logged-out | Signed Out | post-sign-out confirmation page (unguarded) |
The table is illustrative —
route-access.tsis the contract (corrected 2026-07-12; the previous version cited two scopes that do not exist). Three traps:
- There is no
health:readscope. The ops guard usesops.health(scopes.ts:163OPS_HEALTH; canonicalStellaOpsScopes.cs:865OpsHealth).health:readappears nowhere insrc/.- There is no
vex:exportscope. The evidence guard usesvex:read.vex:exportis not defined inscopes.tsand is not in the canonical catalog.operationsand the evidence family aremergeAnycomposites, not flat any-of lists: they union the scopes shown plusevidenceAudit,feedsAirgap,telemetryParticipationandfederationInstallation(route-access.ts:124-141). The real accepted-scope set is wider than any table can usefully show — readNAV_ACCESS/ROUTE_FAMILY_ACCESS.
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.
A large block of legacy paths (administration/*, admin/*, platform/*, release-control/*, evidence-audit/*, security-risk/*, jobengine/*, …) is preserved as redirectTo entries (via LEGACY_REDIRECT_ROUTES and inline children) so older deep links keep working after the route taxonomy reorganisation. The wildcard ** route renders NotFoundComponent.
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.
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.
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.2 Signals Runtime Dashboard (Sprint 20260208_072) — NOT IMPLEMENTED (removed)
Status (verified against source): The
features/signals/directory no longer exists insrc/Web/StellaOps.Web. The component, service, and models files cited below are absent, there is nosignals.routes.ts, andapp.routes.tsregisters noops/signalsroute. Only the historical Vitest spec files survive undersrc/tests/signals_runtime_dashboard/. Treat this section as a record of a retired surface, not current behaviour.
Signals runtime operations once included a dedicated dashboard route:
- Route:
ops/signals(removed) - Route registration (removed):
src/Web/StellaOps.Web/src/app/app.routes.tssrc/Web/StellaOps.Web/src/app/features/signals/signals.routes.ts
- Feature implementation (removed):
src/Web/StellaOps.Web/src/app/features/signals/signals-runtime-dashboard.component.tssrc/Web/StellaOps.Web/src/app/features/signals/services/signals-runtime-dashboard.service.tssrc/Web/StellaOps.Web/src/app/features/signals/models/signals-runtime-dashboard.models.ts
Former dashboard behavior:
- Aggregates signal runtime metrics (
signals/sec, error rate, average latency) usingSignalsClientand falls back to gateway request summaries when available. - Computes deterministic per-host probe health snapshots (eBPF/ETW/dyld/unknown) from signal payload telemetry.
- Presents provider/status distribution summaries and probe status tables without introducing network-only dependencies beyond existing local API clients.
Verification coverage:
src/Web/StellaOps.Web/src/tests/signals_runtime_dashboard/signals-runtime-dashboard.service.spec.tssrc/Web/StellaOps.Web/src/tests/signals_runtime_dashboard/signals-runtime-dashboard.component.spec.ts
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/v1/advisory-ai/pipeline/summaryfor analysis plans and/api/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. The removed/api/v1/advisory/**recommendation, task, and similar-vulnerability paths are not used by the triage workspace. - 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.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.5 Pipeline Run-Centric View (Sprint 20260208_069)
Release Orchestrator now provides a unified pipeline run-centric surface that links release status, approvals, deployment progress, evidence state, and first-signal telemetry:
Path note (verified against source): This feature now lives under
features/release-orchestrator/, not the historicalfeatures/release-jobengine/path. Therelease-jobenginedirectory no longer exists. Paths below are corrected to the currentrelease-orchestratortree.
- Route registration:
src/Web/StellaOps.Web/src/app/features/release-orchestrator/dashboard/dashboard.routes.tssrc/Web/StellaOps.Web/src/app/features/release-orchestrator/runs/runs.routes.ts
- Feature implementation:
src/Web/StellaOps.Web/src/app/features/release-orchestrator/runs/models/pipeline-runs.models.tssrc/Web/StellaOps.Web/src/app/features/release-orchestrator/runs/services/pipeline-runs.service.tssrc/Web/StellaOps.Web/src/app/features/release-orchestrator/runs/pipeline-runs-list.component.tssrc/Web/StellaOps.Web/src/app/features/release-orchestrator/runs/pipeline-run-detail.component.ts
- Dashboard integration entry point:
src/Web/StellaOps.Web/src/app/features/release-orchestrator/dashboard/dashboard.component.htmlsrc/Web/StellaOps.Web/src/app/features/release-orchestrator/dashboard/dashboard.component.tssrc/Web/StellaOps.Web/src/app/features/release-orchestrator/dashboard/dashboard.component.scss
Run-centric behavior:
- Normalizes recent releases into deterministic
pipeline-<releaseId>run IDs. - Correlates approvals and active deployments to each run for one-table operator triage.
- Provides per-run stage progression (scan, gates, approval, evidence, deployment) with explicit status details.
- Integrates
FirstSignalCardComponenton run detail pages for first-signal evidence visibility.
Verification coverage:
src/Web/StellaOps.Web/src/tests/pipeline_run_centric/pipeline-runs.service.spec.tssrc/Web/StellaOps.Web/src/tests/pipeline_run_centric/pipeline-runs-list.component.spec.ts
3.6 Reachability Center Coverage Summary (Sprint 20260208_070)
Reachability Center now includes explicit asset/sensor coverage summaries and missing-sensor indicators:
- 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
Coverage behavior:
- Runtime coverage rows start empty because no shipped coverage-row producer is wired; the Console does not seed fixture rows into the shipped workspace.
- Fleet coverage is
not evaluatedwhen no coverage rows exist, and sensor coverage isnot evaluatedwhen no sensors are expected. An empty denominator is never rendered as0%. - When neither coverage rows nor witnesses exist after loading, the workspace names the SBOM/call-graph and runtime-sensor setup steps instead of presenting a wall of zero metrics.
- Supplied rows retain the missing-sensor indicator,
missingfilter, and per-row sensor gap labels (all sensors online,missing N sensors).
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
- 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. - 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. - Falls back to an explicit
fixture-unavailableAsset Registry v1 fixture only when the Graph asset endpoint is unavailable because of network failure, 404, or 5xx responses. Authorization and validation failures fail closed and remain visible errors. - Requires Graph list/detail responses to declare
asset-registry.v1; schema drift is treated as a validation error, not fixture-unavailable mode. - Keeps ownership and credential data as opaque refs and filters sensitive attribute expansions from the detail panel.
- When the live Graph endpoint is healthy but the selected tenant has zero asset records, the page shows an action-oriented onboarding state instead of a dead empty table. It links operators to topology setup, runtime-host integrations, registry integrations, image scanning, SCM context, and diagnostics while preserving the truthful
0 assetsstate. - 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.
- 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 Tabs (Sprint 20260525_007)
The /assurance and /compliance pack landing page keeps live readiness summary, operator configuration, and the comparison matrix visible, but the heavy framework detail cards are segmented through the canonical <stella-page-tabs> component:
- Tabs:
NIS2,DORA,CRA. - URL state:
?framework=nis2|dora|cra. - NIS2 tab: control register, incident timeline, effectiveness, and NIS2 compatibility routes.
- DORA tab: review workspace, major incident, Register of Information, TLPT, information sharing, and DORA compatibility route.
- CRA tab: product-security and technical-documentation packs plus their review workspaces and compatibility routes.
The tabbed detail area 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.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.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, guards & interceptors
Authentication is OIDC Authorization Code + PKCE against the Authority module, with DPoP sender-constrained tokens. The implementation lives under src/app/core/auth/.
AuthorityAuthServicedrives the flow:beginLogin()builds a PKCE pair (createPkcePair()) and redirects to the configuredauthorizeEndpoint;/auth/callbackand/auth/silent-refreshroutes complete the exchange. Tokens are exchanged attokenEndpointwithapplication/x-www-form-urlencodedbodies and accompanied by a DPoP proof header (DpopService, algorithmsES256(P-256) /ES384(P-384), defaultES256).EdDSAis NOT supported:DpopKeyStore.toKeyAlgorithm()throwsError('EdDSA DPoP keys are not yet supported.')(core/auth/dpop/dpop-key-store.ts:136-137), so configuringauthority.dpopAlgorithms: ['EdDSA']fails at runtime. This matches../ui/architecture.md§4.1.- Sign-out (
logout()) is self-contained on the client: it first performs a router navigation to the dedicated/logged-outconfirmation page (so the authenticated chrome is torn down immediately — never leaving the operator on the page they were viewing), then clears the local session, console session and DPoP nonce. The Authority session is ended best-effort in a hidden, same-origin iframe pointed atlogoutEndpoint(/authority/connect/logout) withid_token_hintonly (nopost_logout_redirect_uri, so it cannot boot a second SPA instance); any failure is silent and does not affect the signed-out UX. From/logged-out, “Sign in again” re-entersbeginLogin().- The Authority implements the matching OIDC end-session endpoint (
end_session_endpointin discovery; see authority/architecture.md §3.1a andSPRINT_20260608_002_Authority_oidc_end_session_endpoint). It validatesid_token_hint, terminates any Authority session/cookie, and enforces thepost_logout_redirect_uriallow-list — so when a redirect is supplied it must exactly match the client’s registeredpostLogoutRedirectUris(https://stella-ops.local/,https://127.1.0.1/forstella-ops-ui).
- The Authority implements the matching OIDC end-session endpoint (
AuthSessionStoreholds session/token state (signals-based, not NgRx). Access tokens are proactively refreshed ahead of expiry (ACCESS_TOKEN_REFRESH_THRESHOLD_MS;refreshLeewaySecondsconfigurable, default 60). Silent refresh runs through a hidden iframe route with a 10 s timeout.- Scopes are decoded from the JWT
scopeclaim and surfaced through theStellaOpsScopesmap andhasScope/hasAllScopes/hasAnyScopehelpers incore/auth/scopes.ts. The syntheticadminscope short-circuits all scope checks. (Note:scopes.tsis documented as a hand-maintained stub pending the generated SDK; it tracks but is not auto-generated from the backendStellaOpsScopes.cscatalog.) - Two wildcard scopes, plus explicit resource guards — know which rule you are in. Standard rules in the route access model (
core/auth/route-access.ts) allow eitheradminorui.adminto bypass the listed scopes. Rules markedexplicit, which modelrequireExplicitScopesGuardandrequireAnyExplicitScopeGuard, allow neither bypass: the principal must hold the exact backend scope accepted by the resource server. The pinned inventory intests/navigation/route-access-contract.spec.tskeeps Explicit guard declarations and model rules reconciled. Thescopes.tshelpers (hasScope/hasAnyScope/hasAllScopes) continue to short-circuit onadminonly, so do not treatui.adminas sufficient for an Explicit route. - Sidebar route fallback preserves one active destination. A shelf whose own landing guard rejects the current session resolves its href to the first visible child. Both rows stay available, but the shelf suppresses its own active marker so only the child that owns the fallback route is highlighted.
- Route guards (
core/auth,core/config):requireConfigGuard— blocks until runtime config resolves (redirects to/setupwhen config is missing, or into/setup-wizard/wizardwhile setup is incomplete).requireBackendsReachableGuard— blocks until backend reachability is probed.requireAuthGuard— requires an authenticated session; unauthenticated navigation redirects to/signin, which immediately forwards to the Authority sign-in page carrying the attempted deep link asreturnUrl(there is no in-app landing page for signed-out users).requireScopesGuard/requireAnyScopeGuard— all-of / any-of scope gates that redirect to/console/profileon failure.
- HTTP interceptors are registered as multi
HTTP_INTERCEPTORSinapp.config.ts(order: auth → operator-metadata → tenant → global-context):AuthHttpInterceptor— attachesAuthorization+ DPoP headers and handles refresh.OperatorMetadataInterceptor— adds orchestrator operator metadata.TenantHttpInterceptor— stamps the canonical Stella Ops tenant header.GlobalContextHttpInterceptor— propagates global context params.
- Platform scope URL synchronization is owned by
core/context/platform-context-url-sync.service.ts. Angular Router state is authoritative: after authentication is explicitlyauthenticatedand the initial navigation has settled, context changes use one serializedreplaceUrlnavigation soRouter.url,Router.currentUrlTree, the browser address, and renderedqueryParamsHandling="merge"links carry the same activetenant,regions,environments, and optional scope fields. Unrelated query values (including repeated parameters) and fragments are preserved. A context choice made during another navigation is retained and retried afterNavigationEnd,NavigationCancel, orNavigationError; an older destination URL cannot hydrate over that pending choice. Rapid changes coalesce to the latest patch, while loading, refreshing, unauthenticated, and expired sessions perform no scope navigation or guard re-evaluation. URL-to- context hydration remains active on managed routes; setup wizard, auth, sign-in/out, welcome, and console routes remain excluded. Regression coverage is insrc/tests/context/platform-context-url-sync.service.spec.tsandcore/testing/*-scope-links.component.spec.ts. Exact-dist behavioral proof for hydrated scope, a visible context change, normal click, new-tab open, and reload across Dashboard, Security, Releases, and Evidence is recorded inrun-003/tier2-e2e-check.json. - 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— atapp.routes.ts:284-291. Do not send operators to/console/profile: that path loadsInsufficientPermissionsComponent(title “Insufficient Permissions”,app.routes.ts:570-576) 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.- 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
Configuration has two layers.
Build-time flags (src/environments/environment.ts / environment.prod.ts) — minimal, no secrets or hostnames:
// environment.ts (dev)
export const environment = {
production: false,
apiBaseUrl: '/api',
authEnabled: true,
debugMode: true,
};
Runtime configuration is the authoritative source and is fetched at bootstrap by AppConfigService.load() (wired via provideAppInitializer in app.config.ts). It is typed by AppConfig (core/config/app-config.model.ts) and exposed through the APP_CONFIG injection token. Resolution status is one of pending | loaded | missing | error; a missing/error result routes the operator to /setup (via requireConfigGuard). Key shape:
authority(AuthorityConfig):issuer,clientId,authorizeEndpoint,tokenEndpoint,logoutEndpoint?,redirectUri,silentRefreshRedirectUri?,postLogoutRedirectUri?,scope,audience,dpopAlgorithms?,refreshLeewaySeconds?.apiBaseUrls(ApiBaseUrlConfig):gateway?,scanner,policy,concelier,attestor,authority, plus optionalledger,vex,signals,excitor,notify,scheduler. Per-client base URLs are derived from these (preferringgateway, falling back toauthority) by factory providers inapp.config.ts.telemetry?(otlpEndpoint?,sampleRate?),welcome?(legacy — kept in the contract but no longer rendered since the /welcome landing page was replaced by the auto-forwarding/signinroute),doctor?,setup?(setup-wizard resume state).quickstartMode?is deprecated/ignored.
5.1 Standalone console reverse proxy
The standalone Console image (devops/docker/Dockerfile.console) serves the SPA and same-origin API routes through Nginx. Service-owned routes must appear before the generic /api/ Platform catch-all. In particular, /api/v1/vexlens proxies to vexlens.stella-ops.local; otherwise the live consensus page receives a Platform 404 even when VexLens is healthy. The proxy also uses enlarged upstream header buffers because the first-party Console requests the full operator scope set and a successful Authority authorize response can exceed Nginx’s default single-page header buffer. Keep both rules covered by the fresh-DB VexLens live specs under src/Web/StellaOps.Web/e2e/.
Related Documentation
- UI module:
../ui/architecture.md - Authority:
../authority/architecture.md - Auth smoke tests:
../ui/operations/auth-smoke.md
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)
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) —
src/app/core/api/feed-freshness.models.tsis the one client-side model of Concelier’sFeedFreshnessProjection(freshness.status/reasonper source, mutually exclusive summarystateCounts). Intelligence Advisory Sources, the Advisory & VEX catalog (deriveSourceIngestionStateis canonical-first), Security overview, and VulnCatalog coverage all render it verbatim; no surface derives a competing state vocabulary. - 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. - 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.
