The only path by which CORE extends its own active teaching corpus.
Closes ADR-0055 Phase C alongside ADR-0056's cognitive surface.
Three load-bearing calls (recorded in ADR-0057):
1. Replay-equivalence is a precondition, not a permission;
operator --accept remains required.
2. Eligibility = polarity in {affirms, falsifies} AND at least
one source='corpus' evidence pointer AND boundary_clean AND
claim_domain != evaluative (unless --allow-evaluative) AND
proposed_chain complete.
3. Append-only proposal log; corpus history append-only too.
Changes
- teaching/proposals.py — TeachingChainProposal, ReplayEvidence,
ProposalLog (event-sourced replay → current_state), eligibility
predicate, propose_from_candidate, accept/reject/withdraw,
append_chain_to_corpus (the sole corpus-write surface). Uses
TYPE_CHECKING guards to break the circular import with
chat.pack_grounding.
- teaching/replay.py — run_replay_equivalence; swaps _corpus_index
path to a tmp file, runs cognition lane on the active corpus
AND a transient copy with the proposed chain appended, returns
regressed-metrics list; trust-boundary assertion that the active
corpus bytes are byte-identical pre/post.
- teaching/discovery.py — moved chat.pack_grounding /
chat.teaching_grounding imports inside extract_discovery_candidates
to break the cycle (was masked when chat.runtime was the entry
point; surfaced by CLI entry).
- core/cli.py — three new subcommands:
core teaching propose <candidate-jsonl-path> [--allow-evaluative]
core teaching proposals [--state pending|accepted|rejected|withdrawn] [--json]
core teaching review <proposal_id> --accept --review-date YYYY-MM-DD
core teaching review <proposal_id> --reject [--note ...]
core teaching review <proposal_id> --withdraw [--note ...]
- tests/test_teaching_proposals.py — 16 tests covering: every
eligibility gate, proposal_id idempotency, append-only log,
replay-equivalent stays pending, regression auto-rejects with
named regressed metrics, --accept appends one line with typed
Provenance, --accept refused on non-equivalent, state-machine
blocks double-accept, real replay gate runs cognition lane
twice and asserts byte-clean active corpus pre/post.
Invariants preserved
- versor_condition(F) < 1e-6 — C2 touches no algebra path.
- Active corpus bytes byte-identical regardless of replay outcome.
- No clock-time reads, no LLM, no async.
- Proposal-only — accept_proposal is the sole corpus-write path.
Lanes: smoke 67 / cognition 121 / runtime 19 / teaching 17 /
new proposals 16. Cognition eval unchanged.
Open follow-ups (not in scope):
- supersession via operator review action
- cross-pack falsification arbitration (ADR-0056 Call 2 deferred)
- pack-data migration of frame-dependent connectives
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
243 lines
9.9 KiB
Markdown
243 lines
9.9 KiB
Markdown
# ADR-0057 — Teaching-Chain Proposal + Review + Replay-Equivalence Gate (Phase C2)
|
|
|
|
**Status:** Accepted
|
|
**Date:** 2026-05-18
|
|
**Author:** Shay
|
|
**Completes:** ADR-0055 §Decision Phase C (with [ADR-0056](./ADR-0056-contemplation-loop-c1.md))
|
|
|
|
---
|
|
|
|
## Context — how we got here
|
|
|
|
ADR-0055 introduced a four-tier inter-session memory architecture
|
|
and split corpus extension into a **proposal-only** path. ADR-0056
|
|
(Phase C1) implemented the cognitive surface: a contemplated
|
|
`DiscoveryCandidate` carries `polarity`, `claim_domain`, and
|
|
composed `evidence`. C1 explicitly does **not** mutate the active
|
|
teaching corpus — its output is structured evidence on disk.
|
|
|
|
C2 is the **only** path that turns reviewed evidence into a corpus
|
|
mutation. It is the riskiest piece in the chain and gets its own
|
|
ADR for that reason.
|
|
|
|
### Three load-bearing calls
|
|
|
|
#### Call 1 — Replay-equivalence as a *precondition*, not a permission
|
|
|
|
**Choice:** The replay-equivalence eval gate is a *necessary* but
|
|
**not sufficient** condition for corpus append. A proposal that
|
|
passes the gate becomes eligible for operator review; the operator
|
|
still has to accept it explicitly. The gate eliminates regressions;
|
|
the operator decides on the merits.
|
|
|
|
**Why:**
|
|
|
|
- CLAUDE.md doctrine: "Pack mutation is proposal-only until
|
|
reviewed." Eval-passing is not review. A chain that doesn't
|
|
regress metrics can still be wrong, harmful, or off-doctrine.
|
|
- The gate is mechanical (regress on any metric → auto-reject).
|
|
Review is judgment. Conflating them would smuggle in an
|
|
auto-apply path that bypasses human review.
|
|
- Auto-rollback on regression keeps the corpus byte-clean even
|
|
when a proposal is mechanically rejected.
|
|
|
|
**Rejected alternative:** Replay-equivalent ⇒ auto-append. Same
|
|
shape as the smart-mistake C1 was extracted to prevent.
|
|
|
|
#### Call 2 — Eligibility = `polarity != "undetermined"` AND reviewed-evidence floor
|
|
|
|
**Choice:** A `DiscoveryCandidate` is *eligible* to become a
|
|
`TeachingChainProposal` iff:
|
|
|
|
1. `polarity ∈ {"affirms", "falsifies"}` (undetermined cannot
|
|
propose — composing to undetermined means the system itself
|
|
isn't sure).
|
|
2. `evidence` contains at least one `source="corpus"` pointer
|
|
(reviewed-evidence floor — pack residency alone is shape
|
|
evidence, not relation evidence).
|
|
3. `claim_domain != "evaluative"` UNLESS an operator has flagged
|
|
the proposal with `--allow-evaluative` and a strong-tier hedge
|
|
surface is attached (per ADR-0056 evaluative threshold).
|
|
4. `boundary_clean=True` (the source turn was not under refusal
|
|
or hedge — boundary-clean is a guard against polluted
|
|
provenance).
|
|
5. `proposed_chain` is *complete* — non-null `subject`, `intent`,
|
|
`connective`, `object`.
|
|
|
|
**Why:** Each gate corresponds to a doctrinal commitment that
|
|
CLAUDE.md or an earlier ADR already pinned. Eligibility is a
|
|
mechanical check — no judgment. Failing any gate keeps the
|
|
candidate as evidence on disk; eligible ones move on for replay
|
|
+ review.
|
|
|
|
#### Call 3 — Append-only proposal log; corpus history append-only too
|
|
|
|
**Choice:** Every proposal — accepted, rejected (operator),
|
|
auto-rejected (replay regression), or withdrawn — is appended to
|
|
`teaching/proposals/proposals.jsonl` and never deleted. Accepted
|
|
proposals additionally append their `proposed_chain` to the active
|
|
corpus (`teaching/cognition_chains/cognition_chains_v1.jsonl`) with
|
|
typed `Provenance(source="discovery_promoted", adr_id="adr-0057",
|
|
review_date=...)` from ADR-0055 Phase A. The active corpus view
|
|
remains derived via the existing `superseded_by` mechanism — C2
|
|
adds entries, doesn't rewrite history.
|
|
|
|
**Why:**
|
|
|
|
- Append-only history is a CLAUDE.md commitment for replayability.
|
|
- The same `Provenance` schema Phase A introduced is the natural
|
|
home for "where did this chain come from"; `discovery_promoted`
|
|
is the canonical source tag.
|
|
- Future calibration / re-ratification ADRs (Phase D, E) need the
|
|
full record of every proposal, not just the accepted ones.
|
|
|
|
---
|
|
|
|
## Decision — Phase C2 spec
|
|
|
|
### Data shape
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class TeachingChainProposal:
|
|
proposal_id: str # sha256(source_candidate_id + chain payload)
|
|
source_candidate_id: str
|
|
proposed_chain: dict[str, Any] # complete: subject, intent, connective, object
|
|
polarity: Literal["affirms", "falsifies"]
|
|
claim_domain: ClaimDomain
|
|
evidence: tuple[EvidencePointer, ...]
|
|
review_state: Literal["pending", "accepted", "rejected", "withdrawn"]
|
|
operator_note: str = ""
|
|
replay_evidence: ReplayEvidence | None = None
|
|
provenance: Provenance | None = None # populated on accept
|
|
```
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class ReplayEvidence:
|
|
baseline: dict[str, float] # metrics on the active corpus
|
|
candidate: dict[str, float] # metrics with proposed chain appended
|
|
regressed_metrics: tuple[str, ...]
|
|
replay_equivalent: bool
|
|
```
|
|
|
|
### Replay-equivalence gate
|
|
|
|
For every proposal that reaches the gate:
|
|
|
|
1. Snapshot the active corpus file bytes.
|
|
2. Run the cognition lane (public + dev + holdout splits) to
|
|
produce the baseline metric set.
|
|
3. Append the proposed chain to a *temporary copy* of the corpus,
|
|
invalidate the cached `_corpus_index()`, and re-run the lane
|
|
on the same case sets.
|
|
4. Compare metric-for-metric. A metric *regresses* iff its
|
|
candidate value is strictly less than the baseline value
|
|
(no float tolerance — the lane is deterministic).
|
|
5. Restore the original corpus bytes (or never touch the active
|
|
file in the first place — see implementation note below).
|
|
6. If any metric regressed ⇒ `replay_equivalent=False`,
|
|
proposal auto-transitions to `review_state="rejected"`,
|
|
`operator_note="auto_rollback_regression: <metric list>"`.
|
|
7. Otherwise ⇒ `replay_equivalent=True`, proposal stays
|
|
`review_state="pending"` awaiting operator review.
|
|
|
|
**Implementation note (trust boundary):** the gate must never
|
|
write to the active corpus file even transiently. It writes to
|
|
an *isolated path* and patches `_corpus_index()` to load from
|
|
that path via dependency injection. Active-file bytes are
|
|
byte-identical pre/post regardless of outcome.
|
|
|
|
### Operator review surface
|
|
|
|
CLI commands (sibling of the existing `core teaching audit`):
|
|
|
|
```text
|
|
core teaching propose <candidate_id> [--from-sink <path>]
|
|
Convert an eligible enriched DiscoveryCandidate into a
|
|
TeachingChainProposal. Runs the replay-equivalence gate
|
|
immediately. Idempotent on (candidate_id, chain payload).
|
|
|
|
core teaching proposals [--state <pending|accepted|rejected|withdrawn>] [--json]
|
|
List proposals; default lists pending.
|
|
|
|
core teaching review <proposal_id> --accept [--note "..."]
|
|
core teaching review <proposal_id> --reject [--note "..."]
|
|
core teaching review <proposal_id> --withdraw [--note "..."]
|
|
Operator decision. --accept on a replay-equivalent proposal
|
|
appends the chain to the active corpus with typed provenance.
|
|
--accept on a non-equivalent proposal is rejected with an
|
|
explicit error. --reject and --withdraw transition state
|
|
only; the corpus is untouched.
|
|
```
|
|
|
|
### Trust boundary
|
|
|
|
- **No automatic accept.** Replay-equivalence is a precondition,
|
|
not a permission. Only operator `--accept` writes to the corpus.
|
|
- **No corpus rewrites.** Accept appends one new line; entries
|
|
are retired only via the existing `superseded_by` mechanism in
|
|
a separate operator action.
|
|
- **No proposal deletion.** All four review states are terminal
|
|
in the append-only log; "delete" doesn't exist.
|
|
- **No identity / safety / ethics mutation.** Per ADR-0027 and
|
|
ADR-0029, those packs are out of scope for C2.
|
|
- **No clock-time content read.** The `review_date` in
|
|
`Provenance` is the only timestamp; sourced from the operator's
|
|
command invocation, not from runtime hot path.
|
|
|
|
---
|
|
|
|
## Non-goals (explicit)
|
|
|
|
- No async or concurrency primitives — replay is synchronous.
|
|
- No cross-pack arbitration (deferred per ADR-0056 Call 2).
|
|
- No re-ratification of identity / safety / ethics packs.
|
|
- No automatic supersession of existing chains by a new accept;
|
|
supersession is a separate, future operator action.
|
|
- No metric-tolerance bands; the lane is deterministic and any
|
|
regression is real.
|
|
|
|
---
|
|
|
|
## Verification (acceptance criteria)
|
|
|
|
- Eligible enriched candidates produce a `TeachingChainProposal`;
|
|
ineligible ones raise with the failing gate named.
|
|
- The replay-equivalence gate never mutates the active corpus
|
|
file bytes regardless of outcome.
|
|
- A proposal whose chain causes any cognition metric to regress
|
|
auto-transitions to `rejected` with `replay_equivalent=False`
|
|
and an `auto_rollback_regression` note.
|
|
- A replay-equivalent proposal stays `pending` until operator
|
|
decision.
|
|
- `core teaching review --accept` on a `pending` +
|
|
replay-equivalent proposal appends one line to the active
|
|
corpus with `Provenance(source="discovery_promoted",
|
|
adr_id="adr-0057")` and re-runs the active corpus through
|
|
`_corpus_index()` cleanly (no new drops).
|
|
- `core teaching review --accept` on a non-equivalent proposal
|
|
raises and refuses to append.
|
|
- The proposals log is append-only; replaying it reconstructs
|
|
the same review-state for every entry.
|
|
- `versor_condition(F) < 1e-6` invariant preserved (no algebra
|
|
touched).
|
|
- `core eval cognition` numbers unchanged on splits that don't
|
|
include accepted-proposal cases.
|
|
|
|
---
|
|
|
|
## Cross-References
|
|
|
|
- [ADR-0021](./ADR-0021-epistemic-status.md) — `EpistemicStatus`
|
|
COHERENT promotion semantics; C2 is the mechanical surface.
|
|
- [ADR-0027](./ADR-0027-identity-packs.md) /
|
|
[ADR-0029](./ADR-0029-safety-pack.md) /
|
|
[ADR-0033](./ADR-0033-ethics-pack.md) — packs out of scope.
|
|
- [ADR-0052](./ADR-0052-teaching-grounded-surface.md) — the
|
|
active corpus this loop appends to.
|
|
- [ADR-0055](./ADR-0055-inter-session-memory-discovery-promotion.md)
|
|
— the parent design; Phase A's `Provenance` and `superseded_by`
|
|
are the substrate this ADR builds on.
|
|
- [ADR-0056](./ADR-0056-contemplation-loop-c1.md) — the cognitive
|
|
surface whose output feeds C2's eligibility gate.
|