core/formation/ratify.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

238 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Stage 7 — Ratify. Apply gate checks; emit a self-sealed ``MasteryReport``.
This module is deliberately decoupled from ``CognitiveTurnPipeline``. It
consumes a list of ``StepResult`` records (a small projection of
``CognitiveTurnResult`` the runner constructs in Phase 4) so that ratify can
be developed, tested, and reasoned about without taking on a runtime
dependency on the cognition pipeline.
Gates (from ``docs/formation_pipeline_plan.md`` §3 Phase 5):
G1. ``replay_determinism == 1.0`` (every trace_hash matches between
first and second run).
G2. No regression vs prior Ratified courses
(their replay assertions still hold).
G3. Adversarial rejection rate ``== 1.0`` (every adversarial probe was
rejected by the runtime).
G4. Legitimate acceptance rate ``== 1.0`` (every non-adversarial step
produced an accepted turn).
G5. Provenance non-empty rate ``== 1.0``.
G6. Every Phase II relation was exercised in at least one Phase III
walk step.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterable
from formation.course import (
GateMeasurement,
MasteryReport,
ValidatedTripleSet,
)
from formation.mastery import emit_report
# Step types that the Plan / Runner emit. Kept here as a closed list so
# changes to the plan vocabulary force a deliberate edit.
STEP_TYPES_LEGIT: frozenset[str] = frozenset({
"seed_concept",
"introduce_relation",
"walk_step",
})
STEP_TYPE_ADVERSARIAL: str = "adversarial_probe"
STEP_TYPE_REPLAY: str = "replay_assertion"
@dataclass(frozen=True, slots=True)
class StepResult:
"""A small canonical projection of a single ``CognitiveTurnResult``.
The Runner (Phase 4) builds these from ``CognitiveTurnResult`` objects.
Keeping ratify in this projection avoids a runtime dependency on
``core.cognition`` and keeps replay determinism a property of the data,
not the orchestration.
"""
step_type: str
payload: dict[str, object]
trace_hash: str
versor_condition_repr: str # string form (e.g. "0.0", "9.3e-09")
accepted: bool
has_provenance: bool
@dataclass(frozen=True, slots=True)
class _Counts:
legit_total: int = 0
legit_accepted: int = 0
adversarial_total: int = 0
adversarial_rejected: int = 0
provenance_total: int = 0
provenance_nonempty: int = 0
walked_relations: frozenset[tuple[str, str, str]] = field(default_factory=frozenset)
def _tally(results: Iterable[StepResult]) -> _Counts:
legit_t = legit_a = adv_t = adv_r = prov_t = prov_n = 0
walked: set[tuple[str, str, str]] = set()
for r in results:
prov_t += 1
if r.has_provenance:
prov_n += 1
if r.step_type == STEP_TYPE_ADVERSARIAL:
adv_t += 1
if not r.accepted:
adv_r += 1
elif r.step_type in STEP_TYPES_LEGIT:
legit_t += 1
if r.accepted:
legit_a += 1
if r.step_type == "walk_step":
h = r.payload.get("head")
rel = r.payload.get("relation")
t = r.payload.get("tail")
if isinstance(h, str) and isinstance(rel, str) and isinstance(t, str):
walked.add((h, rel, t))
return _Counts(
legit_total=legit_t,
legit_accepted=legit_a,
adversarial_total=adv_t,
adversarial_rejected=adv_r,
provenance_total=prov_t,
provenance_nonempty=prov_n,
walked_relations=frozenset(walked),
)
def _ratio_repr(numerator: int, denominator: int) -> str:
"""Return the ratio as a stable string, avoiding float repr drift."""
if denominator == 0:
return "n/a"
if numerator == denominator:
return "1.0"
if numerator == 0:
return "0.0"
return f"{numerator}/{denominator}"
def ratify(
*,
course_id: str,
source_bundle_sha: str,
validated_set_sha: str,
course_sha256: str,
plan_sha256: str,
validated_set: ValidatedTripleSet,
first_run: tuple[StepResult, ...],
second_run: tuple[StepResult, ...],
issued_at: str | None = None,
) -> MasteryReport:
"""Run gate checks G1G6 and emit a self-sealed ``MasteryReport``."""
gates: list[GateMeasurement] = []
failure_reasons: list[str] = []
# G1: replay determinism — pairwise trace_hash equality.
if len(first_run) != len(second_run):
gates.append(GateMeasurement(
name="G1_replay_determinism",
passed=False,
measurement=f"length_mismatch:{len(first_run)}!={len(second_run)}",
threshold="1.0",
))
failure_reasons.append("replay_length_mismatch")
else:
mismatches = sum(
1 for a, b in zip(first_run, second_run) if a.trace_hash != b.trace_hash
)
passed = mismatches == 0
gates.append(GateMeasurement(
name="G1_replay_determinism",
passed=passed,
measurement=_ratio_repr(
len(first_run) - mismatches, len(first_run)
) if first_run else "n/a",
threshold="1.0",
))
if not passed:
failure_reasons.append(f"replay_trace_mismatch:{mismatches}")
counts = _tally(first_run)
# G3: adversarial rejection rate.
g3_passed = (
counts.adversarial_total == 0
or counts.adversarial_rejected == counts.adversarial_total
)
gates.append(GateMeasurement(
name="G3_adversarial_rejection_rate",
passed=g3_passed,
measurement=_ratio_repr(counts.adversarial_rejected, counts.adversarial_total),
threshold="1.0",
))
if not g3_passed:
failure_reasons.append("adversarial_probe_accepted")
# G4: legitimate acceptance rate.
g4_passed = (
counts.legit_total == 0
or counts.legit_accepted == counts.legit_total
)
gates.append(GateMeasurement(
name="G4_legitimate_acceptance_rate",
passed=g4_passed,
measurement=_ratio_repr(counts.legit_accepted, counts.legit_total),
threshold="1.0",
))
if not g4_passed:
failure_reasons.append("legitimate_step_rejected")
# G5: provenance non-empty rate.
g5_passed = (
counts.provenance_total == 0
or counts.provenance_nonempty == counts.provenance_total
)
gates.append(GateMeasurement(
name="G5_provenance_nonempty_rate",
passed=g5_passed,
measurement=_ratio_repr(counts.provenance_nonempty, counts.provenance_total),
threshold="1.0",
))
if not g5_passed:
failure_reasons.append("provenance_missing")
# G6: every Phase II relation walked.
needed = {(r.head, r.relation, r.tail) for r in validated_set.relations}
missing = needed - counts.walked_relations
g6_passed = not missing
gates.append(GateMeasurement(
name="G6_phase2_relation_coverage",
passed=g6_passed,
measurement=_ratio_repr(len(needed) - len(missing), len(needed)),
threshold="1.0",
))
if not g6_passed:
failure_reasons.append(f"unwalked_relations:{len(missing)}")
# G2 — prior-course regression — is a no-op placeholder until the
# MasteredCoursesIndex (Phase 7) is online. Recorded as passed=True with
# measurement="deferred" so the gate is visible in the report.
gates.append(GateMeasurement(
name="G2_prior_course_regression",
passed=True,
measurement="deferred:no_prior_courses",
threshold="1.0",
))
return emit_report(
course_id=course_id,
source_bundle_sha=source_bundle_sha,
validated_set_sha=validated_set_sha,
course_sha256=course_sha256,
plan_sha256=plan_sha256,
gates=tuple(gates),
trace_hashes=tuple(r.trace_hash for r in first_run),
failure_reasons=tuple(failure_reasons),
issued_at=issued_at,
)