core/tests/test_register_runtime_threading.py
Shay 6207b5fd0e feat(register): R1–R4 register pack subsystem — deterministic surface variation
Introduces the presentation axis as a fourth pack class (sibling to identity /
safety / ethics), orthogonal to the truth path. Same input + same packs +
same register ⇒ bit-for-bit reproducible surface; varying any of the three ⇒
genuinely different output. No stochastic sampling.

ADR-0068 (R1): RegisterPack frozen dataclass, loader, ratify script, seam test.
  - default_neutral_v1 ratified as null register.

ADR-0069 (R2): realizer register parameter threaded through 9 composer entry
  points; RuntimeConfig.register_pack_id; three byte-identity invariants
  (A: None ≡ pre-R2 unregistered; B: None ≡ default_neutral_v1; C: trace_hash
  invariant under register). Amended to default-with-lint after 167-call-site
  scout: composers default to UNREGISTERED, AST lint enforces explicit
  register= at runtime call sites.

ADR-0070 (R3): terse_v1 register, first non-neutral pack. realizer_overrides
  schema with known-keys allow-list (disclosure_domain_count ∈ {1,2,3}).
  build_pack_surface_candidate reads override with fail-soft clamp. New
  invariant register_invariant_grounding asserts grounding_source +
  trace_hash byte-identical across {None, neutral, terse}.

ADR-0071 (R4): seeded surface variation via convivial_v1.
  chat/register_variation.py applies SHA-256-seeded marker selection from
  bounded discourse-marker buckets. ChatResponse.pre_decoration_surface routes
  truth-path surface to core/cognition/pipeline.py so trace_hash stays
  invariant under register (the load-bearing architectural fix — initially
  invariant C failed under convivial because decoration was leaking into
  trace_hash via response.surface). Empty-string marker entries now
  legitimate ("no marker this turn" is a valid seed pick). realizer_overrides
  schema widened with per_intent nested block (validated against IntentTag
  whitelist; wired but not exercised by convivial). Two new invariants:
  seeded_variation_replay_equivalence (fresh runtimes → byte-identical) and
  seeded_variation_turn_distinct (same prompt across turns → ≥2 distinct
  surfaces).

ADR-0072 (R5, draft): telemetry + operator surface — TurnEvent gains
  register_id and register_variant_id, core chat --register flag, core demo
  register-tour. Status: Proposed; not yet implemented.

Three ratified register packs ship: default_neutral_v1 (null), terse_v1
(disclosure_domain_count=1), convivial_v1 (3 openings × 3 closings).

Verification:
  - 84 register tests pass + 1 documented skip
  - Curated lanes green: smoke 67, cognition 120+1s, teaching 17, packs 6,
    runtime 19, algebra 132
  - Cognition eval byte-identical to pre-register baseline:
    public 100/100/91.7/100, holdout 100/100/83.3/100
  - Full lane: 2608 passed, 4 skipped, 1 failed (pre-existing
    test_cli_demo.py "Combined Demo" → "Run Every Demo" rename, unrelated)

Truth-path isolation: chat/register_variation.py is realizer-side; the seam
test (tests/test_register_pack_seam.py) refuses imports of packs.register
from intent classification, propagation, vault recall, trace hashing, and
algebra.
2026-05-19 16:52:36 -07:00

138 lines
5.1 KiB
Python

