ADR-038 — Evidence artifact nomenclature: Decision Capsule vs Evidence Pack vs Evidence Bundle
- Status: Accepted
- Date: 2026-07-20
- Deciders: G8 cross-cutting (Fable user-review remediation, phase 2 backend-remnants — the “capsule / pack / bundle nomenclature unification”
[decision]item) - Relates: ADR-020 (CGS Merkle tree), ADR-025 (operator-signed decisions), ADR-036 (local RFC 3161 TSA); the
evidence-capsules-canonical-cutoverFE effort - Evidence:
docs/implplan/_evidence/20260717_manual_review/fable-user-review-report.md§1 (capsule-500), §8 (nomenclature split);-backend-remnants.md“Cross-cutting / product”
Context
The product ships three artifacts that all wear “evidence” branding, and the 2026-07-19 Fable review found the three-way naming split “leaks all the way to a 500” and “confuses navigation” (§8), with the sharpest failure in §1:
The console capsule route
/evidence/capsules/:iderrored “Unable to load evidence pack … Unknown error” because it calledGET /v1/evidence-packs/{id}→ HTTP 500 — a different, empty store (evidence_packs) than the one the sealed capsule actually lives in (decision_capsules). “Capsule vs pack vs bundle nomenclature leaks all the way to a 500.”
The three artifacts are genuinely different concepts that happen to share the word “evidence” — merging them into one word would be wrong. What was wrong was (a) no settled vocabulary, and (b) one route cross-wired to the wrong store. This ADR settles the vocabulary and records the store contract so the collision cannot recur.
Decision
Keep the three artifacts distinct, with one canonical name, store, and route each. Never cross-wire a route to another artifact’s store.
| Artifact | What it is | Owning module / store | Canonical route | User-facing name |
|---|---|---|---|---|
| Decision Capsule | The sealed, signed (DSSE), content-addressed manifest of one release/gate decision’s evidence references (SBOM / policy / VEX / reachability) + retention. The cryptographic proof unit of a decision. Lifecycle: create → seal → verify → replay → export. | EvidenceLocker evidence_locker.decision_capsules (CapsuleEndpoints, ICapsuleRepository) | /api/v1/evidence/capsules[/{id}] | Decision Capsule |
| Evidence Pack | An Advisory-AI investigation’s claims + evidence items about a vulnerability subject (CVE / finding / component), content-digested and DSSE-signable. Analytical output of Advisory AI — not a decision seal. | AdvisoryAI (StellaOps.Evidence.Pack, IEvidencePackStore) | /v1/evidence-packs[/{packId}] | Advisory Evidence Pack |
| Evidence Bundle | An operator-assembled, downloadable ZIP export aggregating audit artifacts (SBOM / VEX / attestations / policy / vuln) for a subject or time-range. A packaging / export unit. | Audit / EvidenceLocker export path | /evidence/bundles | Evidence Bundle |
Naming contract (binding):
- Routes and stores stay 1:1 and never cross-wire. The capsule route reads
decision_capsules; the pack route reads the evidence-pack store; the bundle route reads the bundle export. A capsule id resolved against the pack store (the original bug) is forbidden. - Unknown ids return
404, never500. The capsule read (CapsuleEndpoints.GetCapsuleAsync) answers authoritatively fromdecision_capsulesand returns404 capsule_not_foundfor an unknown id. - User-facing copy uses the qualified name — “Decision Capsule”, “Advisory Evidence Pack”, “Evidence Bundle” — at any surface where two of them could be confused. Never a bare “pack” / “capsule” / “bundle” at a collision point.
Status of implementation (what already landed vs. deferred)
Data-path — DONE (the [H] capsule-500 fix):
- Backend:
GET /api/v1/evidence/capsules/{capsuleId}(src/EvidenceLocker/StellaOps.EvidenceLocker/Api/Capsules/CapsuleEndpoints.cs,GetCapsuleAsync) reads thedecision_capsulesstore viaICapsuleRepositoryand returns404(never500) for an unknown id — “the capsule-native read the console capsule route must use instead of the AdvisoryAI evidence-pack store.” - Frontend:
DecisionCapsuleClient(src/Web/StellaOps.Web/src/app/core/api/decision-capsule.client.ts) targets/api/v1/evidence/capsules; the capsule detail route (/evidence/capsules/:capsuleId, title “Decision Capsule”) renders a dual-store viewer that checks the capsule store and the legacy pack store and shows an honest404naming both stores only when neither has the record — no silent 500, no faked empty state.
Deferred (cosmetic, tracked here so it is not lost): the FE component and folder that serve the capsule route are still named EvidencePackViewerComponent / src/Web/StellaOps.Web/src/app/features/evidence-pack/ even though the route is the Decision Capsule route. The user never sees this (route title/breadcrumb say “Decision Capsule”); it is an internal rename only. It is deliberately deferred because a component/folder rename is a high-collision FE refactor best done in a quiet FE pass, not into concurrent multi-agent churn.
Consequences
- Positive: the capsule-route 500 is gone; the three artifacts have one settled vocabulary, store, and route each; auditors reach the right store; navigation no longer conflates them.
- Negative / residual: an internal FE name (
EvidencePackViewerComponent) still reads as a “pack” while serving a “capsule” — cosmetic, deferred above. - Guardrail: any future “evidence X” surface must map to exactly one of the three rows above and read that row’s store. A new store or route that blurs the boundary should update this ADR first.
Related work (co-shipped in the same G8 pass, not part of this decision)
The slow console list endpoints flagged in the review §12 were fixed alongside this nomenclature ratification: the /console/admin/users list N+1 (per-user role query + fresh DB connection per user → one batched query), the console-dashboard serial upstream fan-out (findings + VEX + run-health now fetched concurrently), and a composite (tenant_id, type, name) index for GET /api/v1/integrations?type=…. Those are engineering fixes, recorded in the sprint/PR — not architectural decisions — and are noted here only so a reader tracing the G8 cross-cutting pass finds them.
