core/formation/promote.py
Shay 64c5bc4619 feat(epistemic): truth-seeking schema audit — 3 leaks closed, 4 new lanes, 3 new invariants
Audit of the one-mutation-path invariant (ADR-0021 §3) found three leaks
where pack authority or session-state writes could substitute for coherence
judgment. All three landed fixes or partial closures in this push.

Leaks closed:
- Leak A: pack vocab defaulted to COHERENT — flipped to SPECULATIVE in
  language_packs/{compiler,schema}.py; docstring corrected to align with
  ADR-0021 (it was rationalizing the leak).
- Leak B: vault.recall was epistemic-blind — VaultStore.store() now stamps
  every entry with EpistemicStatus (default SPECULATIVE); recall(min_status=)
  filters to admissible-as-evidence tier. All 4 vault-write sites updated.
- Leak C (write-side): generate/proposition.py:198 stored articulated
  propositions unmarked — now stamps SPECULATIVE, breaking the
  fabrication-feedback loop in principle. Read-side audit of 5 call sites
  is the residual.

New architectural invariants (tests/test_architectural_invariants.py):
- INV-21: one-mutation-path allowlist (caught Leak C on first run)
- INV-22: pack lexicon default is SPECULATIVE (Leak A guard)
- INV-23: vault recall epistemic-aware (Leak B guard)

New eval lanes:
- teaching_injection_resistance — ships GREEN at 1.00/1.00/0 (the
  structural anti-injection claim is real and measurable)
- refusal_calibration — honest gap: 0% refusal, 0% fabrication
- contradiction_detection — honest gap: 50% flag via versor-delta heuristic,
  100% false-positive; motivates the proper coherence-checker
- articulation_of_status — honest gap: 0% speculative articulation, 60%
  false certainty; output-side leak surface

New benchmarks:
- benchmarks/footprint.py — total deployed runtime is 7.06 MiB
  (109,358x smaller than Llama 3.1 405B, runs offline, no GPU)
- benchmarks/learning_curve.py — monotonic + replay-deterministic curve
  per lane

Documentation:
- docs/truth_seeking_schema.md — foundational architectural commitment,
  five rules, mapped to human failure modes, leaks published openly
- evals/CLAIMS.md — five-tier public claims doc; Tier 4.5 publishes
  known gaps with named fixes; verification contract at top
- README.md — new pillar between algebraic substrate and language pillar

Includes in-flight formation pipeline scaffolding (formation/, tests/formation/,
docs/formation_pipeline_plan.md) and minor CLI/contracts/gitignore edits
that were already in the working tree at session start.

Verification: 798 passed, 2 skipped, 1 deselected (pre-existing pack-count
test drift unrelated to schema changes).
2026-05-17 07:27:41 -07:00

154 lines
5 KiB
Python

"""Stage 8 — Promote. The only SPECULATIVE → COHERENT bridge.
Promotion is the **single** authorized mutation path from a Mastered course
into the language pack / teaching store. Every other artifact in the
pipeline is proposal-only. Per ADR-0021 §Schema impact, the bridge here
routes through ``teaching/review.py`` so the existing review gate is the
sole pack-mutation entry point in the entire system.
Promotion requirements (all must hold):
P1. The ``MasteryReport`` must be ``ratified == True``.
P2. Its ``report_sha256`` must verify (self-seal intact).
P3. Every ``requires_courses`` entry in the ``SubjectSpec`` must be
present in the ``MasteredCoursesIndex``.
P4. Promotion is idempotent — calling ``promote`` twice with the same
``report_sha256`` is a no-op.
Promotion stamps every triple with ``report_sha256`` so the teaching store
carries the chain-of-custody back to the receipt that authorized the
mutation.
"""
from __future__ import annotations
from dataclasses import dataclass
from formation.course import (
MasteryReport,
SubjectSpec,
ValidatedTripleSet,
)
from formation.index import MasteredCourseEntry, MasteredCoursesIndex
from formation.mastery import verify_report
from generate.intent import DialogueIntent, IntentTag
from teaching.correction import CorrectionCandidate
from teaching.epistemic import EpistemicStatus
from teaching.review import (
ReviewOutcome,
ReviewedTeachingExample,
review_correction,
)
from formation.hashing import sha256_of
class PromoteRefused(Exception):
"""Raised when a promotion is refused. Carries the failure reason."""
@dataclass(frozen=True, slots=True)
class PromotedTriple:
"""A reviewed teaching example carrying the authorizing Mastery Report SHA."""
report_sha256: str
example: ReviewedTeachingExample
triple: tuple[str, str, str]
@dataclass(frozen=True, slots=True)
class PromotionResult:
report_sha256: str
course_id: str
promoted: tuple[PromotedTriple, ...]
rejected: tuple[PromotedTriple, ...]
idempotent_skipped: bool = False
def _candidate_for(
triple: tuple[str, str, str], report_sha: str
) -> CorrectionCandidate:
head, relation, tail = triple
correction_text = f"{head} {relation.replace('_', ' ')} {tail}."
intent = DialogueIntent(tag=IntentTag.CORRECTION, subject=head)
candidate_id = sha256_of(
{"head": head, "relation": relation, "tail": tail, "report": report_sha}
)[:16]
return CorrectionCandidate(
correction_text=correction_text,
intent=intent,
prior_surface="",
prior_turn=-1,
candidate_id=candidate_id,
)
def promote(
*,
report: MasteryReport,
spec: SubjectSpec,
validated_set: ValidatedTripleSet,
index: MasteredCoursesIndex,
) -> PromotionResult:
"""Run the Promote gate and route validated triples through the review path."""
# P1: ratified.
if not report.ratified:
raise PromoteRefused(
f"report not ratified: failure_reasons={list(report.failure_reasons)}"
)
# P2: seal intact.
if not verify_report(report):
raise PromoteRefused("report self-seal does not verify")
# P3: prerequisites.
missing = [
c for c in spec.requires_courses if not index.contains_course(c)
]
if missing:
raise PromoteRefused(f"missing prerequisites: {missing}")
# P4: idempotency — already promoted under this exact report SHA?
if index.contains_report(report.report_sha256):
return PromotionResult(
report_sha256=report.report_sha256,
course_id=report.course_id,
promoted=(),
rejected=(),
idempotent_skipped=True,
)
promoted: list[PromotedTriple] = []
rejected: list[PromotedTriple] = []
for rel in validated_set.relations:
triple = (rel.head, rel.relation, rel.tail)
candidate = _candidate_for(triple, report.report_sha256)
example = review_correction(
candidate, epistemic_status=EpistemicStatus.SPECULATIVE
)
record = PromotedTriple(
report_sha256=report.report_sha256, example=example, triple=triple
)
if example.outcome is ReviewOutcome.ACCEPTED:
promoted.append(record)
else:
rejected.append(record)
# Register in the index even if some triples were rejected by the review
# path — the report itself is ratified, and individual triple rejections
# are audit data, not a failure of the course.
index.add(MasteredCourseEntry(
course_id=report.course_id,
report_sha256=report.report_sha256,
issued_at=report.issued_at,
course_sha256=report.course_sha256,
validated_set_sha=report.validated_set_sha,
))
return PromotionResult(
report_sha256=report.report_sha256,
course_id=report.course_id,
promoted=tuple(promoted),
rejected=tuple(rejected),
idempotent_skipped=False,
)