Eager store resolution — every infrastructure store resolves at startup

Status: binding. Two owner rulings, the second widening the first:

  1. Owner ruling 2026-08-16 §4 — docs/product/decisions/20260816-owner-inputs-post-topology-close.md: “I agree with (b), with (a) for stores on the critical path.”
  2. Owner ruling 2026-08-19 (work-queue B12 — docs/product/decisions/20260818-owner-work-queue.md — resolving the SPRINT_20260722_009 DOC-6 design fork): “extend it everywhere as we already [do]. let’s make it consistent everywhere.” Decoded against item 4’s option letters: the eager-startup-resolution shape (a) extends from critical-path stores to ALL store families in every host; data-plane QA probes © remain the behavioural visibility layer; and the separate doctor-check mechanism (b) — funded as 009 DOC-6 and blocked on the ten-family design fork — is dropped as superseded. For a store that resolves eagerly, a doctor check adds visibility but no protection; consistency is worth more than the residual reporting nuance.

The critical-path criterion below is retained as history and as the severity guide (it still tells you how bad a broken store is — whole data plane vs one feature area) — but it no longer decides whether a store resolves eagerly. Since 2026-08-19 the answer is: it does.

The failure this exists to prevent

On 2026-08-15 packsregistry-web returned HTTP 500 on its entire data plane while reporting Up (healthy). SeedFsPacksRegistryBlobStore’s constructor called Directory.CreateDirectory on a path it cannot write (/app is mode 500 in the hardened base image). Three things combined:

  1. the constructor had an eager side effect that could fail;
  2. the DI registration was a lazy singleton factory, so nothing constructed it at startup;
  3. /healthz resolved nothing, so the health probe could not see it.

It was found by accident, while probing an unrelated routing question. Nothing in monitoring would have surfaced it. Two more instances of the same shape were found the same day by sweeping for it.

The posture (since 2026-08-19)

The criterion — is a store on the critical path? (severity guide)

A store is critical-path when the service’s data plane is unusable without it: if the store cannot construct, substantially every non-health endpoint fails.

Apply it by asking what else stops working, not by how important the store feels:

QuestionCritical-pathNot critical-path
Which endpoints fail if it cannot construct?substantially all of themone, or one feature area
Is there a useful degraded mode?no — the service serves nothingyes — the rest of the API still works
Is it injected by a core repository/service that nearly every handler uses?yesno

Worked examples, all measured rather than judged:

StoreVerdictWhy
retired IPacksRegistryBlobStore (packsregistry)CRITICAL — historical defectThe former combined seed/read-write seam was injected by PostgresPackRepository and PostgresAttestationRepository, so every data endpoint failed. Proven: /api/v1/packs, /mirrors, /compliance/summary and the offline-seed export all returned 500. JOB-11 replaced it with the closed immutable-seed reader and upload-store seams inventoried below.
FileReEnvelopeCheckpointStore (packsregistry)not critical — but resolves eagerly since 2026-08-19Backs exactly one endpoint, POST /api/v1/packs/re-envelope. Under the 2026-08-16 ruling it stayed lazy pending a check; the 2026-08-19 consistency ruling lifted the restriction. Its current constructor is bound to the explicit upload root and deliberately creates/repairs and write-probes its durable control directory, so eager resolution makes an unavailable checkpoint authority fail startup rather than a running re-envelope job.
IFeedSnapshotBlobStore (replay)not critical — but resolves eagerly since 2026-08-19Backs the four feed-snapshot routes. Same widening as above; same side-effect-free constructor since 3a645c5a8d.

The criterion is discriminating — it does not classify everything as critical, which is what makes it usable as a severity guide. Since 2026-08-19 it no longer gates eager resolution.

The inventory — every store family, every resolving host

Verified against src/ 2026-08-24 and pinned by EagerStoreResolutionConformanceTests in the architecture pack (src/__Tests/architecture/StellaOps.Architecture.Contracts.Tests/), whose estate sweep fails the build when a NEW store-shaped interface appears without either an eager resolve in every registering host or a recorded not-applicable ruling.

