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

229 lines
6.9 KiB
Python

"""Tests for ``formation.smelter`` — deterministic, pattern-based extraction.
The Smelter turns raw text spans into candidate objects suitable for the
Forge. It is pure regex/string ops: no LLM, no network, no async, no floats
in emitted dataclasses, and emitted tuples are sorted for stable ordering.
"""
from __future__ import annotations
import pytest
from formation import (
ConceptCandidate,
CounterCandidate,
OrderingHint,
OreBundle,
RelationCandidate,
SourceRef,
canonical_json,
)
from formation.course import OreEntry
from formation.smelter import SmeltedBundle, smelt
_SHA_A = "a" * 64
_SHA_B = "b" * 64
_RETRIEVED = "2026-05-16T00:00:00Z"
def _bundle(*shas: str) -> OreBundle:
return OreBundle(
subject_id="test",
entries=tuple(
OreEntry(
source_sha=sha,
url=f"https://example.test/{sha[:6]}",
adapter="smelter/pattern_v1",
retrieved_at=_RETRIEVED,
byte_length=0,
)
for sha in shas
),
)
@pytest.mark.unit
def test_single_concept_definition_produces_one_candidate() -> None:
text = "Wisdom is defined as practical judgment."
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
assert isinstance(out, SmeltedBundle)
assert len(out.concepts) == 1
concept = out.concepts[0]
assert isinstance(concept, ConceptCandidate)
assert concept.canonical_term == "wisdom"
assert "practical judgment" in concept.definition
assert len(concept.sources) == 1
assert concept.sources[0].source_sha == _SHA_A
assert concept.sources[0].adapter == "smelter/pattern_v1"
@pytest.mark.unit
def test_repeated_canonical_term_collapses_to_one_concept() -> None:
text = (
"Wisdom is defined as practical judgment. "
"Wisdom means knowing what is good. "
"Wisdom is a virtue."
)
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
terms = [c.canonical_term for c in out.concepts]
assert terms.count("wisdom") == 1
concept = out.concepts[0]
assert len(concept.sources) >= 1
@pytest.mark.unit
def test_relation_roundtrips_through_parse_triple() -> None:
from teaching.relation_parse import parse_triple
text = "Wisdom is judgment. Glargle floob bloop."
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
for rel in out.relations:
assert isinstance(rel, RelationCandidate)
parsed = parse_triple(f"{rel.head} {rel.relation} {rel.tail}")
assert parsed is not None
# At least one relation extracted (wisdom is judgment).
assert any(r.head == "wisdom" and r.tail == "judgment" for r in out.relations)
@pytest.mark.unit
def test_counter_sentence_produces_counter_candidate() -> None:
text = "It is a misconception that the earth is flat."
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
assert len(out.counters) == 1
counter = out.counters[0]
assert isinstance(counter, CounterCandidate)
assert counter.head == "earth"
assert counter.relation == "is"
assert counter.tail == "flat"
@pytest.mark.unit
def test_ordering_hint_from_requires() -> None:
text = "Calculus requires algebra."
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
assert len(out.ordering_hints) == 1
hint = out.ordering_hints[0]
assert isinstance(hint, OrderingHint)
assert hint.before == "algebra"
assert hint.after == "calculus"
@pytest.mark.unit
def test_ordering_hint_from_depends_on() -> None:
text = "Geometry depends on logic."
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
assert any(h.before == "logic" and h.after == "geometry" for h in out.ordering_hints)
@pytest.mark.unit
def test_ordering_hint_from_before() -> None:
text = "Before calculus, algebra."
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
assert any(h.before == "algebra" and h.after == "calculus" for h in out.ordering_hints)
@pytest.mark.unit
def test_determinism_byte_identical_canonical_json() -> None:
text = (
"Wisdom is defined as practical judgment. "
"Calculus requires algebra. "
"It is a misconception that the earth is flat. "
"Wisdom is judgment."
)
out1 = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
out2 = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
payload1 = _to_dict(out1)
payload2 = _to_dict(out2)
assert canonical_json(payload1) == canonical_json(payload2)
@pytest.mark.unit
def test_empty_source_text_yields_empty_bundle() -> None:
out = smelt(_bundle(_SHA_A), {_SHA_A: ""}, _RETRIEVED)
assert out.concepts == ()
assert out.relations == ()
assert out.counters == ()
assert out.ordering_hints == ()
@pytest.mark.unit
def test_no_source_texts_at_all() -> None:
out = smelt(_bundle(), {}, _RETRIEVED)
assert out.concepts == ()
assert out.relations == ()
@pytest.mark.unit
def test_same_triple_from_two_sources_dedups_with_two_refs() -> None:
text_a = "Wisdom is judgment."
text_b = "Wisdom is judgment."
out = smelt(
_bundle(_SHA_A, _SHA_B),
{_SHA_A: text_a, _SHA_B: text_b},
_RETRIEVED,
)
rels = [r for r in out.relations if r.head == "wisdom" and r.tail == "judgment"]
assert len(rels) == 1
rel = rels[0]
shas = {s.source_sha for s in rel.sources}
assert shas == {_SHA_A, _SHA_B}
@pytest.mark.unit
def test_stable_ordering_of_emitted_tuples() -> None:
text = (
"Zebra is defined as a striped horse. "
"Apple is defined as a fruit. "
"Mango is defined as a fruit."
)
out = smelt(_bundle(_SHA_A), {_SHA_A: text}, _RETRIEVED)
terms = [c.canonical_term for c in out.concepts]
assert terms == sorted(terms)
def _to_dict(b: SmeltedBundle) -> dict:
def _src(s: SourceRef) -> dict:
return {
"source_sha": s.source_sha,
"span": s.span,
"adapter": s.adapter,
"retrieved_at": s.retrieved_at,
}
return {
"concepts": [
{
"canonical_term": c.canonical_term,
"definition": c.definition,
"sources": [_src(s) for s in c.sources],
}
for c in b.concepts
],
"relations": [
{
"head": r.head,
"relation": r.relation,
"tail": r.tail,
"sources": [_src(s) for s in r.sources],
}
for r in b.relations
],
"counters": [
{
"head": c.head,
"relation": c.relation,
"tail": c.tail,
"sources": [_src(s) for s in c.sources],
}
for c in b.counters
],
"ordering_hints": [
{
"before": h.before,
"after": h.after,
"sources": [_src(s) for s in h.sources],
}
for h in b.ordering_hints
],
}