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.
210 lines
7 KiB
Python
210 lines
7 KiB
Python
"""The ``procedural`` template — ordered state transitions.
|
|
|
|
The unit of mastery is a state machine, not a DAG of definitions. Each
|
|
relation is interpreted as an action: ``head`` is the precondition state
|
|
and ``tail`` is the postcondition state. The full set of relations must
|
|
form a single linear chain — branches, cycles, or disconnected components
|
|
raise ``ValueError`` at template render time, so the failure surfaces
|
|
before composition rather than at ratification.
|
|
|
|
``ordering_hints`` are surfaced as additional explicit constraints that
|
|
the chain must respect (a hint ``before -> after`` means ``before`` must
|
|
appear at or before ``after`` in the linear order). A hint that
|
|
contradicts the chain raises ``ValueError``.
|
|
|
|
Paradigm-specific gates:
|
|
linear_order_strict
|
|
every_transition_walked_exactly_once
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from formation.candidate import RelationCandidate
|
|
from formation.course import SubjectSpec, ValidatedTripleSet
|
|
from formation.templates._common import (
|
|
adversarial_block,
|
|
concept_payload,
|
|
course_id,
|
|
geometric_dependencies,
|
|
phase_5_payload,
|
|
relation_payload,
|
|
sorted_concepts,
|
|
sorted_counters,
|
|
sorted_hints,
|
|
strict_linear_topo,
|
|
subject_payload,
|
|
substrate_invariants_payload,
|
|
)
|
|
|
|
TEMPLATE_ID: str = "procedural"
|
|
TEMPLATE_VERSION: str = "1.0.0"
|
|
|
|
# Canned procedural-violation probes. Every procedural course inherits
|
|
# these so the order-respecting refusal is exercised even when no
|
|
# domain-specific counter is supplied.
|
|
_PROCEDURAL_VIOLATION_PROBES: tuple[dict[str, str], ...] = (
|
|
{
|
|
"probe_id": "procedural_precondition_violation",
|
|
"prompt": "Apply a step whose precondition state has not been established.",
|
|
"expected_outcome": "rejected",
|
|
"rationale": "Each step requires its precondition state to be current.",
|
|
},
|
|
{
|
|
"probe_id": "procedural_step_skip",
|
|
"prompt": "Skip an intermediate step in the declared chain.",
|
|
"expected_outcome": "rejected",
|
|
"rationale": "Procedural template requires every transition to be walked.",
|
|
},
|
|
{
|
|
"probe_id": "procedural_back_edge",
|
|
"prompt": "Re-apply a prior step after a later step has completed.",
|
|
"expected_outcome": "rejected",
|
|
"rationale": "Linear order is strict; back-edges are not permitted.",
|
|
},
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ProceduralTemplate:
|
|
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.relations:
|
|
raise ValueError(
|
|
"procedural: at least one transition relation required"
|
|
)
|
|
|
|
chain = strict_linear_topo(validated_set.relations)
|
|
_validate_hints_against_chain(chain, validated_set.ordering_hints)
|
|
|
|
concepts = sorted_concepts(validated_set.concepts)
|
|
counters = sorted_counters(validated_set.counters)
|
|
|
|
transitions = [_transition_payload(i, r) for i, r in enumerate(chain)]
|
|
states = _state_payload(chain)
|
|
canonical_walk = _canonical_walk(chain)
|
|
|
|
body: dict[str, object] = {
|
|
"course_id": course_id(spec, self.template_id, self.template_version),
|
|
"paradigm": "ordered_state_transitions",
|
|
"template_id": self.template_id,
|
|
"template_version": self.template_version,
|
|
"source_bundle_sha": source_bundle_sha,
|
|
"subject": subject_payload(spec),
|
|
"geometric_dependencies": geometric_dependencies(chain),
|
|
"substrate_invariants": substrate_invariants_payload(),
|
|
"phase_1_state_seeding": {
|
|
"states": states,
|
|
"concepts": [concept_payload(c) for c in concepts],
|
|
},
|
|
"phase_2_transition_scaffolding": {
|
|
"transitions": transitions,
|
|
"relations": [relation_payload(r) for r in chain],
|
|
},
|
|
"phase_3_linear_procedural_walk": {
|
|
"walks": [canonical_walk],
|
|
},
|
|
"phase_4_epistemic_boundary_hardening": {
|
|
"adversarial_corrections": adversarial_block(
|
|
counters, canned=_PROCEDURAL_VIOLATION_PROBES,
|
|
),
|
|
},
|
|
"phase_5_ratified_consolidation": phase_5_payload(
|
|
extra_gates=(
|
|
"linear_order_strict",
|
|
"every_transition_walked_exactly_once",
|
|
),
|
|
),
|
|
}
|
|
return body
|
|
|
|
|
|
def _validate_hints_against_chain(
|
|
chain: list[RelationCandidate],
|
|
ordering_hints: tuple,
|
|
) -> None:
|
|
"""Each hint's ``before`` must appear at-or-before ``after`` in the chain order."""
|
|
order: dict[str, int] = {}
|
|
if chain:
|
|
order[chain[0].head] = 0
|
|
for i, r in enumerate(chain, start=1):
|
|
order[r.tail] = i
|
|
for h in sorted_hints(ordering_hints):
|
|
if h.before not in order or h.after not in order:
|
|
# Hints over unrelated nodes are ignored — not load-bearing.
|
|
continue
|
|
if order[h.before] > order[h.after]:
|
|
raise ValueError(
|
|
f"procedural: ordering_hint {h.before!r} -> {h.after!r} "
|
|
"contradicts the linear chain order"
|
|
)
|
|
|
|
|
|
def _state_payload(chain: list[RelationCandidate]) -> list[dict[str, object]]:
|
|
"""States are nodes in the chain, deduplicated, in linear-visit order."""
|
|
seen: set[str] = set()
|
|
out: list[dict[str, object]] = []
|
|
if not chain:
|
|
return out
|
|
seq = [chain[0].head] + [r.tail for r in chain]
|
|
for index, name in enumerate(seq):
|
|
if name in seen:
|
|
continue
|
|
seen.add(name)
|
|
out.append(
|
|
{
|
|
"state_id": f"state_{index:04d}",
|
|
"name": name,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _transition_payload(
|
|
index: int, relation: RelationCandidate
|
|
) -> dict[str, object]:
|
|
return {
|
|
"transition_id": f"transition_{index:04d}",
|
|
"action": relation.relation,
|
|
"precondition_state": relation.head,
|
|
"postcondition_state": relation.tail,
|
|
"sources": [
|
|
{
|
|
"source_sha": s.source_sha,
|
|
"span": s.span,
|
|
"adapter": s.adapter,
|
|
"retrieved_at": s.retrieved_at,
|
|
}
|
|
for s in sorted(
|
|
relation.sources,
|
|
key=lambda s: (s.source_sha, s.adapter, s.retrieved_at),
|
|
)
|
|
],
|
|
}
|
|
|
|
|
|
def _canonical_walk(chain: list[RelationCandidate]) -> dict[str, object]:
|
|
return {
|
|
"walk_id": "walk_0000",
|
|
"kind": "linear_total",
|
|
"steps": [
|
|
{
|
|
"head": r.head,
|
|
"relation": r.relation,
|
|
"tail": r.tail,
|
|
"step_index": str(i),
|
|
}
|
|
for i, r in enumerate(chain)
|
|
],
|
|
}
|
|
|
|
|
|
__all__ = ["ProceduralTemplate", "TEMPLATE_ID", "TEMPLATE_VERSION"]
|