core/scripts/ratify_register_packs.py
Shay 5a78b0e37b feat(register): ADR-0077 — substantive register knobs + layering boundary (R6)
R5 (ADR-0072) shipped the register *machinery*; ADR-0074's orthogonality
tour proved the axis was decoratively orthogonal to anchor-lens but
inspection of the cognition-eval surfaces revealed two structural gaps:

* On pack-grounded DEFINITION/RECALL/COMPARISON composers, the only
  realizer override any register consumed was `disclosure_domain_count`
  — which only fires on the no-gloss disclosure path.  Under terse_v1,
  every gloss-DEFINITION cell was byte-identical to default_neutral_v1.
* The register-tour's `surfaces_vary_at_least_once` gate could be
  satisfied by convivial's decorative wrapper alone, masking that
  regression in CI.

R6 closes both:

Layering separation (the load-bearing fix):
* New TurnEvent/ChatResponse field `register_canonical_surface` carries
  the composer output BEFORE any register transformation.  The pipeline
  hashes this field for `trace_hash`, preserving R5's invariant that
  per-prompt trace_hash is CONSTANT across registers even while
  substantive transforms produce visibly different surfaces.

Substantive transforms (`chat/register_substantive.py`):
* terse_v1 gains 3 bool knobs: `drop_provenance_tag`, `compress_gloss`,
  `drop_articles` — all pure regex transforms on the canonical surface.
* convivial_v1 gains `append_semantic_domain_clause` — appends a single
  bounded "Related: <atom>." clause using the lemma's pack atoms.
* default_neutral_v1 leaves overrides empty; substantive transform is
  byte-identical no-op (preserves `byte_identity_null_lift`).
* C1 (ADR-0075) safety preserved: drop_articles refuses to drop
  articles following `not` (avoids R3 violations); no knob combination
  trips R2/R3.

Strengthened tour gate (`evals/register_tour/run_tour.py`):
* Replaces `surfaces_vary_at_least_once` with two falsifiable claims:
  - `terse_substantively_differs_from_neutral_on_pack_grounded_definition`
  - `convivial_substantively_differs_from_neutral_on_pack_grounded_definition`
  Both restrict to DEFINITION+pack-grounded cells and require
  difference beyond whitespace/punctuation.
* New claim `register_canonical_surfaces_identical` directly proves
  the layering separation.
* Preserves R5's `all_grounding_sources_identical` +
  `all_trace_hashes_identical`.

Pack ratification:
* Loader widened to accept `bool` for closed-set R6 keys
  (drop_provenance_tag / compress_gloss / drop_articles /
  append_semantic_domain_clause).
* `_KNOWN_OVERRIDE_KEYS` ratify gate extended with same.
* terse_v1 + convivial_v1 reratified with new knobs; companion
  mastery reports re-sealed.  default_neutral_v1 unchanged.

Invariants pinned:
* `invariant_register_canonical_surface_constant_across_registers` (new)
* `invariant_terse_substantively_distinct_from_neutral` (new)
* `invariant_convivial_substantively_distinct_from_neutral` (new)
* `invariant_realizer_no_illegal_articulation` (C1, preserved)
* `invariant_realizer_guard_byte_identity_on_currently_passing_cases`
  (C1, preserved)

Verification:
* `core eval cognition`: 100.0% / 91.7% / 100.0% / 100.0% — byte-
  identical under default_neutral_v1.
* `core demo register-tour`: all 5 claims green, exit 0.
* `core demo anchor-lens-tour`: green (no anchor-lens code touched).
* `core demo orthogonality-tour`: green (5/5 claims).
* Full lane: 2858 passed, 1 pre-existing failure
  (test_all_preamble_explains_combined_run, carried forward
  unchanged from main).  56 new R6 tests across three files.
2026-05-19 23:39:11 -07:00

305 lines
11 KiB
Python

