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.
249 lines
8.7 KiB
Python
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"]
|