core/formation/templates/falsification.py
Shay 7feb239fdd feat(formation/templates): four new course templates + shared helpers
Adds the four templates called out in docs/teaching_order.md so the formation
pipeline can ratify more than just definitional ontologies:

* composed_relation — Layer 4.  Chains are the unit of mastery; each chain of
  length >= 2 emits a composed_relations entry with composition_kind
  (transitive | lifting), an inferred relation, and chain-break adversarial
  probes drawn from counters or canned.
* procedural — ordered state transitions; strict_linear_topo refuses
  branches, cycles, and disconnected components at render time.
  ordering_hints validated against the linear chain.  Canned violation
  probes for precondition_violation / step_skip / back_edge.
* falsification — counter-example-driven.  Counters move to Phase 2 paired
  with coherent alternatives drawn from relations sharing the same head.
  Unmatched counters surface in unmatched_counters; false-coherent probes
  emitted per pair.
* identity_anchor — Layer 1 seeding.  Concepts interpreted as identity axes
  ranked by ordering_hints; counters interpreted as override attempts;
  canned IDENTITY_OVERRIDE_PROBES always appended.

Common helpers extracted to formation/templates/_common.py: canonical
constants (MAX_VERSOR_CONDITION, RATIFICATION_GATES, PROMOTION_PATH,
IDENTITY_OVERRIDE_PROBES, NORMALIZATION_FORBIDDEN_SITES), deterministic
ordering (sorted_concepts/_counters/_hints, topo_sorted_relations,
strict_linear_topo), payload builders, geometric_dependencies,
maximal_chain_walks, adversarial_block, course_id, subject_payload,
substrate_invariants_payload, phase_5_payload.

formation/templates/__init__.py now dispatches via a lazy-import _REGISTRY
keyed by template_id; registered_template_ids() exposed for callers and
tests.  definition.py refactored to use _common verbatim — byte-stability
preserved (existing test_compose.py still passes; test_sha_stable_across_
subprocess unchanged).

Tests: 44 new tests across test_template_{composed_relation,procedural,
falsification,identity_anchor,registry}.py.  Each new template gets
determinism, paradigm-structure, error-handling, and cross-subprocess SHA
stability tests; registry test asserts the five known ids and that
identical inputs through different templates produce different SHAs.

Formation suite: 138 -> 182 passing.  cognition (121) and smoke (67)
suites unchanged.  ratify.py enforcement of the new paradigm-specific
gates (every_composed_relation_replayed, linear_order_strict, etc.)
remains a documented follow-up — templates declare the gates in their
phase_5 body so the ratifier extension is purely additive.
2026-05-17 18:59:15 -07:00

249 lines
8.7 KiB
Python