"""Ratify register packs (ADR-0068, Plan Phase R1).
For each pack in ``packs/register/<register_id>.json``:
1. Verify schema fields are present and well-shaped (delegated to the
loader, run with ``require_ratified=False``).
2. Compute the canonical ``pack_source_sha`` (SHA-256 of the pack with
``mastery_report_sha256`` blanked) — self-referential provenance.
3. Apply the R1 gate: only **null registers** (empty
``realizer_overrides`` + empty ``discourse_markers``) are ratifiable
through this script. R3 will widen this gate to cover non-null
registers with their own ratification method.
4. Build a self-sealed ``MasteryReport``-shaped dict capturing the
evidence claim (``byte_identity_null_lift``).
5. Write ``<register_id>.mastery_report.json``.
6. Update the pack's ``mastery_report_sha256`` field to match.
Idempotent: re-running on an already-ratified pack produces
byte-identical files. Run whenever a register pack changes.
Trust boundary
--------------
This script writes to ``packs/register/`` on disk. It is operator-only
(no runtime entrypoint). Pack-id sanitisation, schema validation, and
seal verification all flow through ``packs/register/loader.py`` — see
``ADR-0051`` doctrine.
"""
from __future__ import annotations
import hashlib
import json
import sys
from pathlib import Path
from typing import Any, Callable
from formation.hashing import self_seal
from packs.register.loader import (
RegisterPackError,
load_register_pack,
)
PACKS_DIR = Path(__file__).resolve().parents[1] / "packs" / "register"
ISSUED_AT = "2026-05-19T00:00:00Z"
REGISTER_IDS: tuple[str, ...] = (
"default_neutral_v1",
"terse_v1",
"convivial_v1",
)
#: Valid IntentTag names for ``per_intent`` keys (ADR-0071, R4). This
#: is the trust-boundary whitelist — operators cannot ship unknown
#: intent names. Sourced from ``generate.intent.IntentTag``.
def _valid_intent_names() -> frozenset[str]:
from generate.intent import IntentTag
return frozenset(t.name for t in IntentTag)
# Known realizer_overrides keys (ADR-0070, Phase R3). The allow-list is
# the trust boundary against arbitrary operator-authored data driving
# realizer dispatch. Each entry maps a key name to a validator that
# returns True iff the value is in-bounds for R3.
def _is_bool(v: Any) -> bool:
return isinstance(v, bool)
_KNOWN_OVERRIDE_KEYS: dict[str, Callable[[Any], bool]] = {
"disclosure_domain_count": lambda v: isinstance(v, int)
and not isinstance(v, bool)
and v in (1, 2, 3),
# ADR-0077 (R6) — substantive register knobs. Each is a strict
# bool; the loader's closed-set boolean allow-list mirrors this.
"drop_provenance_tag": _is_bool,
"compress_gloss": _is_bool,
"drop_articles": _is_bool,
"append_semantic_domain_clause": _is_bool,
}
def _canonical_pack_bytes_for_hashing(pack: dict) -> bytes:
"""Serialize the pack with ``mastery_report_sha256`` blanked."""
cleaned = dict(pack)
cleaned["mastery_report_sha256"] = ""
return json.dumps(
cleaned, sort_keys=True, separators=(",", ":")
).encode("utf-8")
def _pack_source_sha(pack: dict) -> str:
return hashlib.sha256(_canonical_pack_bytes_for_hashing(pack)).hexdigest()
def _is_null_register(pack: dict) -> bool:
overrides = pack.get("realizer_overrides", {})
markers = pack.get("discourse_markers", {})
if not isinstance(overrides, dict) or overrides:
return False
if not isinstance(markers, dict):
return False
for bucket in ("openings", "transitions", "closings"):
if markers.get(bucket):
return False
return True
def _validate_overrides_known_keys(pack: dict, register_id: str) -> None:
"""R4 gate — allow-list known realizer_overrides keys + per_intent
nested block. Unknown keys are refused.
"""
overrides = pack.get("realizer_overrides", {})
if not isinstance(overrides, dict):
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides is not a dict"
)
valid_intents = _valid_intent_names()
for k, v in overrides.items():
if k == "per_intent":
_validate_per_intent_block(v, register_id, valid_intents)
continue
validator = _KNOWN_OVERRIDE_KEYS.get(k)
if validator is None:
raise SystemExit(
f"R4 gate refuses {register_id!r}: unknown realizer_overrides "
f"key {k!r}. Known keys: "
f"{sorted(list(_KNOWN_OVERRIDE_KEYS) + ['per_intent'])}"
)
if not validator(v):
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides[{k!r}]"
f"={v!r} out of bounds"
)
def _validate_per_intent_block(
block: object, register_id: str, valid_intents: frozenset[str],
) -> None:
"""R4 gate — per_intent intents must be valid IntentTag names; each
sub-dict's keys must be in the same allow-list as flat keys.
"""
if not isinstance(block, dict):
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides.per_intent "
"must be a dict"
)
for intent_name, sub in block.items():
if intent_name not in valid_intents:
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides"
f".per_intent[{intent_name!r}] is not a valid IntentTag. "
f"Valid intents: {sorted(valid_intents)}"
)
if not isinstance(sub, dict):
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides"
f".per_intent[{intent_name!r}] must be a dict"
)
for sub_k, sub_v in sub.items():
validator = _KNOWN_OVERRIDE_KEYS.get(sub_k)
if validator is None:
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides"
f".per_intent[{intent_name!r}] unknown key {sub_k!r}. "
f"Known keys: {sorted(_KNOWN_OVERRIDE_KEYS)}"
)
if not validator(sub_v):
raise SystemExit(
f"R4 gate refuses {register_id!r}: realizer_overrides"
f".per_intent[{intent_name!r}][{sub_k!r}]={sub_v!r} "
"out of bounds"
)
def _markers_have_content(pack: dict) -> bool:
"""True iff openings or closings has at least one entry (R4 needs
one of these populated for a marker-using ratification).
Empty-string entries count as content — the seed may legitimately
pick "no marker this turn" from a bucket like ``["", "So,"]``.
"""
markers = pack.get("discourse_markers", {})
if not isinstance(markers, dict):
return False
for bucket in ("openings", "closings"):
items = markers.get(bucket, [])
if isinstance(items, list) and len(items) >= 1:
return True
return False
def _ratify_one(pack_path: Path, register_id: str) -> tuple[dict, dict[str, Any]]:
"""Returns (updated_pack_dict, mastery_report_dict). Does not write."""
pack = json.loads(pack_path.read_text(encoding="utf-8"))
load_register_pack(
register_id,
search_paths=(pack_path.parent,),
require_ratified=False,
)
pack_source_sha = _pack_source_sha(pack)
# R4 gate: realizer_overrides must contain only known keys
# (including the per_intent nested block); discourse markers may
# be populated, but a register claiming non-null marker status
# must have at least one of openings/closings populated. Null
# registers pass trivially.
_validate_overrides_known_keys(pack, register_id)
null_register = _is_null_register(pack)
overrides = pack.get("realizer_overrides", {}) or {}
markers = pack.get("discourse_markers", {}) or {}
markers_used = _markers_have_content(pack)
transitions_reserved = bool(
isinstance(markers, dict) and markers.get("transitions")
)
if markers_used:
ratification_method = "seeded_variation_replay_equivalence"
elif null_register:
ratification_method = "byte_identity_null_lift"
else:
ratification_method = "known_key_overrides_invariant_grounding"
bucket_sizes = {
b: (
len(markers.get(b, []))
if isinstance(markers, dict)
and isinstance(markers.get(b, []), list)
else 0
)
for b in ("openings", "transitions", "closings")
}
report: dict[str, Any] = {
"register_id": register_id,
"schema_version": "1.0.0",
"issued_at": ISSUED_AT,
"pack_source_sha256": pack_source_sha,
"ratification_method": ratification_method,
"ratified": True,
"evidence": {
"realizer_overrides_empty": not bool(overrides),
"realizer_overrides_keys": sorted(overrides.keys()),
"discourse_markers_empty": not markers_used
and not transitions_reserved,
"marker_bucket_sizes": bucket_sizes,
"transitions_reserved": transitions_reserved,
"depth_preference": str(pack.get("depth_preference", "")),
},
"failure_reasons": [],
"report_sha256": "",
}
sealed = self_seal(report, sha_field="report_sha256")
pack["mastery_report_sha256"] = sealed["report_sha256"]
return pack, sealed
def main() -> int:
updated = 0
skipped = 0
for register_id in REGISTER_IDS:
pack_path = PACKS_DIR / f"{register_id}.json"
if not pack_path.is_file():
print(f"skip: {pack_path} not found", file=sys.stderr)
continue
try:
pack_after, report_dict = _ratify_one(pack_path, register_id)
except RegisterPackError as exc:
print(f"refused: {register_id}{exc}", file=sys.stderr)
return 1
report_path = PACKS_DIR / f"{register_id}.mastery_report.json"
report_text = json.dumps(report_dict, indent=2, sort_keys=True) + "\n"
prior_report = (
report_path.read_text(encoding="utf-8")
if report_path.is_file()
else ""
)
prior_pack = json.loads(pack_path.read_text(encoding="utf-8"))
if (
prior_pack.get("mastery_report_sha256")
== pack_after["mastery_report_sha256"]
and prior_report == report_text
):
print(
f"idempotent: {register_id} already ratified at "
f"{pack_after['mastery_report_sha256'][:12]}"
)
skipped += 1
continue
pack_path.write_text(
json.dumps(pack_after, indent=2) + "\n", encoding="utf-8",
)
report_path.write_text(report_text, encoding="utf-8")
print(
f"ratified: {register_id}"
f"{pack_after['mastery_report_sha256'][:12]}"
)
updated += 1
print(f"\nratified {updated} pack(s); {skipped} already current")
return 0
if __name__ == "__main__":
raise SystemExit(main())