Skip to content

Make evidence pointers per-source (per contributing party) #1347

Description

@njhensley

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.mdPointer 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-91BuildPointer / WritePointer (single-element, overwrite)
  • pkg/evidence/attestation/types.go:192-231Pointer, 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

Metadata

Metadata

Assignees

Fields

No fields configured for Enhancement.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions