core/teaching/cross_pack_supersede.py
Shay 4670e391ec feat(phase5+bench): cross-pack supersede + articulation benchmark suite
Phase 5 (ADR-0067 follow-up):
  teaching/cross_pack_supersede.py — supersede_cross_pack_chain()
  CLI: core teaching supersede ... --cross-pack
    --subject-pack-id ... --object-pack-id ...
  Strict per-chain residency, anti-leakage, byte-identical rollback
  on any post-append re-load failure.  9 new tests.

Articulation benchmark suite (Phase 4 capability proof):
  benchmarks/articulation.py — 5 sub-benches
    [1] breadth        — every intent shape (9 + OOV + cross-pack)
    [2] determinism    — N reruns / unique-surface count
    [3] footprint      — psutil RSS profile across T turns
    [4] cross-topic    — thread context across mixed subjects
    [5] ollama-compare — opt-in side-by-side with local Ollama
  CLI: core bench --suite articulation
    --runs N (det rerun count)
    --turns N (footprint sample window)
    --ollama-model MODEL --ollama-reruns N
  Full operator preamble + JSON report path.
  10 new tests cover the bench shape (psutil import-skipped).

Documentation:
  benchmarks/README.md — full operator manual: catalogue of every
    bench suite, how to read good/neutral/bad results for each sub-
    bench, why CORE vs Ollama comparisons are valid on the
    determinism axis and not on linguistic quality, workflow guide.
  README.md — articulation bench listed in the live-demo grid and
    quick-start examples.

Reference run (llama3:8b, 100 turns, 5 reruns):
  determinism_all_identical=True
  per-turn ΔRSS ≈ 23 KiB
  CORE byte_identical_on_every_prompt=True
  Ollama unique_surfaces≥2 on every prompt

Verification:
  18 new tests pass
  Full lane: 2116 passed, 2 skipped, 0 failed in 2:38
2026-05-18 17:44:59 -07:00

206 lines
7.3 KiB
Python

