core/docs/decisions/ADR-0094-proposal-source-provenance.md
Shay 57fa078aaa docs: mark ADR-0094/0095/0098/0099 accepted
Sibling reconciliation PR to #104. The four ADRs explicitly called out as
the 'current implementation frontier' in PR #104 are already implemented
to the same evidence bar as the eight ADRs that PR accepted:

- ADR-0094: teaching/source.py + proposal schema widening + migration
  script; tests/test_proposal_source.py green
- ADR-0095: teaching/from_miner.py + miner_loop_closure lane;
  SHA-pinned in scripts/verify_lane_shas.py; tests/test_miner_proposals.py
  green
- ADR-0098: core/demos/contract.py + adapter surface + demo_composition
  lane; SHA-pinned; tests/test_demo_composition.py green
- ADR-0099: core/demos/showcase.py + public_demo lane;
  SHA-pinned; tests/test_public_showcase.py green

Three of four lanes are SHA-pinned in CI (a stricter bar than several
already-accepted ADRs). Local pytest run: 85/85 passed across the four
tests/test_*.py files in 17s.

Also refreshes docs/decisions/README.md:
- flips the four table rows to Accepted (2026-05-22)
- rewrites the 'Current frontier' section now that no ADR-0091..0102
  entry is unimplemented
- enumerates candidate next directions (curriculum proposals,
  language-specific holdout splits, expert-demo ratification)

Docs-only change; no runtime code touched.
2026-05-22 07:09:14 -07:00

5.1 KiB

ADR-0094 — Proposal Source Provenance

Status: Accepted Date: 2026-05-21 Accepted: 2026-05-22 Author: CORE agents + reviewers


Acceptance evidence

Accepted after the sealed ProposalSource schema, exhaustive-match consumers, and deterministic migration landed:

  • teaching/source.py defines the sealed ProposalSource type with kind ∈ {operator, miner, curriculum}, serialization, and parse-time rejection of unknown kinds.
  • teaching/proposals.py widens PackMutationProposal / TeachingProposal to carry a required source: ProposalSource field; parsing fails without it.
  • teaching/store.py consumers branch on source.kind via exhaustive match.
  • teaching/migrate_proposals_source_field.py provides the one-shot deterministic rewriter; two runs produce byte-identical output.
  • tests/test_proposal_source.py exercises round-trip serialization, parse-time rejection, exhaustive-match enforcement, and migration determinism.
  • tests/test_epistemic_invariants.py pins proposal_source_exhaustive_match.

Context

teaching/proposals/proposals.jsonl and the surrounding review flow (teaching/review.py, teaching/store.py, ADR-0057) currently assume proposals originate from a single source: an operator authoring through the existing teaching CLI. The schema has no typed source field.

Two near-term ADRs widen this:

  • ADR-0095 introduces miner-sourced proposals (contemplation Phase 5 loop closure).
  • A future ADR will introduce curriculum-sourced proposals (deferred per the rewrite blueprint).

Without a typed source field, downstream consumers (teaching/review.py, capability ledger evidence rows, audit telemetry) will branch on string prefixes informally. That is the same shape of mistake ADR-0067's explicit subject_pack_id / object_pack_id fields prevented for cross-pack chains.


Decision

Introduce a sealed ProposalSource type widening PackMutationProposal / TeachingProposal schemas. The widening is schema-only; no runtime behavior changes under this ADR.

Sealed type

@dataclass(frozen=True, slots=True)
class ProposalSource:
    kind: Literal["operator", "miner", "curriculum"]
    source_id: str  # "" for kind="operator"; miner_id or course_id otherwise
    emitted_at_revision: str  # git SHA at emission

    def serialize(self) -> str:
        # "operator", "miner:articulation_quality", "curriculum:math_logic_v1"
        return self.kind if not self.source_id else f"{self.kind}:{self.source_id}"

Schema migration

  • New field on PackMutationProposal and TeachingProposal: source: ProposalSource.
  • Default for existing operator-authored proposals: ProposalSource(kind="operator", source_id="", emitted_at_revision=<head>).
  • Migration is one-shot at ADR landing: a deterministic rewriter walks existing proposals.jsonl files, attaches the default operator source, and rewrites in sorted order. Migration is a reviewed proposal itself.

Consumer rules

  • teaching/review.py performs exhaustive match on source.kind. Any new kind requires a new ADR adding a branch.
  • Telemetry events (chat/telemetry.py) carry source.serialize() as a string field. Redact-by-default already covers proposal content; source is non-sensitive and emitted plainly.
  • Capability ledger evidence rows (ADR-0091) include source in their provenance trail.

What this ADR does not do

  • Does not introduce miner-sourced proposals (ADR-0095).
  • Does not introduce curriculum-sourced proposals.
  • Does not change review thresholds. Source affects audit, not gate.

Invariant

proposal_source_exhaustive_match — every code path that branches on proposal.source.kind uses Python match with explicit cases for each sealed-type value; the type checker refuses a non-exhaustive match. A proposal without source fails parsing.


Lane

evals/proposal_source_schema/ (new, small):

  • positive: round-trip serialization for each kind
  • negative: missing source field rejected at parse
  • negative: unknown kind rejected at parse
  • migration: existing proposals.jsonl rewritten deterministically; two runs produce identical bytes

Trust Boundary

source_id is a typed string but flows through user-adjacent surfaces (telemetry, ledger reports). It is sanitized via core/_safe_display.safe_display at all surface emission points, matching the discipline established in ADR-0051. The migration rewriter runs only when invoked explicitly; no implicit on-load mutation.


Consequences

  • ADR-0095 can introduce kind="miner" without secondary schema churn.
  • A future curriculum ADR can introduce kind="curriculum" the same way.
  • Existing operator review flow is unchanged; the new field is set to its operator default on every existing proposal.

PR Checklist

  • Capability added: typed provenance schema for proposals.
  • Invariant proved: proposal_source_exhaustive_match.
  • Lane proving it: evals/proposal_source_schema/.
  • Hidden normalization / stochastic fallback / approximate recall / unreviewed mutation: none. Migration is explicit and reviewed.
  • Trust boundary: source_id sanitized at every surface; no implicit on-load rewrites.