Decision (2026-06-12): Option A adopted — by the interim corroboration dashboard epic #1400 (implementation: #1401). Directory-per-source, add-only immutable pointers (recipes/evidence/<slug>/<source>/<bundle-digest>.yaml). See docs/design/013-interim-evidence-dashboard.md → Pointer file topology. Note: first-party UAT ingests directly (no per-run commit); committed per-source pointers are the community/partner channel.
Summary
Evidence pointers today are keyed solely by recipe slug — recipes/evidence/<slug>.yaml — and hold a single attestation. WritePointer does an unconditional os.WriteFile of a freshly-built, single-element pointer (pkg/evidence/attestation/pointer.go:35-91), and the verifier hard-rejects more than one attestation (pkg/evidence/verifier/pointer.go:74-83). The consequence: when two contributing parties submit evidence for the same recipe, the second submission silently overwrites the first. The signer identity (PointerSigner.Identity / Issuer) is the de-facto "source" but is not part of the path, so it cannot disambiguate.
This issue proposes making pointers per-source, where a source is the contributing party that produced the evidence (bound to its signer OIDC identity). Two layouts are on the table; this issue analyzes and compares them.
Context: what a per-source layout improves over the deferred design
ADR-007 (docs/design/007-recipe-evidence.md:604-608, 734) already reserves a schema-2.0 path: keep the one <slug>.yaml file but let attestations[] grow, adding a role: (primary / supplementary / negative). That "single shared file, longer list" approach has two structural weaknesses that both proposals below fix:
- Guaranteed git merge conflicts — every new source edits the same file's list; concurrent PRs collide.
- Muddy ownership — one file owned by everyone;
CODEOWNERS can't scope review to "the party that owns this entry," and nothing stops one party rewriting another's list entry.
Giving each source its own path eliminates both. The open question is which per-source layout.
Option A — directory per source (add-only files)
recipes/evidence/<slug>/<source>/<bundle-digest>.yaml
Each contributing party owns a subdirectory; each validation run lands a new, immutable pointer file (named by bundle digest for content-addressed uniqueness). Nothing is ever modified in place — this matches today's WritePointer, which already just writes a fresh file.
- + Write-once/immutable → no read-modify-write, zero merge conflicts ever (every run is a uniquely-named new file).
- + Cleanest ownership:
CODEOWNERS rule per recipes/evidence/<slug>/<source>/ directory.
- + Natural home for future per-source side-artifacts (logs pointer, redaction variants, README).
- + Smallest code delta — no append/merge logic; keeps the "bundle is authoritative, pointer is regenerated" model intact.
- − Directory grows unbounded over time (many small files); consumers need glob + "latest/best" selection logic.
Option B — one file per source (mutable, append into list)
recipes/evidence/<slug>/<source>.yaml # attestations[] accumulates this party's runs
Each party owns a single YAML and appends new attestations into its own attestations[] list — reusing the list structure the schema already defines.
- + Compact and bounded: file count = number of sources; a party's full history reads top-to-bottom in one file.
- + Directly uses the already-designed
attestations[] list (no schema invention beyond per-source pathing).
- − Requires new read-modify-write append logic — today's
WritePointer overwrites; appending means loading the existing file, verifying prior entries are untouched, and re-emitting.
- − Concurrent PRs from the same source conflict on that file (rare, but real).
- − Mutable file: every PR diff touches an existing committed file, so review/CI must guard against silent rewriting of earlier entries.
Comparison
| Dimension |
A: dir per source (add-only) |
B: file per source (append) |
| Cross-source overwrite |
Eliminated (separate dirs) |
Eliminated (separate files) |
| Merge conflicts |
None (unique filenames) |
Same-source concurrent pushes conflict |
| New code needed |
Minimal — keeps overwrite-with-fresh-file |
Read-modify-write append + tamper guard |
| Mutability |
Immutable, add-only |
Mutable per-source |
CODEOWNERS granularity |
Per directory (cleanest) |
Per file glob (workable) |
| File-count growth |
Grows per run (needs pruning/selection) |
Bounded by source count |
| Consumer discovery |
Glob <slug>/*/*.yaml + select |
Glob <slug>/*.yaml |
| Alignment w/ current code |
WritePointer already writes fresh |
attestations[] list already designed |
| Provenance of each run |
One file = one run (trivial) |
Entries within a shared list |
Recommendation (for discussion)
Lean Option A (directory per source, immutable add-only files): it removes the most failure modes (no merge logic, no in-place mutation, no tamper surface), gives the cleanest ownership boundary, and is the smallest delta from today's overwrite-a-fresh-file behavior. A hybrid is worth considering — Option A's <slug>/<source>/ directory for ownership, with a single appendable pointer inside per Option B's semantics — if compactness matters more than immutability.
Cross-cutting work either option requires
- Define and bind "source." Derive the
<source> slug from the signer OIDC identity/issuer and enforce (CI + CODEOWNERS) that a party can only write under its own path — otherwise the path is squattable.
- Schema bump. Move off the hard single-attestation reject (
verifier/pointer.go:74-83); version the pointer schema.
- Verifier/consumers. Replace the fixed-path load and hardcoded
Attestations[0] access (verifier/fetch.go:136-139, verify.go:147-148) with glob-based discovery and multi-source iteration/aggregation.
- Emit/publish path construction. Update
emit.go:166 and publish.go:120, which hardcode recipes/evidence/<RecipeName>.yaml.
- Recipe-health (
docs/design/009-recipe-health-tracking.md) — confirm how multi-source evidence rolls up into a recipe's status.
- Docs/ADR. Amend ADR-007's pointer-layout section and the deferred-features table.
References
pkg/evidence/attestation/pointer.go:35-91 — BuildPointer / WritePointer (single-element, overwrite)
pkg/evidence/attestation/types.go:192-231 — Pointer, PointerAttestation, PointerSigner
pkg/evidence/verifier/pointer.go:74-83 — single-attestation hard reject
pkg/evidence/verifier/{fetch.go:136-139,verify.go:147-148} — Attestations[0] consumers
pkg/evidence/attestation/{emit.go:166,publish.go:120} — hardcoded path
docs/design/007-recipe-evidence.md:576-614, 734 — pointer schema + deferred multi-instance
Summary
Evidence pointers today are keyed solely by recipe slug —
recipes/evidence/<slug>.yaml— and hold a single attestation.WritePointerdoes an unconditionalos.WriteFileof a freshly-built, single-element pointer (pkg/evidence/attestation/pointer.go:35-91), and the verifier hard-rejects more than one attestation (pkg/evidence/verifier/pointer.go:74-83). The consequence: when two contributing parties submit evidence for the same recipe, the second submission silently overwrites the first. The signer identity (PointerSigner.Identity/Issuer) is the de-facto "source" but is not part of the path, so it cannot disambiguate.This issue proposes making pointers per-source, where a source is the contributing party that produced the evidence (bound to its signer OIDC identity). Two layouts are on the table; this issue analyzes and compares them.
Context: what a per-source layout improves over the deferred design
ADR-007 (
docs/design/007-recipe-evidence.md:604-608, 734) already reserves a schema-2.0 path: keep the one<slug>.yamlfile but letattestations[]grow, adding arole:(primary / supplementary / negative). That "single shared file, longer list" approach has two structural weaknesses that both proposals below fix:CODEOWNERScan't scope review to "the party that owns this entry," and nothing stops one party rewriting another's list entry.Giving each source its own path eliminates both. The open question is which per-source layout.
Option A — directory per source (add-only files)
Each contributing party owns a subdirectory; each validation run lands a new, immutable pointer file (named by bundle digest for content-addressed uniqueness). Nothing is ever modified in place — this matches today's
WritePointer, which already just writes a fresh file.CODEOWNERSrule perrecipes/evidence/<slug>/<source>/directory.Option B — one file per source (mutable, append into list)
Each party owns a single YAML and appends new attestations into its own
attestations[]list — reusing the list structure the schema already defines.attestations[]list (no schema invention beyond per-source pathing).WritePointeroverwrites; appending means loading the existing file, verifying prior entries are untouched, and re-emitting.Comparison
CODEOWNERSgranularity<slug>/*/*.yaml+ select<slug>/*.yamlWritePointeralready writes freshattestations[]list already designedRecommendation (for discussion)
Lean Option A (directory per source, immutable add-only files): it removes the most failure modes (no merge logic, no in-place mutation, no tamper surface), gives the cleanest ownership boundary, and is the smallest delta from today's overwrite-a-fresh-file behavior. A hybrid is worth considering — Option A's
<slug>/<source>/directory for ownership, with a single appendable pointer inside per Option B's semantics — if compactness matters more than immutability.Cross-cutting work either option requires
<source>slug from the signer OIDC identity/issuer and enforce (CI +CODEOWNERS) that a party can only write under its own path — otherwise the path is squattable.verifier/pointer.go:74-83); version the pointer schema.Attestations[0]access (verifier/fetch.go:136-139,verify.go:147-148) with glob-based discovery and multi-source iteration/aggregation.emit.go:166andpublish.go:120, which hardcoderecipes/evidence/<RecipeName>.yaml.docs/design/009-recipe-health-tracking.md) — confirm how multi-source evidence rolls up into a recipe's status.References
pkg/evidence/attestation/pointer.go:35-91—BuildPointer/WritePointer(single-element, overwrite)pkg/evidence/attestation/types.go:192-231—Pointer,PointerAttestation,PointerSignerpkg/evidence/verifier/pointer.go:74-83— single-attestation hard rejectpkg/evidence/verifier/{fetch.go:136-139,verify.go:147-148}—Attestations[0]consumerspkg/evidence/attestation/{emit.go:166,publish.go:120}— hardcoded pathdocs/design/007-recipe-evidence.md:576-614, 734— pointer schema + deferred multi-instance