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.

Read-model currency (2026-10-06, PCD-408). The consolidated host and local adapter are current source; FND-20/FND-26 froze the predecessor host/store. The August deployment snapshots below are historical. Current read contract and runtime reconciliation separates OCI image identity, document byte identity, accepted source and dated estate observations. PCD-398 retains the current image/deployment follow-up; no deployment is established by this correction.

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-event — scanner.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_roots†event-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_projection_offsetsprojector checkpointssource-of-truthcarry
ledger_snapshotswithdrawn snapshot storageretiredPCD-316: forward migration 024 drops only the empty table; unexpected historical rows block retirement
ledger_attestations, ledger_attestation_pointersattestation anchoringevidencecarry
vex_decisions†, vex_override_envelopes†DC-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†rebuildable projectionderivedcarry with documented rebuild path
finding_history†, triage_actions†projector timeline and triage-action copies of ledger eventsretiredcarried, then DIP-8: forward migration 028 drops both because the projector wrote them and nothing read them; ledger_events stays the record
fix_verifications†, audit_bundles†verification/audit familiesretiredcarried, then P13-4: forward migration 013 drops both (zero production writers or readers)
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; Graph owns the upstream event log; Findings owns the projection sink and checkpoint. The current host selects HttpGraphAssetRegistryEventSource behind the single default-off Findings:GraphAssetRegistry:Enabled gate. Disabled, the projection plane is dormant and the host boots against its owned database without a Graph schema; the graphless production boot and off/on composition tests pin this. The feed contract is asset-registry-feed-contract. PCD-330 (2026-10-06) withdrew the unselected direct Graph database reader and legacy selector, preserving their original files under the Findings frozen subtree. Live feed activation remains separate 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_results — RULED 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. At this design’s date its only reader was 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 ran the job synchronously and returned the result in its own 202 body, so the table cached results the caller already had — a drop candidate at the cutover, not a copy candidate. Current state (2026-09-30): FND-10 deleted both routes with StellaOps.RiskEngine.WebService on 2026-08-27. The Findings baseline still creates the table (001_findings_consolidated_baseline.sql:951), and only tests reference its store (PostgresRiskScoreResultStore), so no deployable reads or writes it.

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)