Store familyHost(s) resolving eagerlyNotes
IImmutablePacksRegistrySeedReaderpacksregistry-webCurrent closed read-only seam for operator-supplied immutable seed bytes; always registered and explicitly resolved after host build.
IPacksRegistryUploadStorepacksregistry-webCurrent closed durable-mutation seam; always registered as either the atomic upload store or the typed unavailable adapter, and explicitly resolved after host build.
IReEnvelopeCheckpointStorepacksregistry-webJoined 2026-08-19 (consistency ruling).
IFeedSnapshotBlobStorereplay-webJoined 2026-08-19. The 2026-08-15 defect host (3a645c5a8d).
IArtifactObjectStorescanner-web, scanner-workerRustFs or S3 driver; both constructors are side-effect-free (no network, no filesystem).
ISbomExportBlobStorescanner-webThree registration branches (ObjectStorage / Filesystem / dev fallback); exactly one is always registered. NOTE: the ObjectStorage branch resolves IContentAddressedBlobStore, which scanner-web itself never registers — a Provider=ObjectStorage deployment was broken at first use before and is a loud startup failure now. Live deployments use Filesystem.
IContentAddressedBlobStoreregistry-web, sbomservice, vulnerabilities-webIndependent owner registrations. The hub requires its configured durable symbol CAS and resolves it eagerly; BIN-10 freezes the predecessor BinaryIndex/Symbols registrations.
IDevPortalOfflineObjectStoreexportcenter-workerFilesystem-backed; clean constructor.
IRiskBundleObjectStoreexportcenter-workerFilesystem-backed; clean constructor.
IAuditBundlePayloadStoreexportcenter-webRegistered only on the durable (Postgres) audit-bundle path; the in-memory runtime legitimately has none.
IEvidenceObjectStoreevidence-locker-web, evidence-locker-workerNo constructor-time filesystem write. The FileSystem-kind store creates its temporary and destination directories only on the first real StoreAsync operation, so eager resolution is valid on a read-only deployment. Focused direct-construction and DI-resolution tests pin that contract; a first-write round trip pins directory creation and storage behavior.
IOciContentStore (release-orchestrator L2)ro-webEither the RustFs-backed adapter or the deliberate NoOp fail-safe; both construct.
IManifestBlobAdmissionStorero-webIn-memory implementation — no infrastructure failure mode; resolved for consistency.
IObjectStoreClient— not applicableNever a DI seam: no host registers it. Its production implementation is constructed inline inside ro-web’s L2 factory, whose registered surface (IOciContentStore) is inventoried above.

How to implement

Resolve after builder.Build() and before any request can be served:

var app = builder.Build();
_ = app.Services.GetService<IMyCriticalStore>();

Three rules, each learned from a real defect:

One more rule the widening added:

What happened to the doctor-check half ((b))

Superseded — owner ruling 2026-08-19 (work-queue B12). The mechanism was funded as SPRINT_20260722_009 DOC-6 and blocked on a real gap: the ruling’s “bind stores the way the doctor context binds the data source” analogy fails because storage has ten service-owned interfaces and no universal type — there is nothing to GetService<T>() for generically. Rather than build a shared marker interface or a config-driven probe SDK across ten families, the owner extended eager resolution everywhere and dropped the check mechanism. The limit that decision accepts, stated honestly: eager resolution proves construction, not writability — a root that mounts read-only still fails at first write, per request. That residual is covered by the AppRootWritabilityConformanceTests static gate (unmounted/:ro roots fail the build) and by data-plane QA probes, not by a runtime health check.

Historical references for the dropped mechanism (kept because the traps generalise to any health check): register the doctor context BEFORE AddServiceDoctorChecks — an implicitly-bound context reads as null and reports HEALTHY; and red-proof any check against the pre-fix code — a check that cannot fail is the defect it was meant to catch. EvidenceLockerWriteCheck (src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Storage/Checks/EvidenceLockerWriteCheck.cs) remains the model of a config-driven write probe, currently unreachable (its plugin family is orphaned — see 009’s Decisions & Risks).