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

96 lines
3.4 KiB
Python

"""Canonical JSON serialization + SHA-256 helpers for the Formation Pipeline.
Every artifact in the pipeline is content-addressed. Two artifacts with the
same logical content must produce the same SHA byte-for-byte, across runs and
across Python sessions. This module owns the canonical form.
Rules:
- ``canonical_json`` sorts keys and uses tight separators.
- Tuples and lists serialize identically (lists in JSON).
- Floats are not permitted in hashed payloads; use strings. This avoids
platform-dependent float repr.
- ``self_seal`` computes a SHA over a dict whose ``sha`` field has been
blanked, then writes the SHA in. Verifiers reverse the process.
No pickle anywhere. Pickle defeats replay determinism and is a
code-execution attack surface (see CLAUDE.md trust doctrine).
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
def canonical_json(payload: Any) -> bytes:
"""Serialize ``payload`` to canonical UTF-8 JSON bytes.
Sorted keys, tight separators, ensure_ascii=False, no trailing newline.
Floats are rejected because their repr varies subtly across platforms.
"""
_reject_floats(payload)
return json.dumps(
payload,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
def sha256_of(payload: Any) -> str:
"""Return the lowercase hex SHA-256 of the canonical JSON of ``payload``."""
return hashlib.sha256(canonical_json(payload)).hexdigest()
def self_seal(payload: dict[str, Any], sha_field: str = "report_sha256") -> dict[str, Any]:
"""Return ``payload`` with ``sha_field`` set to its self-sealing SHA.
The SHA is computed over the payload with ``sha_field`` blanked to the
empty string. This is the standard self-sealing convention; verifiers
reproduce the SHA by blanking the field and re-hashing.
"""
if sha_field not in payload:
raise ValueError(f"self_seal: payload missing required field {sha_field!r}")
sealed = dict(payload)
sealed[sha_field] = ""
digest = sha256_of(sealed)
sealed[sha_field] = digest
return sealed
def verify_seal(payload: dict[str, Any], sha_field: str = "report_sha256") -> bool:
"""Return True iff ``payload[sha_field]`` matches its self-sealing SHA."""
if sha_field not in payload:
return False
claimed = payload[sha_field]
if not isinstance(claimed, str) or not claimed:
return False
probe = dict(payload)
probe[sha_field] = ""
return sha256_of(probe) == claimed
def _reject_floats(payload: Any) -> None:
"""Walk ``payload`` and raise if any float is found.
Floats are non-deterministic across platforms in subtle ways (and ``json``
emits them with repr-style precision). Pipeline artifacts must encode
numeric quantities as strings or integers.
"""
if isinstance(payload, float):
raise TypeError(
"canonical_json: float values are forbidden in hashed payloads; "
"encode numbers as strings or integers"
)
if isinstance(payload, dict):
for key, value in payload.items():
if not isinstance(key, str):
raise TypeError(
f"canonical_json: dict keys must be strings, got {type(key).__name__}"
)
_reject_floats(value)
elif isinstance(payload, (list, tuple)):
for item in payload:
_reject_floats(item)