Findings consolidation — S0 target design (SPRINT_20260722_010 FND-1)

Status: APPROVED by the owner 2026-08-03 (ledger round 88, ruling C). Verification base: the sprint’s recorded dig rounds (12/13/15 internals, X9/X11/X18/X19, DC-32/DC-34), the live migration baselines (tables re-enumerated from src/ this pass), and the 2026-08-03 build-boundary register.

Owner rulings landed 2026-08-25 (receipts: SPRINT_20260722_010 Decisions & Risks): Q-21 dedicated-recovery — advisory-generation recovery gets a global, opt-in findings:recovery scope granted to no base persona or service client; an explicitly assigned incident-recovery operator may hold/rollback/restore/resume, every transition demands a reason + incident reference and lands in append-audit evidence, rollback sets a durable non-expiring activation hold, and no automatic expiry or SQL escape exists. Q-23 historical ruling, superseded 2026-08-28 — the first design kept one service-global scanner.scans stream/cursor behind a cross-tenant projection scope and asked each consumer to partition by exact tenant. It is retained here as decision provenance, not as the current contract. Q-22 full-detail-eventscanner.scan.completed payload v2 embeds the two consumer-read collections, content-addressed and self-verifying (hashes recomputed from the payload; scanId provenance-only); the Findings consumer drops the second fetch and its detail client/expectation seam; mandatory companions: a declared completion-stream retention window (now the exact tenant prefix family) + a byte-bounded catch-up page; DC-06 amended on the record for this stream. All Those 2026-08-25 shapes were assigned to FND-X18-7.

Q-23 final redesign landed in source 2026-08-28 (SCN-7/FND): scanner.scans remains the logical wire identity, while Scanner maps each exact tenant injectively to its own physical stream, epoch, sequence, retention floor and remote-consumer cursor. Catch-up and cursor routes derive that tenant from the authenticated request and require only the tenant-bound machine scope scanner:projection:read. Findings uses a tenant-distinct local lease/checkpoint identity, registers or resets its producer cursor before a from-zero rebuild, and reports progress only after its own projection/checkpoint transaction commits. The cross-tenant scanner:projection:read-all design and global cursor are retired.