"""The ``falsification`` template — counter-example-driven mastery.
The unit of mastery is the rejected claim plus the coherent alternative.
Where ``definition`` makes ``relations`` load-bearing and ``counters``
adversarial, this template reverses the polarity: ``counters`` are the
primary content of Phase 2 and each counter must be paired with a
``coherent_alternative`` drawn from ``relations`` that share the same
``head``.
Pairing rule:
* A counter is *paired* if at least one relation has ``head == counter.head``.
The lexically-smallest such relation by ``(relation, tail)`` is its
coherent alternative.
* A counter with no matching relation is recorded under
``unmatched_counters`` in Phase 2. This is allowed (the template still
succeeds), but the ratifier should treat unmatched counters as a flag
for follow-up curation.
Phase 4 emits a *false-coherent* probe per pair: a near-miss that looks
like the alternative but is itself a counter (the same ``head`` with a
different ``tail`` — drawn from the remaining counters if available, else
a canned generic probe).
Paradigm-specific gates:
counter_rejection_rate_eq_1
alternative_acceptance_rate_eq_1
"""
from __future__ import annotations
from dataclasses import dataclass
from formation.candidate import CounterCandidate, RelationCandidate
from formation.course import SubjectSpec, ValidatedTripleSet
from formation.templates._common import (
IDENTITY_OVERRIDE_PROBES,
concept_payload,
counter_payload,
course_id,
geometric_dependencies,
phase_5_payload,
relation_payload,
sorted_concepts,
sorted_counters,
source_payload,
sorted_sources,
subject_payload,
substrate_invariants_payload,
topo_sorted_relations,
)
TEMPLATE_ID: str = "falsification"
TEMPLATE_VERSION: str = "1.0.0"
@dataclass(frozen=True, slots=True)
class FalsificationTemplate:
template_id: str = TEMPLATE_ID
template_version: str = TEMPLATE_VERSION
def render(
self,
validated_set: ValidatedTripleSet,
spec: SubjectSpec,
source_bundle_sha: str,
) -> dict[str, object]:
if not validated_set.counters:
raise ValueError(
"falsification: at least one counter required"
)
concepts = sorted_concepts(validated_set.concepts)
relations = topo_sorted_relations(validated_set.relations)
counters = sorted_counters(validated_set.counters)
pairs, unmatched = _build_polarity_pairs(counters, relations)
polarity_walks = _polarity_walks(pairs)
adversarial = _falsification_adversarial(pairs, counters)
body: dict[str, object] = {
"course_id": course_id(spec, self.template_id, self.template_version),
"paradigm": "counter_example_polarity",
"template_id": self.template_id,
"template_version": self.template_version,
"source_bundle_sha": source_bundle_sha,
"subject": subject_payload(spec),
"geometric_dependencies": geometric_dependencies(relations),
"substrate_invariants": substrate_invariants_payload(),
"phase_1_ontological_seeding": {
"concepts": [concept_payload(c) for c in concepts],
},
"phase_2_falsification_corpus": {
"polarity_pairs": pairs,
"unmatched_counters": [counter_payload(c) for c in unmatched],
"supporting_relations": [relation_payload(r) for r in relations],
},
"phase_3_polarity_walks": {
"walks": polarity_walks,
},
"phase_4_epistemic_boundary_hardening": {
"adversarial_corrections": adversarial,
},
"phase_5_ratified_consolidation": phase_5_payload(
extra_gates=(
"counter_rejection_rate_eq_1",
"alternative_acceptance_rate_eq_1",
),
),
}
return body
def _build_polarity_pairs(
counters: list[CounterCandidate],
relations: list[RelationCandidate],
) -> tuple[list[dict[str, object]], list[CounterCandidate]]:
"""Match each counter to the lex-smallest relation sharing its head."""
by_head: dict[str, list[RelationCandidate]] = {}
for r in relations:
by_head.setdefault(r.head, []).append(r)
for head in by_head:
by_head[head].sort(key=lambda r: (r.relation, r.tail))
pairs: list[dict[str, object]] = []
unmatched: list[CounterCandidate] = []
for i, c in enumerate(counters):
candidates = by_head.get(c.head, [])
if not candidates:
unmatched.append(c)
continue
alt = candidates[0]
pairs.append(
{
"pair_id": f"pair_{i:04d}",
"rejected_claim": {
"head": c.head,
"relation": c.relation,
"tail": c.tail,
"sources": [
source_payload(s) for s in sorted_sources(c.sources)
],
},
"coherent_alternative": {
"head": alt.head,
"relation": alt.relation,
"tail": alt.tail,
"sources": [
source_payload(s) for s in sorted_sources(alt.sources)
],
},
}
)
return pairs, unmatched
def _polarity_walks(pairs: list[dict[str, object]]) -> list[dict[str, object]]:
walks: list[dict[str, object]] = []
for i, pair in enumerate(pairs):
rejected = pair["rejected_claim"]
alt = pair["coherent_alternative"]
assert isinstance(rejected, dict) and isinstance(alt, dict)
walks.append(
{
"walk_id": f"walk_{i:04d}",
"kind": "polarity_flip",
"steps": [
{
"head": str(rejected["head"]),
"relation": str(rejected["relation"]),
"tail": str(rejected["tail"]),
"polarity": "reject",
},
{
"head": str(alt["head"]),
"relation": str(alt["relation"]),
"tail": str(alt["tail"]),
"polarity": "accept",
},
],
}
)
return walks
def _falsification_adversarial(
pairs: list[dict[str, object]],
counters: list[CounterCandidate],
) -> list[dict[str, object]]:
"""For each pair, emit one false-coherent probe.
A false-coherent probe shares the pair's ``head`` but a different ``tail``,
sourced from another counter when available. When no other counter
matches, emit a canned ``false_coherent_generic`` probe. Identity
override probes always close the list.
"""
probes: list[dict[str, object]] = []
used_counters: set[tuple[str, str, str]] = set()
for pair in pairs:
rc = pair["rejected_claim"]
assert isinstance(rc, dict)
pair_id = str(pair["pair_id"])
used_counters.add((str(rc["head"]), str(rc["relation"]), str(rc["tail"])))
for pair in pairs:
rc = pair["rejected_claim"]
assert isinstance(rc, dict)
head = str(rc["head"])
pair_id = str(pair["pair_id"])
false_coherent: CounterCandidate | None = None
for c in counters:
triple = (c.head, c.relation, c.tail)
if triple in used_counters:
continue
if c.head == head:
false_coherent = c
used_counters.add(triple)
break
if false_coherent is not None:
probes.append(
{
"probe_id": f"false_coherent_{pair_id}",
"head": false_coherent.head,
"relation": false_coherent.relation,
"tail": false_coherent.tail,
"expected_outcome": "rejected",
"sources": [
source_payload(s)
for s in sorted_sources(false_coherent.sources)
],
}
)
else:
probes.append(
{
"probe_id": f"false_coherent_{pair_id}",
"head": head,
"relation": "near_miss",
"tail": "spurious_alternative",
"expected_outcome": "rejected",
"sources": [],
}
)
for canned in IDENTITY_OVERRIDE_PROBES:
probes.append(dict(canned))
return probes
__all__ = ["FalsificationTemplate", "TEMPLATE_ID", "TEMPLATE_VERSION"]