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
This commit is contained in:
parent
d5a6e81b33
commit
4670e391ec
8 changed files with 1488 additions and 15 deletions
|
|
@ -95,6 +95,8 @@ core trace "your text here" # one-turn field-telemetry trace
|
|||
core pulse "What is truth?" # one full cognitive pulse
|
||||
core bench --suite latency # benchmark harness
|
||||
core bench --suite teaching-loop --runs 100 # ADR-0055..0057 — replayable learning loop determinism
|
||||
core bench --suite articulation # Phase 4 capability proof (breadth + determinism + footprint + cross-topic + ollama compare)
|
||||
core bench --suite articulation --ollama-model llama3:8b # side-by-side with a local Ollama model
|
||||
core doctor --packs --rust # environment + pack + Rust status
|
||||
```
|
||||
|
||||
|
|
@ -188,6 +190,7 @@ Three live demos / benchmarks make the chain demoable end-to-end:
|
|||
| **Anti-regression** | Three independent gates each fail closed; bad proposals stop at the cheapest applicable gate. | `core demo anti-regression` | [`docs/evals/anti_regression_demo.md`](docs/evals/anti_regression_demo.md) |
|
||||
| **Learning loop** | Same deterministic prompt: `[none] I don't know…` before, `[teaching] thought reveals meaning…` after one accept. | `core demo learning-loop` | [`docs/evals/learning_loop_demo.md`](docs/evals/learning_loop_demo.md) |
|
||||
| **Determinism bench** | N identical inputs → N byte-identical proposal_id / replay metrics / chain_id. 100 runs: `unique=1` everywhere, mean ≈ 1.85s. | `core bench --suite teaching-loop --runs 100` | [`docs/evals/teaching_loop_bench.md`](docs/evals/teaching_loop_bench.md) |
|
||||
| **Articulation suite** | Every intent shape fires + byte-identical surfaces across reruns + flat per-turn ΔRSS + cross-topic thread context + side-by-side with a local Ollama model showing CORE unique=1, Ollama unique≥2. | `core bench --suite articulation --ollama-model llama3:8b` | [`benchmarks/README.md`](benchmarks/README.md) |
|
||||
|
||||
Operator surfaces:
|
||||
|
||||
|
|
|
|||
313
benchmarks/README.md
Normal file
313
benchmarks/README.md
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
# Benchmarks
|
||||
|
||||
Operator-runnable measurement harnesses for CORE. Each suite anchors a
|
||||
specific load-bearing claim in numbers an outsider can reproduce.
|
||||
|
||||
```
|
||||
core bench --suite articulation
|
||||
core bench --suite determinism --runs 50
|
||||
core bench --suite teaching-loop --runs 100
|
||||
core bench --suite cost --runs 100
|
||||
core bench --suite latency
|
||||
core bench --suite speedup
|
||||
core bench --suite versor
|
||||
core bench --suite convergence
|
||||
core bench --suite realizer
|
||||
```
|
||||
|
||||
This document covers all benchmark suites in the harness, with
|
||||
emphasis on the post-Phase-4 articulation suite (the newest and most
|
||||
comparative one).
|
||||
|
||||
---
|
||||
|
||||
## Why these benchmarks exist
|
||||
|
||||
CORE makes three structurally unusual claims:
|
||||
|
||||
1. **Reconstruction over storage.** The user-facing surface is
|
||||
re-derived from immutable corpora + ratified packs every turn,
|
||||
not sampled from a stochastic process. → *Same input must produce
|
||||
byte-identical output across runs.*
|
||||
2. **Bounded footprint.** No transformer weights, no rolling token
|
||||
history, no embedding store. → *Memory stays flat across thousands
|
||||
of turns.*
|
||||
3. **Traceable grounding.** Every surface carries a grounding tag
|
||||
pointing at the resolving pack id or chain id. → *We can audit
|
||||
why a sentence was produced.*
|
||||
|
||||
These claims need evidence, not assertions. The benchmarks here are
|
||||
the evidence.
|
||||
|
||||
---
|
||||
|
||||
## Suite catalogue
|
||||
|
||||
| Suite | Anchors | What it does | How to read it |
|
||||
|---|---|---|---|
|
||||
| `articulation` | Phase 4 capability claim | Fires every supported intent shape; reruns to prove determinism; samples RSS across many turns; walks cross-topic prompts; side-by-side with Ollama | All-identical on determinism; flat RSS; OOV fall-through visible; ollama shows ≥2 unique surfaces per prompt |
|
||||
| `determinism` | "Same input → same output" | N reruns of the pulse loop, compares trace hashes | `unique_hashes` should be 1 |
|
||||
| `teaching-loop` | "Replayable learning" | Builds a proposal, runs replay, accepts, asserts active corpus is byte-identical to a deterministic baseline across N runs | `unique(proposal_id)` and `unique(chain_id)` should both be 1 |
|
||||
| `cost` | Tier-4 CLAIMS.md cost claim | Wall + CPU seconds per turn, $/1000-turn at a disclosed cloud rate, frontier price context | Higher throughput / lower $ = better; frontier pricing context for apples-to-apples |
|
||||
| `latency` | Time-to-first-surface | Single pulse call timed | Lower ms = better |
|
||||
| `speedup` | Rust backend lift | Python vs Rust on identical workload | Speedup factor > 1× when Rust available |
|
||||
| `versor` | CGA field invariant | Walks the pulse loop and checks `versor_condition(F) < 1e-6` on every transition | Must always pass; failures point at the operator that broke closure |
|
||||
| `convergence` | Pulse loop terminates | Field returns to a stable state within bounded iterations | Bounded — no runaway |
|
||||
| `realizer` | Pack template coverage | Counts realizer hits vs misses on a fixed prompt set | Higher coverage = better |
|
||||
|
||||
---
|
||||
|
||||
## The `articulation` suite (Phase 4)
|
||||
|
||||
The newest and most operator-facing bench. Anchors the post-ADR-0067
|
||||
claim that CORE can:
|
||||
|
||||
- Reach every supported intent shape (DEFINITION / RECALL / CAUSE /
|
||||
VERIFICATION / COMPARISON / CORRECTION / PROCEDURE / NARRATIVE /
|
||||
EXAMPLE) plus the OOV fall-through plus cross-pack chains.
|
||||
- Emit byte-identical surfaces across reruns.
|
||||
- Hold memory roughly flat across hundreds of turns.
|
||||
- Maintain thread context across topic shifts.
|
||||
- Outperform stochastic models on the determinism axis the system is
|
||||
actually designed for.
|
||||
|
||||
### Running it
|
||||
|
||||
```
|
||||
# Quick smoke (no Ollama):
|
||||
core bench --suite articulation --runs 5 --turns 50
|
||||
|
||||
# Full run with Ollama side-by-side (needs `ollama` on PATH + a model):
|
||||
core bench --suite articulation \
|
||||
--runs 10 --turns 200 \
|
||||
--ollama-model llama3:8b --ollama-reruns 3 \
|
||||
--report bench_reports/articulation.json
|
||||
|
||||
# Machine-readable JSON:
|
||||
core bench --suite articulation --json
|
||||
```
|
||||
|
||||
Flags:
|
||||
|
||||
- `--runs N` — Determinism rerun count per prompt. Higher is stricter.
|
||||
- `--turns N` — Footprint sample run length. The bench drives one
|
||||
`ChatRuntime` through `N` cold-start prompts and samples RSS.
|
||||
- `--ollama-model MODEL` — Ollama model id (e.g. `llama3:8b`,
|
||||
`granite3.3:8b`). Omit to skip the side-by-side.
|
||||
- `--ollama-reruns N` — Per-prompt rerun count for Ollama. The bench
|
||||
measures `unique_surfaces / reruns` for each side.
|
||||
- `--report PATH` — Write the full JSON report to disk in addition to
|
||||
printing.
|
||||
|
||||
### Sub-benches
|
||||
|
||||
#### 1. Intent breadth
|
||||
|
||||
Sends one prompt per intent shape (12 prompts total covering 9
|
||||
intents + OOV + 2 cross-pack variants) through fresh `ChatRuntime`
|
||||
instances. Reports the classified intent, the grounding tag, and a
|
||||
surface snippet.
|
||||
|
||||
**Read it like this:**
|
||||
|
||||
- **Good:** Every intent fires; grounding tier matches the prompt
|
||||
(e.g. CAUSE on `knowledge` should route to `teaching`).
|
||||
- **Neutral:** A prompt routes to `vault` instead of `pack`/`teaching`
|
||||
— that's CORE's normal recall path on warm vaults, but the breadth
|
||||
bench uses fresh runtimes so vault hits would indicate a stub
|
||||
injection issue worth investigating.
|
||||
- **Bad:** Any prompt routes to `none` when the breadth set says it
|
||||
shouldn't. Means a pack/teaching path regressed.
|
||||
|
||||
#### 2. Determinism
|
||||
|
||||
Five prompts × N reruns × fresh `ChatRuntime` each time. Counts
|
||||
unique surfaces per prompt.
|
||||
|
||||
**Read it like this:**
|
||||
|
||||
- **Good:** `unique_surfaces == 1` for every prompt. This is the
|
||||
*primary* claim — any failure here is load-bearing.
|
||||
- **Neutral:** `unique_surfaces > 1` only for prompts that route
|
||||
through `vault` (vault recall is content-similar but not
|
||||
byte-stable across compile permutations); reroute the prompt or
|
||||
fix the cold-start path.
|
||||
- **Bad:** `unique_surfaces > 1` on a pack/teaching/cross-pack
|
||||
prompt. That means a supposedly deterministic composer is reading
|
||||
non-deterministic state. Stop and bisect.
|
||||
|
||||
#### 3. Memory footprint
|
||||
|
||||
Single `ChatRuntime`, `turns` prompts (cycling through the breadth
|
||||
set), RSS sampled every `sample_every` turns via psutil.
|
||||
|
||||
**Read it like this:**
|
||||
|
||||
- **Good:** Per-turn ΔRSS in the low tens of KiB. Vault grows
|
||||
bounded with stored states; pack caches are immutable.
|
||||
- **Neutral:** Linear growth on the first ~100 turns as caches warm
|
||||
up, then flat. Inspect the samples list — if it plateaus, healthy.
|
||||
- **Bad:** Per-turn ΔRSS in MiB or growth that does not plateau.
|
||||
Points at unbounded list/dict accumulation; check vault eviction
|
||||
and any `lru_cache(maxsize=None)` introductions.
|
||||
|
||||
#### 4. Cross-topic context
|
||||
|
||||
One runtime with `thread_anaphora=True`. Walks 8 prompts that switch
|
||||
between cognition, relations, and cross-pack subjects. Reports per-
|
||||
turn anaphora-fire status.
|
||||
|
||||
**Read it like this:**
|
||||
|
||||
- **Good:** The state survives across the walk — `thread_context`
|
||||
retains every turn's `TurnSummary`. The bench prints which turns
|
||||
fired anaphora.
|
||||
- **Neutral / expected:** `anaphora_fire_count == 0` once the vault
|
||||
has content. Per ADR-0066 §Future ADRs, anaphora today fires only
|
||||
when BOTH the prior and current turn are pack/teaching tier. After
|
||||
the first turn populates the vault, recall hits the vault and the
|
||||
anaphora prefix is suppressed. This is the architectural ceiling,
|
||||
not a defect.
|
||||
- **Bad:** Exceptions or `grounding_source == "none"` on prompts
|
||||
that should ground. Means an intent route or pack mount broke.
|
||||
|
||||
#### 5. Ollama side-by-side
|
||||
|
||||
Opt-in. Runs three prompts through both CORE and an Ollama model,
|
||||
each `N` times. Reports unique-surface count per side.
|
||||
|
||||
**Read it like this:**
|
||||
|
||||
- **Good (the whole point):** CORE shows `unique_surfaces == 1` for
|
||||
every prompt regardless of rerun count. Ollama shows
|
||||
`unique_surfaces >= 2` on most prompts even with low rerun count
|
||||
because LLMs are stochastic.
|
||||
- **Why this matters:** A user asking the same question twice should
|
||||
get the same answer. CORE guarantees this structurally. LLMs at
|
||||
`temperature=0` come close but still vary because of GPU
|
||||
non-determinism, MoE routing, and sampling on tie-break logits.
|
||||
- **Comparing surfaces:** Don't compare *content quality* between
|
||||
CORE and Ollama. They optimise different objectives:
|
||||
- CORE: traceable, deterministic, every token sourced.
|
||||
- Ollama: fluent, broad, stochastic, no provenance.
|
||||
- **What "fail" looks like:** Ollama is not on PATH → skipped (not
|
||||
failed). Ollama returns `<ollama error: ...>` → that prompt is
|
||||
excluded from the unique-surface count.
|
||||
|
||||
### Comparison caveat
|
||||
|
||||
CORE and Ollama are not running the same task in a fair sense:
|
||||
|
||||
- **Different vocabulary surface area.** Ollama has read most of the
|
||||
internet. CORE has 3 ratified packs (~150 lemmas total). Asking
|
||||
`"What is photosynthesis?"` of CORE produces the OOV invitation by
|
||||
design; asking it of Ollama produces a paragraph. *Neither is
|
||||
wrong* — the systems make different promises.
|
||||
- **Different success criteria.** CORE wins on determinism,
|
||||
provenance, footprint, replayability. Ollama wins on coverage and
|
||||
fluency. The bench measures the axes CORE was designed for.
|
||||
- **Different latency profile.** CORE: single-digit ms. Ollama:
|
||||
hundreds of ms to seconds depending on model. The articulation
|
||||
bench does not time Ollama; the `cost` bench is the place for
|
||||
per-turn timing.
|
||||
|
||||
If a prompt is in CORE's pack vocabulary, the side-by-side is
|
||||
direct: same prompt, identical surface from CORE every run, varying
|
||||
surface from Ollama every run. If a prompt is OOV for CORE, the
|
||||
side-by-side is informational: shows what the gradient does (CORE
|
||||
admits it doesn't know; Ollama hallucinates plausibly).
|
||||
|
||||
---
|
||||
|
||||
## Other suites — quick reference
|
||||
|
||||
### `determinism`
|
||||
|
||||
`core bench --suite determinism --runs 50`
|
||||
|
||||
The original determinism bench: drives the *pulse loop* (not the
|
||||
chat runtime) N times, hashes the full trace, asserts a single
|
||||
unique hash. The articulation suite's determinism sub-bench tests
|
||||
the chat runtime; this one tests the deeper pulse loop.
|
||||
|
||||
### `teaching-loop`
|
||||
|
||||
`core bench --suite teaching-loop --runs 100`
|
||||
|
||||
Anchors ADR-0055..0057. Drives the discovery → proposal → replay →
|
||||
accept loop end-to-end N times. Asserts the resulting active
|
||||
corpus is byte-identical to a deterministic baseline and that the
|
||||
proposal id + chain id are stable across runs.
|
||||
|
||||
### `cost`
|
||||
|
||||
`core bench --suite cost --runs 100`
|
||||
|
||||
Measures wall-seconds, CPU-seconds, throughput, and derives
|
||||
$/1000-turn at a disclosed cloud-instance rate. Reports frontier
|
||||
LLM per-token pricing context. Energy/joules is *not* reported
|
||||
because honest measurement requires privileged RAPL/IOKit access.
|
||||
|
||||
### `latency` / `speedup` / `versor` / `convergence` / `realizer`
|
||||
|
||||
These are the original benches in `benchmarks/run_benchmarks.py` —
|
||||
each runs a single measurement and emits a `BenchResult(passed,
|
||||
metric, unit, detail)`. Run them when investigating regressions in
|
||||
the specific axis named.
|
||||
|
||||
---
|
||||
|
||||
## Operator workflow
|
||||
|
||||
1. **After any non-trivial change**, run:
|
||||
```
|
||||
core test --suite cognition -q
|
||||
core eval cognition
|
||||
core bench --suite articulation --runs 5 --turns 50
|
||||
```
|
||||
2. **Before merging a PR that touches surface composers, runtime,
|
||||
packs, or teaching corpora**, also run:
|
||||
```
|
||||
core bench --suite teaching-loop --runs 50
|
||||
core bench --suite articulation --runs 20 --turns 200 \
|
||||
--ollama-model llama3:8b --report bench_reports/<branch>.json
|
||||
```
|
||||
3. **When investigating a regression**, run the targeted suite:
|
||||
- Determinism breakage → `--suite determinism --runs 50`
|
||||
- Memory growth → `--suite articulation --turns 1000`
|
||||
- Versor closure error → `--suite versor`
|
||||
- Pack template miss → `--suite realizer`
|
||||
|
||||
The bench reports go under `bench_reports/` (gitignored by
|
||||
default). Include the JSON report path in the PR description when
|
||||
the change is bench-relevant.
|
||||
|
||||
---
|
||||
|
||||
## What the benchmarks intentionally do NOT do
|
||||
|
||||
- **Score linguistic quality.** Fluency, helpfulness, "naturalness"
|
||||
— none of these are CORE's optimisation target. The system
|
||||
optimises for determinism, provenance, and bounded footprint.
|
||||
- **Report fabricated joules.** Energy measurement requires
|
||||
privileged RAPL/IOKit access we don't have in a plain Python
|
||||
process. `cost.cpu_seconds` is the honest proxy.
|
||||
- **Compare against LLMs on tasks LLMs are designed for.** A bench
|
||||
that asks "who writes the better essay?" is the wrong question
|
||||
for CORE. The Ollama side-by-side measures the *one axis where
|
||||
the comparison is meaningful*: same input → same output.
|
||||
- **Bench-game CORE.** Every prompt set is in-tree; modifications
|
||||
show up in PRs; no "private eval set" trick.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new bench
|
||||
|
||||
1. Drop a module in `benchmarks/<name>.py` with a `run_<name>()`
|
||||
entrypoint returning a dataclass `Report` with `as_dict()` and
|
||||
`summary()` methods.
|
||||
2. Add a dispatch branch in `core/cli.py:cmd_bench`.
|
||||
3. Add the suite name to `bench.add_argument("--suite", choices=...)`.
|
||||
4. Add a row to the catalogue table at the top of this README.
|
||||
5. Add tests under `tests/test_<name>_bench.py` that pin the report
|
||||
shape (not the runtime behaviour — that's covered by lanes).
|
||||
512
benchmarks/articulation.py
Normal file
512
benchmarks/articulation.py
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
"""Articulation benchmark suite — Phase 4 capability proof.
|
||||
|
||||
Anchors the post-Phase-4 claim set in numbers rather than rhetoric.
|
||||
|
||||
Sub-benches:
|
||||
|
||||
1. **breadth** — Fires every supported intent shape (9 today:
|
||||
DEFINITION / RECALL / CAUSE / VERIFICATION / COMPARISON /
|
||||
CORRECTION / PROCEDURE / NARRATIVE / EXAMPLE) plus the OOV
|
||||
fall-through and the cross-pack chain shape. Reports the
|
||||
``grounding_source`` and a snippet of the surface for each.
|
||||
|
||||
2. **determinism** — Runs the same prompt set N times in fresh
|
||||
``ChatRuntime`` instances and asserts byte-identical surfaces
|
||||
across every run. The whole *premise* of CORE is that the
|
||||
surface is reconstructed from immutable corpora + ratified
|
||||
packs, so any drift here is a load-bearing defect.
|
||||
|
||||
3. **footprint** — Drives ``ChatRuntime`` through ``turns`` cold-
|
||||
start prompts and samples RSS (psutil) every K turns. Reports
|
||||
start RSS / peak RSS / end RSS / per-turn delta. Catches
|
||||
unbounded cache growth or pack-reload leaks.
|
||||
|
||||
4. **cross-topic** — Mounts a single ``ChatRuntime`` with
|
||||
``thread_anaphora=True`` and walks a multi-topic prompt
|
||||
sequence that crosses cognition + relations + cross-pack
|
||||
subjects. Reports the count of turns where the anaphora
|
||||
prefix fired and which thread positions it referenced — the
|
||||
concrete signal that turn-level composition is doing real work.
|
||||
|
||||
5. **ollama-compare** — Opt-in side-by-side. Sends a fixed prompt
|
||||
set to (a) ``ChatRuntime`` and (b) a local Ollama model.
|
||||
Reports both surfaces verbatim and a determinism-delta: CORE
|
||||
emits byte-identical surface on N reruns; Ollama emits
|
||||
``unique_surfaces > 1`` even with ``temperature=0`` on most
|
||||
prompts. Skipped (status: ``skipped`` instead of ``failed``)
|
||||
when the ``ollama`` binary is not on ``PATH``.
|
||||
|
||||
The whole suite is deterministic on the CORE side — no clock-time
|
||||
or RNG influence on what gets emitted. Walltime sampling lives in
|
||||
``benchmarks.cost``; this module focuses on capability + identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# Curated prompt set — every intent shape + OOV + cross-pack.
|
||||
INTENT_PROBE_PROMPTS: tuple[tuple[str, str], ...] = (
|
||||
("DEFINITION", "What is knowledge?"),
|
||||
("RECALL", "Recall truth."),
|
||||
("CAUSE", "Why does knowledge exist?"),
|
||||
("VERIFICATION", "Does memory require recall?"),
|
||||
("COMPARISON", "Compare knowledge and wisdom."),
|
||||
("CORRECTION", "No, that's wrong."),
|
||||
("PROCEDURE", "How do I define a concept?"),
|
||||
("NARRATIVE", "Tell me about truth."),
|
||||
("EXAMPLE", "Give me an example of knowledge."),
|
||||
("OOV_FALLBACK", "What is photosynthesis?"),
|
||||
("CROSS_PACK_VERIFICATION", "Does identity require family?"),
|
||||
("CROSS_PACK_CAUSE", "Why does understanding exist?"),
|
||||
)
|
||||
|
||||
# Cross-topic walk — exercises thread anaphora across cognition,
|
||||
# relations, and cross-pack subjects.
|
||||
CROSS_TOPIC_PROMPTS: tuple[str, ...] = (
|
||||
"Why does light exist?", # CAUSE — light
|
||||
"What is truth?", # DEFINITION — truth (light's object)
|
||||
"Why does knowledge exist?", # CAUSE — knowledge
|
||||
"Tell me about family.", # NARRATIVE — family (relations)
|
||||
"Does identity require family?", # VERIFICATION — cross-pack
|
||||
"What is parent?", # DEFINITION — relations
|
||||
"Give me an example of memory.", # EXAMPLE
|
||||
"Compare truth and knowledge.", # COMPARISON
|
||||
)
|
||||
|
||||
# Determinism rerun set — short prompts that exercise every grounding
|
||||
# tier we care about.
|
||||
DETERMINISM_PROMPTS: tuple[str, ...] = (
|
||||
"What is truth?",
|
||||
"Why does knowledge exist?",
|
||||
"Tell me about family.",
|
||||
"Does identity require family?",
|
||||
"Give me an example of memory.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Report shapes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IntentProbe:
|
||||
label: str
|
||||
prompt: str
|
||||
intent_tag: str
|
||||
grounding_source: str
|
||||
surface_snippet: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeterminismCase:
|
||||
prompt: str
|
||||
runs: int
|
||||
unique_surfaces: int
|
||||
sample: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FootprintSample:
|
||||
turn: int
|
||||
rss_bytes: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CrossTopicTurn:
|
||||
turn: int
|
||||
prompt: str
|
||||
intent_tag: str
|
||||
grounding_source: str
|
||||
anaphora_fired: bool
|
||||
surface_snippet: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OllamaPair:
|
||||
prompt: str
|
||||
core_surface: str
|
||||
core_unique_surfaces_on_5_reruns: int
|
||||
ollama_surface: str
|
||||
ollama_unique_surfaces_on_5_reruns: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArticulationReport:
|
||||
breadth: list[IntentProbe] = field(default_factory=list)
|
||||
determinism: list[DeterminismCase] = field(default_factory=list)
|
||||
determinism_all_identical: bool = True
|
||||
footprint: list[FootprintSample] = field(default_factory=list)
|
||||
footprint_start_bytes: int = 0
|
||||
footprint_peak_bytes: int = 0
|
||||
footprint_end_bytes: int = 0
|
||||
footprint_per_turn_delta_bytes: float = 0.0
|
||||
cross_topic: list[CrossTopicTurn] = field(default_factory=list)
|
||||
anaphora_fire_count: int = 0
|
||||
ollama: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"breadth": [p.__dict__ for p in self.breadth],
|
||||
"determinism": [c.__dict__ for c in self.determinism],
|
||||
"determinism_all_identical": self.determinism_all_identical,
|
||||
"footprint_samples": [s.__dict__ for s in self.footprint],
|
||||
"footprint_start_bytes": self.footprint_start_bytes,
|
||||
"footprint_peak_bytes": self.footprint_peak_bytes,
|
||||
"footprint_end_bytes": self.footprint_end_bytes,
|
||||
"footprint_per_turn_delta_bytes": round(
|
||||
self.footprint_per_turn_delta_bytes, 2
|
||||
),
|
||||
"cross_topic": [t.__dict__ for t in self.cross_topic],
|
||||
"anaphora_fire_count": self.anaphora_fire_count,
|
||||
"ollama": self.ollama,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sub-benches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _snippet(s: str, n: int = 120) -> str:
|
||||
s = " ".join(s.split())
|
||||
return s if len(s) <= n else s[: n - 1] + "…"
|
||||
|
||||
|
||||
def _classify_prompt(prompt: str) -> str:
|
||||
"""Re-derive the intent label from the prompt text for the report.
|
||||
|
||||
``ChatResponse`` does not surface the classified ``IntentTag`` — it
|
||||
is internal to the turn loop. Recomputing on the same text is
|
||||
deterministic and pack-free; safe for benchmark labelling.
|
||||
"""
|
||||
from generate.intent import classify_intent
|
||||
try:
|
||||
intent = classify_intent(prompt)
|
||||
return intent.tag.name
|
||||
except Exception:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def bench_breadth() -> list[IntentProbe]:
|
||||
from chat.runtime import ChatRuntime
|
||||
out: list[IntentProbe] = []
|
||||
for label, prompt in INTENT_PROBE_PROMPTS:
|
||||
rt = ChatRuntime()
|
||||
resp = rt.chat(prompt)
|
||||
out.append(IntentProbe(
|
||||
label=label,
|
||||
prompt=prompt,
|
||||
intent_tag=_classify_prompt(prompt),
|
||||
grounding_source=getattr(resp, "grounding_source", "unknown"),
|
||||
surface_snippet=_snippet(resp.surface),
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def bench_determinism(runs: int = 20) -> tuple[list[DeterminismCase], bool]:
|
||||
from chat.runtime import ChatRuntime
|
||||
cases: list[DeterminismCase] = []
|
||||
all_identical = True
|
||||
for prompt in DETERMINISM_PROMPTS:
|
||||
seen: set[str] = set()
|
||||
sample = ""
|
||||
for _ in range(runs):
|
||||
rt = ChatRuntime()
|
||||
resp = rt.chat(prompt)
|
||||
seen.add(resp.surface)
|
||||
if not sample:
|
||||
sample = resp.surface
|
||||
unique = len(seen)
|
||||
cases.append(DeterminismCase(
|
||||
prompt=prompt, runs=runs, unique_surfaces=unique,
|
||||
sample=_snippet(sample),
|
||||
))
|
||||
if unique != 1:
|
||||
all_identical = False
|
||||
return cases, all_identical
|
||||
|
||||
|
||||
def bench_footprint(
|
||||
turns: int = 200,
|
||||
sample_every: int = 25,
|
||||
) -> tuple[list[FootprintSample], int, int, int, float]:
|
||||
"""Drive a single ChatRuntime through ``turns`` cold-start prompts
|
||||
and sample RSS every ``sample_every`` turns.
|
||||
|
||||
Uses a single runtime so the bench measures cache/vault growth,
|
||||
not per-process startup overhead.
|
||||
"""
|
||||
import psutil
|
||||
from chat.runtime import ChatRuntime
|
||||
|
||||
proc = psutil.Process()
|
||||
rt = ChatRuntime()
|
||||
|
||||
samples: list[FootprintSample] = []
|
||||
start = proc.memory_info().rss
|
||||
samples.append(FootprintSample(turn=0, rss_bytes=start))
|
||||
peak = start
|
||||
prompts = [p for _, p in INTENT_PROBE_PROMPTS]
|
||||
n = len(prompts)
|
||||
for t in range(1, turns + 1):
|
||||
rt.chat(prompts[t % n])
|
||||
if t % sample_every == 0 or t == turns:
|
||||
rss = proc.memory_info().rss
|
||||
samples.append(FootprintSample(turn=t, rss_bytes=rss))
|
||||
peak = max(peak, rss)
|
||||
end = samples[-1].rss_bytes
|
||||
per_turn = (end - start) / max(turns, 1)
|
||||
return samples, start, peak, end, per_turn
|
||||
|
||||
|
||||
def bench_cross_topic() -> tuple[list[CrossTopicTurn], int]:
|
||||
"""Walk the CROSS_TOPIC_PROMPTS list on ONE runtime with
|
||||
``thread_anaphora=True`` and report which turns fired the
|
||||
anaphora prefix.
|
||||
"""
|
||||
from chat.runtime import ChatRuntime
|
||||
from core.config import RuntimeConfig
|
||||
|
||||
rt = ChatRuntime(config=RuntimeConfig(thread_anaphora=True))
|
||||
out: list[CrossTopicTurn] = []
|
||||
fires = 0
|
||||
for i, prompt in enumerate(CROSS_TOPIC_PROMPTS):
|
||||
resp = rt.chat(prompt)
|
||||
# Anaphora prefix has the shape ``(Recalling turn N: ...)``.
|
||||
fired = resp.surface.startswith("(Recalling turn")
|
||||
if fired:
|
||||
fires += 1
|
||||
out.append(CrossTopicTurn(
|
||||
turn=i,
|
||||
prompt=prompt,
|
||||
intent_tag=_classify_prompt(prompt),
|
||||
grounding_source=getattr(resp, "grounding_source", "unknown"),
|
||||
anaphora_fired=fired,
|
||||
surface_snippet=_snippet(resp.surface),
|
||||
))
|
||||
return out, fires
|
||||
|
||||
|
||||
def _have_ollama() -> bool:
|
||||
return shutil.which("ollama") is not None
|
||||
|
||||
|
||||
def _ollama_complete(model: str, prompt: str, timeout: float = 30.0) -> str:
|
||||
"""Single completion via ``ollama run`` — deterministic-as-possible
|
||||
(seed pinned, ``num_predict`` capped). Returns stdout text or an
|
||||
error placeholder; never raises.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ollama", "run", model, "--", prompt],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout.strip() or result.stderr.strip()
|
||||
except (subprocess.TimeoutExpired, OSError) as exc:
|
||||
return f"<ollama error: {exc}>"
|
||||
|
||||
|
||||
def bench_ollama_compare(
|
||||
model: str | None = None,
|
||||
prompts: Iterable[str] = DETERMINISM_PROMPTS,
|
||||
core_reruns: int = 5,
|
||||
ollama_reruns: int = 5,
|
||||
) -> dict[str, Any]:
|
||||
"""Side-by-side: CORE vs Ollama on a fixed prompt set.
|
||||
|
||||
Returns a dict with ``status`` ∈ {``ran``, ``skipped``}, and on
|
||||
``ran`` includes per-prompt CORE+Ollama surfaces plus a
|
||||
determinism count for each (unique surfaces across N reruns).
|
||||
"""
|
||||
if not _have_ollama() or model is None:
|
||||
return {
|
||||
"status": "skipped",
|
||||
"reason": (
|
||||
"ollama binary not on PATH" if not _have_ollama()
|
||||
else "no model specified"
|
||||
),
|
||||
}
|
||||
|
||||
from chat.runtime import ChatRuntime
|
||||
pairs: list[OllamaPair] = []
|
||||
for prompt in prompts:
|
||||
# CORE: rerun N times, count unique surfaces.
|
||||
core_seen: set[str] = set()
|
||||
core_sample = ""
|
||||
for _ in range(core_reruns):
|
||||
rt = ChatRuntime()
|
||||
r = rt.chat(prompt)
|
||||
core_seen.add(r.surface)
|
||||
if not core_sample:
|
||||
core_sample = r.surface
|
||||
# Ollama: rerun N times, count unique surfaces.
|
||||
ollama_seen: set[str] = set()
|
||||
ollama_sample = ""
|
||||
for _ in range(ollama_reruns):
|
||||
txt = _ollama_complete(model, prompt)
|
||||
ollama_seen.add(txt)
|
||||
if not ollama_sample:
|
||||
ollama_sample = txt
|
||||
pairs.append(OllamaPair(
|
||||
prompt=prompt,
|
||||
core_surface=_snippet(core_sample, n=240),
|
||||
core_unique_surfaces_on_5_reruns=len(core_seen),
|
||||
ollama_surface=_snippet(ollama_sample, n=240),
|
||||
ollama_unique_surfaces_on_5_reruns=len(ollama_seen),
|
||||
))
|
||||
return {
|
||||
"status": "ran",
|
||||
"model": model,
|
||||
"core_reruns": core_reruns,
|
||||
"ollama_reruns": ollama_reruns,
|
||||
"pairs": [p.__dict__ for p in pairs],
|
||||
"core_byte_identical_on_every_prompt": all(
|
||||
p.core_unique_surfaces_on_5_reruns == 1 for p in pairs
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Orchestrator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def run_articulation_suite(
|
||||
*,
|
||||
determinism_runs: int = 20,
|
||||
footprint_turns: int = 200,
|
||||
footprint_sample_every: int = 25,
|
||||
ollama_model: str | None = None,
|
||||
ollama_core_reruns: int = 5,
|
||||
ollama_reruns: int = 3,
|
||||
) -> ArticulationReport:
|
||||
"""Run every sub-bench and return the consolidated report."""
|
||||
report = ArticulationReport()
|
||||
|
||||
report.breadth = bench_breadth()
|
||||
det_cases, det_ok = bench_determinism(runs=determinism_runs)
|
||||
report.determinism = det_cases
|
||||
report.determinism_all_identical = det_ok
|
||||
(
|
||||
samples, start, peak, end, per_turn,
|
||||
) = bench_footprint(
|
||||
turns=footprint_turns, sample_every=footprint_sample_every,
|
||||
)
|
||||
report.footprint = samples
|
||||
report.footprint_start_bytes = start
|
||||
report.footprint_peak_bytes = peak
|
||||
report.footprint_end_bytes = end
|
||||
report.footprint_per_turn_delta_bytes = per_turn
|
||||
ct_turns, ct_fires = bench_cross_topic()
|
||||
report.cross_topic = ct_turns
|
||||
report.anaphora_fire_count = ct_fires
|
||||
report.ollama = bench_ollama_compare(
|
||||
model=ollama_model,
|
||||
prompts=DETERMINISM_PROMPTS[:3], # subset — ollama is slow
|
||||
core_reruns=ollama_core_reruns,
|
||||
ollama_reruns=ollama_reruns,
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def format_summary(report: ArticulationReport) -> str:
|
||||
out: list[str] = []
|
||||
out.append("=" * 76)
|
||||
out.append("Articulation benchmark suite")
|
||||
out.append("=" * 76)
|
||||
out.append("")
|
||||
out.append("[1/5] Intent breadth — every supported intent shape:")
|
||||
for p in report.breadth:
|
||||
out.append(
|
||||
f" {p.label:30s} {p.intent_tag:14s} {p.grounding_source:9s} "
|
||||
f"{_snippet(p.surface_snippet, 80)}"
|
||||
)
|
||||
out.append("")
|
||||
out.append("[2/5] Determinism — same prompt → byte-identical surface:")
|
||||
for c in report.determinism:
|
||||
flag = "OK" if c.unique_surfaces == 1 else "FAIL"
|
||||
out.append(
|
||||
f" [{flag}] {c.runs} runs / {c.unique_surfaces} unique surface(s) "
|
||||
f"{_snippet(c.prompt, 50)}"
|
||||
)
|
||||
out.append(
|
||||
f" all_identical = {report.determinism_all_identical}"
|
||||
)
|
||||
out.append("")
|
||||
out.append("[3/5] Memory footprint — single runtime, repeated turns:")
|
||||
if report.footprint:
|
||||
out.append(
|
||||
f" start = {report.footprint_start_bytes / 1024 / 1024:.1f} MiB "
|
||||
f"peak = {report.footprint_peak_bytes / 1024 / 1024:.1f} MiB "
|
||||
f"end = {report.footprint_end_bytes / 1024 / 1024:.1f} MiB"
|
||||
)
|
||||
out.append(
|
||||
f" per-turn ΔRSS = "
|
||||
f"{report.footprint_per_turn_delta_bytes / 1024:.2f} KiB"
|
||||
)
|
||||
out.append("")
|
||||
out.append("[4/5] Cross-topic context — thread anaphora across subjects:")
|
||||
for t in report.cross_topic:
|
||||
marker = "↩" if t.anaphora_fired else " "
|
||||
out.append(
|
||||
f" {marker} turn {t.turn} [{t.intent_tag:12s} {t.grounding_source:9s}]"
|
||||
f" {_snippet(t.prompt, 40)}"
|
||||
)
|
||||
out.append(f" anaphora fired on {report.anaphora_fire_count} turn(s)")
|
||||
out.append(
|
||||
" note: thread anaphora today fires only when BOTH the prior and current "
|
||||
"turn are pack/teaching tier (ADR-0066 §Future ADRs). After the first "
|
||||
"turn populates the vault, subsequent turns recall from vault and the "
|
||||
"anaphora prefix is suppressed. This bench measures both thread-context "
|
||||
"retention (state survives across topic shifts) and the current anaphora "
|
||||
"fire rate (which is the architectural ceiling, not a defect)."
|
||||
)
|
||||
out.append("")
|
||||
out.append("[5/5] Ollama side-by-side:")
|
||||
status = report.ollama.get("status", "skipped")
|
||||
if status == "skipped":
|
||||
out.append(f" skipped — {report.ollama.get('reason', '')}")
|
||||
else:
|
||||
out.append(
|
||||
f" model = {report.ollama['model']} "
|
||||
f"core_byte_identical_on_every_prompt = "
|
||||
f"{report.ollama['core_byte_identical_on_every_prompt']}"
|
||||
)
|
||||
for pair in report.ollama["pairs"]:
|
||||
out.append("")
|
||||
out.append(f" prompt: {pair['prompt']}")
|
||||
out.append(
|
||||
f" CORE [{pair['core_unique_surfaces_on_5_reruns']} unique] "
|
||||
f"{_snippet(pair['core_surface'], 200)}"
|
||||
)
|
||||
out.append(
|
||||
f" ollama [{pair['ollama_unique_surfaces_on_5_reruns']} unique] "
|
||||
f"{_snippet(pair['ollama_surface'], 200)}"
|
||||
)
|
||||
out.append("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ArticulationReport",
|
||||
"INTENT_PROBE_PROMPTS",
|
||||
"CROSS_TOPIC_PROMPTS",
|
||||
"DETERMINISM_PROMPTS",
|
||||
"bench_breadth",
|
||||
"bench_determinism",
|
||||
"bench_footprint",
|
||||
"bench_cross_topic",
|
||||
"bench_ollama_compare",
|
||||
"run_articulation_suite",
|
||||
"format_summary",
|
||||
]
|
||||
156
core/cli.py
156
core/cli.py
|
|
@ -942,20 +942,49 @@ def cmd_teaching_supersede(args: argparse.Namespace) -> int:
|
|||
from chat.teaching_grounding import _CORPUS_PATH
|
||||
from teaching.supersede import SupersessionError, supersede_chain
|
||||
|
||||
try:
|
||||
new_chain_id = supersede_chain(
|
||||
old_chain_id=args.old_chain_id,
|
||||
subject=args.subject,
|
||||
intent=args.intent,
|
||||
connective=args.connective,
|
||||
object_=args.object,
|
||||
review_date=args.review_date,
|
||||
corpus_path=_CORPUS_PATH,
|
||||
operator_note=args.note,
|
||||
new_chain_id=args.new_chain_id,
|
||||
)
|
||||
except SupersessionError as exc:
|
||||
_die(str(exc), code=1)
|
||||
cross_pack = bool(getattr(args, "cross_pack", False))
|
||||
subj_pack = (getattr(args, "subject_pack_id", "") or "").strip()
|
||||
obj_pack = (getattr(args, "object_pack_id", "") or "").strip()
|
||||
|
||||
if cross_pack or subj_pack or obj_pack:
|
||||
# ADR-0067 — cross-pack supersede. Both pack ids are required
|
||||
# when any cross-pack flag is set.
|
||||
if not subj_pack or not obj_pack:
|
||||
_die(
|
||||
"cross-pack supersede requires --subject-pack-id and "
|
||||
"--object-pack-id",
|
||||
code=2,
|
||||
)
|
||||
from teaching.cross_pack_supersede import supersede_cross_pack_chain
|
||||
try:
|
||||
new_chain_id = supersede_cross_pack_chain(
|
||||
old_chain_id=args.old_chain_id,
|
||||
subject=args.subject,
|
||||
intent=args.intent,
|
||||
connective=args.connective,
|
||||
object_=args.object,
|
||||
subject_pack_id=subj_pack,
|
||||
object_pack_id=obj_pack,
|
||||
review_date=args.review_date,
|
||||
new_chain_id=args.new_chain_id,
|
||||
)
|
||||
except SupersessionError as exc:
|
||||
_die(str(exc), code=1)
|
||||
else:
|
||||
try:
|
||||
new_chain_id = supersede_chain(
|
||||
old_chain_id=args.old_chain_id,
|
||||
subject=args.subject,
|
||||
intent=args.intent,
|
||||
connective=args.connective,
|
||||
object_=args.object,
|
||||
review_date=args.review_date,
|
||||
corpus_path=_CORPUS_PATH,
|
||||
operator_note=args.note,
|
||||
new_chain_id=args.new_chain_id,
|
||||
)
|
||||
except SupersessionError as exc:
|
||||
_die(str(exc), code=1)
|
||||
|
||||
print(f"superseded : {args.old_chain_id}")
|
||||
print(f"new chain_id : {new_chain_id}")
|
||||
|
|
@ -1601,6 +1630,51 @@ Usage:
|
|||
"""
|
||||
|
||||
|
||||
_ARTICULATION_BENCH_PREAMBLE = """
|
||||
================================================================================
|
||||
Articulation Benchmark Suite (Phase 4 capability proof)
|
||||
================================================================================
|
||||
|
||||
Reference: benchmarks/articulation.py + benchmarks/README.md.
|
||||
|
||||
Anchors the post-ADR-0067 claim set in numbers:
|
||||
|
||||
[1] Intent breadth — every supported intent shape fires (9 + OOV
|
||||
+ cross-pack), grounding tier matches prompt.
|
||||
[2] Determinism — same prompt → byte-identical surface across
|
||||
N reruns (fresh ChatRuntime each time).
|
||||
[3] Memory footprint — single runtime, T cold-start prompts, RSS
|
||||
sampled via psutil; per-turn ΔRSS reported.
|
||||
[4] Cross-topic context — opt-in thread_anaphora; walks 8 prompts
|
||||
across cognition + relations + cross-pack.
|
||||
[5] Ollama side-by-side — same prompts on CORE + a local Ollama
|
||||
model; CORE unique=1 every prompt, Ollama
|
||||
shows the stochastic delta.
|
||||
|
||||
Read it like this:
|
||||
|
||||
GOOD — determinism_all_identical=True, per-turn ΔRSS in KiB, every
|
||||
intent grounds, Ollama unique>1 on most prompts.
|
||||
NEUTRAL — anaphora_fire_count=0 after first turn (architectural
|
||||
ceiling per ADR-0066 §Future ADRs; see README §3.4).
|
||||
BAD — determinism failure on pack/teaching path, per-turn ΔRSS
|
||||
in MiB, any intent routes to ``none`` it shouldn't.
|
||||
|
||||
Comparison caveat:
|
||||
CORE and Ollama optimise different objectives. CORE: traceable,
|
||||
deterministic, every token sourced. Ollama: fluent, broad,
|
||||
stochastic, no provenance. The bench measures the axes CORE was
|
||||
designed for; it does NOT score linguistic quality.
|
||||
|
||||
Usage:
|
||||
core bench --suite articulation # quick
|
||||
core bench --suite articulation --runs 20 --turns 200
|
||||
core bench --suite articulation --ollama-model llama3:8b # full
|
||||
core bench --suite articulation --json --report report.json
|
||||
================================================================================
|
||||
"""
|
||||
|
||||
|
||||
_ALL_PREAMBLE = """
|
||||
================================================================================
|
||||
Combined Demo — Full ADR-0024 Chain Evidence
|
||||
|
|
@ -1917,6 +1991,32 @@ def cmd_bench(args: argparse.Namespace) -> int:
|
|||
write_report(report)
|
||||
return 0
|
||||
|
||||
if args.suite == "articulation":
|
||||
from benchmarks.articulation import (
|
||||
format_summary,
|
||||
run_articulation_suite,
|
||||
)
|
||||
if not args.json:
|
||||
_print_preamble(_ARTICULATION_BENCH_PREAMBLE)
|
||||
a_report = run_articulation_suite(
|
||||
determinism_runs=args.runs,
|
||||
footprint_turns=getattr(args, "turns", 200),
|
||||
ollama_model=getattr(args, "ollama_model", None),
|
||||
ollama_reruns=getattr(args, "ollama_reruns", 3),
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps(a_report.as_dict(), ensure_ascii=False, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(format_summary(a_report))
|
||||
if args.report:
|
||||
report_path = Path(args.report)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(
|
||||
json.dumps(a_report.as_dict(), ensure_ascii=False, indent=2)
|
||||
)
|
||||
print(f"report written: {report_path}")
|
||||
return 0
|
||||
|
||||
from benchmarks.run_benchmarks import run_benchmarks
|
||||
|
||||
if args.suite == "teaching-loop" and not args.json:
|
||||
|
|
@ -2248,6 +2348,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
teaching_supersede.add_argument(
|
||||
"--review-date", required=True, help="YYYY-MM-DD",
|
||||
)
|
||||
teaching_supersede.add_argument(
|
||||
"--cross-pack", action="store_true",
|
||||
help="ADR-0067 — target the cross-pack corpus instead of in-pack",
|
||||
)
|
||||
teaching_supersede.add_argument(
|
||||
"--subject-pack-id", default="",
|
||||
help="cross-pack only: subject lemma's resident pack id",
|
||||
)
|
||||
teaching_supersede.add_argument(
|
||||
"--object-pack-id", default="",
|
||||
help="cross-pack only: object lemma's resident pack id",
|
||||
)
|
||||
teaching_supersede.add_argument("--note", default="", help="operator note")
|
||||
teaching_supersede.add_argument(
|
||||
"--new-chain-id", default=None,
|
||||
|
|
@ -2297,11 +2409,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
help="run benchmark harness (determinism, latency, speedup, versor audit)",
|
||||
description="run benchmark harness",
|
||||
)
|
||||
bench.add_argument("--suite", choices=["determinism", "latency", "speedup", "versor", "convergence", "realizer", "cost", "teaching-loop"],
|
||||
bench.add_argument("--suite", choices=["determinism", "latency", "speedup", "versor", "convergence", "realizer", "cost", "teaching-loop", "articulation"],
|
||||
help="run a specific benchmark suite")
|
||||
bench.add_argument("--runs", type=int, default=20, metavar="N", help="run count for determinism benchmark (also turns count for cost suite)")
|
||||
bench.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
||||
bench.add_argument("--report", metavar="PATH", help="write JSON report to file")
|
||||
bench.add_argument(
|
||||
"--turns", type=int, default=200, metavar="N",
|
||||
help="articulation suite: footprint sample count (default 200)",
|
||||
)
|
||||
bench.add_argument(
|
||||
"--ollama-model", default=None, metavar="MODEL",
|
||||
help="articulation suite: ollama model id to compare against "
|
||||
"(e.g. llama3:8b); omit to skip the Ollama sub-bench",
|
||||
)
|
||||
bench.add_argument(
|
||||
"--ollama-reruns", type=int, default=3, metavar="N",
|
||||
help="articulation suite: per-prompt rerun count for ollama "
|
||||
"(higher = better unique-surface measurement; default 3)",
|
||||
)
|
||||
bench.set_defaults(func=cmd_bench)
|
||||
|
||||
demo = subparsers.add_parser(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ dependencies = [
|
|||
"pytest>=9.0.3",
|
||||
"pytest-asyncio>=1.3.0",
|
||||
"pytest-xdist>=3.6",
|
||||
"psutil>=7.0",
|
||||
"pyyaml>=6.0",
|
||||
"ruff>=0.15.12",
|
||||
]
|
||||
|
|
|
|||
206
teaching/cross_pack_supersede.py
Normal file
206
teaching/cross_pack_supersede.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""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"]
|
||||
149
tests/test_articulation_bench.py
Normal file
149
tests/test_articulation_bench.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""Smoke + contract tests for the articulation benchmark suite.
|
||||
|
||||
These are tests for the **bench itself** — not the underlying runtime
|
||||
behaviour, which is exercised by the cognition lane. The bench is
|
||||
load-bearing for the post-Phase-4 capability claims, so each sub-
|
||||
bench gets a focused test that pins the shape of its report.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from benchmarks.articulation import (
|
||||
INTENT_PROBE_PROMPTS,
|
||||
CROSS_TOPIC_PROMPTS,
|
||||
bench_breadth,
|
||||
bench_cross_topic,
|
||||
bench_determinism,
|
||||
bench_footprint,
|
||||
bench_ollama_compare,
|
||||
run_articulation_suite,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Breadth
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def breadth_report():
|
||||
return bench_breadth()
|
||||
|
||||
|
||||
def test_breadth_covers_every_supported_intent_shape(breadth_report) -> None:
|
||||
labels = [p.label for p in breadth_report]
|
||||
expected = [label for label, _ in INTENT_PROBE_PROMPTS]
|
||||
assert labels == expected
|
||||
|
||||
|
||||
def test_breadth_emits_per_prompt_grounding_tag(breadth_report) -> None:
|
||||
for p in breadth_report:
|
||||
assert p.grounding_source in {
|
||||
"vault", "teaching", "pack", "partial", "oov", "none",
|
||||
}
|
||||
|
||||
|
||||
def test_breadth_oov_prompt_routes_oov(breadth_report) -> None:
|
||||
oov = next(p for p in breadth_report if p.label == "OOV_FALLBACK")
|
||||
assert oov.grounding_source == "oov"
|
||||
# The OOV invitation always names the unfamiliar token; the
|
||||
# ``PackMutationProposal`` callout follows but may be truncated
|
||||
# by the snippet limit.
|
||||
assert "photosynthesis" in oov.surface_snippet
|
||||
assert "haven't learned" in oov.surface_snippet
|
||||
|
||||
|
||||
def test_breadth_cross_pack_verification_routes_teaching(breadth_report) -> None:
|
||||
cp = next(
|
||||
p for p in breadth_report
|
||||
if p.label == "CROSS_PACK_VERIFICATION"
|
||||
)
|
||||
assert cp.grounding_source == "teaching"
|
||||
assert "cross-pack-grounded" in cp.surface_snippet
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Determinism
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_determinism_byte_identical_across_runs() -> None:
|
||||
cases, all_identical = bench_determinism(runs=5)
|
||||
assert all_identical is True
|
||||
for c in cases:
|
||||
assert c.unique_surfaces == 1, (
|
||||
f"prompt {c.prompt!r} produced {c.unique_surfaces} unique "
|
||||
f"surfaces across {c.runs} runs"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Footprint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_footprint_emits_samples_and_bounds() -> None:
|
||||
pytest.importorskip("psutil")
|
||||
samples, start, peak, end, per_turn = bench_footprint(
|
||||
turns=20, sample_every=10,
|
||||
)
|
||||
assert len(samples) >= 2 # start + at least one mid/end sample
|
||||
assert peak >= start
|
||||
assert end >= 0
|
||||
# Per-turn ΔRSS must be a small number; if it's huge we have a leak.
|
||||
# 1 MiB / turn is a hard ceiling for the smoke test.
|
||||
assert abs(per_turn) < 1_048_576, (
|
||||
f"per-turn ΔRSS too large ({per_turn} bytes); possible leak"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cross-topic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_cross_topic_visits_every_prompt() -> None:
|
||||
turns, _fires = bench_cross_topic()
|
||||
assert len(turns) == len(CROSS_TOPIC_PROMPTS)
|
||||
for i, t in enumerate(turns):
|
||||
assert t.turn == i
|
||||
assert t.prompt == CROSS_TOPIC_PROMPTS[i]
|
||||
# Every cross-topic turn either grounds via a recognised tier
|
||||
# or returns ``none`` — never a raw exception escape.
|
||||
assert t.grounding_source in {
|
||||
"vault", "teaching", "pack", "partial", "oov", "none",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ollama (skipped when binary absent)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ollama_compare_skips_cleanly_when_no_model_specified() -> None:
|
||||
"""Calling without ``model`` argument is the documented opt-out."""
|
||||
result = bench_ollama_compare(model=None)
|
||||
assert result["status"] == "skipped"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Orchestrator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_run_articulation_suite_emits_shaped_report() -> None:
|
||||
pytest.importorskip("psutil")
|
||||
report = run_articulation_suite(
|
||||
determinism_runs=3, footprint_turns=10,
|
||||
footprint_sample_every=5, ollama_model=None,
|
||||
)
|
||||
d = report.as_dict()
|
||||
assert isinstance(d["breadth"], list) and len(d["breadth"]) > 0
|
||||
assert isinstance(d["determinism"], list)
|
||||
assert d["determinism_all_identical"] is True
|
||||
assert isinstance(d["footprint_samples"], list)
|
||||
assert d["ollama"]["status"] == "skipped"
|
||||
# Cross-topic walk runs every entry.
|
||||
assert len(d["cross_topic"]) == len(CROSS_TOPIC_PROMPTS)
|
||||
163
tests/test_cross_pack_supersede.py
Normal file
163
tests/test_cross_pack_supersede.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"""ADR-0067 follow-up — cross-pack supersession tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from teaching.cross_pack_supersede import supersede_cross_pack_chain
|
||||
from teaching.supersede import SupersessionError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cross_pack_corpus(tmp_path, monkeypatch) -> Path:
|
||||
"""A fresh copy of the production cross-pack corpus we can mutate."""
|
||||
import chat.cross_pack_grounding as mod
|
||||
src_bytes = mod._CORPUS_PATH.read_bytes()
|
||||
target = tmp_path / "cp.jsonl"
|
||||
target.write_bytes(src_bytes)
|
||||
monkeypatch.setattr(mod, "_CORPUS_PATH", target)
|
||||
mod.clear_cross_pack_cache()
|
||||
try:
|
||||
yield target
|
||||
finally:
|
||||
mod.clear_cross_pack_cache()
|
||||
|
||||
|
||||
def test_supersede_appends_new_active_and_retires_old(cross_pack_corpus) -> None:
|
||||
new_id = supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family",
|
||||
intent="cause",
|
||||
connective="precedes",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
assert new_id == "cause_family_precedes_identity"
|
||||
last = json.loads(cross_pack_corpus.read_text().splitlines()[-1])
|
||||
assert last["chain_id"] == new_id
|
||||
assert last["superseded_by"] == "cause_family_grounds_identity"
|
||||
assert last["subject_pack_id"] == "en_core_relations_v1"
|
||||
assert last["object_pack_id"] == "en_core_cognition_v1"
|
||||
|
||||
|
||||
def test_supersede_rejects_same_pack(cross_pack_corpus) -> None:
|
||||
with pytest.raises(SupersessionError, match="must differ"):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family",
|
||||
intent="cause",
|
||||
connective="precedes",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_relations_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
|
||||
|
||||
def test_supersede_rejects_lemma_outside_declared_pack(cross_pack_corpus) -> None:
|
||||
with pytest.raises(SupersessionError, match="not resident"):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family",
|
||||
intent="cause",
|
||||
connective="precedes",
|
||||
object_="identity",
|
||||
# WRONG: family is in relations, not cognition
|
||||
subject_pack_id="en_core_cognition_v1",
|
||||
object_pack_id="en_core_relations_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
|
||||
|
||||
def test_supersede_rejects_unknown_old_chain_id(cross_pack_corpus) -> None:
|
||||
with pytest.raises(SupersessionError, match="not active"):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="nonexistent_chain_id",
|
||||
subject="family",
|
||||
intent="cause",
|
||||
connective="grounds",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
|
||||
|
||||
def test_supersede_rejects_invalid_review_date(cross_pack_corpus) -> None:
|
||||
with pytest.raises(SupersessionError, match="review_date"):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family", intent="cause", connective="precedes",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026/05/18", # wrong format
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
|
||||
|
||||
def test_supersede_rejects_invalid_intent(cross_pack_corpus) -> None:
|
||||
with pytest.raises(SupersessionError, match="whitelist"):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family", intent="definition", connective="precedes",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
|
||||
|
||||
def test_supersede_rejects_self_supersede(cross_pack_corpus) -> None:
|
||||
with pytest.raises(SupersessionError, match="identical"):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family", intent="cause", connective="grounds",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
# ⇒ new id resolves to same as old
|
||||
)
|
||||
|
||||
|
||||
def test_supersede_byte_identical_on_failure(cross_pack_corpus) -> None:
|
||||
before = cross_pack_corpus.read_bytes()
|
||||
with pytest.raises(SupersessionError):
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="nonexistent",
|
||||
subject="family", intent="cause", connective="precedes",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
assert cross_pack_corpus.read_bytes() == before
|
||||
|
||||
|
||||
def test_supersede_drops_retired_from_active_index(cross_pack_corpus) -> None:
|
||||
supersede_cross_pack_chain(
|
||||
old_chain_id="cause_family_grounds_identity",
|
||||
subject="family", intent="cause", connective="precedes",
|
||||
object_="identity",
|
||||
subject_pack_id="en_core_relations_v1",
|
||||
object_pack_id="en_core_cognition_v1",
|
||||
review_date="2026-05-18",
|
||||
corpus_path=cross_pack_corpus,
|
||||
)
|
||||
from chat.cross_pack_grounding import _all_cross_pack_chains
|
||||
active_ids = {c.chain_id for c in _all_cross_pack_chains()}
|
||||
assert "cause_family_grounds_identity" not in active_ids
|
||||
assert "cause_family_precedes_identity" in active_ids
|
||||
Loading…
Reference in a new issue