Q-21 implementation landed 2026-08-26; the same day’s Q-23 slice is historical: migration 007 adds the durable advisory_generation_recovery_hold singleton and the append-only advisory_generation_recovery_audit; the coordinator writes hold + audit in the same transaction as each transition (rollback establishes the hold atomically, resume alone removes it after the active/staging witness test) and the ordinary projector refuses activation while the hold exists; findings-web serves the four findings:recovery-gated POST adapters at /api/findings/v1/security/advisory-generations/recovery/*. That slice also introduced the now-superseded cross-tenant projection scope and grant; the preceding paragraph records the replacement contract that current source implements.

Q-22 implementation landed 2026-08-26 (FND-X18-7 slice): scanner.scan.completed is payload v2 — the event embeds componentPurls/reachabilityFindings, the consumer (ScannerSecurityProjectionConsumer) recomputes both set hashes from the embedded payload and admits only on match, with no second owner fetch; the staged digest-detail client method and expectation seam are DELETED (the route survives on Scanner for Policy). scanner-web declares the tenant-stream prefix retention window (RemotelyConsumed, deployment-supplied, fail-closed activation) and the catch-up page is byte-bounded, with the Findings client stating its own byte budget per page. v1 envelopes are skip-and-count residue cured by rescan/bootstrap (§2.11 — no compatibility path). The tenant-bound contract is p19.9.

Hosted projection pass final tenant-bound shape landed in source 2026-08-28 (FND-X18-7 / SCN-7): findings-worker now composes the leased retained-feed pass behind the SAME single default-off gate as the owner client (Findings:ScannerSecurityOwner:Enabled, the X18-6 one-flag precedent). The pass reads the configured exact tenant from its durable P6 checkpoint through the head observed at pass start, hands every v2 envelope to ScannerSecurityProjectionConsumer (whose fenced transaction is the only thing that advances the checkpoint), refuses gaps/epoch mismatches fail-closed, admits a producer epoch change only when the new epoch is fully retained from sequence one — and only through the consumer’s tenant-local atomic reset path — and counts v1 skip-and-count residue. It registers a producer cursor when no checkpoint exists and reports each durable position only after the consumer transaction returns committed. It authenticates with a DEDICATED Authority token client as confidential stellaops-findings-scanner-projection (secret env FINDINGS_SCANNER_PROJECTION_CLIENT_SECRET, scope exactly scanner:projection:read, built in an isolated container so a sibling lane’s client credentials can never be substituted); enabled-with-missing deployment values refuses startup naming the exact key, and the source ships none. The gate stays default-off: the shipped stellaops vs intended api://scanner Scanner audience alignment remains the live-activation prerequisite.

Q-21’s exit is now provable, and abandoned generations are recorded rather than inferred (FND-X18-7, 2026-08-27). Migration 010 adds abandoned_at / abandonment_code to advisory_projection_generation: an explicit terminal marker written by the corpus projector at the instant its bootstrap/re-derive activation commits the checkpoint jump that strands a staging generation, carrying the reason the call site already holds (artifact_reclaimed, checkpoint_rederived). Q-21 recovery never writes it — the marking statement itself refuses while an activation hold exists, so a rollback’s staging row stays catch-up-eligible and prunable-never. Three readers use it: the resume admission test (which now considers only catch-up-eligible generations — a provably unfulfillable generation is dead, not pending; the ruling’s no-expiry and no-SQL-escape prohibitions are untouched), the served X-StellaOps-Source-State pending-generation predicate, and prune eligibility. hold and rollback additionally pre-flight their own exit and refuse the new stable code advisory_generation_resume_unreachable(409) naming the blocking generation, where “unreachable” means: no current witness, outside the materialization pass’s work-discovery predicate, AND at or below the durable corpus checkpoint — so a live import is admitted and only a generation the consumer has provably moved past is refused.

Amended 2026-08-04 (FND-3 implementation). Building the baseline surfaced two enumeration gaps in §2.4 — the analytics materialized views and functions were never dispositioned (F-FND3-1) and schema_version was miscounted as a data table (F-FND3-2). Both are corrected in place below, and §2.5 records the decisions the baseline actually applies. Neither changes an owner ruling.

1. Current → target shape

CurrentTarget
Deployablesfindings-ledger-web, riskengine-web + riskengine-worker, findings-security-web, findings-vulncorrelationfindings-web + findings-worker (scoring) — one family
Sourcesrc/Findings/StellaOps.Findings.Ledger.WebService, StellaOps.RiskEngine.*, StellaOps.Findings.Security.*, StellaOps.Findings.VulnCorrelation.*`src/Findings/StellaOps.Findings.WebService
DALStellaOps.Findings.Ledger (migrations in migrations/), StellaOps.RiskEngine.Infrastructure, StellaOps.Findings.Security.Persistence, StellaOps.Findings.VulnCorrelation.PersistenceStellaOps.Findings.Persistence, fresh 001_* baseline, database stellaops_findings, env STELLAOPS_POSTGRES_FINDINGS_CONNECTION fail-closed

2. Table inventory + dispositions (P13)

Re-enumerated from the live baselines this pass (34 + 6 + 11 + 1 objects incl. _default partition twins).

2.1 findings (ledger family) — the SOURCE-OF-TRUTH core

Table (+_default twin where partitioned)PurposeP13 classDisposition
ledger_events†, ledger_merkle_rootsevent-sourced, Merkle-anchored ledgerappend-audit / evidenceCARRY, Merkle-preserving move (S9 copies per-tenant partitions and verifies recomputed roots per tenant BEFORE revocation — the chain gate)
ledger_snapshots, ledger_projection_offsetssnapshot + projector checkpointsoperationalcarry
ledger_attestations, ledger_attestation_pointersattestation anchoringevidencecarry
vex_decisions†, vex_override_envelopesDC-32: THE overlay system of record (tenant VEX decisions, DSSE envelopes, supersedes-chains)evidencecarry + add the overlay outbox (DC-29 lineage; Policy’s 007 projector consumes)
vex_statement_trust_overridesource-trust overrides (PostgresVexStatementTrustStore)evidencecarry (same outbox)
findings_projection†, finding_historyrebuildable projectionsderivedcarry with documented rebuild path
triage_actions†, fix_verifications†, audit_bundlestriage/verification/audit familiesauditcarry
risk_explanations, runtime_scores, score_history, runtime_timeline_events, runtime_trace_aggregates, runtime_tracesscoring + runtime-signal read modelsderived/windowedcarry; scoring consolidates with RiskEngine (§2.2) — dedupe the score stores in JOB-4-equivalent (FND-4) ⚠
asset_registry_events, asset_registry_projection_offsetsasset registry feedoperationalcarry; producer sweep DONE (2026-08-03): these are NOT twins. graph.asset_registry_events is the raw upstream event log written by Graph.Indexer’s AssetIndexingPipeline (023’s family); findings.asset_registry_events is this family’s PROJECTION SINK with different columns (graph_event_id/graph_occurred_at/projected_at), fed by PostgresGraphAssetRegistryEventSource reading FROM graph.asset_registry_events cross-schema (the ledger role holds SELECT on graph — a §2.7-class read). Both stay with their owners; the cross-schema read becomes an event/API seam when the databases split (coordinate 010×023; the regression guard AssetRegistryEventVisibilityMigrationTests pins today’s direction). Until then, startup admission points any PostgreSQL source-probe failure to the recovery runbook; a later missing schema/table or denied privilege fails closed rather than freezing the checkpoint, while ordinary runtime transients retain bounded retry. The 010×023 coordinate RESOLVED in source (2026-08-26): the Graph-owned catch-up feed (GET /graph/asset-registry/events, closed StellaOps.Graph.Contracts leaf, dedicated machine scope graph:asset-registry:read-all per the Q-23 shape) replaces the cross-schema read on the CONSOLIDATED host only — findings-web selects HttpGraphAssetRegistryEventSource behind the single default-off Findings:GraphAssetRegistry:Enabled gate (disabled: the projection plane is dormant and the host boots healthy against stellaops_findings, which has no graph schema — proven by FindingsWebGraphlessProductionBootTests), with seam state in findings.asset_registry_feed_state (migration 008) and the checkpoint unchanged in asset_registry_projection_offsets; the predecessor keeps this same-database read byte-identical until FND-10. Contract: asset-registry-feed-contract. Live activation (deployed graph-api + armed seam + one event end to end) remains GRA-9/FND-9 window work.
airgap_imports, orchestrator_exports, webhook_registrationsintegration edgesoperationalcarry

† = LIST-partitioned by tenant_id.

2.2 riskengine (1 table)

risk_score_resultsRULED 2026-08-05 (FND-4, D-FND4-3): carries 1:1, does NOT fold. The “two scoring stores” premise does not hold — there is only one scoring store. This table is job-execution state: keyed by an ephemeral job_id, carrying job-outcome columns (success, error), scoped by an opaque subject rather than finding_id, and with no tenant_id column at all, versus runtime_scores keyed (tenant_id, finding_id) with a bounded 0–100 score and a derivation window. Its only reader is GET /risk-scores/jobs/{jobId}. Two open items were handed to FND-9 rather than settled here: it is the one table in the consolidated database with no tenant column while fourteen neighbours are RLS-FORCED; and POST /risk-scores/jobs runs the job synchronously and returns the result in its own 202 body, so the table caches results the caller already has — a drop candidate at the cutover, not a copy candidate.

2.3 findings_security (6 tables)

security_finding_projection, security_disposition_projection, security_sbom_component_projection, security_sbom_graph_projection, security_risk_snapshot, finding_disposition_cache — ALL derived-rebuildable projections. Carry with rebuild paths, BUT their inputs change (X18: the vuln.* and FROM scanner. joins die — §4). The live DB’s second copy (release.security_finding_projection, no code writer) dispositions with 018 (likely retire).

2.4 analytics (11 tables)

raw_sboms, raw_attestations, artifacts, artifact_components, components, component_vulns, attestations, daily_*_counts, schema_versionderived-cache class (the family ingests its own copies via AnalyticsIngestionService); P14 mis-homing candidates. Per-table reader sweep DONE (2026-08-03):

⚠ The 11-table count was not the whole schema (CORRECTED 2026-08-04, FND-3, F-FND3-1). analytics also holds 4 materialized views (mv_supplier_concentration, mv_license_distribution, mv_vuln_exposure, mv_attestation_coverage) and 12 functions, which this re-enumeration never dispositioned. Their dispositions:

2.5 Baseline decisions applied (FND-3, 2026-08-04)

The fresh baseline is src/Findings/__Libraries/StellaOps.Findings.Persistence/Migrations/001_findings_consolidated_baseline.sql.

Staging. It ships in a NEW assembly because all four legacy migration homes are scanned without a resource prefix by their own hosts, and two of those hosts (findings-ledger-web, riskengine-web) run live against the shared platform database. The target name StellaOps.Findings.Persistence was never taken by a legacy assembly, so unlike the Notify sibling this needs no .Consolidated suffix and FND-10 deletes the legacy DALs without a rename. Platform’s central migrator is independently blocked: its FindingsLedger and RiskEngine plugins were removed by PAC-6, discovery is an AppDomain scan for IMigrationModulePlugin, and a test pins that this assembly implements none. (2026-09-14, SPRINT_20260722_021 PLT-4: the scan and the plugin contract are deleted outright; the central migrator no longer exists.)

Arithmetic. 34 (findings, incl. 9 _default twins) + 1 + 6 + 11 = 52 enumerated objects → 39 tables + 9 partition twins in the baseline. The 25 findings tables are the 34 minus the 9 twins; analytics drops 4 of its 11 (vex_overrides DC-32; the two daily_*_counts per A4; schema_version per F-FND3-2).

Schemas. All four schema names are reproduced inside the one database stellaops_findings (plus findings_ledger_app for the RLS helper), because §5/FND-9 copies them as four units and the Merkle-preserving move rides that shape. They share ONE migration ledger, findings.schema_migrations. Amended 2026-08-05 (FND-7): a seventh schema, findings_archive, holds no data tables — it is the destination the SCR-4 ledger-class override detaches offboarded tenant partitions into (§3). It lives in the migration rather than in runtime DDL so archiving never needs a privilege the service does not already hold on its own database.

Partitioning. All nine LIST-by-tenant_id parents keep their partitioning and _default twins; a test asserts strategy, key column, and the twin set. This is what makes FND-9’s per-tenant Merkle-root re-verification possible — but only in principle as things stand. Corrected 2026-08-05 (FND-7, F-FND7-2): nothing in src/ ever creates a per-tenant partition, so today every tenant’s rows land in the _default twin and there is no per-tenant unit for the chain gate to copy or for the SCR-4 handler to archive. FND-9 must either create the partitions in the window or redefine the gate; the new doctor.findings.ledger.tenant-partition-coverage check measures the gap continuously so the answer is known before the window opens rather than during it.

Overlay outbox (DC-29/DC-32). Not a new Findings-local table: the baseline registers StellaOps.Eventing.Reliability (the P6 outbox/inbox/checkpoint/fenced-lease core) into the same database, so this family joins the one outbox lineage rather than forking a second.

Enum reconciliation (F-FND3-3). findings.ledger_event_type is declared with all 23 labels in one CREATE TYPE — the collapsed baseline’s 10 plus the four NIS2, four CRA, and the five that 004_ledger_event_type_supported_values had to add live after a missing attestation.pointer_linked broke the write path.

RLS (F-FND3-4, F-FND3-5). The live posture is RLS enabled, not rolled back: 007_enable_rls_rollback.sql is inert twice over (its name classifies it Release, and the csproj removes it from the embedded resource set). The baseline reproduces ENABLE + FORCE on 14 tables plus the findings_ledger_admin bypass role. ledger_snapshots keeps its divergent posture (ENABLE only, reading app.tenant_id instead of app.current_tenant) verbatim; FND-4 owns unifying the session-variable vocabulary. Amended 2026-08-05 (FND-7, F-FND7-1): the findings_ledger_admin bypass role does not do what R-FND3-8 assumes. Measured on the converged baseline: granting MEMBERSHIP of it confers nothing (BYPASSRLS is a non-inheritable role attribute), and SET ROLE to it bypasses RLS while discarding every privilege, because the baseline creates the role with no grants — the read then fails 42501 permission denied for schema findings, which looks like a privilege bug and is really the bypass path. ALTER ROLE <owner> BYPASSRLS is the only one-step remedy that works unchanged. The new doctor.findings.ledger.rls-posture check reports, on a live host, which of the remedies is missing.

Zero seed (§2.11). Nothing is inserted, including the two legacy projection-offset bootstrap rows — both writers upsert with ON CONFLICT DO UPDATE, so those rows were never load-bearing. CREATE EXTENSION pgcrypto is also dropped: gen_random_uuid() is built in from PostgreSQL 13 and the estate runs 16, so the owner role needs no superuser to converge.

3. Cross-service seams

SeamDisposition
X9 (security_finding_projection “release copy”)DISSOLVED by live evidence (2026-08-03 read-only query): security_finding_projection exists ONLY in findings_security — the release.* twin from the 2026-07-22 dig has since been dropped. 018’s S0 verifies nothing recreates it; no disposition work remains here
X11 (FROM scanner. joins in the projection store)RESOLVED in the active consolidated source by SCN-7/FND: Scanner serves the exact authenticated tenant’s retained v2 feed; the consumer verifies both producer set hashes and commits replacement, inbox and checkpoint under one fence in stellaops_findings. Migrations 006 and 012 make the derived head plus source sequence/event identities tenant-scoped; epoch reset rebuilds only that tenant. The hosted pass registers/resets the producer cursor and reports progress only after the destination transaction commits. AddActiveGenerationSecurityReadModel removes the predecessor PostgresSecurityFindingProjectionStore concrete and all carried read interfaces before registering the local store. The predecessor file retains its former scanner.* SQL for its separately tracked retirement, but it is not resolvable from the active consolidated container. The gate is still default-off and live cursor/lag proof remains window work; this row does not claim deployment.
X18 (Findings.Security’s vuln.* joins)Owner item 21(b), implementation in progress: FND-X18-1/2 completed the producer-owned metadata/publication wire and retained range-readable hub seam; FND-X18-4 embeds the global generation/advisory/exploit and tenant SBOM/component/match schema in StellaOps.Findings.Persistence; FND-X18-5 adds the dark retained-corpus consumer with verified, resumable import and fenced A/B activation. FND-X18-6 is DONE with the pure matcher, manifest witness, dark SBOM state repository, default-off authenticated SbomService owner client, database-free deterministic component parser, fixed-fence snapshot/catch-up/tombstone coordinator, and atomic materializer for either the exact active or staging manifest. findings-worker registers and schedules those seams under the existing P6 lease when the single direct-owner gate is explicitly enabled; it heartbeats between snapshot/event transactions, renews immediately before each atomic materialization, and discovers only complete active/staging generations whose witness is missing or behind the canonical checkpoint. The materializer’s FOR SHARE fencing row lock prevents successor takeover from interleaving until its transaction commits. The staged compose overlay leaves the gate off pending producer setup. The materializer preserves the other generation, keyset-pages live components against the selected matcher rows, rechecks exact P6/generation/count state and publishes the manifest witness in the same transaction; forward migration 005 preserves a real pre-stream head 0 rather than fabricating sequence 1. The parser covers exact CycloneDX top-level components and current SbomService SPDX top-level packages; SPDX 3 JSON-LD @graph remains explicitly unsupported. FND-X18-7 is DOING: the consolidated host has its local active-generation adapter and handler-owned state/as-of/digest response pin, while the predecessor/live route swap, remaining owner projections and runtime acceptance remain open. Binding schema/protocol: security-read-model.md § X18.
X19 (SbomService’s direct write)Source seams exist but remain inactive: SbomService owns the closed sbom.versions contract, transactional upload/retirement stream, bounded snapshot, remote-consumer retention and exact-version raw-document route; their source-side activation remains separately gated. Findings’ default-off worker composition now registers the authenticated direct-owner client and durable projection coordinator, but the staged overlay leaves the one activation flag false until the producer setup/window. The dead best-effort Concelier POST is not replaced until host activation and rollback are proved live. Match results are tenant-keyed Findings data; SBOM inventory remains SbomService authority. The legacy read-triggered SbomVersionCreatedEvent is explicitly not this seam.
Notify NIS2 incident ledger hand-offSource-prepared, default-off, not deployed (NTF-9): findings-worker pulls the stable logical notify.nis2-incident-ledger contract through the closed StellaOps.Notify.Nis2Ledger.Contracts seam, while both producer persistence and destination checkpoint/lease identity use the exact tenant’s lowercase-UTF8-hex physical partition. Notify derives that partition only from the authenticated context and echoes the resolved tenant; the consumer refuses startup/catch-up when it differs from its configured token tenant and rejects a known hand-off payload for any other tenant before inbox admission. A P6 fenced destination transaction combines tenant-partitioned transport admission, the immutable tenant-scoped findings.ledger_events append or exact duplicate decision, and checkpoint advance. Restart, exact transport replay, domain EventId replay, changed-content rollback, same-EventId cross-tenant isolation and checkpoint non-interference are covered against real PostgreSQL baselines. Activation requires explicit owner origin, auth plus a durable token-cache root, polling/page/lease/response/time budgets, and the producer retention contract; absent/false registers no worker. The synchronous Notify-to-Findings source edge remains the rollback path until the successor is deployed, catches up to the recorded owner head, reports that durable cursor, the producer flag is flipped, and live proof passes.
Overlay outbox → PolicyNEW in this merge (DC-29/DC-32): ledger emits overlay events Policy’s 007 projector consumes; the ledger is already event-sourced — evolve, never fork
Tenant lifecycle (SCR-4)this family is the named LEDGER-CLASS override: on tenant.deleted, Merkle partitions archive-then-detach, never delete. DONE 2026-08-05 (FND-7): FindingsTenantLifecycleHandler (src/Findings/__Libraries/StellaOps.Findings.Operations/TenantLifecycle/) is registered on findings-web and pinned by FindingsTenantLifecycleHandlerTests (6 cases: the chain is archived and nothing deleted; only the two derived parents are dropped; dry-run touches nothing; suspension is not deletion; redelivery is idempotent; a tenant without its own partition is WITHHELD rather than taking the DEFAULT partition with it). The destination is the new findings_archive schema in the baseline, and the disposal class is read from each parent’s P13 header comment so the migration stays authoritative for what may be destroyed. findings-web is the estate’s FIRST consumer-side adopter of AddTenantLifecycleConsumer— the 027 contract’s consumer half had never been wired into any host (F-FND7-4).

The NIS2 consumer owns a separate Auth.Client service provider, file token cache and authenticated owner HTTP pipeline. Its notify.operator identity does not replace the advisory consumer’s root identity or the Scanner consumer’s dedicated identity. Root logging, time and any egress policy are forwarded to the NIS2 provider. Enabling this consumer does not require disabling those existing lanes.

4. Build-boundary dispositions (2026-08-03 register)

RESOLVED 2026-08-05 (FND-4). Nine of the family’s fourteen pins are retired and the register stands at 180. findings-ledger-web fell from 9 pairs / 104 projects in closure to 3 pairs / 57, and findings-security-web reached zero pairs. The lever was a single edge: the ledger host referenced the whole StellaOps.Scanner.Reachability library for one namespace of DTOs it merely deserializes, which cost it 38 foreign projects across six pins; the pure DTO now ships in the producer-owned closed StellaOps.Scanner.Reachability.Contracts. Separately, StellaOps.Findings.Disposition.Contracts was classified cross-service-client-sdk — its three pins were a classification gap, not a defect. The three surviving ledger pins (attestor 4, signer 1, evidence-locker 1) are real VEX-override-attestation and evidence-capsule uses riding sprints 011 and 019. The pre-FND-4 picture is kept below for the record:

5. What S1 must decide

  1. Merge confirmed per ADR-039 D14 (no deviation found by this S0).
  2. Scoring-store unification (riskengine’s risk_score_results vs the ledger-side runtime_scores/score_history family) — one store shape out of FND-4. ANSWERED (FND-4, D-FND4-3): no unification needed — risk_score_results is job state, not a score; it carries 1:1. See §2.2.
  3. analytics cache survival set (named readers only) + vex_overrides retirement (DC-32).
  4. The findings-ledger-web → signals edge: extraction candidate vs runtime seam (dig first — the Policy precedent says look for the pure-library-stuck-in-a-host shape). ANSWERED (FND-4): the signals pin was never a direct edge — by 2026-08-05 it arrived transitively via Scanner.Reachability → Signals.Ebpf, and the EWS algebra the host really uses had already been extracted to the closed StellaOps.Signals.EvidenceWeightedScore SDK. The pure-library-stuck-in-a-host shape was real but sat one level out: the mini-map DTO inside Scanner.Reachability. Splitting it killed signals along with five other pins.

S0 verification COMPLETE (2026-08-03). Route + Console + deploy inventory:

6. The consolidated API surface (FND-5, 2026-08-05)

The nine routes above collapse into ONE gateway group: ^/api/findings/v1(.*)http://findings.stella-ops.local/api/findings/v1$1.

Old gateway prefixNew path under the group
/api/v1/finding-ledger/*/api/findings/v1/ledger/*
/api/v1/scoring/*/api/findings/v1/scoring/*
/api/v1/capabilities/*/api/findings/v1/capabilities/*
/vuln/ledger/events/api/findings/v1/ledger/events
/v1/alerts/*/api/findings/v1/alerts/*
/v1/vex-decisions/*/api/findings/v1/vex-decisions/*
/api/vuln-explorer/*/api/findings/v1/vuln-explorer/*
/api/v2/security/*/api/findings/v1/security/*
/api/risk/aggregated-status/api/findings/v1/risk/aggregated-status

This table is the binding input to the host composition — the consolidated host must serve and advertise these paths, or the swap points at nothing. Currency (2026-08-23): the owner rejected static gateway rewrites and selected native dual mapping. findings-web now mirrors every carried backend RouteEndpoint under its canonical /api/findings/v1/* prefix before Router discovery is refreshed, so the aliases are present in the existing HELLO endpoint catalogue and legacy paths stay available for rollback. The one deliberate exception is /api/vuln-explorer/*: no Findings backend owns that already-mismatched contract, so it remains an explicit product disposition rather than an invented alias. FND-5 remains blocked on that disposition and its live FND-9 gateway forcing proof.

Three things the S0 inventory above could not have known, all verified against src/ on 2026-08-05 and recorded in full in the sprint’s D-FND5 dispositions:

The swap is a script, not a patch — tools/scripts/fnd5-swap-findings-routes.ps1 driven by tools/scripts/fnd5-findings-route-swap.json, guarded by FindingsConsolidatedRouteConfigTests (same manifest), with the window recipe in docs/runbooks/findings/findings-cutover-fnd5-route-swap.md.

Currency (2026-09-04) — this swap is RETIRED; the script refuses. The 2026-08-28 owner ruling (“all microservices needs to autopublish no router configuration for microservices endpoints”) was executed by SPRINT_20260809_001 RAR-5/RAR-7; both gateway route tables now hold 22 routes and none of the eight rows it replaced survives. Since ced8a2d286 tools/scripts/fnd5-swap-findings-routes.ps1 reports the swap as RETIRED and exits 2 on both -Check and a bare run. Findings is served by the auto-published layer. Read the runbook banner before acting on anything in this section; remediation is tracked as SPRINT_20260904_001 RRS-1.

Console side: every Findings-plane path now resolves through src/Web/StellaOps.Web/src/app/core/api/api-path-prefixes.ts, so the window’s Console work is six values in one file.

7. X18 implementation boundary (owner item 21(b), 2026-08-19)

The detailed target is security-read-model.md § X18; SPRINT 010 decomposes it as FND-X18-1 through FND-X18-7. The architecture boundary is fixed: