core/scripts/verify_definitional_closure.py
Shay 48282eef8d
feat(adr-0084): definitional layer — proposal + substrate (schema/loader/closure) (#64)
* docs(adr-0084): propose definitional layer + prompt-diversity suite

Three companion artifacts proposing the next substantive design step
after ADR-0083:

1. ADR-0084 (Proposed) — Definitional Layer for Lexicon Packs
   Optional `definition` block on pack entries: gloss,
   definitional_atoms, predicates_invited, definition_version,
   provenance.  Pack-level opt-in.  Closure rule: every word in a
   gloss must resolve to a same-pack lemma, another mounted pack's
   lemma, or a primitive in a new `packs/primitives/` pack.
   NO composer change in this ADR (sequenced for ADR-0085) —
   ratify substrate before any consumer depends on it.

2. evals/prompt_diversity/ (Proposed) — companion eval lane
   ~50 cases across question-shape × sophistication × domain,
   measuring three new metrics: response_shape_fit,
   audit_in_surface_rate (quantifies the trust-boundary leak into
   user surfaces), gloss_quote_rate (zero today; rises with future
   gloss-aware composer).  No v1 pass thresholds — the lane
   establishes a baseline distribution so future work has
   something to move.  26 seed cases authored covering all 21
   categories.

3. docs/handoff/ADR-0084-pack-content-brief.md — paste-ready brief
   for a cheaper/faster dev agent to produce the pack content in
   parallel.  Self-contained, 5 sequenced phases (primitives pack
   → extend 9 existing glosses → add to relations/anchors → write
   closure verifier → run safety lanes), explicit don't-touch list
   (no composer / runtime / algebra / Greek+Hebrew packs / schema
   parser), no-LLM-glosses discipline, per-phase acceptance.

Discovery while drafting: 9 packs already carry glosses.jsonl
under language_packs/data/ with a flat schema (78 entries in
en_core_cognition_v1 alone).  The brief reflects that — most
work is extending existing entries, not authoring from scratch.

Strategic context: ADR-0083 raised the *depth* ceiling on chain
composition; ADR-0084 raises the *fidelity* ceiling.  The φ
separation probe (memory: phi-separation-falsified) established
that semantic capability lives in chain composition, not in φ
geometry, so deepening the composer's substrate is the natural
next step.  ADR-0084 → 0085 (gloss-aware composer) → 0086
(predicate licensing at ratification) is the planned sequence.

* feat(adr-0084): substrate — schema parser, primitives loader, closure verifier

Substrate-only code-side for ADR-0084 (Definitional Layer for Lexicon Packs).
No composer touches the new fields yet; consumer integration is ADR-0085.

Schema (additive, default preserves byte-identity)
  - LanguagePackManifest.definitional_layer: bool = False
  - compiler loader propagates the flag from manifest.json

language_packs/definitions.py (new)
  - GlossEntry dataclass: lemma, gloss, pos, definitional_atoms,
    predicates_invited, definition_version, provenance_ids
  - parse_gloss_entry(payload, *, strict) — strict mode enforces ADR-0084
    §Schema validation row-by-row: required keys, typed lists, no
    unknown keys, positive definition_version; lax mode preserves the
    legacy two-field shape for back-compat
  - load_pack_glosses(pack_id, *, strict) with cache + clear hook
  - verify_definitional_closure(pack_id, *, mounted_pack_lemmas,
    primitive_lemmas, strict) returning tuple[ClosureViolation, ...];
    case-insensitive resolution; cycles permitted per ADR

packs/primitives/loader.py (new)
  - Sister loader to packs/safety/ and packs/identity/
  - PrimitivesPack frozen dataclass with .lemmas frozenset
  - Gates: checksum match, kind=='primitives', definitional_layer:true,
    never_auto_mutable:true, pack_id matches dir, primitive_count
    cross-check, duplicate-lemma rejection, path-traversal rejection,
    strict per-entry schema with allow-list
  - DEFAULT_PRIMITIVES_PACK = 'en_semantic_primitives_v1'

tests/test_adr_0084_definitional_substrate.py
  - 38 tests covering strict parser (each required key rejection, unknown
    key rejection, empty predicates_invited allowed, empty
    definitional_atoms rejected, invalid definition_version), lax
    parser back-compat, load_pack_glosses (missing/strict raise/lax
    skip/malformed JSON), closure verifier (same-pack/primitive/mounted/
    unresolved/case-insensitive), primitives loader (every gate), and
    a back-compat check that every shipped pack still ratifies with
    definitional_layer=False

Lanes: smoke 67/0, cognition 120/0/1, teaching 17/0, runtime 19/0,
packs 6/0. Cognition eval byte-identical 100/91.7/100/100.

When the content PR lands (primitives.jsonl + extended glosses.jsonl
under ADR-0084-pack-content-brief.md), the gate catches any closure-rule
violation without further code change.

* feat(evals): prompt_diversity lane runner — measurement instrument for ADR-0084+

Implements the runner against the existing contract.md + 26-case v1
public split.  Lane auto-discovered by evals.framework via the standard
contract + runner convention.

Runner (evals/prompt_diversity/runner.py)
  - run_lane(cases, *, config, workers) -> LaneReport
  - 5 metrics: intent_accuracy, versor_closure_rate (carried over from
    cognition), plus the three new lane-specific metrics —
    response_shape_fit, audit_in_surface_rate, gloss_quote_rate
  - breakdown dict groups by (question_shape, sophistication, domain)
    per contract §How to read the output
  - mirrors evals.cognition.runner's parallel worker pattern

Per-shape classifier (deliberately substring/regex-simple at v1)
  - predicate_identity, explanation, sequence, two_subject_contrast,
    narrative, honest_disclosure
  - Unknown shape => neutral pass (don't penalise new categories)

Audit-leak detector
  - trust-boundary preamble markers (teaching-grounded (, pack-grounded
    (, No session evidence yet.)
  - dotted semantic-domain tag regex (cognition.illumination, etc.)

Gloss-quote detector
  - resolves expected_terms via chat.pack_resolver.resolve_gloss
  - 4-token contiguous-window match against surface (high-confidence
    "gloss actually quoted", not "shared one common word")

Tests (tests/test_prompt_diversity_runner.py — 23)
  - shape classifier parametrized over the six expected_shape values
  - audit-leak detector parametrized over preamble + tag + clean cases
  - end-to-end on v1 public:
      * versor_closure_rate == 1.0 (only v1 pass threshold per contract)
      * every metric in [0, 1]
      * breakdown groups present with the four per-cell metrics
      * diversity gate: >=5 question shapes, >=3 domains
        (defends against future regressions that collapse the suite
         back to a cognition-shaped fixture)

v1/public baseline (26 cases)
  intent_accuracy      : 65.4%   (contract predicted 70-85%)
  versor_closure_rate  : 100.0%  (only v1 pass threshold)  PASS
  response_shape_fit   : 53.8%   (contract predicted low)
  audit_in_surface_rate: 42.3%   (contract predicted ~100%)
  gloss_quote_rate     :  7.7%   (contract predicted 0%)

Three baseline surprises worth noting in the report (NOT failures —
the v1 lane is explicitly there to establish the distribution):

  - audit_in_surface_rate at 42% (not 100%) means the chain-walk leak
    fires on ~11/26; the other 15 are honest-disclosure cases that
    emit no audit envelope.  Sharpens the future surface-vs-envelope
    ADR's actual target: grounded surfaces specifically.
  - response_shape_fit at 54% (not "low") — classifier likely has
    false positives on the ", which " cause-marker.  Worth tightening
    once we have an ADR-0085 baseline to compare against.
  - intent_accuracy at 65% (below predicted 70-85%) — classifier dips
    harder on adversarial/cross-pack than expected.  Real gap.

All five smoke/cognition/teaching/runtime/packs lanes still green;
core eval cognition byte-identical 100/91.7/100/100.

* feat(packs): ADR-0084 pack content (primitives + extend glosses + closure verifier) (#65)

* feat(packs): ADR-0084 pack content

* feat(packs): repair ADR-0084 definitional content

* test(adr-0084): adjust substrate manifest tests for post-#65 content reality

PR #65 flipped definitional_layer:true on 13 English packs (9 core +
4 relations + collapse-anchors).  The substrate's previous test
test_existing_packs_unchanged asserted that en_core_cognition_v1 +
en_core_relations_v1 still had definitional_layer:False — which was
the right pre-content invariant but is wrong post-content.

Replace it with two complementary tests that hold against real content:

  - test_non_opted_packs_default_false:
      pins that packs that DIDN'T flip the flag (en_minimal_v1,
      he_core_cognition_v1, grc_logos_cognition_v1) still surface
      definitional_layer=False through the loader.  Defends against
      a future change accidentally flipping the flag on a non-opted
      pack.

  - test_opted_packs_carry_flag:
      pins that packs that DID flip the flag (en_core_cognition_v1,
      en_core_relations_v1) surface definitional_layer=True through
      the loader.  Proves the substrate's manifest-field propagation
      works against real ratified content, not just fixture packs.

Net: +1 test, same intent (substrate ratifies the manifest field
correctly), now with real-content coverage on both sides of the gate.

All 62 ADR-0084 substrate + prompt-diversity tests pass.
2026-05-20 15:25:25 -07:00

172 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Verify ADR-0084 definitional closure across mounted definitional packs."""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DATA_ROOT = REPO_ROOT / "language_packs" / "data"
PRIMITIVES_PATH = REPO_ROOT / "packs" / "primitives" / "en_semantic_primitives_v1" / "primitives.jsonl"
TOKEN_RE = re.compile(r"[A-Za-z_]+")
STOPWORDS = {"is", "of", "to", "by", "the", "a", "an", "that", "which"}
PREPOSITIONS = {
"in",
"on",
"at",
"for",
"with",
"from",
"into",
"between",
"before",
"after",
"during",
"while",
"until",
"since",
"through",
"within",
"without",
"as",
"under",
"over",
"about",
"against",
"toward",
"towards",
"across",
"than",
}
SKIP_TOKENS = STOPWORDS | PREPOSITIONS
def _load_jsonl(path: Path) -> list[dict]:
rows: list[dict] = []
if not path.exists():
return rows
for line in path.read_text(encoding="utf-8").splitlines():
if line.strip():
rows.append(json.loads(line))
return rows
def _load_resolution_set() -> set[str]:
lemmas: set[str] = set()
for lex_path in DATA_ROOT.glob("*/lexicon.jsonl"):
for row in _load_jsonl(lex_path):
lemma = row.get("lemma")
if isinstance(lemma, str):
lemmas.add(lemma)
for gloss_path in DATA_ROOT.glob("*/glosses.jsonl"):
for row in _load_jsonl(gloss_path):
lemma = row.get("lemma")
if isinstance(lemma, str):
lemmas.add(lemma)
for row in _load_jsonl(PRIMITIVES_PATH):
lemma = row.get("lemma")
if isinstance(lemma, str):
lemmas.add(lemma)
return lemmas
def _definitional_pack_manifests() -> list[Path]:
manifests: list[Path] = []
for manifest_path in DATA_ROOT.glob("*/manifest.json"):
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception:
continue
if manifest.get("definitional_layer") is True:
manifests.append(manifest_path)
return sorted(manifests)
def _content_tokens(gloss: str) -> list[str]:
tokens = [token.lower() for token in TOKEN_RE.findall(gloss)]
ordered: list[str] = []
for token in tokens:
if token in SKIP_TOKENS:
continue
if token not in ordered:
ordered.append(token)
return ordered
def verify() -> dict:
resolution_set = _load_resolution_set()
total_entries = 0
total_atoms = 0
unresolved_by_pack: dict[str, list[str]] = defaultdict(list)
mismatched_atoms_by_pack: dict[str, list[str]] = defaultdict(list)
for manifest_path in _definitional_pack_manifests():
pack_dir = manifest_path.parent
gloss_path = pack_dir / "glosses.jsonl"
pack_id = pack_dir.name
for row in _load_jsonl(gloss_path):
total_entries += 1
atoms = row.get("definitional_atoms", [])
if not isinstance(atoms, list):
unresolved_by_pack[pack_id].append(f"{row.get('lemma','<unknown>')}:<non-list-atoms>")
continue
gloss = row.get("gloss")
if not isinstance(gloss, str):
mismatched_atoms_by_pack[pack_id].append(f"{row.get('lemma','<unknown>')}:<missing-gloss>")
continue
expected_atoms = _content_tokens(gloss)
if atoms != expected_atoms:
mismatched_atoms_by_pack[pack_id].append(
f"{row.get('lemma','<unknown>')}:expected={expected_atoms}:actual={atoms}"
)
for atom in atoms:
total_atoms += 1
if not isinstance(atom, str) or atom not in resolution_set:
unresolved_by_pack[pack_id].append(f"{row.get('lemma','<unknown>')}:{atom}")
unresolved_by_pack = {pack: items for pack, items in unresolved_by_pack.items() if items}
mismatched_atoms_by_pack = {pack: items for pack, items in mismatched_atoms_by_pack.items() if items}
return {
"total_entries_checked": total_entries,
"total_definitional_atoms_checked": total_atoms,
"unresolved_by_pack": unresolved_by_pack,
"mismatched_atoms_by_pack": mismatched_atoms_by_pack,
"definitional_pack_count": len(_definitional_pack_manifests()),
"primitive_pack_path": str(PRIMITIVES_PATH.relative_to(REPO_ROOT)),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
args = parser.parse_args()
report = verify()
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
print(f"total entries checked: {report['total_entries_checked']}")
print(f"total definitional_atoms checked: {report['total_definitional_atoms_checked']}")
if report["unresolved_by_pack"]:
print("unresolved tokens grouped by pack:")
for pack, items in sorted(report["unresolved_by_pack"].items()):
print(f"- {pack}: {', '.join(items)}")
else:
print("unresolved tokens grouped by pack: none")
if report["mismatched_atoms_by_pack"]:
print("gloss/atom mismatches grouped by pack:")
for pack, items in sorted(report["mismatched_atoms_by_pack"].items()):
print(f"- {pack}: {', '.join(items)}")
else:
print("gloss/atom mismatches grouped by pack: none")
return 0 if not report["unresolved_by_pack"] and not report["mismatched_atoms_by_pack"] else 1
if __name__ == "__main__":
raise SystemExit(main())