"""Runtime-threading lint test (ADR-0069 amendment).
The ADR-0069 amendment relaxed the original "required keyword-only
register parameter" rule to "default = unregistered sentinel," because
the original rule landed on 167 call sites across 15 test files.
The relaxation introduces a seam risk: an R3 site could call
``teaching_grounded_surface(lemma, intent)`` without ``register=``, and
the call would silently fall through to ``UNREGISTERED`` even though
the runtime has a non-neutral register loaded.
This lint test AST-parses ``chat/runtime.py`` and asserts that every
call to a register-aware composer passes ``register=`` explicitly. The
seam guarantee originally provided by a required parameter now lives
here.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
RUNTIME_PATH = REPO_ROOT / "chat" / "runtime.py"
#: Composer functions that accept ``register=`` at R2. Every call to
#: any of these from ``chat/runtime.py`` must pass ``register=``
#: explicitly. When R3 widens composer logic, add new entries here.
REGISTER_AWARE_COMPOSERS: frozenset[str] = frozenset(
{
"pack_grounded_surface",
"pack_grounded_correction_surface",
"pack_grounded_procedure_surface",
"pack_grounded_comparison_surface",
"teaching_grounded_surface",
"teaching_grounded_surface_composed",
"cross_pack_grounded_surface",
"narrative_grounded_surface",
"example_grounded_surface",
# ADR-0070 (R3) — internal selector-ready builder that
# pack_grounded_surface delegates to. Carries register= so
# the disclosure_domain_count override threads through. Not
# called from chat/runtime.py directly, so the
# runtime-threading lint passes trivially.
"build_pack_surface_candidate",
}
)
def _called_function_name(call: ast.Call) -> str | None:
"""Return the bare name of a Call's target, or ``None`` if not a name."""
func = call.func
if isinstance(func, ast.Name):
return func.id
if isinstance(func, ast.Attribute):
return func.attr
return None
def _has_register_kwarg(call: ast.Call) -> bool:
return any(kw.arg == "register" for kw in call.keywords)
def _find_unthreaded_calls(text: str) -> list[tuple[str, int]]:
"""Return ``[(function_name, line_number), ...]`` for any call to a
register-aware composer that does not pass ``register=``.
"""
tree = ast.parse(text)
bad: list[tuple[str, int]] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
name = _called_function_name(node)
if name is None or name not in REGISTER_AWARE_COMPOSERS:
continue
if not _has_register_kwarg(node):
bad.append((name, node.lineno))
return bad
def test_runtime_threads_register_to_every_composer_call():
if not RUNTIME_PATH.is_file():
pytest.skip("chat/runtime.py missing — environment issue")
text = RUNTIME_PATH.read_text(encoding="utf-8")
unthreaded = _find_unthreaded_calls(text)
assert not unthreaded, (
"ADR-0069 amendment violation: chat/runtime.py calls "
"register-aware composer(s) without passing register= "
"explicitly. Every site below must thread "
"register=self.register_pack. R3 cannot silently fall through "
"to UNREGISTERED.\n\n"
+ "\n".join(
f" - {name} at chat/runtime.py:{lineno}"
for name, lineno in unthreaded
)
)
def test_register_aware_composer_set_is_synced():
"""Guard: every composer with a ``register=`` parameter in chat/*
must appear in REGISTER_AWARE_COMPOSERS. If a new composer is
added and forgotten here, this test fails so the lint stays
complete.
"""
composer_files = (
REPO_ROOT / "chat" / "pack_grounding.py",
REPO_ROOT / "chat" / "teaching_grounding.py",
REPO_ROOT / "chat" / "cross_pack_grounding.py",
REPO_ROOT / "chat" / "narrative_surface.py",
REPO_ROOT / "chat" / "example_surface.py",
)
discovered: set[str] = set()
for path in composer_files:
if not path.is_file():
continue
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not isinstance(node, ast.FunctionDef):
continue
if node.name.startswith("_"):
continue
param_names = [a.arg for a in node.args.kwonlyargs] + [
a.arg for a in node.args.args
]
if "register" in param_names:
discovered.add(node.name)
missing = discovered - REGISTER_AWARE_COMPOSERS
extra = REGISTER_AWARE_COMPOSERS - discovered
assert not missing, (
"Register-aware composer not in REGISTER_AWARE_COMPOSERS — add "
f"these names to the set in this test file: {sorted(missing)}"
)
assert not extra, (
"REGISTER_AWARE_COMPOSERS lists composers that no longer accept "
f"register= — remove them: {sorted(extra)}"
)