* 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.
301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""ADR-0084 — definitional-layer substrate.
|
|
|
|
Owns the schema and load path for the extended ``glosses.jsonl`` entries
|
|
introduced by ADR-0084. This module is the *substrate* layer only — no
|
|
composer here consumes the new fields, and no surface is generated from
|
|
them. ADR-0085 adds the consumer; ADR-0086 adds predicate licensing at
|
|
ratification.
|
|
|
|
Design notes
|
|
------------
|
|
|
|
- ADR-0084 attaches the definitional block to the per-lemma gloss entry,
|
|
NOT to ``lexicon.jsonl``. The lexicon checksum stays the immutable
|
|
seal; ``glosses.jsonl`` remains the additive overlay that already exists
|
|
for the 9 core packs.
|
|
- Pack-level opt-in via ``LanguagePackManifest.definitional_layer``. A
|
|
pack without the flag parses today exactly as before — the older
|
|
``(pos, gloss)`` two-tuple shape is preserved in
|
|
:mod:`chat.pack_resolver`.
|
|
- Strict-mode parsing (``definitional_layer: true``) enforces:
|
|
* every required field present and well-typed
|
|
* unknown keys inside the gloss entry rejected
|
|
* ``predicates_invited`` may be an empty list (migration aid)
|
|
* ``definition_version`` is a positive int
|
|
- Closure-rule enforcement lives in :func:`verify_definitional_closure`;
|
|
it is invoked by the ratification gate (and by the standalone
|
|
``scripts/verify_definitional_closure.py`` dev tool authored alongside
|
|
the content PR).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
|
|
_PACK_ROOT = Path(__file__).parent / "data"
|
|
|
|
|
|
# Required keys inside a strict (definitional_layer=True) gloss entry.
|
|
_REQUIRED_KEYS: frozenset[str] = frozenset(
|
|
{
|
|
"lemma",
|
|
"gloss",
|
|
"definitional_atoms",
|
|
"predicates_invited",
|
|
"definition_version",
|
|
"provenance_ids",
|
|
}
|
|
)
|
|
|
|
# All allowed keys inside a strict gloss entry. Any key outside this
|
|
# set is rejected — ADR-0084 §Schema validation, "unrecognised key
|
|
# inside ``definition`` rejected (strict gate)".
|
|
_ALLOWED_KEYS: frozenset[str] = _REQUIRED_KEYS | frozenset({"pos"})
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GlossEntry:
|
|
"""One parsed ``glosses.jsonl`` entry.
|
|
|
|
Fields beyond ``(lemma, gloss, pos)`` are populated only for entries
|
|
coming from a pack with ``definitional_layer: true``. Legacy
|
|
two-field entries map to default values for the ADR-0084 fields.
|
|
"""
|
|
|
|
lemma: str
|
|
gloss: str
|
|
pos: str = ""
|
|
definitional_atoms: tuple[str, ...] = field(default_factory=tuple)
|
|
predicates_invited: tuple[str, ...] = field(default_factory=tuple)
|
|
definition_version: int = 0
|
|
provenance_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
|
|
|
|
class DefinitionalSchemaError(ValueError):
|
|
"""Raised when a strict-mode gloss entry violates the ADR-0084 schema."""
|
|
|
|
|
|
def _require_nonempty_str(payload: dict, key: str, *, lemma_hint: str) -> str:
|
|
value = payload.get(key)
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma_hint!r} requires non-empty string field {key!r}"
|
|
)
|
|
return value.strip()
|
|
|
|
|
|
def _require_str_tuple(
|
|
payload: dict, key: str, *, lemma_hint: str, allow_empty: bool
|
|
) -> tuple[str, ...]:
|
|
raw = payload.get(key)
|
|
if not isinstance(raw, list) or any(not isinstance(x, str) or not x for x in raw):
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma_hint!r} requires {key!r} to be a list of non-empty strings"
|
|
)
|
|
if not raw and not allow_empty:
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma_hint!r} requires non-empty {key!r}"
|
|
)
|
|
return tuple(raw)
|
|
|
|
|
|
def parse_gloss_entry(payload: dict, *, strict: bool) -> GlossEntry:
|
|
"""Parse a single ``glosses.jsonl`` row.
|
|
|
|
``strict=True`` applies the ADR-0084 schema gate: required fields,
|
|
typed lists, no unknown keys, positive ``definition_version``.
|
|
|
|
``strict=False`` preserves the back-compat two-field shape — any
|
|
extra ADR-0084 fields present are read opportunistically but never
|
|
required.
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
raise DefinitionalSchemaError(f"gloss entry must be a JSON object, got {type(payload).__name__}")
|
|
|
|
_maybe_lemma = payload.get("lemma")
|
|
lemma_hint: str = _maybe_lemma if isinstance(_maybe_lemma, str) and _maybe_lemma else "<unknown>"
|
|
|
|
if strict:
|
|
unknown = set(payload.keys()) - _ALLOWED_KEYS
|
|
if unknown:
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma_hint!r} carries unrecognised key(s): {sorted(unknown)}"
|
|
)
|
|
missing = _REQUIRED_KEYS - set(payload.keys())
|
|
if missing:
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma_hint!r} missing required key(s): {sorted(missing)}"
|
|
)
|
|
|
|
lemma = _require_nonempty_str(payload, "lemma", lemma_hint=lemma_hint)
|
|
gloss = _require_nonempty_str(payload, "gloss", lemma_hint=lemma)
|
|
definitional_atoms = _require_str_tuple(
|
|
payload, "definitional_atoms", lemma_hint=lemma, allow_empty=False
|
|
)
|
|
predicates_invited = _require_str_tuple(
|
|
payload, "predicates_invited", lemma_hint=lemma, allow_empty=True
|
|
)
|
|
definition_version_raw = payload.get("definition_version")
|
|
if not isinstance(definition_version_raw, int) or isinstance(definition_version_raw, bool) or definition_version_raw < 1:
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma!r} requires positive int definition_version"
|
|
)
|
|
provenance_ids = _require_str_tuple(
|
|
payload, "provenance_ids", lemma_hint=lemma, allow_empty=False
|
|
)
|
|
pos_raw = payload.get("pos", "")
|
|
if not isinstance(pos_raw, str):
|
|
raise DefinitionalSchemaError(
|
|
f"gloss entry for lemma={lemma!r} requires pos to be a string when present"
|
|
)
|
|
return GlossEntry(
|
|
lemma=lemma,
|
|
gloss=gloss,
|
|
pos=pos_raw,
|
|
definitional_atoms=definitional_atoms,
|
|
predicates_invited=predicates_invited,
|
|
definition_version=definition_version_raw,
|
|
provenance_ids=provenance_ids,
|
|
)
|
|
|
|
# Lax / back-compat path: accept the original (lemma, gloss[, pos])
|
|
# shape; surface ADR-0084 fields only if they happen to be present
|
|
# and well-typed. Silently drop malformed extras — never fabricate.
|
|
lemma_raw = payload.get("lemma")
|
|
gloss_raw = payload.get("gloss")
|
|
if not isinstance(lemma_raw, str) or not lemma_raw.strip():
|
|
raise DefinitionalSchemaError("gloss entry missing lemma")
|
|
if not isinstance(gloss_raw, str) or not gloss_raw.strip():
|
|
raise DefinitionalSchemaError(f"gloss entry for lemma={lemma_raw!r} missing gloss")
|
|
|
|
pos_raw = payload.get("pos", "")
|
|
pos = pos_raw if isinstance(pos_raw, str) else ""
|
|
|
|
def _safe_str_tuple(key: str) -> tuple[str, ...]:
|
|
raw = payload.get(key)
|
|
if isinstance(raw, list) and all(isinstance(x, str) and x for x in raw):
|
|
return tuple(raw)
|
|
return ()
|
|
|
|
dv = payload.get("definition_version")
|
|
definition_version = dv if isinstance(dv, int) and not isinstance(dv, bool) and dv > 0 else 0
|
|
|
|
return GlossEntry(
|
|
lemma=lemma_raw.strip(),
|
|
gloss=gloss_raw.strip(),
|
|
pos=pos,
|
|
definitional_atoms=_safe_str_tuple("definitional_atoms"),
|
|
predicates_invited=_safe_str_tuple("predicates_invited"),
|
|
definition_version=definition_version,
|
|
provenance_ids=_safe_str_tuple("provenance_ids"),
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=64)
|
|
def _load_pack_glosses_cached(pack_id: str, strict: bool) -> tuple[GlossEntry, ...]:
|
|
path = _PACK_ROOT / pack_id / "glosses.jsonl"
|
|
if not path.exists():
|
|
return ()
|
|
out: list[GlossEntry] = []
|
|
for line_no, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
payload = json.loads(line)
|
|
except json.JSONDecodeError as exc:
|
|
if strict:
|
|
raise DefinitionalSchemaError(
|
|
f"{pack_id}/glosses.jsonl:{line_no}: malformed JSON ({exc})"
|
|
) from exc
|
|
continue
|
|
try:
|
|
out.append(parse_gloss_entry(payload, strict=strict))
|
|
except DefinitionalSchemaError:
|
|
if strict:
|
|
raise
|
|
continue
|
|
return tuple(out)
|
|
|
|
|
|
def load_pack_glosses(pack_id: str, *, strict: bool) -> tuple[GlossEntry, ...]:
|
|
"""Return all parsed gloss entries for *pack_id*.
|
|
|
|
``strict=True`` enforces the ADR-0084 schema gate row-by-row; a
|
|
single violation aborts the load with :class:`DefinitionalSchemaError`.
|
|
Strict callers should pass ``manifest.definitional_layer`` so opt-in
|
|
is driven from the manifest, not from the call site.
|
|
"""
|
|
return _load_pack_glosses_cached(pack_id, strict)
|
|
|
|
|
|
def clear_definitions_cache() -> None:
|
|
"""Drop the gloss-entry cache.
|
|
|
|
Test-only escape hatch — ratified packs are immutable in production
|
|
and this cache is never invalidated outside tests.
|
|
"""
|
|
_load_pack_glosses_cached.cache_clear()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Closure-rule verifier
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ClosureViolation:
|
|
"""One unresolved ``definitional_atoms`` reference."""
|
|
|
|
pack_id: str
|
|
lemma: str
|
|
unresolved_token: str
|
|
|
|
|
|
def verify_definitional_closure(
|
|
pack_id: str,
|
|
*,
|
|
mounted_pack_lemmas: Iterable[str],
|
|
primitive_lemmas: Iterable[str],
|
|
strict: bool = True,
|
|
) -> tuple[ClosureViolation, ...]:
|
|
"""Return every unresolved ``definitional_atoms`` reference in *pack_id*.
|
|
|
|
A token resolves if it is (a) another lemma in the same pack's gloss
|
|
set, (b) a lemma in any of *mounted_pack_lemmas* (which the caller
|
|
builds from currently-mounted packs), or (c) a primitive in
|
|
*primitive_lemmas*. Per ADR-0084 §Definitional closure rule,
|
|
cycles are permitted — co-reference between two pack lemmas is
|
|
valid.
|
|
|
|
The empty tuple is a pass. Anything non-empty fails ratification.
|
|
"""
|
|
entries = load_pack_glosses(pack_id, strict=strict)
|
|
if not entries:
|
|
return ()
|
|
|
|
same_pack = {e.lemma.strip().lower() for e in entries}
|
|
mounted = {lem.strip().lower() for lem in mounted_pack_lemmas if lem}
|
|
primitives = {lem.strip().lower() for lem in primitive_lemmas if lem}
|
|
|
|
violations: list[ClosureViolation] = []
|
|
for entry in entries:
|
|
for token in entry.definitional_atoms:
|
|
key = token.strip().lower()
|
|
if not key:
|
|
continue
|
|
if key in same_pack or key in mounted or key in primitives:
|
|
continue
|
|
violations.append(
|
|
ClosureViolation(
|
|
pack_id=pack_id,
|
|
lemma=entry.lemma,
|
|
unresolved_token=token,
|
|
)
|
|
)
|
|
return tuple(violations)
|