Unknowns Architecture
Verified against commit
a6090ebaa2(2026-10-01). Re-verify with:grep -rnE 'Map(Get|Post|Put|Delete|Group|Methods)\(' src/Unknowns/StellaOps.Unknowns.WebService; grep -n 'CheckId =>' src/Unknowns/StellaOps.Unknowns.WebService/Doctor/UnknownsDoctorChecks.cs; ls src/Unknowns/__Libraries/StellaOps.Unknowns.Persistence/Migrations; grep -rn 'AddHostedService\|AddUnknownsRescan' src/Unknowns/StellaOps.Unknowns.WebService; grep -n 'unknowns-web:' -A80 devops/release/bundle/docker-compose.yml
Scope. Architecture for the Unknowns microservice: a structured registry that tracks unresolved components, symbols, and mappings that Scanner and other analyzers cannot definitively identify. This dossier is for implementers and operators of the Unknowns service and for analyzer authors that feed it.
Source-reconciliation note. Reconciled against
src/Unknowns/and the canonical scope catalogsrc/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs. The Unknown registry (/api/unknowns/*) is fully wired and persisted. The Grey Queue surface (/api/grey-queue/*) and the SLA / metrics / lifecycle background services are scaffolded in source but NOT activated at runtime — see §6 Implementation status for the exact gaps.
0) Mission & boundaries
Mission. Provide a structured registry for tracking unknown components, unresolved symbols, and incomplete mappings. Enable visibility into coverage gaps and guide future enhancement priorities.
Boundaries.
- Unknowns is a standalone Minimal-API microservice (
StellaOps.Unknowns.WebService) with its own HTTP API surface and schema ownership (unknownsPostgreSQL schema). - Unknowns is independently deployable and is not consolidated into Policy or any other module.
- Unknowns does not guess identities. It records what cannot be determined and (where evidence exists) attaches provenance hints with hypotheses.
- All unknowns are classified (
UnknownKind) and triaged (HOT/WARM/COLD bands) for actionability. - The rescan consumer (§3.6) reads the Vulnerabilities hub’s owner API (
/api/vulnerabilities/v1/...) under its own Authority client; it never reaches another service’s database. - The Scanner projection (§3.7) is the registry’s producer: it reads Scanner’s retained
scanner.scan.completedfeed under its own Authority client and writesunknowns.unknownin this service’s own database. - No project outside
src/Unknowns/**references this family any more (re-verified againstsrc/on 2026-08-23: zeroProjectReferencehits toStellaOps.Unknowns.*outside the tree, only tombstone comments inStellaOps.Platform.Persistence.csprojandStellaOps.Scanner.Worker.csproj). The earlier claim that Scanner, Signals and Platform consumed Unknowns libraries byProjectReferencewas true until SPRINT_20260722_020 W3-13, which found thescanner-worker → unknownsedge dead (a 65-type sweep found zero uses) and deleted it, and deleted the Platform edge with the DC-26 central-migrator plugin. Signals never had one.
Deployment facts. Compose service
unknowns-web(container${STELLAOPS_ESTATE_PREFIX:-stellaops}-unknowns-web, imagestellaops/unknowns-web, network aliasunknowns.stella-ops.local); port 8080; router service nameunknowns. It ships in the release bundle (devops/release/bundle/docker-compose.yml) and the local stack (devops/compose/docker-compose.stella-services.yml);devops/compose/docker-compose.unknowns.ymlis the own-database cutover overlay and is not in the default compose file list. Storage: own PostgreSQL databasestellaops_unknownsviaSTELLAOPS_POSTGRES_UNKNOWNS_CONNECTION; the host fails closed without that service-specific connection and has no generic shared-database fallback.
Boundary decision (Sprint 206, 2026-02-25): Unknowns retains its own schema ownership. No source consolidation into Policy and no DbContext merge. See
docs-archive/implplan/2026-03-04-completed-sprints/SPRINT_20260225_206_Policy_absorb_unknowns.md(archived) for rationale.
1) Solution & project layout
Layout under src/Unknowns/:
src/Unknowns/
├─ StellaOps.Unknowns.WebService/ # Standalone Minimal API host (Program.cs)
│ ├─ Doctor/UnknownsDoctorChecks.cs # doctor-check/v1: own-database cutover, schema census, tenant isolation
│ ├─ Endpoints/
│ │ ├─ UnknownsEndpoints.cs # /api/unknowns (registry; WIRED)
│ │ └─ GreyQueueEndpoints.cs # /api/grey-queue (answers 501 until a store is wired — see §6)
│ ├─ Security/UnknownsPolicies.cs # Unknowns.Read / Unknowns.Write / Unknowns.DoctorHealth policy names
│ ├─ Rescan/ # the rescan consumer (§3.6): options, hub client, pass + hosted worker
│ ├─ ScannerProjection/ # the producer (§3.7): feed client, deriver, store, consumer, pass + hosted worker
│ ├─ Translations/en-US.unknowns.json # localized endpoint descriptions
│ └─ ServiceCollectionExtensions.cs # AddUnknownsServices + DatabaseHealthCheck
├─ StellaOps.Unknowns.Services/ # SLA / metrics / lifecycle / watchdog services
│ │ # (scaffolded; NOT registered in the host — see §6)
│ ├─ UnknownsLifecycleService.cs
│ ├─ UnknownsMetricsService.cs
│ ├─ UnknownsSlaMonitorService.cs
│ ├─ GreyQueueWatchdogService.cs
│ ├─ SlaCalculator.cs / UnknownsSlaHealthCheck.cs
│ └─ Abstractions.cs
├─ __Libraries/
│ ├─ StellaOps.Unknowns.Core/ # Models, enums, provenance hints, analysis services
│ │ ├─ Models/ # Unknown, GreyQueueEntry, ProvenanceHint(+Type/Evidence), UnknownRanking, NativeUnknownContext
│ │ ├─ Hints/ # IProvenanceHintBuilder, ProvenanceHintBuilder
│ │ ├─ Services/ # UnknownTriageScorer (§3.3), NativeUnknownClassifier, RuntimeSignalIngester, UnknownProofEmitter, UnknownRanker
│ │ ├─ Repositories/ # IUnknownRepository, IGreyQueueRepository
│ │ └─ Schemas/provenance-hint.schema.json
│ ├─ StellaOps.Unknowns.Persistence/ # ACTIVE persistence
│ │ ├─ UnknownsConnectionResolver.cs # canonical-first, fail-closed connection resolution
│ │ ├─ UnknownsSchemaTopology.cs # database / schema / table / migration names
│ │ ├─ Postgres/ # UnknownsDataSource, PostgresUnknownRepository (runtime path used by the host)
│ │ ├─ EfCore/ # UnknownsDbContext, UnknownEntity, UnknownEfRepository, CompiledModels
│ │ ├─ Extensions/UnknownsPersistenceExtensions.cs # EF Core registration alternative (not used by host)
│ │ └─ Migrations/ # 001_v1_unknowns_baseline.sql, 002_unknowns_retention_classes.sql, 003_unknowns_rescan_schedule.sql (+_archived/, excluded from embedding)
│ └─ StellaOps.Unknowns.Persistence.EfCore/ # ORPHAN — referenced by no csproj (listed only in src/StellaOps.sln); do not use
│
└─ __Tests/
├─ StellaOps.Unknowns.Core.Tests/
├─ StellaOps.Unknowns.Persistence.Tests/ # PostgresUnknownRepositoryTests
└─ StellaOps.Unknowns.WebService.Tests/ # endpoint, persistence, startup-migration, health tests
Two persistence trees, one active. The host (
ServiceCollectionExtensions.AddUnknownsServices) registersIUnknownRepository → PostgresUnknownRepositorybacked byUnknownsDataSource(raw Npgsql). The EF Core path (UnknownsPersistenceExtensions.AddUnknownsPersistence/…WithCompiledModel,UnknownsDbContext,UnknownEfRepository) lives in the same project but is not registered by the WebService. The separateStellaOps.Unknowns.Persistence.EfCoreproject is an orphan referenced by no.csproj; its disposition is an open owner decision.
2) HTTP API surface
Two endpoint groups are mapped in Program.cs (MapUnknownsEndpoints() and MapGreyQueueEndpoints()). Both route groups call .RequireTenant() and resolve the tenant exclusively from the envelope-bound stellaops:tenant claim via HttpContext.RequireTenant() (a missing tenant is rejected with 400 tenant_missing); the legacy X-Tenant-Id request header is never honoured as a tenancy source (SPRINT_20260611_001, UNKNOWNS-1). Every endpoint also requires an authorization policy. Health endpoints (/healthz, /readyz, /health) carry no authorization policy; the doctor surface requires ops.health (§2.5).
2.1 Unknowns registry — /api/unknowns (WIRED)
Group default policy: Unknowns.Read (scope unknowns:read). All routes below are GET.
| Route | Name | Description |
|---|---|---|
GET /api/unknowns/ | ListUnknowns | Paginated list (skip, take=50; optional asOf bitemporal, kind, severity filters; subjectRef returns the OPEN unknowns recorded against exactly that purl / bom-ref, tenant-scoped — the per-component read the policy unknowns gate makes, SPRINT_20260905_002 PLC-2) |
GET /api/unknowns/{id:guid} | GetUnknownById | Single unknown with hints; 404 if absent |
GET /api/unknowns/{id:guid}/hints | GetUnknownHints | Provenance hints only (ProvenanceHintsResponse) |
GET /api/unknowns/{id:guid}/history | GetUnknownHistory | Bitemporal history (optional from/to; currently returns current state as a single entry) |
GET /api/unknowns/triage/{band} | GetUnknownsByTriageBand | By triage band (limit=50, offset=0) |
GET /api/unknowns/hot-queue | GetHotQueue | HOT unknowns for immediate processing (limit=50) |
GET /api/unknowns/high-confidence | GetHighConfidenceHints | Unknowns whose hints meet minConfidence (default 0.7, limit=50) |
GET /api/unknowns/summary | GetUnknownsSummary | Counts by kind / severity / triage band + total open |
The list endpoint is repository-driven:
asOf→AsOfAsync; elsekind→GetByKindAsync; elseseverity→GetBySeverityAsync; elseGetOpenUnknownsAsync+CountOpenAsync(seeUnknownsEndpoints.ListUnknowns).
2.2 Grey Queue — /api/grey-queue (forward design; returns 501 until a store is wired — see §6)
Group default policy: Unknowns.Read. Mutating routes additionally require Unknowns.Write and emit audit events (.Audited(AuditModules.Unknowns, …)).
Runtime gate (BL-4, SPRINT_20260713_002).
MapGreyQueueEndpointschecks at startup whether anIGreyQueueRepositoryis registered (IServiceProviderIsService). The live host wires none, so the full table below is not mapped; instead the entire/api/grey-queue/*surface answers501 Not Implemented(title: grey_queue_not_implemented,application/problem+json, anonymous) — a truthful “not yet built” signal rather than the former500from an unresolved DI dependency. The real handlers below map automatically the moment a repository is registered (which the WebService test host does, exercising them). Routes are kept mapped so integrators see an unbuilt-but-planned surface.
| Route | Verb | Policy | Audit action |
|---|---|---|---|
/api/grey-queue/ | GET | Read | — |
/api/grey-queue/{id:guid} | GET | Read | — |
/api/grey-queue/by-unknown/{unknownId:guid} | GET | Read | — |
/api/grey-queue/ready | GET | Read | — |
/api/grey-queue/triggers/feed/{feedId} | GET | Read | — |
/api/grey-queue/triggers/tool/{toolId} | GET | Read | — |
/api/grey-queue/triggers/cve/{cveId} | GET | Read | — |
/api/grey-queue/summary | GET | Read | — |
/api/grey-queue/{id:guid}/transitions | GET | Read | — |
/api/grey-queue/ | POST | Write | enqueue |
/api/grey-queue/{id:guid}/process | POST | Write | start_processing |
/api/grey-queue/{id:guid}/result | POST | Write | record_result |
/api/grey-queue/{id:guid}/resolve | POST | Write | resolve |
/api/grey-queue/{id:guid}/dismiss | POST | Write | dismiss |
/api/grey-queue/expire | POST | Write | expire |
/api/grey-queue/{id:guid}/assign | POST | Write | assign |
/api/grey-queue/{id:guid}/escalate | POST | Write | escalate |
/api/grey-queue/{id:guid}/reject | POST | Write | reject |
/api/grey-queue/{id:guid}/reopen | POST | Write | reopen |
Audit action constants are defined in StellaOps.Audit.Emission (AuditModules.Unknowns = "unknowns", AuditActions.Unknowns.*).
2.3 Authorization scopes
Defined in the canonical catalog StellaOps.Auth.Abstractions/StellaOpsScopes.cs:
| Scope | Constant | Used by |
|---|---|---|
unknowns:read | StellaOpsScopes.UnknownsRead | Unknowns.Read policy (all GET routes) |
unknowns:write | StellaOpsScopes.UnknownsWrite | Unknowns.Write policy (grey-queue mutations) |
unknowns:admin | StellaOpsScopes.UnknownsAdmin | Defined in the catalog; not referenced by any Unknowns endpoint policy (no Unknowns.Admin policy is registered in Program.cs) |
| ops.health | StellaOpsScopes.OpsHealth | Unknowns.DoctorHealth policy (doctor surface) |
Policies are wired via AddStellaOpsScopePolicy in Program.cs; policy name constants live in Security/UnknownsPolicies.cs (Unknowns.Read, Unknowns.Write, Unknowns.DoctorHealth).
2.4 Health
/healthz (liveness, Predicate=false → ignores DB) and /readyz (readiness, only the ready-tagged DatabaseHealthCheck) are explicitly anonymous; /health runs all registered checks. The container healthcheck and the gateway poll these probes.
2.5 Doctor (doctor-check/v1)
GET /doctor/unknowns/checks (MapServiceDoctorEndpoints, policy Unknowns.DoctorHealth, scope ops.health). Program.cs registers the ServiceDoctorContext explicitly against the connection this host resolved, because UnknownsDataSource does not surface as a bare NpgsqlDataSource and an implicit context would report a false green. Module checks (Doctor/UnknownsDoctorChecks.cs):
| Check id | Category | What it proves |
|---|---|---|
doctor.unknowns.database.own-database-cutover | Config | The host connection came from the canonical variable and names stellaops_unknowns; reports legacy-key and dead generic-key (ConnectionStrings:Default, ConnectionStrings:UnknownsDb) sources |
doctor.unknowns.database.schema-census | DataIntegrity | Catalog-only census of the declared table, views, and migration ledger (see §4) |
doctor.unknowns.database.tenant-isolation | DataIntegrity | RLS posture on unknowns.unknown, including the unknowns_app.require_current_tenant() predicate |
The standard service checks are added by AddServiceDoctorChecks("unknowns"). Registration with the Platform doctor registry is opt-in through Doctor:Registration:Enabled; when enabled, Authority, ClientId, and ClientSecret are required and must not be blank.
3) Contracts & data model
3.1 Unknown DTO (UnknownDto, from UnknownsEndpoints.cs)
Field names below match the serialized record exactly:
{
"id": "0f3b…-uuid",
"tenantId": "default",
"subjectHash": "<sha256-hex>",
"subjectType": "Binary",
"subjectRef": "/usr/lib/libfoo.so",
"kind": "UnknownBuildId",
"severity": "High",
"sourceScanId": "…",
"sourceGraphId": "…",
"sourceSbomDigest": "sha256:…",
"validFrom": "2025-01-15T10:30:00Z",
"validTo": null,
"resolvedAt": null,
"resolutionType": null,
"resolutionRef": null,
"compositeScore": 0.72,
"triageBand": "Hot",
"isOpen": true,
"isResolved": false,
"provenanceHints": [ /* ProvenanceHintDto[] */ ],
"bestHypothesis": "Binary matches openssl 1.1.1k from debian",
"combinedConfidence": 0.95,
"primarySuggestedAction": "verify_build_id",
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
The domain record Unknown (in Core/Models/Unknown.cs) additionally carries bitemporal sysFrom/sysTo, full scoring inputs (popularityScore, deploymentCount, exploitPotentialScore, uncertaintyScore/uncertaintyFlags, centralityScore/degreeCentrality/betweennessCentrality, stalenessScore/daysSinceAnalysis), rescan scheduling (rescanAttempts, nextScheduledRescan, lastAnalyzedAt), and evidence hashes (evidenceSetHash, graphSliceHash) — these are persisted but not all surfaced on the DTO.
3.2 UnknownKind (classification)
Core/Models/Unknown.cs defines 15 kinds. The baseline (migration 001) declared the first 10 in the unknowns.unknown_kind PostgreSQL enum; 003_unknowns_rescan_schedule.sql (SPRINT_20260929_005 PCD-190) adds the 5 native-binary kinds (Sprint 3500-0013-0001), so every C# kind can be stored and both repositories map all of them:
| Kind | DB enum value | Added by |
|---|---|---|
MissingSbom | missing_sbom | 001 |
AmbiguousPackage | ambiguous_package | 001 |
MissingFeed | missing_feed | 001 |
UnresolvedEdge | unresolved_edge | 001 |
NoVersionInfo | no_version_info | 001 |
UnknownEcosystem | unknown_ecosystem | 001 |
PartialMatch | partial_match | 001 |
VersionRangeUnbounded | version_range_unbounded | 001 |
UnsupportedFormat | unsupported_format | 001 |
TransitiveGap | transitive_gap | 001 |
MissingBuildId | missing_build_id | 003 |
UnknownBuildId | unknown_build_id | 003 |
UnresolvedNativeLibrary | unresolved_native_library | 003 |
HeuristicDependency | heuristic_dependency | 003 |
UnsupportedBinaryFormat | unsupported_binary_format | 003 |
Other enums: UnknownSubjectType (Package, Ecosystem, Version, SbomEdge, File, Runtime, Binary; the DB subject_type enum gained binary in 003), UnknownSeverity (Critical/High/Medium/Low/Info), ResolutionType (FeedUpdated, SbomProvided, ManualMapping, Superseded, FalsePositive, WontFix), TriageBand (Hot/Warm/Cold).
3.3 Triage scoring (HOT / WARM / COLD)
Composite score and band assignment are computed both in PL/pgSQL helpers (unknowns.calc_composite_score, unknowns.assign_band) and reflected in C#:
- Composite =
clamp01(wP·P + wE·E + wU·U + wC·C + wS·S)with default weights wP=0.25, wE=0.25, wU=0.25, wC=0.15, wS=0.10. - HOT ≥ 0.70, WARM 0.40–0.69, COLD < 0.40.
popularity_score = min(1, log10(1+deployments)/log10(101));staleness_score = min(1, age_days/14).
UnknownTriageScorer (Core/Services/UnknownTriageScorer.cs, SPRINT_20260929_005 PCD-190) computes the same composite in C# and is the only code that stamps next_scheduled_rescan:
- P from the row’s
deployment_count; E from its severity (critical 1.0, high 0.75, medium 0.5, low 0.25, info 0.0, unset 0.5); U and C are the stored scores its producer recorded (no producer measures centrality, so the trace listsCunderunavailableFactors); S from the days sincelast_analyzed_at, or since creation when the row was never analysed. Each factor and the composite are rounded to four decimals. - The next rescan is due by band: HOT +15 minutes, WARM +24 hours, COLD +7 days. Only the kinds a rescan can re-resolve are scheduled (
UnknownRescanPolicy:UnknownBuildId,MissingFeed); every other kind is scored withnext_scheduled_rescanleft empty. - The scorer is pure: the caller passes the instant, and the same row and instant give the same score and the same
scoring_tracebytes ("scorer":"unknowns.triage.v1").
3.4 Provenance hints
ProvenanceHint (Core/Models/ProvenanceHint.cs) is an immutable record with snake_case JSON: hint_id (content-addressed, format hint:sha256:<hex24>), type, confidence (0–1), confidence_level, summary, hypothesis, evidence, suggested_actions, generated_at, source.
Hint types (ProvenanceHintType, exactly 15): BuildIdMatch, DebugLink, ImportTableFingerprint, ExportTableFingerprint, SectionLayout, StringTableSignature, CompilerSignature, PackageMetadata, DistroPattern, VersionString, SymbolPattern, PathPattern, CorpusMatch, SbomCrossReference, AdvisoryCrossReference.
Confidence levels (HintConfidence): VeryHigh (≥0.9), High (0.7–0.9), Medium (0.5–0.7), Low (0.3–0.5), VeryLow (<0.3).
Suggested action (SuggestedAction): action, priority (1 = highest), effort (low/medium/high), description, optional link. The endpoint DTO (SuggestedActionDto) exposes action, priority, description, and url (mapped from link).
Typed evidence (ProvenanceEvidence): at most one of build_id, debug_link, import_fingerprint, export_fingerprint, section_layout, compiler, distro_pattern, version_string, corpus_match, plus a raw JSON escape hatch.
JSON Schema: src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Schemas/provenance-hint.schema.json.
The hint-combination confidence boost (single → two-agreeing → three-agreeing) is implemented in
Core/Hints/ProvenanceHintBuilder.cs/Core/Services/UnknownRanker.cs. Exact numeric examples are not re-stated here to avoid drift from the builder; consult those classes and__Tests/.../Hints/HintCombinationTests.csfor the authoritative formula.
3.5 Grey Queue model (scaffolded)
GreyQueueEntry (Core/Models/GreyQueueEntry.cs) carries a DSSE-style evidence bundle (SBOM slice, advisory snippet, VEX evidence, diff traces, reachability evidence — each with an integrity hash), a deterministic fingerprint, trigger conditions (feeds/tools/CVEs/PURL patterns), and processing metadata (processingAttempts, maxAttempts=10, retry scheduling).
State machine (GreyQueueStateMachine): statuses Pending, Processing, Retrying, UnderReview, Escalated, Resolved, Rejected, Failed, Expired, Dismissed. Resolved/Expired are terminal; Rejected/Failed/Dismissed can return to Pending; UnderReview requires an assignee.
GreyQueueReason (10): InsufficientVex, ConflictingVex, MissingReachability, AmbiguousIdentity, FeedNotAvailable, ToolUnsupported, BinaryAnalysisInconclusive, BackportUncertain, MultipleInterpretations, AwaitingUpstreamAdvisory.
GreyQueueResolution (7): FeedUpdate, ToolUpdate, VexReceived, ManualResolution, Superseded, NotApplicable, Expired.
3.6 Rescan consumer (WIRED; SPRINT_20260929_005 PCD-190)
A hosted worker in unknowns-web (Rescan/UnknownsRescanPass.cs, UnknownsRescanWorker) rescans each configured tenant’s due unknowns: open rows whose next_scheduled_rescan has passed (IUnknownRepository.GetDueForRescanAsync, served by the rebuilt ix_unknown_rescan_schedule). A rescan is a re-read of the Vulnerabilities hub, by kind (owner ruling 2026-10-01: hub re-resolution only; no Scanner image rescan):
| Kind | Hub read | Facts or issues found | None found |
|---|---|---|---|
UnknownBuildId | GET /api/vulnerabilities/v1/binaries/build-ids/{subjectRef} | resolved FeedUpdated, resolution_ref vulnerabilities-hub:build-id:<id> | attempt no-hub-facts, rescored and rescheduled by band |
MissingFeed | GET /api/vulnerabilities/v1/products/{productKey}/issues, the key derived from the subject purl by ProductKeyV1 | resolved FeedUpdated, resolution_ref vulnerabilities-hub:product:<key> | attempt no-hub-issues, rescored and rescheduled |
MissingFeed without a hub key (not a purl, or pkg:generic) | none | attempt not-rescannable:no-product-key, schedule cleared | |
| any other kind | none | attempt not-rescannable:<Kind>, schedule cleared |
A resolution records the attempt (resolved), closes valid_to and notes the count and up to ten vulnerability ids. A failed hub read (transport, a non-2xx answer, a token refusal) records its reason (for example hub-http-503) and reschedules by band, so an outage delays a rescan instead of retrying it every poll.
- Identity. Dedicated confidential client
stellaops-unknowns-vulnerability-hub(advisory:read aoc:verify, the hub’sAdvisoryReadpolicy; audiencestellaops; tenantsdefault e2e-lab), one token per tenant through a token client in its own container. The client id and scope are pinned in code; the secret comes fromUNKNOWNS_VULNERABILITY_HUB_CLIENT_SECRET. - Lease. One pass per tenant at a time under
unknowns:rescan:tenant:<tenant>in this service’s owneventingschema (AddEventingReliability), renewed inline before each unknown; a failed renewal ends the pass before its next write. The writes are the repository’s own and are idempotent per row. - Configuration (
Unknowns:Rescan):Enabled(default off in code; compose sets${UNKNOWNS_RESCAN_ENABLED:-true}),Tenants(defaultdefault),PollSeconds(60),BatchSize(100),LeaseSeconds(120, must exceedHub:RequestTimeoutSeconds), andHub:BaseAddress,Hub:Issuer,Hub:MetadataAddress,Hub:TokenEndpoint,Hub:ClientSecret,Hub:RequestTimeoutSeconds(30),Hub:MaximumResponseBytes(4 MiB). Enabled with incomplete settings, the host fails at startup naming the key. - Rows. The consumer acts only on rows a producer created; the producer is the Scanner projection (§3.7).
3.7 Scanner projection, the producer (WIRED; SPRINT_20260929_005 PCD-250)
A hosted worker in unknowns-web (ScannerProjection/, UnknownsScannerProjectionWorker) follows each configured tenant’s partition of Scanner’s retained scanner.scan.completed v2 feed (GET /api/v1/scans/completed/events, consumer id unknowns.scan-unknowns-v1) and records the unknowns each completed scan reports. It is the PCD-215 projection shape: one envelope per transaction, in which the lease fence, the inbox admission, the rows and the checkpoint commit together, so the checkpoint never passes an event whose unknowns did not land; the producer cursor is reported only after that commit.
- What becomes an unknown (
ScanUnknownsDeriver). The feed carries one kind today: an unresolved native binary, which Scanner writes aspkg:generic/<file>@unknown?...with itsbuild-id,arch,osandchecksum(NativePurlBuilder.FromUnresolvedBinary, the only writer of that version). A purl qualifies only with abuild-idorchecksumqualifier.- With a build id:
UnknownBuildId, subject typeBinary, subject the build id, severity Low. The rescan (§3.6) re-reads the hub’s facts for it. - Without one:
MissingBuildId, subject the purl itself, severity Medium. No hub read answers it, so it is not scheduled. The severities areNativeUnknownClassifier’s. Every other component purl is ignored.
- With a build id:
- Rows (
ScanUnknownsStore). One open row per tenant, subject and kind (uq_unknown_one_open_per_subject): a subject already open is left as it is, so a re-scan, a replay or another image carrying the same binary adds nothing. A subject the rescan resolved is closed, and a later scan that reports it again opens a new row. Each row is scored at creation byUnknownTriageScorer(deployment count 1, uncertainty 0.30 with theno_provenanceflag), so anUnknownBuildIdis due for its first rescan by band (COLD: 7 days).valid_fromis the scan’s completion,sys_fromthe recording;created_byisunknowns-scanner-projection;contextnames the scan, image, event, purl, file name, build id, checksum, architecture and platform.source_scan_idstays empty: Scanner’s scan ids are 40-hex strings, not UUIDs. - Epochs and retention. A new producer epoch moves the checkpoint and replays from sequence one; no row is deleted (the table is source-of-truth). A FIRST consumption of a stream already pruned starts at Scanner’s retention horizon; a consumer with a checkpoint that falls behind stops fail-closed.
- Identity. Dedicated confidential client
stellaops-unknowns-scanner-projectionholding onlyscanner:projection:read(audiencesstellaopsandapi://scanner; tenantsdefault e2e-lab), the fourth named SCN-7/Q-23 identity (owner ruling 2026-10-01); one token per exact tenant, minted through a token client in its own container; secretUNKNOWNS_SCANNER_PROJECTION_CLIENT_SECRET. - Lease.
unknowns:scanner-projection:tenant:<tenant>in this service’seventingschema, fenced inside each transaction and renewed between envelopes. - Configuration (
Unknowns:ScannerProjection):Enabled(default off in code; compose sets${UNKNOWNS_SCANNER_PROJECTION_ENABLED:-true}),Tenants(defaultdefault),BaseAddress(Scanner),Issuer,MetadataAddress,TokenEndpoint,ClientSecret,PollSeconds(15),AcquireRetrySeconds(15),LeaseSeconds(300),EventPageLimit(256),MaximumResponseBytes(4 MiB),RequestTimeoutSeconds(30). Enabled with incomplete settings, the host fails at startup naming the key.
4) Persistence
The service owns the PostgreSQL database stellaops_unknowns(its own role on the shared server, ADR-039). Inside it the owning schema is unknowns, plus the unknowns_app helper schema that holds only the RLS predicate function. Since SPRINT_20260929_005 PCD-190 the same database also carries the shared eventingschema (AddEventingReliability, its own startup migration chain and ledger), whose eventing.leases table holds the rescan consumer’s per-tenant lease (§3.6); a bare NpgsqlDataSource named Unknowns.Eventing, built from the resolver’s connection, serves it.
Connection resolution (UnknownsConnectionResolver, precedence order): environment variable STELLAOPS_POSTGRES_UNKNOWNS_CONNECTION; configuration key Postgres:Unknowns:ConnectionString. No other key is read; the former alias Postgres:ConnectionString is ignored. A canonical source that is present but blank is refused instead of falling back; with no source the host fails closed at startup (UnknownsPostgresOptionsValidator also rejects a whitespace connection string). Other PostgresOptions bind from the Postgres section. Startup convergence is mandatory: ServiceCollectionExtensions.AddUnknownsServices (the host path) and UnknownsPersistenceExtensions (the EF path) both call AddStartupMigrations(UnknownsDataSource.DefaultSchemaName, "Unknowns.Persistence", typeof(UnknownsDataSource).Assembly), and the persistence project embeds Migrations/**/*.sql (explicitly excluding Migrations/_archived/** per Sprint 20260512_026 to avoid leaf-name collisions in the migration runner).
Migrations (active, in apply order; UnknownsSchemaTopology.OwnedMigrations is the exhaustive list): 001_v1_unknowns_baseline.sql, 002_unknowns_retention_classes.sql and 003_unknowns_rescan_schedule.sql. The 002 file is comment-only: it records retention-class: source-of-truth on unknowns.unknown (rows are never deleted; the closed bitemporal rows are the history). The 003 file (PCD-190) adds the binary subject type and the five native-binary kinds to the enums (§3.2) and rebuilds the five partial indexes the baseline built on sys_to = 'infinity' (ix_unknown_triage_band, ix_unknown_hot_queue, ix_unknown_rescan_schedule, now keyed (tenant_id, next_scheduled_rescan), ix_unknown_kind_band, ix_unknown_uncertainty_flags) and the triage_summary view on the open-row predicate valid_to IS NULL AND sys_to IS NULL: no open row ever has sys_to = 'infinity', so those indexes were empty and the view summarised nothing. The index count stays 21. The 001 baseline is the pre-1.0 chain (001_initial_schema.sql, 002_provenance_hints.sql, 003_unknowns_scoring_drift_repair.sql) was collapsed into it. Those three filenames now exist only under Migrations/_archived/pre_1.0/mig061/ and are excluded from embedding; cite the baseline, not the archived names. Folded content, in preserved source order:
- (from
001_initial_schema.sql) — schemas, enum types, the bitemporalunknowns.unknowntable (with full scoring columns), indexes, views (unknowns.current,unknowns.resolved,unknowns.triage_summary), temporal/scoring PL/pgSQL functions (as_of,count_by_kind,count_by_severity,calc_popularity_score,calc_staleness_score,assign_band,calc_composite_score), row-level security (unknown_tenant_isolation,FORCE ROW LEVEL SECURITY, with no cluster-global bypass role), and anupdated_attrigger. - (from
002_provenance_hints.sql) — addsprovenance_hints(JSONB),best_hypothesis,combined_confidence,primary_suggested_action; GIN + partial confidence indexes;validate_provenance_hintsfunction +chk_provenance_hints_validconstraint. - (from
003_unknowns_scoring_drift_repair.sql) — idempotent reconciliation for databases that drifted when the runner previously picked up archived pre-1.0 scripts (Sprint 20260512_026, UNKNOWNS_DRIFT-01). No-op on fresh DBs.
There is no
grey_queuetable. No active migration createsunknowns.grey_queue(or any grey-queue table). The only table created isunknowns.unknown. See §6.
Tenant isolation relies on the app.tenant_id session GUC, enforced by RLS via unknowns_app.require_current_tenant(). Startup migrations run as the constrained unknowns database owner (NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS) and create no cluster-global role. Because RLS is forced, that owner also needs the tenant GUC for tenant-scoped access.
doctor.unknowns.database.schema-census is deliberately a catalog-only structural check. It must not issue an unscoped count against unknowns.unknown: under the production owner posture that would correctly fail with SQLSTATE P0001, while granting global visibility would defeat the RLS boundary. Tenant-scoped repository probes, not the estate census, prove domain-row visibility.
5) Observability
Metrics and tracing are defined in StellaOps.Unknowns.Services/UnknownsMetricsService.cs (Meter and ActivitySource both named StellaOps.Unknowns, version 1.0.0):
- Gauges:
unknowns_queue_depth,unknowns_queue_depth_hot,unknowns_queue_depth_warm,unknowns_queue_depth_cold,unknowns_sla_compliance_rate,unknowns_sla_compliance. - Histograms:
unknowns_processing_time_seconds,unknowns_resolution_time_hours(labelband). - Counter:
unknowns_state_transitions_total(labelsfrom_state,to_state). - Structured log messages via
UnknownsLogging(LoggerMessagesource-gen) and W3Ctraceparentpropagation helpers (TraceContextPropagation).
These meters live in
UnknownsMetricsService, which is not registered in the running host (see §6); they emit only if/when that background service is wired.
6) Implementation status (what is and isn’t wired at runtime)
This section is the load-bearing correction versus prior revisions of this dossier.
WIRED (functional in the running stellaops-unknowns-web):
- The
/api/unknowns/*registry (including thesubjectRefread the Policy unknowns gate uses), backed byIUnknownRepository → PostgresUnknownRepository(registered inAddUnknownsServices). AddStartupMigrationsfor theunknownsschema; theunknowns.unknowntable + provenance-hint columns.- Authentication/authorization (
unknowns:read/unknowns:write/ops.health), tenant middleware, Router integration, localization, audit emission, CORS, health endpoints, and the doctor-check/v1 surface. - Provenance-hint persistence (
AttachProvenanceHintsAsync,GetWithHighConfidenceHintsAsyncinPostgresUnknownRepository), with deterministic ordering and tenant scoping (Sprint 304). - The rescan consumer (§3.6, SPRINT_20260929_005 PCD-190): the hosted
UnknownsRescanWorker, registered byAddUnknownsRescanbehindUnknowns:Rescan:Enabled, andUnknownTriageScorer(§3.3). It is the first caller ofGetDueForRescanAsync,UpdateScoresAsyncandRecordRescanAttemptAsync. - The Scanner projection (§3.7, PCD-250): the hosted
UnknownsScannerProjectionWorker, registered byAddUnknownsScannerProjectionbehindUnknowns:ScannerProjection:Enabled, the registry’s first producer.
SCAFFOLDED but NOT wired (will not function at runtime as-is):
- Grey Queue endpoints (
GreyQueueEndpoints.MapGreyQueueEndpoints, mapped inProgram.cs) resolveIGreyQueueRepositoryvia[FromServices], but no concreteIGreyQueueRepositoryis registered anywhere in the host DI, and nogrey_queuetable exists in any migration. As of BL-4 (SPRINT_20260713_002) the mapping is gated on repository availability (IServiceProviderIsService): with no store wired, the whole/api/grey-queue/*surface returns a documented501 Not Implemented(grey_queue_not_implemented) instead of the former runtime500. The full handler set (§2.2) maps automatically once a repository is registered. Treat the Grey Queue HTTP surface as a forward design that is not yet operational. - Background services in
StellaOps.Unknowns.Services(UnknownsLifecycleService,UnknownsMetricsService,UnknownsSlaMonitorService,GreyQueueWatchdogService,UnknownsSlaHealthCheck) all depend onIGreyQueueRepositoryand are not registered (Program.cscontains noAddHostedServicefor them). The rescan worker (§3.6), the Scanner projection worker (§3.7) and the eventing retention pruners are the host’s only background services. - Core analysis services (
NativeUnknownClassifier,RuntimeSignalIngester,UnknownProofEmitter,UnknownRanker,ProvenanceHintBuilder) are not referenced by the WebService host; they are library building blocks awaiting integration.UnknownTriageScoreris the one Core service the host uses. - The standalone
StellaOps.Unknowns.Persistence.EfCoreproject is an orphan referenced by no.csproj.
Follow-up flag for maintainers. Option (b) — gate the grey-queue mapping so it fails honestly until backed — is done (BL-4, SPRINT_20260713_002): the surface now returns
501 Not Implementedrather than500. The remaining forward work is option (a): register a concreteIGreyQueueRepositoryimplementation + add agrey_queuemigration + register the background services, at which point the gate lights up the real API with no further mapping changes.
Related Documentation
- Scanner:
../scanner/architecture.md - Signals:
../signals/architecture.md - Policy:
../policy/architecture.md(Policy Engine and Policy Gateway readGET /api/unknownsover HTTP throughUnknownsGateChecker, configured byPolicy:UnknownsGate:UnknownsApiUrl; Policy owns no Unknowns persistence or source) - Boundary decision (archived):
docs-archive/implplan/2026-03-04-completed-sprints/SPRINT_20260225_206_Policy_absorb_unknowns.md
Advisory Gap Status (2026-03-04 Batch)
Status: provenance-hints persistence delivered in Sprint 304.
AttachProvenanceHintsAsyncandGetWithHighConfidenceHintsAsyncare implemented in active repositories:src/Unknowns/__Libraries/StellaOps.Unknowns.Persistence/Postgres/Repositories/PostgresUnknownRepository.cs(runtime path used by the host)src/Unknowns/__Libraries/StellaOps.Unknowns.Persistence/EfCore/Repositories/UnknownEfRepository.cs(EF path, not registered by the host)
- High-confidence retrieval applies deterministic ordering (
combined_confidence DESC,id ASC) and tenant scoping. - The provenance-hint columns are added to
unknowns.unknownbysrc/Unknowns/__Libraries/StellaOps.Unknowns.Persistence/Migrations/001_v1_unknowns_baseline.sql(the section folded in from the archived002_provenance_hints.sql). - The duplicate project
src/Unknowns/__Libraries/StellaOps.Unknowns.Persistence.EfCore/**is an orphan (referenced by no csproj) — do not extend it. - Startup convergence is mandatory for the owned
unknownsschema (AddStartupMigrations+ embeddedMigrations/**/*.sql, archived scripts excluded). Fresh PostgreSQL volumes converge through service startup, not manual init scripts.
Closure sprint (archived):
docs-archive/implplan/2026-03-04-completed-sprints/SPRINT_20260304_304_Unknowns_provenance_hints_persistence_completion.md
