core/packs/primitives/loader.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

285 lines
10 KiB
Python

"""Primitives-pack loader (ADR-0084).
Loads ``packs/primitives/<pack_id>/{manifest.json,primitives.jsonl}``
with byte-checksum verification and strict schema parsing.
Why a separate loader (not :mod:`language_packs.compiler`)
----------------------------------------------------------
Primitives are not lexicon entries — they have no surface, no morphology,
no semantic-domain assignment, no manifold coordinate. They are a flat
set of terminal symbols consulted by
:func:`language_packs.definitions.verify_definitional_closure` as the
``floor`` argument. Treating them as a degenerate ``LexicalEntry`` would
import irrelevant machinery and blur the substrate boundary the ADR is
deliberately drawing.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Iterable
class PrimitivesPackError(RuntimeError):
"""Raised when a primitives pack is missing, malformed, or unverified.
Inherits from :class:`RuntimeError` (not :class:`ValueError`) on the
same principle as :class:`packs.safety.SafetyPackError` — a missing
primitives pack is a fail-closed runtime condition for any system
that opts into the definitional layer, not a recoverable
input-validation error.
"""
DEFAULT_PRIMITIVES_PACK: str = "en_semantic_primitives_v1"
_PACK_ROOT: Path = Path(__file__).resolve().parent
_REQUIRED_MANIFEST_KEYS: frozenset[str] = frozenset(
{
"pack_id",
"language",
"kind",
"definitional_layer",
"version",
"issued_at",
"checksum",
"primitive_count",
"never_auto_mutable",
"provenance",
}
)
_ALLOWED_PRIMITIVE_KEYS: frozenset[str] = frozenset(
{"lemma", "category", "pos", "primitive_version", "provenance_ids"}
)
_REQUIRED_PRIMITIVE_KEYS: frozenset[str] = frozenset(
{"lemma", "category", "primitive_version", "provenance_ids"}
)
@dataclass(frozen=True, slots=True)
class Primitive:
"""One ratified primitive entry."""
lemma: str
category: str
pos: str
primitive_version: int
provenance_ids: tuple[str, ...]
@dataclass(frozen=True, slots=True)
class PrimitivesPack:
"""Loaded primitives pack — immutable terminal-symbol floor."""
pack_id: str
language: str
version: int
issued_at: str
primitives: tuple[Primitive, ...]
checksum: str
@property
def lemmas(self) -> frozenset[str]:
"""Return the lower-cased frozen set of primitive lemmas."""
return frozenset(p.lemma.lower() for p in self.primitives)
def _validate_pack_id(pack_id: object) -> str:
if not isinstance(pack_id, str) or not pack_id:
raise PrimitivesPackError(f"primitives pack_id must be a non-empty string, got {pack_id!r}")
if ".." in pack_id or "/" in pack_id or "\\" in pack_id or pack_id.startswith("."):
raise PrimitivesPackError(f"primitives pack_id contains forbidden path token(s): {pack_id!r}")
for ch in pack_id:
if not (ch.isascii() and (ch.isalnum() or ch in {"_", "-"})):
raise PrimitivesPackError(
f"primitives pack_id must be alphanumeric/_/-, got {pack_id!r}"
)
return pack_id
def _parse_primitive(payload: object, *, line_no: int, pack_id: str) -> Primitive:
if not isinstance(payload, dict):
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: entry must be a JSON object"
)
unknown = set(payload.keys()) - _ALLOWED_PRIMITIVE_KEYS
if unknown:
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: unrecognised key(s): {sorted(unknown)}"
)
missing = _REQUIRED_PRIMITIVE_KEYS - set(payload.keys())
if missing:
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: missing required key(s): {sorted(missing)}"
)
lemma = payload["lemma"]
category = payload["category"]
primitive_version = payload["primitive_version"]
provenance_ids = payload["provenance_ids"]
pos = payload.get("pos", "")
if not isinstance(lemma, str) or not lemma.strip():
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: lemma must be a non-empty string"
)
if not isinstance(category, str) or not category.strip():
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: category must be a non-empty string"
)
if not isinstance(primitive_version, int) or isinstance(primitive_version, bool) or primitive_version < 1:
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: primitive_version must be a positive int"
)
if not isinstance(provenance_ids, list) or not provenance_ids or any(
not isinstance(x, str) or not x for x in provenance_ids
):
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: provenance_ids must be a non-empty list of strings"
)
if not isinstance(pos, str):
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: pos must be a string when present"
)
return Primitive(
lemma=lemma.strip(),
category=category.strip(),
pos=pos,
primitive_version=primitive_version,
provenance_ids=tuple(provenance_ids),
)
@lru_cache(maxsize=8)
def _load_primitives_pack_cached(pack_id: str) -> PrimitivesPack:
pack_dir = _PACK_ROOT / pack_id
manifest_path = pack_dir / "manifest.json"
primitives_path = pack_dir / "primitives.jsonl"
if not manifest_path.exists():
raise PrimitivesPackError(f"primitives pack manifest missing at {manifest_path}")
if not primitives_path.exists():
raise PrimitivesPackError(f"primitives pack jsonl missing at {primitives_path}")
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise PrimitivesPackError(f"primitives pack manifest malformed: {exc}") from exc
if not isinstance(manifest, dict):
raise PrimitivesPackError("primitives pack manifest must be a JSON object")
missing = _REQUIRED_MANIFEST_KEYS - set(manifest.keys())
if missing:
raise PrimitivesPackError(
f"primitives pack manifest missing required key(s): {sorted(missing)}"
)
if manifest["kind"] != "primitives":
raise PrimitivesPackError(
f"primitives pack manifest kind must be 'primitives', got {manifest['kind']!r}"
)
if manifest["definitional_layer"] is not True:
raise PrimitivesPackError(
"primitives pack manifest must have definitional_layer: true"
)
if manifest["never_auto_mutable"] is not True:
raise PrimitivesPackError(
"primitives pack manifest must have never_auto_mutable: true"
)
if manifest["pack_id"] != pack_id:
raise PrimitivesPackError(
f"primitives pack manifest pack_id {manifest['pack_id']!r} != directory {pack_id!r}"
)
primitives_bytes = primitives_path.read_bytes()
actual_checksum = hashlib.sha256(primitives_bytes).hexdigest()
expected_checksum = manifest["checksum"]
if not isinstance(expected_checksum, str) or actual_checksum != expected_checksum:
raise PrimitivesPackError(
f"primitives pack checksum mismatch for {pack_id}: "
f"{actual_checksum} != {expected_checksum}"
)
primitives: list[Primitive] = []
for line_no, raw_line in enumerate(primitives_bytes.decode("utf-8").splitlines(), start=1):
line = raw_line.strip()
if not line:
continue
try:
payload = json.loads(line)
except json.JSONDecodeError as exc:
raise PrimitivesPackError(
f"{pack_id}/primitives.jsonl:{line_no}: malformed JSON ({exc})"
) from exc
primitives.append(_parse_primitive(payload, line_no=line_no, pack_id=pack_id))
declared_count = manifest["primitive_count"]
if not isinstance(declared_count, int) or declared_count != len(primitives):
raise PrimitivesPackError(
f"primitives pack manifest declares primitive_count={declared_count} "
f"but {len(primitives)} entries parsed from primitives.jsonl"
)
# Lemma-uniqueness gate — a primitive must be the floor, so the
# same lemma must not be ratified twice within one pack.
seen: set[str] = set()
for primitive in primitives:
key = primitive.lemma.lower()
if key in seen:
raise PrimitivesPackError(
f"primitives pack {pack_id} ratifies lemma {primitive.lemma!r} more than once"
)
seen.add(key)
version_raw = manifest["version"]
if not isinstance(version_raw, int):
raise PrimitivesPackError(
f"primitives pack manifest version must be an int, got {version_raw!r}"
)
issued_at_raw = manifest["issued_at"]
if not isinstance(issued_at_raw, str) or not issued_at_raw:
raise PrimitivesPackError("primitives pack manifest issued_at must be a non-empty string")
language_raw = manifest["language"]
if not isinstance(language_raw, str) or not language_raw:
raise PrimitivesPackError("primitives pack manifest language must be a non-empty string")
return PrimitivesPack(
pack_id=pack_id,
language=language_raw,
version=version_raw,
issued_at=issued_at_raw,
primitives=tuple(primitives),
checksum=actual_checksum,
)
def load_primitives_pack(pack_id: str = DEFAULT_PRIMITIVES_PACK) -> PrimitivesPack:
"""Load and verify a primitives pack from disk.
Fails closed on any error (missing files, checksum mismatch, schema
violation). Result is cached — subsequent calls return the same
immutable instance.
"""
return _load_primitives_pack_cached(_validate_pack_id(pack_id))
def clear_primitives_cache() -> None:
"""Drop the primitives-pack cache.
Test-only escape hatch — ratified primitives packs are immutable in
production and this cache is never invalidated outside tests.
"""
_load_primitives_pack_cached.cache_clear()
def union_primitive_lemmas(pack_ids: Iterable[str]) -> frozenset[str]:
"""Return the lower-cased union of every primitive lemma across *pack_ids*."""
out: set[str] = set()
for pack_id in pack_ids:
out.update(load_primitives_pack(pack_id).lemmas)
return frozenset(out)