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_010Decisions & Risks): Q-21 dedicated-recovery — advisory-generation recovery gets a global, opt-infindings:recoveryscope 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-globalscanner.scansstream/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.completedpayload v2 embeds the two consumer-read collections, content-addressed and self-verifying (hashes recomputed from the payload;scanIdprovenance-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.scansremains 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 scopescanner: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-tenantscanner:projection:read-alldesign 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_holdsingleton and the append-onlyadvisory_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 fourfindings: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.completedis payload v2 — the event embedscomponentPurls/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-workernow 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 toScannerSecurityProjectionConsumer(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 confidentialstellaops-findings-scanner-projection(secret envFINDINGS_SCANNER_PROJECTION_CLIENT_SECRET, scope exactlyscanner: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 shippedstellaopsvs intendedapi://scannerScanner 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_codetoadvisory_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 astaginggeneration, 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 servedX-StellaOps-Source-Statepending-generation predicate, and prune eligibility.holdandrollbackadditionally pre-flight their own exit and refuse the new stable codeadvisory_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_versionwas 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
| Current | Target | |
|---|---|---|
| Deployables | findings-ledger-web, riskengine-web + riskengine-worker, findings-security-web, findings-vulncorrelation | findings-web + findings-worker (scoring) — one family |
| Source | src/Findings/StellaOps.Findings.Ledger.WebService, StellaOps.RiskEngine.*, StellaOps.Findings.Security.*, StellaOps.Findings.VulnCorrelation.* | `src/Findings/StellaOps.Findings.WebService |
| DAL | StellaOps.Findings.Ledger (migrations in migrations/), StellaOps.RiskEngine.Infrastructure, StellaOps.Findings.Security.Persistence, StellaOps.Findings.VulnCorrelation.Persistence | StellaOps.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) | Purpose | P13 class | Disposition |
|---|---|---|---|
ledger_events†, ledger_merkle_roots† | event-sourced, Merkle-anchored ledger | append-audit / evidence | CARRY, Merkle-preserving move (S9 copies per-tenant partitions and verifies recomputed roots per tenant BEFORE revocation — the chain gate) |
ledger_snapshots, ledger_projection_offsets | snapshot + projector checkpoints | operational | carry |
ledger_attestations, ledger_attestation_pointers | attestation anchoring | evidence | carry |
vex_decisions†, vex_override_envelopes† | DC-32: THE overlay system of record (tenant VEX decisions, DSSE envelopes, supersedes-chains) | evidence | carry + add the overlay outbox (DC-29 lineage; Policy’s 007 projector consumes) |
vex_statement_trust_override | source-trust overrides (PostgresVexStatementTrustStore) | evidence | carry (same outbox) |
findings_projection†, finding_history† | rebuildable projections | derived | carry with documented rebuild path |
triage_actions†, fix_verifications†, audit_bundles† | triage/verification/audit families | audit | carry |
risk_explanations, runtime_scores, score_history, runtime_timeline_events, runtime_trace_aggregates, runtime_traces | scoring + runtime-signal read models | derived/windowed | carry; scoring consolidates with RiskEngine (§2.2) — dedupe the score stores in JOB-4-equivalent (FND-4) ⚠ |
asset_registry_events, asset_registry_projection_offsets | asset registry feed | operational | carry; 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_registrations | integration edges | operational | carry |
† = 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. 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_version — derived-cache class (the family ingests its own copies via AnalyticsIngestionService); P14 mis-homing candidates. Per-table reader sweep DONE (2026-08-03):
components/artifact_components/component_vulns— named in-family reader (VulnerabilityCorrelationServicecorrelation SQL) → CARRY.raw_sboms/raw_attestations/artifacts/attestations— ingestion-side only (writer + dedup reads inside the same service) → CARRY as the ingestion working set.daily_vulnerability_counts/daily_component_counts— the named reader is FOREIGN: Platform’sPlatformAnalyticsQueryExecutor(the gateway routes^/api/analyticsto platform-web, which reads these tables directly). OWNER RULING (2026-08-03, review item A4): RE-HOME to Platform, but FINDINGS-AGNOSTIC — Platform gains a GENERIC metric-series store (named series, no findings-domain columns or semantics); the findings family PUBLISHES its daily counts into it as named series over events/API; NIS-purpose reporting reads generic series only. Platform never computes from findings data.vex_overrides— ZERO code readers anywhere insrc/→ RETIRE confirmed (DC-32); the Policypolicy_vex_overrides_totalmetric is a name collision, not a reader.schema_version— CORRECTED 2026-08-04 (FND-3, F-FND3-2): this is not a derived-cache table at all, it is a hand-rolled migration-version marker superseded by the startup migration ledger. NOT recreated — carrying it would be carrying legacy migration history.
⚠ 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:
mv_*,sp_top_suppliers,sp_license_heatmap,sp_vuln_exposure,sp_fixable_backlog,sp_attestation_gaps,sp_mttr_by_severity,compute_daily_rollups(),refresh_all_views()— NOT carried. No non-test source insrc/Findingsreferences any of them; the named readers are Platform’sPlatformAnalyticsService(queries) andPlatformAnalyticsMaintenanceService(refresh + rollup). That is precisely the cross-owner computation the A4 ruling forbids (“Platform never computes from findings data”), and it cannot survive the database split in any case. They followdaily_*_countsinto Platform’s generic metric-series surface.normalize_supplier,categorize_license,parse_purl— CARRY. These have named in-family callers in the ingestion path.update_updated_at_column()— NOT carried; no trigger in the schema uses it.
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
| Seam | Disposition |
|---|---|
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-off | Source-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 → Policy | NEW 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:
findings-ledger-web— 9 foreign pins, the family’s real S0 workload:signals(compiles the Signals DEPLOYABLE — same shape as the Policy case fixed in4159ae78f5; check for another extractable pure library first),attestor(+signer),scanner(Scanner.Reachability),concelier(→ hub SDK/artifact via 003 repoints),evidence-locker,policy,replay,binaryindex. Each needs classify→extract/replace per the owner rule; NONE may carry intofindings-webas implementation references.findings-security-web → findings-ledgerandfindings-vulncorrelation → findings-ledger— INTRA-family after the merge; die by consolidation.findings-vulncorrelation—concelier(hub repoint),scanner(DC-15 seam).riskengine-web/riskengine-worker— zero foreign pins on the BUILD boundary only. CORRECTED 2026-08-05 (FND-4, F-FND4-1): reading this row as “clean, no seam work” is wrong.RiskEngine.Worker/Worker.csopens the vulnerability plane’s Postgres directly and readsvuln.advisory_cvss+vuln.kev_flagsto build snapshot bundles — a §8.2/DC-15-class cross-service DATABASE read this inventory missed. Dark today (riskengine-worker does not run in the live stack); rides SPRINT_20260722_003’s corpus/hub-event seam with the family’s othervuln.*readers.
5. What S1 must decide
- Merge confirmed per ADR-039 D14 (no deviation found by this S0).
- Scoring-store unification (riskengine’s
risk_score_resultsvs the ledger-sideruntime_scores/score_historyfamily) — one store shape out of FND-4. ANSWERED (FND-4, D-FND4-3): no unification needed —risk_score_resultsis job state, not a score; it carries 1:1. See §2.2. analyticscache survival set (named readers only) +vex_overridesretirement (DC-32).- The
findings-ledger-web → signalsedge: 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 viaScanner.Reachability → Signals.Ebpf, and the EWS algebra the host really uses had already been extracted to the closedStellaOps.Signals.EvidenceWeightedScoreSDK. The pure-library-stuck-in-a-host shape was real but sat one level out: the mini-map DTO insideScanner.Reachability. Splitting it killed signals along with five other pins.
S0 verification COMPLETE (2026-08-03). Route + Console + deploy inventory:
- Gateway routes (9) — to
findings.stella-ops.local(findings-ledger-web):/api/v1/finding-ledger,/api/v1/scoring,/api/v1/capabilities,/vuln/ledger/events,/v1/alerts,/v1/vex-decisions,/api/vuln-explorer; tofindings-security.stella-ops.local:/api/v2/security,/api/risk/aggregated-status. Plus/api/analytics→ platform-web (which reads this family’sanalytics.daily_*tables, §2.4). No routes target riskengine or vulncorrelation hosts — both are API-dark from the gateway. - Console typed clients:
security-findings.client.ts+artifact-triage.client.ts(both/api/v2/security/*),vex-trust-override.api.ts(vex-decision surface) — all route through the gateway prefixes above; the UI-E2 prefix registry carries them as a group. - Deploy reality (live inspection 2026-08-03): the FLAGSHIP compose defines none of the four hosts;
findings-ledger-web+riskengine-webrun in the live main stack viastella-services.yml(riskengine-WORKER does not run there), whilefindings-security-webandfindings-vulncorrelationlive in their own override files and are NOT RUNNING — so the Console’s/api/v2/securitysurface is dark in the live main stack despite its routes and typed clients. The merge’s compose delta therefore consolidates two RUNNING hosts and two dark ones intofindings-web+findings-worker, and the S7 forcing function must light the security surface, not merely preserve it. All four current hosts ride override/services files — the family enters the flagship only WITH its ownstellaops_findingsdatabase (§1). - CURRENCY NOTE (2026-08-10, SPRINT_20260722_010). The bullet above is a 2026-08-03 snapshot and the deploy half no longer holds. All five predecessor deployables now run and report
healthy— findings-ledger-web, findings-security-web, findings-vulncorrelation, riskengine-web and riskengine-worker — withdocker-compose.findings-security.override.ymlnow in the live stack’sconfig_fileslabel. The route inventory above is unchanged and still correct. The conclusion drawn from “dark” has inverted:/api/v2/security/findingsand/api/risk/aggregated-statusare not unserved, they are failing — 49 dispatch errors in 48 h, all42P01: relation "vuln.sbom_canonical_match" does not exist, because the G1 cutover (2026-08-04) dropped thevulnschema this family’s derive path joins. The S7 forcing function therefore has to repair the security surface (X18 re-homing), not merely light it. To verify current state rather than trusting either snapshot:docker ps --format '{{.Names}}' | grep -E 'findings|risk'anddocker logs --since 24h stellaops-findings-security-web | grep 42P01. - CURRENT PREFLIGHT (2026-08-24). Keep four evidence planes separate. Source: both target hosts independently Release-publish; the family closure/fail-closed/overlay conformance is 8/8, native canonical-route + Router HELLO publication is 1/1, and Router swap config is 10/10. Target database:
stellaops_findingsnow exists, owned by non-superuser loginfindings, at 11 MB with zero sessions; Findings 001–005 plus Eventing 001–004 have converged 59 regular relations and nine partitioned parents, while an exact sweep of all 57 non-ledger regular relations returns zero rows. Live hosts: the target roles still have zero containers, all five predecessors are healthy, and the staged overlay is absent from the liveconfig_fileslabel. Routes: all nine logical replacements remain pending in both configs (18 manifest matches). Therefore database presence and source readiness do not advance FND-9: FND-X18-7 owner projections/forcing/rollback and the reversible deploy/canary/route window remain outstanding.
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 prefix | New 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:
PreserveAuthHeadersis inert on a regex route.Program.cs:368builds the JWT-passthrough prefix list fromRoutes.Where(r => r.PreserveAuthHeaders && !r.IsRegex), and every findings route is regex — soAuthorizationis stripped on all nine today and identity arrives through the signed identity envelope. The consolidated host must be built against the envelope.- The old prefixes do not fail cleanly when removed.
/api/risk/aggregated-statusfalls through to policy-engine and/api/vuln-explorer/*to platform-web (both answer from the WRONG service);/vuln/ledger/events,/v1/alertsand/v1/vex-decisionsreach the SPA static fallback and return 200text/html. Only the/api/v1/*and/api/v2/*prefixes 404. /api/v2/securitywas never unambiguously this family’s. Notifier serves its own/api/v2/security/*surface; the collision is invisible only because the gateway sends the whole v2 prefix to findings-security.
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_001RAR-5/RAR-7; both gateway route tables now hold 22 routes and none of the eight rows it replaced survives. Sinceced8a2d286tools/scripts/fnd5-swap-findings-routes.ps1reports the swap as RETIRED and exits 2 on both-Checkand 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 asSPRINT_20260904_001RRS-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:
- global, immutable corpus/advisory/exploit generations live as a Findings-owned projection without tenant columns;
- SbomService upload/document authority feeds tenant-keyed Findings SBOM/component/match rows;
- the version-range matcher is a Findings-family library using only classified neutral foundations and producer-owned closed contracts — never the separate Vulnerabilities-owned DC-30 consumer SDK
StellaOps.VulnMatch.Core; - corpus publication and SBOM version streams use real epoch/sequence catch-up; multi-GB parts use content-addressed handles plus byte/line resume, never inline event payloads;
- corpus activation requires a durable match witness for the exact staging manifest whose epoch/sequence equals the locked
findings-advisory-sbom-v1checkpoint; a generic checkpoint can never certify a newly imported manifest, and epoch identity is never compared numerically; - one fenced A/B activation makes corpus, metadata, exploit evidence and matches visible together; temporary producer failure serves the last good generation as explicitly stale, while cold or corrupt state keeps the typed 503;
- FND-9 remains downstream of producer seams, shadow bootstrap/matching, local-route cutover, live upload-to-match acceptance, and rollback proof. FND-X18-1/2/4/5 are DONE in source: X18-5 adds the dark direct-owner corpus consumer, resumable content-addressed three-section import, health truth, and the fenced A/B transaction, but deliberately leaves the new staging generation inactive until X18-6 supplies its real SBOM match fence. X18-6 now has the manifest-specific durable witness, activation verifier, and the dark Findings-owned snapshot-stage/event-order/tombstone repository prerequisite. The repository replaces snapshot rows and the exact P6 checkpoint atomically, treats epochs as opaque identities, rejects gaps, and prevents a retired version from being resurrected. Its default-off authenticated direct-owner client now binds the closed snapshot, catch-up, exact-document and checkpoint wire with explicit tenant tokens, bounded responses and typed failure. Its pure component parser now binds those verified documents to deterministic component rows for exact CycloneDX 1.4-1.6 and the current top-level-package SPDX 2.3/3.0 upload shape; producer-recognized SPDX 3 JSON-LD remains closed. Its match materializer accepts one exact complete active or staging generation in steady state and one explicitly named retained generation only for rollback preparation, keeps every other generation untouched, replaces only the target manifest under the locked SBOM P6 checkpoint, proves exact live-version/ component/matched/unmatched counts, and publishes that manifest’s witness in the same transaction. Migration 005 forward-corrects only the source-cursor check so a real pre-stream head zero remains zero.
findings-workernow registers and schedules the owner client, coordinator and materializer behind the one default-off owner flag, owns one shared P6 lease across each pass, heartbeats between projection transactions, renews before each atomic materialization, and rebuilds only complete active/staging manifests whose witness is behind the canonical checkpoint. The staged overlay keeps that flag false pending producer setup. The consolidated source graph now has the tenant-exact active-generation read adapter plus a handler-owned request pin that labels successful bodies only with the state/as-of/digest read in their PostgreSQL snapshot. FND-X18-7 now also has the internal fenced prior-generation selector: it refreshes the exact retained prior witness, atomically makes the former current generationstaging, keeps both sides eligible for ordinary SBOM catch-up, and explicitly restores the former current only after its witness reaches the locked checkpoint. It deletes no generation/corpus rows and has no foreign-schema fallback. Its X11 replacement now uses migrations 006 and 012 for a rebuildable tenant/digest head and tenant-scoped source identities; supported reads consume exact local CVE/PURL reachability facts without folding absence to false. The bounded retained-page client and hosted projection pass are composed behind one explicit default-off gate: the leased pass reads only its authenticated exact tenant, verifies both producer set hashes, commits the v2 projection/inbox/checkpoint in the Findings database, and then reports the durable producer cursor. From-zero registration and epoch reset are tenant-local.AddActiveGenerationSecurityReadModelremoves every earlier registration of the carried read interfaces from the consolidated container; the predecessor foreign-SQL store was frozen tosrc/__Obsoleted/by FND-26 (2026-08-29), so there is no residual compiled fallback. Q-22’s byte-bounded full-detail event is the sole projection payload, and Q-23’s final tenant-bound scope isscanner:projection:read; the former service-global cursor andscanner:projection:read-allgrant are superseded. The shippedstellaopsversus intendedapi://scannerresource/client audience alignment remains the activation prerequisite, so the gate stays default-off. Live activation, Release projection, artifact triage, runtime forcing and the predecessor route swap remain absent; the Q-21 audited operator surface now exists on findings-web (header note). FND-X18-7 retains the live read-cutover/acceptance chain, while FND-X18-3’s frozen SbomService seam remains inactive pending its separately owned activation decision.
