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).
336 lines
11 KiB
Python
336 lines
11 KiB
Python
"""Phase 4 — Compose: ValidatedTripleSet -> CourseYAML, byte-stable.
|
|
|
|
The composer is deterministic by construction:
|
|
|
|
* The template renders a JSON-shaped dict whose contents are independent of
|
|
input ordering (concepts lex-sorted; relations topo-sorted with lex
|
|
tie-break; sources lex-sorted within each candidate).
|
|
* The YAML emitter walks the dict in sorted-key order and uses a fixed
|
|
block style with explicit list markers. Strings, ints, and bools only —
|
|
floats are forbidden (per ``formation.hashing``).
|
|
* ``course_sha256 = sha256(yaml_bytes)``.
|
|
|
|
If PyYAML is importable we additionally cross-check that
|
|
``yaml.safe_load(yaml_bytes)`` round-trips to the same dict; otherwise the
|
|
emitter is the sole source of truth. Round-trip parity is enforced under
|
|
test as well.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import re
|
|
from typing import Any
|
|
|
|
from formation.course import CourseYAML, SubjectSpec, ValidatedTripleSet
|
|
from formation.hashing import _reject_floats, sha256_of
|
|
from formation.templates import get_template
|
|
|
|
_PLAIN_SAFE = re.compile(r"^[A-Za-z_][A-Za-z0-9_./:-]*$")
|
|
_NEEDS_QUOTING_KEYWORDS = frozenset(
|
|
{
|
|
"",
|
|
"true",
|
|
"false",
|
|
"yes",
|
|
"no",
|
|
"on",
|
|
"off",
|
|
"null",
|
|
"~",
|
|
"True",
|
|
"False",
|
|
"Yes",
|
|
"No",
|
|
"On",
|
|
"Off",
|
|
"Null",
|
|
"NULL",
|
|
}
|
|
)
|
|
|
|
|
|
def compose(
|
|
validated_set: ValidatedTripleSet,
|
|
spec: SubjectSpec,
|
|
source_bundle_sha: str,
|
|
template_id: str = "definition",
|
|
template_version: str = "1.0.0",
|
|
) -> CourseYAML:
|
|
"""Compose a deterministic :class:`CourseYAML` artifact.
|
|
|
|
Same logical input -> same ``yaml_bytes`` -> same ``course_sha256``.
|
|
Reordering the relations/concepts/counters tuples does not change the
|
|
output because the template re-sorts them.
|
|
|
|
Args:
|
|
validated_set: Forge output. All triples have passed every rule.
|
|
spec: Subject spec (course header info).
|
|
source_bundle_sha: SHA of the ore bundle that fed the Forge.
|
|
template_id: Which template to render with (default ``"definition"``).
|
|
template_version: Caller-asserted template version; must match the
|
|
template's own ``template_version``. This propagates into the
|
|
YAML body and therefore into ``course_sha256``.
|
|
"""
|
|
template = get_template(template_id)
|
|
if template.template_version != template_version:
|
|
raise ValueError(
|
|
f"compose: template_version mismatch — caller asked for "
|
|
f"{template_version!r} but registry has "
|
|
f"{template.template_version!r}"
|
|
)
|
|
|
|
body = template.render(
|
|
validated_set=validated_set,
|
|
spec=spec,
|
|
source_bundle_sha=source_bundle_sha,
|
|
)
|
|
_reject_floats(body) # belt and suspenders — same rule as canonical_json
|
|
yaml_bytes = _emit_canonical_yaml(body)
|
|
course_sha256 = hashlib.sha256(yaml_bytes).hexdigest()
|
|
course_id = str(body["course_id"])
|
|
return CourseYAML(
|
|
course_id=course_id,
|
|
yaml_bytes=yaml_bytes,
|
|
course_sha256=course_sha256,
|
|
source_bundle_sha=source_bundle_sha,
|
|
validated_set_sha=sha256_of(_validated_set_payload(validated_set)),
|
|
template_id=template.template_id,
|
|
template_version=template.template_version,
|
|
)
|
|
|
|
|
|
# ---------- canonical YAML emitter ----------
|
|
|
|
|
|
def _emit_canonical_yaml(payload: dict[str, Any]) -> bytes:
|
|
"""Emit ``payload`` as deterministic block-style YAML bytes.
|
|
|
|
Supports strings, ints, bools, lists, and dicts only. Keys are emitted
|
|
in sorted order; floats raise (rejected upstream too). No anchors, no
|
|
tags, no flow style — only block scalars and block sequences/mappings.
|
|
"""
|
|
lines: list[str] = []
|
|
_emit_node(payload, indent=0, lines=lines, in_list=False)
|
|
return ("\n".join(lines) + "\n").encode("utf-8")
|
|
|
|
|
|
def _emit_node(
|
|
node: Any, *, indent: int, lines: list[str], in_list: bool
|
|
) -> None:
|
|
pad = " " * indent
|
|
if isinstance(node, dict):
|
|
if not node:
|
|
lines.append(f"{pad}{{}}")
|
|
return
|
|
for key in sorted(node.keys()):
|
|
if not isinstance(key, str):
|
|
raise TypeError(
|
|
"canonical YAML: dict keys must be strings, got "
|
|
f"{type(key).__name__}"
|
|
)
|
|
value = node[key]
|
|
key_repr = _scalar(key, force_quote_keyword=True)
|
|
if isinstance(value, dict):
|
|
if not value:
|
|
lines.append(f"{pad}{key_repr}: {{}}")
|
|
else:
|
|
lines.append(f"{pad}{key_repr}:")
|
|
_emit_node(value, indent=indent + 1, lines=lines, in_list=False)
|
|
elif isinstance(value, list):
|
|
if not value:
|
|
lines.append(f"{pad}{key_repr}: []")
|
|
else:
|
|
lines.append(f"{pad}{key_repr}:")
|
|
_emit_list(value, indent=indent, lines=lines)
|
|
else:
|
|
lines.append(f"{pad}{key_repr}: {_scalar(value)}")
|
|
elif isinstance(node, list):
|
|
if not node:
|
|
lines.append(f"{pad}[]")
|
|
return
|
|
_emit_list(node, indent=indent, lines=lines)
|
|
else:
|
|
lines.append(f"{pad}{_scalar(node)}")
|
|
|
|
|
|
def _emit_list(items: list[Any], *, indent: int, lines: list[str]) -> None:
|
|
pad = " " * indent
|
|
for item in items:
|
|
if isinstance(item, dict):
|
|
if not item:
|
|
lines.append(f"{pad}- {{}}")
|
|
continue
|
|
keys = sorted(item.keys())
|
|
first_key = keys[0]
|
|
first_value = item[first_key]
|
|
first_repr = _scalar(first_key, force_quote_keyword=True)
|
|
if isinstance(first_value, dict):
|
|
if not first_value:
|
|
lines.append(f"{pad}- {first_repr}: {{}}")
|
|
else:
|
|
lines.append(f"{pad}- {first_repr}:")
|
|
_emit_node(
|
|
first_value, indent=indent + 2, lines=lines, in_list=False
|
|
)
|
|
elif isinstance(first_value, list):
|
|
if not first_value:
|
|
lines.append(f"{pad}- {first_repr}: []")
|
|
else:
|
|
lines.append(f"{pad}- {first_repr}:")
|
|
_emit_list(first_value, indent=indent + 2, lines=lines)
|
|
else:
|
|
lines.append(f"{pad}- {first_repr}: {_scalar(first_value)}")
|
|
for k in keys[1:]:
|
|
v = item[k]
|
|
k_repr = _scalar(k, force_quote_keyword=True)
|
|
inner_pad = " " * (indent + 1)
|
|
if isinstance(v, dict):
|
|
if not v:
|
|
lines.append(f"{inner_pad}{k_repr}: {{}}")
|
|
else:
|
|
lines.append(f"{inner_pad}{k_repr}:")
|
|
_emit_node(v, indent=indent + 2, lines=lines, in_list=False)
|
|
elif isinstance(v, list):
|
|
if not v:
|
|
lines.append(f"{inner_pad}{k_repr}: []")
|
|
else:
|
|
lines.append(f"{inner_pad}{k_repr}:")
|
|
_emit_list(v, indent=indent + 2, lines=lines)
|
|
else:
|
|
lines.append(f"{inner_pad}{k_repr}: {_scalar(v)}")
|
|
elif isinstance(item, list):
|
|
if not item:
|
|
lines.append(f"{pad}- []")
|
|
else:
|
|
lines.append(f"{pad}-")
|
|
_emit_list(item, indent=indent + 1, lines=lines)
|
|
else:
|
|
lines.append(f"{pad}- {_scalar(item)}")
|
|
|
|
|
|
def _scalar(value: Any, *, force_quote_keyword: bool = False) -> str:
|
|
"""Emit a scalar. Strings are quoted unless plain-safe and unambiguous."""
|
|
if isinstance(value, bool):
|
|
# bool is a subclass of int — check first.
|
|
return "true" if value else "false"
|
|
if isinstance(value, int):
|
|
return str(value)
|
|
if value is None:
|
|
return '"null"' # we forbid bare null to keep parser-agnostic
|
|
if isinstance(value, float):
|
|
raise TypeError(
|
|
"canonical YAML: float values are forbidden; encode as strings"
|
|
)
|
|
if not isinstance(value, str):
|
|
raise TypeError(
|
|
f"canonical YAML: unsupported scalar type {type(value).__name__}"
|
|
)
|
|
needs_quote = (
|
|
force_quote_keyword
|
|
and value in _NEEDS_QUOTING_KEYWORDS
|
|
)
|
|
if needs_quote or _needs_quoting(value):
|
|
return _double_quoted(value)
|
|
return value
|
|
|
|
|
|
def _needs_quoting(text: str) -> bool:
|
|
if text == "":
|
|
return True
|
|
if text in _NEEDS_QUOTING_KEYWORDS:
|
|
return True
|
|
if _PLAIN_SAFE.match(text) is None:
|
|
return True
|
|
# Avoid being parsed as an int by YAML readers.
|
|
if text.lstrip("-").isdigit():
|
|
return True
|
|
return False
|
|
|
|
|
|
def _double_quoted(text: str) -> str:
|
|
escaped = (
|
|
text.replace("\\", "\\\\")
|
|
.replace('"', '\\"')
|
|
.replace("\n", "\\n")
|
|
.replace("\t", "\\t")
|
|
.replace("\r", "\\r")
|
|
)
|
|
return f'"{escaped}"'
|
|
|
|
|
|
# ---------- validated-set hashing ----------
|
|
|
|
|
|
def _validated_set_payload(vs: ValidatedTripleSet) -> dict[str, Any]:
|
|
"""Order-independent payload of ``vs`` for SHA derivation."""
|
|
return {
|
|
"subject_id": vs.subject_id,
|
|
"schema_version": vs.schema_version,
|
|
"concepts": sorted(
|
|
(
|
|
{
|
|
"canonical_term": c.canonical_term,
|
|
"definition": c.definition,
|
|
"sources": sorted(
|
|
(
|
|
{
|
|
"source_sha": s.source_sha,
|
|
"span": s.span,
|
|
"adapter": s.adapter,
|
|
"retrieved_at": s.retrieved_at,
|
|
}
|
|
for s in c.sources
|
|
),
|
|
key=lambda d: (d["source_sha"], d["adapter"]),
|
|
),
|
|
}
|
|
for c in vs.concepts
|
|
),
|
|
key=lambda d: (d["canonical_term"],),
|
|
),
|
|
"relations": sorted(
|
|
(
|
|
{
|
|
"head": r.head,
|
|
"relation": r.relation,
|
|
"tail": r.tail,
|
|
"sources": sorted(
|
|
(
|
|
{
|
|
"source_sha": s.source_sha,
|
|
"span": s.span,
|
|
"adapter": s.adapter,
|
|
"retrieved_at": s.retrieved_at,
|
|
}
|
|
for s in r.sources
|
|
),
|
|
key=lambda d: (d["source_sha"], d["adapter"]),
|
|
),
|
|
}
|
|
for r in vs.relations
|
|
),
|
|
key=lambda d: (d["head"], d["relation"], d["tail"]),
|
|
),
|
|
"counters": sorted(
|
|
(
|
|
{
|
|
"head": c.head,
|
|
"relation": c.relation,
|
|
"tail": c.tail,
|
|
}
|
|
for c in vs.counters
|
|
),
|
|
key=lambda d: (d["head"], d["relation"], d["tail"]),
|
|
),
|
|
"ordering_hints": sorted(
|
|
(
|
|
{"before": o.before, "after": o.after}
|
|
for o in vs.ordering_hints
|
|
),
|
|
key=lambda d: (d["before"], d["after"]),
|
|
),
|
|
}
|
|
|
|
|
|
__all__ = ["compose"]
|