Update 2026-09-30 (SPRINT_20260928_004 PCD-127): the Platform readers named below (PlatformAnalyticsQueryExecutor, PlatformAnalyticsService, PlatformAnalyticsMaintenanceService) and the /api/analytics/* surface they served are withdrawn, so no service reads the schema. The reader sweep below is the 2026-08-03 record.

raw_sboms, raw_attestations, artifacts, artifact_components, components, component_vulns, attestations, daily_*_counts, schema_version — derived-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 these counts. Since PCD-330 (2026-10-06), DEFAULT-partition rows are reported as Info and healthy because they are the shipped storage design. Missing required partition parents still report Critical. Info does not certify that per-tenant archive/detach is available: the tenant lifecycle handler still withholds that action when a dedicated tenant partition is absent.

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 (since 2026-09-27, PCD-64 ADR-004 exception, the install path creates that role and Findings asserts it at startup; the baseline no longer creates it). PCD-316’s forward migration 024 retires the unused ledger_snapshots table only when empty. Its former app.tenant_id policy and EF mapping have no current consumer. Nonempty historical records are preserved by refusing startup retirement, without CASCADE or role widening. 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.

Currency (2026-10-03, SPRINT_20260930_006 PCD-241). The owner ruling of 2026-10-02 settled R-FND3-8 on the session-variable branch: the owner stays NOBYPASSRLS (ADR-039) and no remedy grants a bypass. The ledger projector, the one runtime path that read a FORCE-RLS table without a tenant, now enumerates the RLS-free registry findings.ledger_tenants (migration 018) and reads each tenant under its own app.current_tenant. doctor.findings.ledger.rls-posture reports a BYPASSRLS or superuser connection as a Warning instead of recommending the attribute. See architecture.md section 2.

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 all earlier carried read-interface registrations before registering the local store. The predecessor store and its foreign SQL are frozen by FND-26 and cannot resolve 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 former vuln.* joins)Consolidated source, reverified 2026-10-06: findings-web selects the local active-generation adapter over Findings-owned advisory/SBOM/match state. FND-X18-1/2/4/5/6/7 and the predecessor route swap belong to the archived acceptance record, not an active DOING claim. FND-20/FND-26 froze the predecessor host/store. The worker composes retained corpus/SBOM consumers and the default-off Scanner v2 consumer. Release-related images and artifact triage retain typed refusals; producer delivery and deployment of newer identity inputs require independent proof under PCD-398. Binding schema/protocol and source/runtime reconciliation: security-read-model.md; image/document identity: section 7.1 below.
X19 (SbomService’s former direct write)Producer/consumer protocol exists in source; deployment is separate evidence: SbomService owns the closed upload/retirement stream, bounded snapshot, consumer retention and exact-version document seam. Findings composes its authenticated owner client and fenced background projection behind the default-off gate. A shipped default does not establish an estate activation flag or live delivery. The former Concelier best-effort POST and read-triggered SbomVersionCreatedEvent are historical, not a replacement authority. Match results remain tenant-keyed Findings data; SBOM documents remain SbomService authority. Current read/projection currency is in the linked security dossier; PCD-398 retains estate image selection and validation.
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.

Currency (2026-09-30): the dual mapping is gone. The endpoints map their /api/findings/v1/* templates directly and the host no longer serves any carried backend path (/api/v1/findings/*, /api/v1/scoring/*, /api/v1/capabilities/*, /vuln/ledger/events, /v1/alerts/*, /v1/vex-decisions/*, /v1/evidence-subgraph/*, /api/v2/security/*, /api/risk/aggregated-status); every consumer moved to the canonical paths in the same change. The /api/vuln-explorer/* disposition was settled as retire-and-rehome (owner ruling Q-1): its one real capability is served at /api/findings/v1/evidence-subgraph/{vulnId}.

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:

PCD-334 preserves the inclusive lastAffected upper bound through matcher-rows v4 import and SBOM materialization. Migration 023 adds nullable last_affected columns to the matcher and match projections; their fixed columns retain the exclusive bound. The bulk COPY column and parameter orders carry the same shape as individual imports. The matcher includes the last vulnerable version, excludes later versions and uses matcher identity findings-corpus-v3. Consumer images must be ready before the owner Worker publishes corrected facts and a new corpus generation.

PCD-334 source verified against 6d3401cdc5 (2026-10-06). Re-verify: pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Findings/__Tests/StellaOps.Findings.Persistence.Tests/StellaOps.Findings.Persistence.Tests.csproj -Method "*LastAffected*" -BuildProjectReferences -MaxThreads 1.

7.1 OCI image and SBOM document identity (2026-10-06, PCD-404)

Verified against implementation commit 6b9cd563b5; the commands below recheck the source and isolated runtime boundary. The owner HTTP path was also reverified after concurrent advisory-matcher changes at 9867bdbda7d93ff8e52acd3a488fb7daa3ccfcc1; final merge evidence (pruned evidence docs/implplan/_evidence/20261006-scanner-jobengine-carry/POST-MERGE.md at db05532c96) records that additional passing check.

The consolidated read adapter identifies an image by its immutable OCI digest. The owner SBOM document has a separate byte digest. Migration 021_findings_sbom_image_identity.sql adds a stored, generated image_digest to the Findings-owned SBOM version projection, derived from a complete registry/repository[:tag]@sha256:<64-lowercase-hex> artifact reference. Forward correction 022_findings_sbom_reference_kind.sql classifies the registry and repository syntax: PURLs and transport URLs have no image identity; numeric registry ports, bracketed IPv6 registries and digest-pinned tags are supported. It rebuilds only the generated column/index and leaves migration 021 intact. Existing projected versions acquire the same identity automatically; new snapshots and uploads use the identical derivation. Tag-only and malformed references have no image identity. The migration preserves document digests, retained versions, retirement state, checkpoints and match fences.

The image-context and image-SBOM routes, digest-scoped findings and Scanner reachability joins use this image identity. Supplying an SBOM document digest to an image route does not alias the image. Available versions remain tenant-scoped and retain the existing selection order: owner occurrence time descending, then version ID. Retiring the selected version permits another retained available version to be selected. Affected inventory still refuses with security_reachability_projection_unavailable until every required Scanner verdict is projected; artifact triage retains its separate explicit refusal.

Re-verify the identity, retained-version, tenant, upgrade and HTTP owner-envelope paths from the repository root:

pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Findings/__Tests/StellaOps.Findings.Persistence.Tests/StellaOps.Findings.Persistence.Tests.csproj -Method "*SbomImageIdentity*" -BuildProjectReferences
pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Findings/__Tests/StellaOps.Findings.Operations.Tests/StellaOps.Findings.Operations.Tests.csproj -Class "*SbomImageIdentityOwnerPathTests*" -BuildProjectReferences