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.
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.pydefines the sealedProposalSourcetype withkind∈ {operator, miner, curriculum}, serialization, and parse-time rejection of unknown kinds.teaching/proposals.pywidensPackMutationProposal/TeachingProposalto carry a requiredsource: ProposalSourcefield; parsing fails without it.teaching/store.pyconsumers branch onsource.kindvia exhaustivematch.teaching/migrate_proposals_source_field.pyprovides the one-shot deterministic rewriter; two runs produce byte-identical output.tests/test_proposal_source.pyexercises round-trip serialization, parse-time rejection, exhaustive-match enforcement, and migration determinism.tests/test_epistemic_invariants.pypinsproposal_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
PackMutationProposalandTeachingProposal: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.jsonlfiles, attaches the default operator source, and rewrites in sorted order. Migration is a reviewed proposal itself.
Consumer rules
teaching/review.pyperforms exhaustive match onsource.kind. Any new kind requires a new ADR adding a branch.- Telemetry events (
chat/telemetry.py) carrysource.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
sourcein 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
sourcefield rejected at parse - negative: unknown
kindrejected at parse - migration: existing
proposals.jsonlrewritten 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_idsanitized at every surface; no implicit on-load rewrites.