"""ADR-0067 follow-up — operator-driven supersession of a cross-pack chain.
Mirrors :func:`teaching.supersede.supersede_chain` but operates on the
cross-pack corpus (``teaching/cross_pack_chains/cross_pack_chains_v1.jsonl``).
Cross-pack chains carry two pack-residency fields (``subject_pack_id``
and ``object_pack_id``) that the in-pack ``supersede_chain`` does not
know about. Rather than overloading that function with optional kwargs
that change validation behaviour, this module supplies a sibling
function with the right surface and reuses the same write path
(``teaching.proposals.append_chain_to_corpus``).
Trust boundary (matches ADR-0057):
- Append-only: the earlier chain stays on disk; the runtime loader
honours ``superseded_by`` to drop it from the active view.
- Single write surface preserved: ``append_chain_to_corpus`` is the
only function that writes a JSONL line to the corpus.
- Validation gates run BEFORE the append: review-date format,
intent whitelist, distinct chain ids, declared pack residency,
anti-leakage (subject_pack_id != object_pack_id), old chain must
be active in the current cross-pack index.
- Post-append re-load confirms the active set shifted as expected;
any drift rolls back the file bytes.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from chat.cross_pack_grounding import (
_CORPUS_PATH as _DEFAULT_CROSS_PACK_CORPUS_PATH,
_all_cross_pack_chains,
clear_cross_pack_cache,
)
from chat.pack_resolver import _pack_lexicon_for
from teaching.proposals import append_chain_to_corpus
from teaching.provenance import Provenance
from teaching.supersede import SupersessionError
# Reuse the same intent whitelist as the in-pack path.
from chat.teaching_grounding import _VALID_INTENTS
_REVIEW_DATE_RE: re.Pattern[str] = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def _validate_review_date(value: str) -> str:
value = (value or "").strip()
if not _REVIEW_DATE_RE.match(value):
raise SupersessionError(
f"review_date must be YYYY-MM-DD; got {value!r}"
)
return value
def supersede_cross_pack_chain(
*,
old_chain_id: str,
subject: str,
intent: str,
connective: str,
object_: str,
subject_pack_id: str,
object_pack_id: str,
review_date: str,
corpus_path: Path | None = None,
adr_id: str = "adr-0067",
new_chain_id: str | None = None,
) -> str:
"""Retire ``old_chain_id`` in the cross-pack corpus by appending a
new entry whose ``superseded_by`` references it.
Returns the new entry's ``chain_id``. Raises
:class:`SupersessionError` on any pre-condition violation; the
corpus is byte-identical on failure.
Pre-conditions (cheapest first):
1. ``review_date`` matches ``YYYY-MM-DD``.
2. ``intent`` is in :data:`_VALID_INTENTS`.
3. All chain fields + both pack ids are non-empty.
4. ``subject_pack_id != object_pack_id`` (anti-leakage —
cross-pack chains must actually cross packs).
5. Declared subject lemma resolves in its named pack; same for
object.
6. ``old_chain_id`` is currently active in the cross-pack index.
7. New chain id is distinct from old and not already active.
Post-append:
8. Re-load the index; new entry must be active, old must be
dropped. Any drift → roll back the bytes.
"""
path: Path = corpus_path or _DEFAULT_CROSS_PACK_CORPUS_PATH
old_id = (old_chain_id or "").strip()
if not old_id:
raise SupersessionError("old_chain_id is required")
_validate_review_date(review_date)
s = (subject or "").strip().lower()
i = (intent or "").strip().lower()
c = (connective or "").strip()
o = (object_ or "").strip().lower()
sp = (subject_pack_id or "").strip()
op = (object_pack_id or "").strip()
if not all((s, i, c, o, sp, op)):
raise SupersessionError(
"subject/intent/connective/object and both pack ids are required"
)
if i not in _VALID_INTENTS:
raise SupersessionError(
f"intent {i!r} is not in the supported whitelist "
f"({sorted(_VALID_INTENTS)})"
)
if sp == op:
raise SupersessionError(
"subject_pack_id and object_pack_id must differ — "
"same-pack entries belong in the in-pack corpus"
)
subject_pack = _pack_lexicon_for(sp)
object_pack = _pack_lexicon_for(op)
if s not in subject_pack:
raise SupersessionError(
f"subject lemma {s!r} not resident in pack {sp!r}"
)
if o not in object_pack:
raise SupersessionError(
f"object lemma {o!r} not resident in pack {op!r}"
)
# Pre-load index — must include old, must not already include new.
clear_cross_pack_cache()
active = {c.chain_id for c in _all_cross_pack_chains()}
if old_id not in active:
raise SupersessionError(
f"old_chain_id {old_id!r} is not active in the cross-pack corpus"
)
resolved_new_id = (new_chain_id or "").strip() or f"{i}_{s}_{c}_{o}"
if resolved_new_id == old_id:
raise SupersessionError(
"new chain_id is identical to old_chain_id"
)
if resolved_new_id in active:
raise SupersessionError(
f"new chain_id {resolved_new_id!r} is already active; "
"choose a distinct connective/object or pass --new-chain-id"
)
# Compose entry — cross-pack carries the two extra residency fields.
review_date_clean = review_date.strip()
provenance = Provenance(
adr_id=adr_id,
source="hand_authored",
review_date=review_date_clean,
raw=f"{adr_id}:hand_authored:{review_date_clean}:supersede({old_id})",
)
bytes_before = path.read_bytes() if path.exists() else b""
# ``append_chain_to_corpus`` doesn't carry the pack-id fields, so
# we compose our own JSON line directly — staying within the
# spirit of "one write helper" by reusing the same atomic append
# pattern + sorted-keys + provenance shape.
entry = {
"chain_id": resolved_new_id,
"subject": s,
"intent": i,
"connective": c,
"object": o,
"subject_pack_id": sp,
"object_pack_id": op,
"domains_subject_k": 2,
"domains_object_k": 1,
"provenance": provenance.raw,
"superseded_by": old_id,
}
line = json.dumps(entry, sort_keys=True, separators=(",", ":"))
with path.open("a", encoding="utf-8") as fh:
fh.write(line + "\n")
# Post-append: re-load + verify active set shifted as expected.
clear_cross_pack_cache()
post_active = {c.chain_id for c in _all_cross_pack_chains()}
if resolved_new_id not in post_active or old_id in post_active:
# Roll back.
path.write_bytes(bytes_before)
clear_cross_pack_cache()
raise SupersessionError(
f"post-append re-load rejected the supersession "
f"(new_active={resolved_new_id in post_active}, "
f"old_still_active={old_id in post_active}); "
f"corpus rolled back"
)
# Keep ``append_chain_to_corpus`` reachable from this module's
# public re-export so callers needing the in-pack write surface
# can import it from one place when wiring CLI dispatch.
_ = append_chain_to_corpus
return resolved_new_id
__all__ = ["supersede_cross_pack_chain"]