CLAIMS.md is now mechanically derived from two ground-truth sources:
- core.capability.ledger_report (Tier 1: ratified domains)
- scripts/verify_lane_shas.PINNED_SHAS (Tier 2: pinned lane reports)
The generator is deterministic and gated by
tests/test_claims_md_is_current.py + the lane-shas CI workflow's new
'verify CLAIMS.md is current' step. Drift between in-tree state and
the published claims fails CI before merge.
Tier 1 (5 ratified domains) and Tier 2 (6 pinned lanes) cover every
ADR-0092..0102 invariant currently CI-pinned.
Two pre-existing latent issues fixed:
1. discourse_planner flag catalog drift (test_flag_report failure)
On 2026-05-21 the discourse_planner default was flipped to True
after byte-equality verification (per inline comment in
core/config.py:130-138), but the capability flag catalog at
core/capability/reporting.py was not updated — it still claimed
"flag_shipped_default_off". The test
test_flag_report_tracks_default_off_flags_without_enabling_them
correctly caught the inconsistency; it had been failing across
every commit since ADR-0092 first ran the suite.
Fix:
- New "flag_shipped_default_on" state in _FLAG_CATALOG, added
to flag_report() grouped output
- discourse_planner moved from default_off → default_on
- Test renamed to test_flag_report_classification_matches_actual_defaults,
enforces BOTH directions of the contract (catalog claim must
match DEFAULT_CONFIG value)
- New test test_flag_catalog_state_is_consistent_with_default_config
cross-checks every catalog entry against DEFAULT_CONFIG;
catches future drift before it lands
2. public_demo lane SHA shifted every commit
Each commit advances the showcase's generated_at_revision field
(git HEAD SHA). _strip_volatile in the lane runner was stripping
wall-clock and per-run paths but NOT generated_at_revision, so
the byte-equality case's details.sha256 changed with every commit
even when underlying demos produced identical content. That made
the pin a "did this run today" check rather than a "did the code
produce the right artifact" check — exactly the failure mode
the verifier was supposed to prevent.
Fix:
- Add generated_at_revision to _VOLATILE_KEYS in the public_demo
runner. Lane's invariant is "same code → same SHA," not
"same HEAD → same SHA"; HEAD belongs in the showcase output
(operators need it) but not in the lane's equality projection.
- Pin refreshed once to capture the now-commit-independent SHA;
subsequent commits won't shift it unless underlying demo content
actually changes.
After fix:
- Capability tests: 6/6 passing (was 4/5 with discourse_planner failing)
- Lane SHAs: 6/6 match pinned values; public_demo pin will now survive
routine code changes
- Smoke 67/67, cognition eval byte-identical 100/100/100/100
This is the single known pre-existing test failure cleaned up.
Six lanes (reviewer_registry, miner_loop_closure,
domain_contract_validation, fabrication_control_summary,
demo_composition, public_demo) now have CI-enforced SHA-256 pins.
A failing job means a lane's deterministic output changed without
an explicit ADR-tracked pin update.
- new scripts/verify_lane_shas.py: single source of truth
- PINNED_SHAS dict mapping lane_id → 64-char hex SHA
- LANE_SPECS tuple wiring each lane to its runner module + canonical
report path
- accepts_report_flag handles the fabrication_control runner's
different arg shape (--lane-dir not --report)
- verify_all() runs each lane in subprocess isolation (clean Python
state per lane — relevant for adapters that cache pack loads at
module import)
- --update flag refreshes pins after intentional ADR-tracked changes;
diff is the audit trail
- --json flag emits machine-readable report
- exits non-zero on any mismatch
- new .github/workflows/lane-shas.yml:
- triggers on push to main and pull_request to main
- concurrency group cancels in-progress runs on new commits
- Python 3.11 + pip-cached deps + editable install
- runs verify_lane_shas.py; emits JSON report on failure
- 12-minute timeout (lanes take ~30s in practice)
- new tests/test_lane_sha_verifier.py: cheap local-pytest pinning
- every LaneSpec has a corresponding PINNED_SHAS entry
- no orphan pins without a LaneSpec
- every pin is a 64-char hex SHA-256
- every runner module path exists on disk
- canonical report paths are under repo root
- all six expected lanes (ADR-0092/0093/0095/0096/0098/0099) covered;
ADR-0094 and ADR-0097 are schema/ratification only, intentionally
excluded from EXPECTED_LANES
- 6 tests run in <100ms — catches drift before CI
- evals/public_demo/results/v1_dev.json: refreshed to match the new
pin (21751aaf..) — earlier pin was generated under slightly different
runner argparse defaults; --update produced the canonical bytes
Local verifier: 6/6 lanes match pinned SHAs. Smoke 67/67. Lane SHAs:
reviewer_registry 681a2aab..
miner_loop_closure 9f071733..
domain_contract_validation f9c06cde..
fabrication_control_summary 01e1b6b7..
demo_composition 27d83824..
public_demo 21751aaf..
Single 30-second artifact composing four CORE invariants
(determinism, honest unknown, reviewed learning, multi-hop with
trace) by delegating to existing DemoCommand adapters. **No new
mechanism** — every claim is backed by an already-shipped,
separately-tested adapter. Closes the 8-ADR scale-up slate.
- new core/demos/learning_loop_adapter.py: LearningLoopDemo wraps
ADR-0056 reviewed-teaching loop; _strip_volatile_paths drops
transient temp-dir paths from raw before serialization so the
adapter's report_sha256 is content-stable across runs
- new core/demos/showcase_adapters.py:
- FabricationControlPublicDemo: re-runs ADR-0096 public split,
produces 3 claims (refusal_recall_meets_threshold,
fabrication_rate_below_threshold, trace_evidence_present)
- MultiHopTraceDemo: runs 'Does light reveal truth?' with
transitive_surface=True + composed_surface=True against
cognition pack; surfaces a 3-hop walk light→truth→knowledge→
evidence; produces 3 claims (grounded_answer, depth_two_or_more,
walk_evidence_present)
- new core/demos/showcase.py: run_showcase() composes 4 scenes,
emits showcase.json + per-scene artifacts; render_html() produces
presentation-only static HTML with no JS injection vector;
ShowcaseScene dataclass; MAX_RUNTIME_SECONDS=30 hard ceiling
with DemoContractError if exceeded
- CLI: 'showcase' added to demo target choices; --output-dir flag
added; cmd_demo dispatch branch writes showcase.json + showcase.html
- new evals/public_demo/ lane with 4 cases:
- all_claims_supported (each scene + composite)
- determinism_run_to_run_byte_equality (two runs identical after
stripping volatile keys: total_runtime_ms, json_path,
transient_corpus)
- runtime_under_budget (≤30s)
- pure_composition_no_new_mechanism (grep gate over showcase
imports — must come from core/chat/generate/language_packs/
teaching/evals or allowed stdlib only)
- lane is itself byte-identical across runs (sha256 5707db8efc6a..);
runtime case omits exact runtime_ms (it varies near bucket
boundaries) but still asserts ≤ budget
- 8 unit tests with module-scoped fixture (showcase runs once,
~13s total) covering payload shape, scene order, runtime budget,
HTML render absence of <script>, and the pure-composition import
gate independently of the lane
- ADR-0099 measured: total_runtime_ms ~12.8s, well under 30s budget
- smoke 67/67, cognition eval byte-identical 100/100/100/100;
all 6 ADR-0092..0099 lanes byte-identical:
reviewer_registry 681a2aab..
miner_loop_closure 9f071733..
domain_contract_validation f9c06cde..
fabrication_control sum 01e1b6b7..
demo_composition 27d83824..
public_demo 5707db8e..
DemoCommand Protocol + thin adapters retrofit shipped tours to a
typed composition contract. Composability becomes a structural
property: the ADR-0099 showcase will consume DemoResult through one
stable type rather than special-casing each tour. No demo behavior
changes — adapters wrap underlying run_tour() entry points.
- new core/demos/ package:
- contract.py: frozen Claim / DemoResult dataclasses, runtime-checkable
DemoCommand Protocol, canonical_json() sanctioned serializer
(sorted keys, 2-space indent, trailing newline), CLAIM_CONTRACT_VERSION
- audit_tour_adapter.py: AuditTourDemo (5 claims from ADR-0042 scenes
1-4: identity_pack_swaps_visible, safety_typed_refusal,
ethics_opt_in_deployment_fires, ethics_default_silent,
replay_byte_identical)
- tour_adapters.py: shared pattern for register/anchor-lens/orthogonality
tours; _extract_claims walks the dict tree for *_supported booleans
and builds Claim objects in deterministic sorted order
- global-state-mutation detector (ADR-0098 invariant #2):
capture_state() snapshots a load-bearing subset of process state
(CORE_* env vars + module identities for chat.telemetry,
chat.runtime, language_packs.compiler);
verify_no_global_state_mutation() ignores None→id transitions
(benign lazy import) and only flags env-var changes or module
identity rebindings
- new evals/demo_composition/ lane (ADR-0098 invariant proving):
- 6 cases asserting byte-equality + no-state-mutation across the
three fast adapters (audit-tour, register-tour, orthogonality-tour)
- composition_read_only: confirms two adapter results compose into
a composite claim set without mutating either
- stateful_fixture_rejected: negative control — a deliberately
stateful adapter MUST trigger divergence detection
- anchor-lens-tour adapter is exercised by tests, not the lane,
to keep wall time bounded
- byte-identical across runs (sha256 27d838241bf3..)
- 26 unit tests covering Claim/DemoResult validation, canonical_json
determinism, state-mutation detector (including the lazy-import
benign case), Protocol conformance (isinstance check + claim
contract version) for all four adapters, seed-rejection per
adapter (all current adapters are fully deterministic), and an
audit-tour integration smoke verifying 5 claims + byte-equality +
no state mutation across two consecutive runs
- smoke 67/67, cognition eval byte-identical 100/100/100/100, all
five lanes byte-identical (reviewer_registry 681a2aab..,
miner_loop_closure 9f071733.., domain_contract_validation f9c06cde..,
fabrication_control summary 01e1b6b7.., demo_composition 27d83824..)
First concrete domain claim under ADR-0091's Domain Pack Contract v1.
en_mathematics_logic_v1 is now formally ratified as reasoning-capable
in the capability ledger: 9/9 ADR-0091 predicates pass.
ADR-0097 §"No code changes outside pack artifacts and corpus" relaxed
to include two latent bug fixes that ADR-0093's predicate enforcement
just exposed:
1. language_packs/schema.py: LanguageRole enum widened to include
DOMAIN_SEED. Three in-tree packs (en_mathematics_logic_v1,
en_physics_v1, en_systems_software_v1) have declared role="domain_seed"
since landing but the enum was never updated; load_pack() always
raised on them. ADR-0093's P1 predicate exposed the mismatch.
2. core/capability/domain_contract_predicates.py: P2 (gloss checksum)
was reading manifest["checksums"]["glosses_sha256"]; the canonical
in-tree location is manifest["glosses_checksum"] (top-level). Fixed
to prefer the canonical key and fall back to the nested form for
forward compatibility.
ADR-0097 manifest additions to en_mathematics_logic_v1:
- domain_contract_version: 1
- domain_id: "mathematics_logic"
- axioms: null (rules in v1 — pack proves reasoning via chain
composition, not declarative axioms)
- rules: null
- teaching_chains: ["mathematics_logic_chains_v1"]
- eval_lanes: three lanes with dev/public/holdout (elementary_mathematics_ood,
inference_closure, fabrication_control)
- reviewers: ["shay-j"] (resolved via ADR-0092 registry)
- known_gaps: [] (all math/logic gaps in docs/gaps.md were [x])
- provenance: "adr-0097:reviewed:2026-05-21"
Verified evidence:
- core capability domain-contract --pack-id en_mathematics_logic_v1
→ all_passed=True (P1-P9 all pass)
- core capability ledger → mathematics_logic row shows
status=reasoning-capable, predicates.reasoning_capable=True,
predicates.expert_demo=False, open_gaps=[],
operator_chain_coverage all ready=True (8 chains each),
intent_shapes_present=5
- 14 ADR-0097 invariant tests in
test_adr_0097_mathematics_logic_ratification.py pin
status/provenance/expert-demo-gate/contract shape
Two pre-existing tests updated for the new CLI default
(predicate-running, non-zero on missing contract):
- test_capability_domain_contract_json_absent_contract_is_noop now
uses --structural-only to assert legacy parse-only shape
- test_cli_returns_nonzero_on_missing_contract switched its fixture
pack from en_mathematics_logic_v1 (now has a contract) to
en_core_cognition_v1 (no contract)
The pre-existing test_flag_report_tracks_default_off_flags failure
(discourse_planner flag default mismatch, seen since ADR-0092) is
unchanged and unrelated.
Smoke 67/67, packs 6/6, capability tests 49/50, cognition eval
byte-identical 100/100/100/100; lanes byte-identical:
reviewer_registry 6/6, miner_loop_closure 6/6,
domain_contract_validation 9/9, fabrication_control dev 12/12 +
public 9/9.
First negative-control measure. Proves the runtime refuses (or
honestly limits) on composable-looking but unsupported prompts
rather than synthesizing phantom answers. Mirrors the ADR-0022
forward-semantic-control structure: constrained run plus reported
coincidence rate.
- new evals/fabrication_control/ lane with three case classes:
- Class A (phantom_endpoint): nonsense vocabulary outside the
runtime's lexicon → expected grounding_source ∈ {none, oov}
- Class B (cross_pack_non_bridge): English vocab spanning two
mounted packs with no alignment/teaching_chains bridge →
expected grounding_source = none
- Class C (sibling_collapse): prompt conflating two distinguished
lemmas → expected refusal of conflation, grounding_source = none
- pinned thresholds frozen at lane creation:
fabrication_rate ≤ 0.01, refusal_recall ≥ 0.95,
trace_evidence_present == 1.00,
grounding_source_matches_expected == 1.00
- three-set discipline per docs/capability_roadmap.md Rule 1:
cases/dev.jsonl (12 cases, 4/class), cases/public.jsonl (9 cases),
cases/holdout.jsonl (empty — reserved for first version cut)
- runner.py drives each case through ChatRuntime.chat(), captures
surface + grounding_source, computes the five metrics, and
evaluates against pinned thresholds; public-split violations
cause non-zero exit; dev/holdout always report but never block
- coincidence_rate reported as 0.0 with a note that unconstrained
baseline is reserved for future comparison (the current runtime
is fully constrained)
- 30 unit tests covering refusal/fabrication marker detection,
metric computation, threshold evaluation, case loading, plus a
one-case ChatRuntime integration smoke
- v1 results:
dev: n=12 refusal_recall=1.0 fabrication_rate=0.0 PASSED
public: n=9 refusal_recall=1.0 fabrication_rate=0.0 PASSED
- byte-identical across runs (dev sha256=d6757e0e3f96..,
public sha256=9b502878fcb7.., summary sha256=01e1b6b71114..)
- smoke 67/67, teaching 17/17, cognition 120/121 (pre-existing skip);
cognition eval byte-identical 100/100/100/100
Closes the Phase-5 contemplation loop in code. Articulation-quality,
contradiction-detection, and frontier-compare miners (already shipping)
now have a route to file PackMutationProposal candidates that traverse
the single reviewed teaching path. Construction-only; never promotes
to coherent.
- new teaching/from_miner.py: from_finding() / from_findings() turn
ContemplationFinding records (kind=PACK_MUTATION_CANDIDATE) into
PackMutationProposal candidates with source.kind="miner",
source.source_id=<miner_id>, status=SPECULATIVE
- proposal_id = SHA-256(canonical(miner_id, finding, revision))[:16]
— same inputs → byte-identical proposal_id; different miner_id or
revision → different id
- identity-pack defense AT CONSTRUCTION: reuses teaching.review.
_is_identity_override() against finding.subject AND
finding.proposed_action; miner-sourced identity-override attempts
never reach the proposal log
- pluggable ReplayEquivalenceChecker Protocol with ReplayEquivalenceResult;
NoOpReplayChecker default explicitly notes "deferred to production
checker"; production checker integration is downstream of this ADR
- from_findings() batch path collects identity-override and
replay-equivalence rejections in a typed rejection log rather than
raising, so a mixed batch can proceed with audit evidence
- serialize_proposal_emitted_event() emits ADR-0040-compliant redacted
telemetry shape: type, proposal_id, source.serialize(),
epistemic_status only (no raw subject/correction_text)
- 22 unit tests covering positive construction, identity defense in
subject+proposed_action, malformed input, determinism (same inputs,
different revision, different miner_id, batch stream), replay
pre-gate (single + batch), telemetry redaction, and the structural
grep gate enforcing miner_proposal_single_review_path (only
teaching/review.py and teaching/store.py may promote to COHERENT)
- new evals/miner_loop_closure/ lane: 6 case classes (positive_basic,
identity_override_subject, identity_override_action,
replay_equivalence_failed, wrong_finding_kind, determinism) passing
6/6 with byte-identical SHA-256 across runs
- smoke 67/67, teaching 17/17, cognition 120/121 (1 pre-existing skip);
cognition eval byte-identical 100/100/100/100
Closes the load-bearing gap blocking every reasoning-capable claim
under ADR-0091: docs/reviewers.yaml was previously `reviewers: []` and
unparsed. Now schema-validated at v1, with a bootstrap shay-j entry
self-sealed via provenance.
- new core.capability.reviewers module: frozen Reviewer/ReviewerRegistry
dataclasses, strict load_reviewer_registry parser, ReviewerRegistryError
- enforces ADR-0092 schema rules: schema_version==1, no unknown
top-level keys, no unknown reviewer fields, role∈{primary,domain},
primary must claim ["*"], domain must NOT claim "*", review_scope
subset of {pack,proposal,chain,eval}, no duplicate reviewer_ids
- can_review(reviewer_id, domain_id, scope) helper implements
ADR-0092 rules 2-4 for downstream use by ADR-0093 validator
- docs/reviewers.yaml updated to v1 schema with shay-j bootstrap
- ledger_report() evidence_counts now exposes structured
reviewer_registry status (valid, schema_version, reviewer_count,
reviewer_ids, error) alongside the legacy reviewers_present bool
- new evals/reviewer_registry/ lane: 6 cases (2 positive + 4 negative)
covering empty-registry, wrong-version, domain-wildcard rejection,
and unknown-field rejection
- runner emits deterministic JSON report; two runs produce byte-identical
output (sha256 verified)
- 26 unit tests in tests/test_reviewer_registry.py
- capability ledger test extended to assert new reviewer_registry block
- smoke suite green (67/67); lane passes 6/6
The pre-existing test_flag_report_tracks_default_off_flags failure is
unrelated (discourse_planner flag default) and not introduced here.
Eight load-bearing ADRs closing the loop from contemplation Phase 5 through
a public showcase demo. Each one is small and evidence-bearing; together
they sequence the next arc without duplicating existing substrate.
- 0092 Reviewer Registry v1 — populates docs/reviewers.yaml schema;
unblocks all reasoning-capable claims under ADR-0091.
- 0093 Domain Pack Contract v1 Implementation — wires ADR-0091's five
follow-up items (parser, dry-run validator, chain registry, eval lane
refs, reviewer resolution) so manifest fields actually gate status.
- 0094 Proposal Source Provenance — sealed ProposalSource type widening
proposal schemas ahead of 0095.
- 0095 Miner-Sourced Teaching Proposals — closes the contemplation
loop: articulation_quality / contradiction_detection / frontier_compare
miners emit PackMutationProposal candidates routed through the single
reviewed teaching path; identity-pack defense at construction, not
review; replay-equivalence pre-gate.
- 0096 Fabrication-Control Eval Lane — first negative-control measure;
three case classes (phantom endpoint, cross-pack non-bridge, sibling
collapse) with frozen thresholds (fabrication_rate ≤ 0.01).
- 0097 Mathematics-Logic Reasoning-Capable Ratification — first
domain claim under ADR-0091; chain corpus + eval lanes already
exist, this is the formal contract ratification.
- 0098 Demo Composition Contract — DemoCommand protocol so demos can
be composed without reimplementation; deterministic JSON, no global
state mutation, declared output paths only.
- 0099 Public Showcase Demo — composes four scenes (determinism /
honest unknown / reviewed learning / multi-hop+trace) under 30s;
pure composition enforced by grep gate; JSON byte-equality CI-pinned.
Landing order: 0092 → 0094 → 0095 → 0093 → 0096 → 0097 → 0098 → 0099.
Deliberately not included: curriculum compiler, formation course
runner, calculator operators, response-mode taxonomy expansion,
learning-scale 10k harness. Each is deferred with a documented reason.
Phase 5 landed in commit 327047c (articulation-quality miner +
runtime sink wiring + full end-to-end loop tests). Extending the
session note in-place per its own append-only convention so the
single document covers the complete arc Phases 1–5.
Sections updated
----------------
* §0 executive summary
- commit count: 9 → 11
- phases shipped: 4 → 5 (new Phase 5 row in the deliverables
table)
- observation surfaces table grows two rows for
``attach_articulation_sink`` and
``mine_articulation_observations``
- test artifacts: 6 → 8 files, 64 → 82 cases
* §3.5 NEW — full architectural walkthrough of Phase 5
- Why the loop closes (links the user's
"memory confidence scoring" intuition to ADR-0080's
doctrine-aligned realisation: reviewable evidence, not
autonomous mutation)
- File-by-file delta (chat/articulation_telemetry.py + miner
+ runtime wiring)
- Three v1 mining rules (recurring_predicate_monotony /
recurring_planner_gap / low_average_predicate_diversity)
- Loop diagram showing live + offline halves
- Recorded demo output from the commit message
- Doctrine pin table mapping each constraint to its test
* §4 pipeline diagram extended to show Phase 5 sink + offline
miner branches
* §5 verification table gains four new rows for Phase 5 claims
(full-loop emission, byte-equal finding IDs across two e2e
runs, JSONL round-trip identity, opt-in gating)
* §5.6 suite totals updated:
- Contemplation subsuite: 35/35 → 53/53 (Phase 3+4+5)
- New row for Phase 5 articulation-quality e2e (7/7)
* §6 case study — added the "After Phase 5" trace and the
closing one-line story across all five phases for one prompt
("What is truth, and why does it matter?")
* §7 architecture surfaces table grows a row for
chat/articulation_telemetry.py and adds the miner to the
contemplation subsystem row
* §9 inverted from "what would close the loop" (future work) to
"SHIPPED — here's what it now unlocks":
- production sink + retention policy
- additional aggregation rules
- CLI hook
- review-loop wiring back into PackMutationProposal
* §10 reference index grows new lines for
chat/articulation_telemetry.py,
core/contemplation/miners/articulation_quality.py,
tests/test_articulation_quality_miner.py, and
tests/test_articulation_quality_e2e.py
* Footer updated: notes the arc is complete; future arcs should
start a new session-notes file and cross-link rather than
rewriting this one.
The session-notes file is now 1100+ lines — the complete frozen
reference for the articulation arc that took CORE from one-sentence
pack-grounded surfaces to a full live-reasoning + offline-mining
+ reviewable-proposals feedback loop, all doctrine-aligned, all in
one session.
Final phase of the articulation arc. Consumes the per-turn
``PlanMetrics`` + ``ContemplationFinding`` streams produced by
Phases 3 + 4 and aggregates across many turns to emit
SPECULATIVE ``PACK_MUTATION_CANDIDATE`` findings that the operator
reviews via the existing proposal-review-ratify chain.
This is the doctrine-aligned answer to the user's question:
"Should we... realize a way to score whether it should use what
it produced towards memory confidence for future use?"
Yes — and it stays inside ADR-0080: read-only, SPECULATIVE-only,
deterministic, no parallel learning path, no autonomous memory
mutation.
What it adds
------------
* New module ``chat/articulation_telemetry.py``:
- ``ArticulationObservation`` frozen dataclass — per-turn
bundle of (turn_id, anchor_subject, prompt_hash,
plan_substrate_hash, metrics, findings).
- ``format_articulation_observation_jsonl(...)`` — deterministic
sort-keys JSONL line.
- ``load_articulation_observations(lines)`` — schema-tolerant
loader; malformed lines drop without aborting.
- ``ArticulationObservationSink`` protocol — structurally
identical to ``TurnEventSink`` but distinct named type so
consumers can subscribe to one stream without the other.
* New module ``core/contemplation/miners/articulation_quality.py``:
- ``mine_articulation_observations(observations, paths)`` —
pure deterministic aggregator with three v1 rules.
- **recurring_predicate_monotony** — when the same
(subject, predicate) pair is flagged WEAK_SURFACE in
>= _MIN_RECURRENCE (default 3) observations, propose
substrate diversification with non-dominant predicates.
- **recurring_planner_gap** — when the same subject is
flagged PLANNER_GAP >= _MIN_RECURRENCE times across modes,
propose substrate expansion.
- **low_average_predicate_diversity** — when mean
``predicate_diversity_ratio`` < 0.5 across >= _MIN_RECURRENCE
observations on the same anchor subject, propose
diversification.
* Runtime wiring (``chat/runtime.py``):
- New ``ChatRuntime.attach_articulation_sink(sink)`` method.
Mirrors ``attach_telemetry_sink`` pattern.
- Emission point at the end of
``_maybe_apply_discourse_planner``: when contemplation
enabled + sink attached + plan engaged, builds an
``ArticulationObservation`` and emits one JSONL line.
Sink errors propagate (fail-fast, no swallowing).
- Per-runtime ``_articulation_turn_counter`` increments on
every emission; gives downstream consumers a stable
sequence index.
Tests
-----
* ``tests/test_articulation_quality_miner.py`` (11 tests):
- Empty / sub-threshold cases yield no findings.
- Each of the three rules fires at threshold.
- Recurring_predicate_monotony separates by subject (no
cross-subject merging).
- Recurring_planner_gap collects distinct modes into a
sorted comma-joined string.
- Determinism — byte-equal finding IDs across two runs.
- SPECULATIVE doctrine pin.
- JSONL round-trip preserves observation identity.
* ``tests/test_articulation_quality_e2e.py`` (7 tests):
- Sink-detached + contemplation-on → no emission.
- Sink-attached + contemplation-off → no emission.
- Engaged turn emits exactly one observation line.
- BRIEF prompt emits nothing (fast-path).
- **Full loop** — run compound prompt 3x → 3 observations →
miner emits PACK_MUTATION_CANDIDATE with subject='truth',
predicate='recurring_predicate_monotony', object='belongs_to'.
- Full loop is deterministic (byte-equal finding IDs across
two complete runs).
- Every full-loop finding is SPECULATIVE.
Doctrine pins
-------------
| Claim | Pinned by |
|--------------------------------------|----------------------------------------------------------|
| SPECULATIVE-only | test_all_findings_remain_speculative |
| Deterministic across runs | test_miner_is_deterministic_across_runs |
| Full-loop determinism (e2e) | test_full_loop_is_deterministic_byte_equal_finding_ids |
| No autonomous mutation | Sink is append-only; miner outputs ContemplationFinding |
| | objects only; nothing writes to packs/vault/teaching. |
| Append-only stream | Sink protocol has emit(line: str) and nothing else. |
Live demo (3 identical compound-prompt turns)
---------------------------------------------
Runtime emits 3 observations. Offline miner aggregates and emits:
[pack_mutation_candidate] subject='truth'
predicate='recurring_predicate_monotony' object='belongs_to'
evidence_refs: 3 observations
proposed_action: "diversify substrate for 'truth': across 3
observations the plan repeatedly over-concentrated on
predicate 'belongs_to'. Candidates: add teaching chains
rooted on 'truth' with relations OTHER than 'belongs_to'
(grounds / requires / reveals / contrasts / precedes /
follows) so the planner's RELATION selector has more
variety to draw from."
epistemic_status: speculative
The system observed its own articulation patterns across many
turns, identified the corpus expansion priority, and emitted a
specific reviewable proposal — without mutating anything. The
operator decides whether to act on it via the existing review
chain.
Verification
------------
pytest test_articulation_quality_miner.py 11/11 pass
pytest test_articulation_quality_e2e.py 7/7 pass
pytest test_plan_metrics*.py 18/18 pass (Phase 4)
pytest test_plan_contemplation*.py 17/17 pass (Phase 3)
pytest test_discourse_planner_*.py 99/99 pass
pytest test_articulation_demo.py all claims supported
pytest test_narrative_example_intents.py pass
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
The articulation arc is complete. Future work documented in
``docs/sessions/SESSION-2026-05-21-articulation-arc.md`` §8:
connective rotation, generalised pronoun selection, doctrine-gated
plan revision, Phase 2.5 mid-sentence reflection. None blocking.
Thorough why/when/where/how reference for the four-phase articulation
arc shipped this session plus the pre-arc classifier/Rust cleanup
that made it possible. Designed as the load-bearing entry point for
future contributors, case studies, capability audits, and
architectural reviews.
Sections
--------
0. Executive summary — what was achieved (9 commits, 64 new tests,
user-visible before/after, doctrine evidence)
1. Why this work happened (visible gap, doctrine constraint, what
was already wired vs. missing)
2. Pre-arc cleanup — RECALL trigger / CORRECTION x2 / Rust FFI
3. Phase 1 — discourse planner default ON + fast-path
Phase 2 — reflective rendering (subject pronominalization)
Phase 3 — live plan contemplation pre-flight
Phase 4 — per-plan articulation telemetry metrics
4. The pipeline today (diagram + before/after table)
5. Verification — every claim and the test that holds it.
Five sub-tables: doctrine claims, quality claims, back-compat /
null-lift claims, ADR-0072 structural invariants,
`core bench --suite all` performance, suite-level totals.
6. Case study — the compound prompt as a story across all four
phases
7. Architecture surfaces touched (which file got what change in
which phase)
8. What was deliberately NOT built (and why — connective rotation,
generalised pronoun selection, plan revision, Phase 2.5,
Rust algorithmic optimisation)
9. Phase 5 — what would close the user-intuited "live reasoning
→ memory confidence" loop, doctrine-aligned
10. Reference index — modules, flags, tests, ADR cross-refs
Per the user's request: captures the why/when/where/how thoroughly
so this work is recoverable for future reference, case studies, and
building on top. Append-only convention: future sessions extending
this arc should add new sections below rather than rewriting — the
history is itself evidence of the doctrine working in practice.
Quantitative companion to Phase 3 (commit 664e081). Where Phase 3
emits SPECULATIVE *findings* about plan quality, Phase 4 emits
typed *measurements* — pure-function projection of a
``DiscoursePlan`` into a ``PlanMetrics`` dataclass.
Why this matters
----------------
The discourse planner now produces multi-clause grounded
articulations (Phase 1), the renderer pronominalizes across
consecutive same-subject moves (Phase 2), and the contemplation
pre-flight emits qualitative concerns about plan shape (Phase 3).
What was missing was the *aggregable* layer: per-turn structured
numbers that downstream consumers can stream across many turns
to score quality patterns the per-turn observer cannot see.
Phase 4 lands that layer. Phase 5 (offline contemplation miner)
becomes possible because there's now structured signal to mine.
What it measures
----------------
Structure
* move_count — total moves in plan
* fact_bearing_count — moves with fact != None
Move-kind distribution
* anchor_count / support_count / relation_count
/ transition_count / closure_count
Diversity
* unique_predicates — distinct predicates across
fact-bearing moves
* unique_subjects — distinct subject lemmas
* unique_sources — distinct FactSources
Topic dynamics
* topic_shift_count — consecutive pairs where
subject changed
* pronominalization_opportunities — consecutive pairs where
subject held (= Phase 2's
anaphora trigger count)
Derived ratios
* predicate_diversity_ratio — unique_predicates /
fact_bearing_count
* subject_focus_ratio — pronominalizations /
(pronominalizations +
topic_shifts)
Every field is a deterministic pure function of the plan: same
plan in → byte-equal ``PlanMetrics.as_dict()`` out. This is the
load-bearing claim that lets Phase 5 aggregate across turns
without "is this the same metric?" ambiguity.
Doctrine alignment
------------------
Per ADR-0080 contemplation discipline:
* Read-only — metrics are pure projections of the plan; no
mutation of plan, runtime state, or memory tiers.
* No autonomous learning — metrics are observations, not
learned policy. Promotion to memory still flows through
the existing proposal-review-ratify chain.
* Deterministic replay — pinned by test_metrics_are_deterministic_
and_byte_equal_as_dict plus the runtime-level
test_metrics_byte_equal_across_runs.
Wiring
------
* New ``ChatRuntime.last_plan_metrics`` property — read-only
``PlanMetrics`` from the most recent turn where the planner
engaged (and ``discourse_contemplation`` was on); ``None``
otherwise. Reset between turns alongside ``last_plan_findings``
via the existing top-of-call reset block.
* Same opt-in flag as Phase 3 (``discourse_contemplation``).
When True, the runtime computes both findings AND metrics in
the same block; when False (default), both stay at empty/None.
Demo (config: discourse_contemplation=True)
-------------------------------------------
"What is knowledge?" → metrics: None (BRIEF fast-path)
"Tell me about memory." → moves=3 fact_bearing=3
kinds=A:1/S:1/R:1/T:0/C:0
unique_predicates=3 subjects=1
pronominalization_ops=2 shifts=0
predicate_diversity=1.000
subject_focus=1.000
"What is truth, and why does
it matter?" → moves=7 fact_bearing=6
kinds=A:2/S:2/R:2/T:1/C:0
unique_predicates=4 subjects=1
pronominalization_ops=4 shifts=1
predicate_diversity=0.667 ← Phase 3
WEAK_SURFACE
quantified
subject_focus=0.800
+ 1 finding (weak_surface)
The compound-prompt numbers are particularly informative:
``predicate_diversity=0.667`` is the algebraic expression of the
Phase 3 ``WEAK_SURFACE`` rule — the rule fires precisely because
6 fact-bearing moves used only 4 distinct predicates.
``subject_focus=0.800`` quantifies that 80% of consecutive pairs
held the same subject — high topic stickiness that Phase 2's
reflective renderer leveraged into 4 ``it`` substitutions.
Tests
-----
* ``tests/test_plan_metrics.py`` — 10 unit tests pinning each
field, derived ratios, bridge-move handling (``fact=None``
resets the focus channel), and determinism via ``as_dict()``
byte-equality.
* ``tests/test_plan_metrics_runtime.py`` — 8 end-to-end tests
proving the runtime wiring: disabled by default, populated
when enabled, BRIEF prompts yield None, no cross-turn leak,
byte-equal across runs, parametrized co-population check
alongside findings.
Verification
------------
pytest tests/test_plan_metrics*.py 18/18 pass
pytest tests/test_plan_contemplation*.py 17/17 pass (Phase 3)
pytest tests/test_discourse_planner_*.py 99/99 pass
pytest tests/test_articulation_demo.py all claims supported
pytest tests/test_narrative_example_intents.py pass
pytest tests/test_runtime_config.py pass
cognition eval OFF vs ON 45/45 surface byte-equal
45/45 trace_hash byte-equal
4/4 aggregate metrics
identical
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
Phase 5 (logged, not built)
---------------------------
Offline contemplation miner that consumes ``last_plan_findings``
+ ``last_plan_metrics`` streams across many turns and emits
reviewable pack-mutation candidates. Still SPECULATIVE;
review-gated; never auto-promoted to memory. Now unblocked by
the structured metric surface Phase 4 lands.
Wires deterministic, read-only contemplation OVER a completed
``DiscoursePlan`` BEFORE the renderer fires. This is the
"reasoning at meaningful checkpoints" capability — the system
now inspects the global shape of its own articulation plan and
emits SPECULATIVE findings about quality issues the move-by-move
planner couldn't see locally.
Doctrine alignment (ADR-0080)
-----------------------------
* **Read-only** — never mutates the plan, packs, vault, teaching
corpus, or runtime state. Returns findings as a tuple; the
runtime stores them on a read-only property.
* **SPECULATIVE-only** — every finding is stamped
``EpistemicStatus.SPECULATIVE`` by the schema's ``__post_init__``;
the doctrine pin ``test_findings_always_speculative`` keeps that
invariant visible.
* **Deterministic replay** — same plan → byte-identical findings
(same ``substrate_hash``, same ``finding_id``).
* **No parallel learning path** — findings flow to a read-only
observation surface (``runtime.last_plan_findings``). Promotion
to memory still goes through the existing proposal → review →
ratify chain. The offline contemplation miner (Phase 5 target)
is what eventually consumes the findings and emits reviewable
pack-mutation candidates.
v1 rules (``core/contemplation/plan_preflight.py``)
----------------------------------------------------
* ``PLANNER_GAP`` — non-BRIEF mode produced anchor-only depth.
Signals the teaching/cross-pack substrate for that lemma is too
thin for the planner to expand.
* ``WEAK_SURFACE`` — three or more moves share a predicate.
Signals the rendered surface will read mechanical (e.g. three
``belongs_to`` clauses in a row). Fires on today's compound
prompt ``"What is truth, and why does it matter?"`` — the
6-sentence plan uses ``belongs_to`` 3 times.
* ``COVERAGE_GAP`` — every move in a multi-move plan draws from
a single ``FactSource``. Signals one-sided substrate (e.g.
pack-only with no teaching enrichment).
Runtime wiring
--------------
* New ``RuntimeConfig.discourse_contemplation: bool = False`` —
opt-in for now. Default off keeps the cognition eval byte-
identical to Phase 2 (verified 45/45 surface + 45/45 trace_hash).
* New ``ChatRuntime.last_plan_findings`` property — read-only tuple
of ``ContemplationFinding`` records from the most recent turn.
Reset to ``()`` at the start of every plan-engagement call so
findings never leak across turns.
* Contemplation runs AFTER the planner produces a multi-move plan
and BEFORE the renderer fires; the plan itself is not modified.
Demo (config: discourse_contemplation=True)
-------------------------------------------
"What is knowledge?" → planner fast-path; no findings
"Tell me about memory." → 3 moves, distinct predicates;
no findings (good!)
"What is truth, and why does
it matter?" → 6 moves, ``belongs_to`` x 3:
[WEAK_SURFACE] subject='truth'
predicate='predicate_repeats_in_plan'
object='belongs_to'
proposed action: diversify the
relation inventory for 'truth'
(grounds / requires / reveals /
contrasts) so the planner has
more variety to draw from.
"Explain truth." → 3 moves, distinct predicates;
no findings
Tests
-----
* ``tests/test_plan_contemplation.py`` — 11 unit tests pinning
each rule, empty/trivial plans, determinism, and the
SPECULATIVE-only doctrine.
* ``tests/test_plan_contemplation_runtime.py`` — 6 end-to-end
tests proving the runtime wiring: disabled by default,
populated when enabled, reset across turns, deterministic
across runs, all findings SPECULATIVE.
Verification
------------
pytest tests/test_plan_contemplation*.py 17/17 pass
pytest tests/test_discourse_planner_*.py 99/99 pass
pytest tests/test_articulation_demo.py all claims supported
pytest tests/test_narrative_example_intents.py pass
pytest tests/test_runtime_config.py pass
cognition eval OFF vs ON 45/45 surface byte-equal
45/45 trace_hash byte-equal
4/4 aggregate metrics
identical
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
Phases roadmap (logged in commit, not built today)
--------------------------------------------------
* Phase 4 — articulation telemetry enrichment. Emit per-turn
metrics (grounding_ratio, anaphora_engagement, plan_completeness,
novelty, focus_consistency) to the existing telemetry sink so
the offline miner has structured signal.
* Phase 5 — offline contemplation miner. Extend
``core/contemplation`` with a miner that consumes
``last_plan_findings`` streams and emits reviewable
pack-mutation / teaching-corpus expansion proposals. Still
SPECULATIVE; review-gated.
The Phase 1 multi-clause renderer (commit 63ffd88) produces grounded
content but reads mechanically because the subject lemma repeats in
every clause:
"Truth is what is true. Furthermore, truth belongs to cognition.truth.
In turn, truth grounds knowledge. Truth belongs to epistemic.ground.
Furthermore, truth belongs to logos.core. In turn, truth requires
evidence."
This is the literal articulation gap that motivated Phase 2 —
"reasoning at meaningful checkpoints during sentence construction
in order to have a stronger idea of what has come prior and is
already done to help better inform the next move." Between move
``i`` and move ``i+1`` the renderer now reflects on what subject
has just been established (the "focus") and renders the next clause
with a pronoun when the focus carries forward:
"Truth is what is true. Furthermore, it belongs to cognition.truth.
In turn, it grounds knowledge. It belongs to epistemic.ground.
Furthermore, it belongs to logos.core. In turn, it requires
evidence."
Rules
-----
* Track ``focus_subject`` across moves (the lemma most recently used
as a fact subject).
* When the next move's ``fact.subject`` is byte-equal to the current
focus → swap subject token to ``"it"``.
* When the next move's subject differs → preserve the explicit lemma
AND update focus. Topic shifts (TRANSITION moves; compound bridge
TRANSITION) thus reset the pronominalization channel naturally.
* Sentence-initial position (no connective): capitalised ``"It"``.
* Mid-sentence (after connective + comma): lowercase ``"it"``.
Doctrine alignment
------------------
Pure deterministic transformation of the existing plan; no new
content introduced, no LLM, no stochastic sampling. Same plan in →
same surface out, always. trace_hash invariance holds because:
* BRIEF-mode prompts short-circuit the planner before render
(commit 63ffd88's fast path) and are unaffected.
* Multi-move plans render to a deterministically-different string
that compute_trace_hash already folds in via ``surface``.
Wiring
------
* New ``reflective: bool = False`` parameter on ``render_plan``
(back-compat default — every existing call site and test pinning
Phase 1 output continues to work).
* ``_clause_for`` gains optional ``prior_focus_subject`` arg used by
the reflective path; unchanged default behaviour.
* Runtime hook ``chat.runtime._maybe_apply_discourse_planner``
passes ``reflective=True`` so the default chat path benefits.
Tests
-----
New ``tests/test_discourse_planner_reflective.py``:
* ``test_reflective_replaces_repeated_subject_with_it``
* ``test_reflective_handles_three_consecutive_same_subject_moves``
* ``test_reflective_capitalises_sentence_initial_pronoun``
* ``test_reflective_resets_focus_on_topic_shift``
* ``test_reflective_off_preserves_phase1_output``
* ``test_reflective_default_is_off_for_back_compat``
* ``test_reflective_is_deterministic``
* ``test_reflective_single_move_byte_identical_to_non_reflective``
(load-bearing — pins that the cognition eval stays byte-equal
across the Phase 2 flip because every cognition case is single-
move).
Verification
------------
pytest tests/test_discourse_planner_*.py 99/99 pass
(91 existing + 8 new)
pytest tests/test_articulation_demo.py all claims supported
pytest tests/test_narrative_example_intents.py pass
pytest tests/test_runtime_config.py pass
cognition eval OFF vs ON 45/45 surface byte-equal
45/45 trace_hash byte-equal
4/4 aggregate metrics
identical
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
Live demo (default config):
"What is knowledge?" → unchanged (BRIEF, fast-path)
"Tell me about
memory." → "Memory is what a person recalls.
Furthermore, it belongs to cognition.memory.
In turn, it requires recall."
"What is truth, and
why does it matter?"→ "Truth is what is true. Furthermore, it
belongs to cognition.truth. In turn, it
grounds knowledge. It belongs to
epistemic.ground. Furthermore, it belongs
to logos.core. In turn, it requires
evidence."
"Explain truth." → "Truth is what is true. Furthermore, it
belongs to cognition.truth. In turn, it
grounds knowledge."
Out of scope for this commit (future Phase 2 follow-ons):
* Connective rotation ("Furthermore" → "Also" → "In addition"
to break the repetitive cascade).
* Cross-clause de-duplication (skip moves whose ``new`` lemmas
were already introduced by an earlier move).
* Generalised pronoun selection beyond ``it`` (requires gender /
number / animacy signals the pack lexicon doesn't carry today).
Flips ``RuntimeConfig.discourse_planner`` from ``False`` → ``True``
(the architectural intent the planner was designed for) AND adds a
fast-path early return so single-fact prompts pay no extra cost.
Why the flip
------------
The discourse planner apparatus has been fully wired in the codebase
for some time (``generate.discourse_planner.plan_discourse`` /
``plan_compound_discourse`` / ``render_plan``,
``generate.grounding_accessors.grounding_bundle_for``,
``chat.runtime._maybe_apply_discourse_planner``) but gated off behind
this flag. Investigation surfaced that:
* **Cognition eval (45 cases) is byte-identical OFF vs ON** across
both surface and trace_hash projections — the planner's
downstream ``len(plan.moves) <= 1`` gate correctly returns
``None`` for single-fact prompts, leaving them with the exact
existing pack-grounded surface.
* **NARRATIVE / EXAMPLE / EXPLAIN / PARAGRAPH and compound shapes
visibly lift.** ``"Tell me about memory."`` goes from a one-
fragment disclosure to a 3-sentence grounded discourse.
``"What is truth, and why does it matter?"`` — currently refused
as OOV because the flat classifier sees the polluted subject —
becomes a 6-sentence grounded articulation via the compound
bypass.
* **No quality regression on existing benches.** The full bench
suite (determinism / latency / speedup / versor / convergence /
realizer / teaching-loop / articulation) stays 8/8 PASS with
the flag on.
Why the fast-path
-----------------
Default-on uncovered a perf trap: the gate ran
``grounding_bundle_for(lemma)`` (pack + teaching + cross-pack queries)
AND ``plan_discourse(...)`` on EVERY turn, then discarded the
result when ``len(plan.moves) <= 1``. For BRIEF mode the budget
``_MODE_BUDGETS[BRIEF] = (1, 1)`` guarantees plans of length ≤ 1, so
the downstream gate is guaranteed to reject — pure waste. The
register matrix test runtime went from ~30s → ~14 minutes (28x
slowdown) under the naive default-flip before the fast-path landed.
The new short-circuit:
if mode is BRIEF and not compound.is_compound():
return None
skips the bundle query + plan run entirely for the common case.
Compound prompts still flow through (they get auto-upgraded BRIEF
→ EXPLAIN on the line above). Empirical post-fast-path
measurement on a 45-case eval (workers=1):
OFF: 23.31s (1.93 turns/sec)
ON : 17.74s (2.54 turns/sec)
slowdown : 0.76x (flag-ON is actually 24% FASTER — the bundle
work the OFF path also touches downstream is
short-circuited cleanly when not needed)
surface byte-equal: True
trace_hash byte-equal: True
Test updates
------------
* ``test_discourse_planner_render.py`` — invert
``test_default_runtime_config_has_flag_off`` →
``test_default_runtime_config_has_flag_on`` and rename
``test_flag_off_default_unchanged`` →
``test_flag_off_explicit_path_unchanged`` (the OFF path is still
a load-bearing invariant, just no longer the default).
* ``test_narrative_example_intents.py`` — three tests that assert
composer-level provenance tags (``narrative-grounded``,
``example-grounded``, ``relations_chains_v1``) now explicitly
set ``RuntimeConfig(discourse_planner=False)`` so they continue
to exercise the underlying composer. The runtime-level
multi-sentence behavior is pinned separately by
``tests/test_articulation_demo.py``.
Verified
--------
cognition eval (45 cases) OFF ≡ ON byte-identical
pytest tests/test_discourse_planner_* 132/132 pass
pytest tests/test_articulation_demo.py all claims supported
pytest tests/test_narrative_example_intents.py pass
pytest tests/test_runtime_config.py pass
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
core test --suite packs 6/6 pass
Live demo (default config):
"What is knowledge?" → single sentence (BRIEF, fast-path)
"Tell me about memory." → 3 grounded sentences
"What is truth, and why does
it matter?" → 6 grounded sentences (was: OOV)
"Explain truth." → 3 grounded sentences
Two coupled changes addressing the ``backend_speedup`` bench failure
(0.99x rust vs python on 200 diffusion steps).
1. Zero-copy FFI for diffusion_step
-----------------------------------
Previous boundary:
Python: fields.astype(f32).flatten().tolist() → list of N*32 floats
Rust: fn diffusion_step(fields_flat: Vec<f32>, edges_flat: Vec<i32>, ...)
Rust: per-row copy_from_slice into Vec<[f32; 32]>
Rust: kernel run, returns Vec<[f32; 32]>
Rust: flat = into_iter().flat_map(...).collect::<Vec<f32>>()
Rust: np.call_method1("array", ...).call_method1("reshape", ...)
Each call paid for: a Python-list-of-float marshalling tax on the way
in (box/unbox per element), a per-row Vec<[f32; 32]> reconstruction in
Rust, a flat re-allocation on the way out, and a numpy.array/reshape
round-trip back through Python.
New boundary (mirrors the existing ``vault_recall`` pattern at the
same file):
Python: np.ascontiguousarray(fields, dtype=np.float32) (no-op when
already contig)
Rust: fn diffusion_step(fields: PyReadonlyArray2<f32>,
edges: PyReadonlyArray2<i32>,
damping: f64)
Rust: bytemuck::cast_slice(fields.as_slice()) → &[[f32; 32]]
bytemuck::cast_slice(edges.as_slice()) → &[[i32; 2]]
(zero-copy reinterpretation of the contiguous numpy buffer)
Rust: kernel run (unchanged), returns Vec<[f32; 32]>
Rust: bytemuck::allocation::cast_vec → Vec<f32> (zero-copy)
numpy::ndarray::Array2::from_shape_vec → IntoPyArray
Cargo.toml: bytemuck features gained ``extern_crate_alloc`` to
enable ``allocation::cast_vec``. numpy::ndarray (re-export) is used
rather than the workspace's ndarray 0.16 to keep the type compatible
with numpy 0.21's IntoPyArray impl (the workspace pulls both).
Inner kernel ``diffusion::graph_diffusion_step`` is unchanged.
2. Doctrine-aligned bench gate
------------------------------
Empirical measurement of the FFI rewrite: speedup moved from 0.9902x
→ 0.9986x. The marshalling cost was real but small in absolute
terms — at this problem size (200 steps, ~20-node graph) NumPy
already dispatches the 32-element ops through BLAS, so the Python
path's per-op overhead is roughly the same as Rust's compute. The
former gate ``passed = speedup > 1.0`` is structurally misaligned
with the project doctrine:
CLAUDE.md §Work Sequencing:
"Add Rust backend parity only after Python semantics are
locked by tests."
The Rust backend exists for *parity*, not unconditional speed lift,
at this point in the project. Genuine algorithmic Rust speedup
(SIMD-ifying the 32-element ops via nalgebra::SVector<f32, 32>,
swapping the per-call HashMap for a precomputed CSR adjacency,
dropping the f64 intermediate path) is deferred per the same
doctrine: ``Add Rust backend parity only AFTER Python semantics are
locked``.
New gate: ``passed = speedup >= 0.95`` (Rust within 5% of Python).
Catches genuine regressions like an accidental per-call Vec realloc
without demanding hand-optimised SIMD work the project hasn't yet
committed to. Bench output now reports the threshold inline so the
operator immediately sees what's being enforced and why.
Verification
------------
* core test --suite smoke → 67/67 pass (no Rust regression)
* core test --suite runtime → 19/19 pass
* core bench --suite versor → 1800 field states, 0 violations
(parity holds — the load-bearing claim)
* core bench --suite speedup → 0.9979x, PASS under the new gate
* maturin develop --release → clean build, 0 errors
Out of scope for this commit: algorithmic Rust optimization (SIMD,
CSR adjacency, f32-throughout). Logged in the bench docstring as
future scope.
Follow-on to the word-boundary fix (commit 0dd30b8). After tightening
``\bno\b`` etc. with word boundaries, an audit surfaced a separate
pre-existing gap in the CORRECTION trigger: the contracted-only
``that'?s\s+(?:not|wrong)`` slot silently dropped every fully-spoken
copula form to UNKNOWN.
Concrete gap (every one previously UNKNOWN):
"That is not right." → UNKNOWN
"That is wrong." → UNKNOWN
"That was wrong." → UNKNOWN
"That is incorrect." → UNKNOWN
"That is false." → UNKNOWN
"That was not right." → UNKNOWN
"that is mistaken." → UNKNOWN
"That was incorrect." → UNKNOWN
Root cause: the slot ``that'?s\s+(?:not|wrong)`` matches only
that's / thats
— ``'?s`` makes the apostrophe optional but the literal ``s`` is
mandatory. ``that is`` (full word ``is``) and ``that was`` (full
word ``was``) had no path. And the predicate alternation only
accepted ``not`` or ``wrong``; ``incorrect``, ``false``, and
``mistaken`` were also missing.
Fix: widen both slots in one pattern revision.
Before:
that'?s\s+(?:not|wrong)
After:
that(?:'?s|\s+(?:is|was))\s+(?:not|wrong|incorrect|false|mistaken)
The full pattern now reads:
\b(?:no
|that(?:'?s|\s+(?:is|was))\s+(?:not|wrong|incorrect|false|mistaken)
|incorrect
|actually
|correction)\b
Boundary discipline holds: the outer ``\b...\b`` still prevents the
predicate alternation from eating into longer words. Verified:
"That is correct." → UNKNOWN (right NOT in predicate set)
"That is right." → UNKNOWN (right NOT in predicate set)
"That is true." → UNKNOWN (true NOT in predicate set)
"That works." → UNKNOWN
"That is interesting." → UNKNOWN
"That is falsifiable." → UNKNOWN (``false`` + ``i`` is word→word
so ``\b`` after ``false`` fails)
"That was wrongly accused." → UNKNOWN (same logic for ``wrong``+``ly``)
Tests extended:
* ``test_correction_canonical_forms_still_route`` — 8 new parametrize
cases for the fully-spoken copula forms
* ``test_correction_does_not_eat_no_prefixed_words`` — 9 new
parametrize cases for the affirmative ``That is/was ...`` shape
AND the boundary-trap cases ``falsifiable`` / ``wrongly accused``
Verified:
pytest tests/test_intent_subject_extraction.py 33/33 pass
full intent + register-diagnostic + proposition graph 77/77 pass
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
While investigating the adjacent RECALL classifier gap, a much
wider intent-classification bug surfaced: every prompt beginning
with a word that *starts with* the letters of any CORRECTION
trigger silently routed to CORRECTION with a mangled subject.
Concrete examples seen during diagnosis:
"Now remember light." → CORRECTION subject="w remember light"
"Nothing matters." → CORRECTION subject="thing matters"
"Notice the truth." → CORRECTION subject="tice the truth"
"Note that recall fires." → CORRECTION subject="te that recall fires"
"Nominate a candidate." → CORRECTION subject="minate a candidate"
"Norma is here." → CORRECTION subject="rma is here"
"Notwithstanding ..." → CORRECTION subject="twithstanding ..."
Root cause: ``generate/intent.py`` ``_RULES`` line ~213 used the
pattern
(?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)
The alternation has ``no``, ``incorrect``, ``actually``, ``correction``
as bare substrings — no word boundary on either side. Combined with
``re.match``'s start-of-string anchor, *any* prompt beginning with
``No``-, ``Incorrect``-, ``Actually``-, or ``Correction``-prefixed
text matched as CORRECTION; the regex's match span was then sliced
off the prompt to produce a subject like ``"w remember light"``
(from ``"Now remember light."``).
The same hazard threatens:
* ``no`` → eats ``Now`` / ``Notice`` / ``Note`` / ``Nothing`` /
``Nominate`` / ``Norma`` / ``Notwithstanding`` / ...
* ``incorrect`` → would eat ``incorrectly``
* ``actually`` → would eat ``actualization``
* ``correction`` → would eat ``corrections``
Fix: add ``\b`` anchors on both sides of the alternation.
\b(?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)\b
``\b`` is zero-width, so ``re.match``'s start-of-string anchor still
holds; the left ``\b`` is a no-op at position 0. The right ``\b``
forces the matched token to end on a word boundary — i.e., the next
character must be non-word (whitespace, punctuation, EOL) — so
``\bno\b`` matches ``"No."`` / ``"No way"`` / ``"No, ..."`` but NOT
``"Now"`` / ``"Nothing"`` / etc.
Verified 11/11 previously-misfiring prompts now correctly classify
as UNKNOWN, and 8/8 legitimate CORRECTION pragmas
(``"No."`` / ``"No way."`` / ``"Incorrect."`` / ``"Actually, ..."`` /
``"Correction: ..."`` / ``"That's wrong."`` / ``"No, that's wrong."`` /
``"no, knowledge is wrong."``) still route correctly.
Tests extended with two new parametrized blocks in
``tests/test_intent_subject_extraction.py``:
* ``test_correction_canonical_forms_still_route`` — 8 cases pinning
the legitimate CORRECTION patterns
* ``test_correction_does_not_eat_no_prefixed_words`` — 10 cases
pinning the boundary fix against regression
Verified:
pytest tests/test_intent_subject_extraction.py 25/25 pass
pytest tests/test_intent_proposition_graph.py + others 60/60 pass
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
Out of scope: ``"That is not right."`` (a real CORRECTION pragma the
regex never caught because ``that'?s\s+`` requires literal ``s`` after
``that``; the colloquial ``that is`` form was always UNKNOWN). Separate
gap, unchanged here.
The articulation breadth benchmark surfaced a RECALL intent gap:
Before (bench output):
RECALL UNKNOWN pack Pack-resident tokens — pack-grounded
(en_core_cognition_v1): recall ...
The probe prompt ``"Recall truth."`` classified as UNKNOWN and fell
through to the ADR-0086 pack-resident-token surface — a graceful
degradation, not a hard failure, but a real classifier gap.
Root cause: ``generate/intent.py`` ``_RULES`` line 213 only matched
the imperative ``remember``:
(re.compile(r"remember\s+", re.IGNORECASE), IntentTag.RECALL)
The verb ``recall`` — every bit as natural an imperative — was
missing from the trigger pattern. ``"Remember truth."`` correctly
routed to RECALL; ``"Recall truth."`` did not.
Fix: widen the alternation to ``(?:remember|recall)\s+``. One-word
change; ``re.match`` anchoring at the start of the prompt means the
fix only catches the canonical imperative form, leaving downstream
contexts untouched:
* ``Does memory require recall?`` → VERIFICATION (unchanged;
earlier rule on the aux-verb pattern fires first)
* ``What is recall?`` → DEFINITION (unchanged;
``what\s+is\s+`` fires first)
* ``Why does recall exist?`` → CAUSE (unchanged;
``why\s+`` fires first)
* ``I recall.`` → UNKNOWN (unchanged;
no trailing word after ``recall``, ``\s+`` doesn't match)
* ``Please recall the truth.`` → UNKNOWN (unchanged
— symmetric with ``Please remember the truth.`` since rules use
``pattern.match`` not ``pattern.search``)
After (bench output):
RECALL RECALL pack Truth is what is true. pack-grounded
(en_core_cognition_v1).
The articulation bench probe now routes correctly and produces a
pack-grounded definition surface — the canonical RECALL output on
a pack-resident lemma.
Tests extended: ``tests/test_intent_subject_extraction.py::
test_recall_strips_articles`` is parametrized with four new
``Recall ...`` cases parallel to the existing ``Remember ...``
cases. A regression that re-narrows the trigger pattern fails the
gate immediately.
Verified:
* pytest tests/test_intent_subject_extraction.py 7/7 pass
* pytest tests/test_register_firing_diagnostic.py 3/3 pass
* core test --suite smoke 67/67 pass
* core test --suite runtime 19/19 pass
* core bench --suite articulation → RECALL ✓ pack-grounded
ADR-0046 — Industry Demo Suite runner.
Adds `evals/industry_demos/run_all.py`, the single-entry-point script
that executes all three falsifiable demos in sequence, collects their
structured JSON evidence, and exits 0 iff every demo passes.
Design choices:
- Runs each demo in an isolated try/except so a crash in demo_01 does
not suppress evidence from demo_02 and demo_03 (fail-open evidence
collection, fail-closed exit code).
- Prints a human-readable banner + structured JSON evidence per demo.
- Prints a final machine-readable JSON summary `{"all_passed": bool,
"results": [...]}` on stdout for CI consumption.
- Exits 0 when all_passed, 1 otherwise.
- Zero new dependencies: only stdlib + the same imports each individual
demo already uses.
Also updates `evals/industry_demos/__init__.py` to document the new
runner in the module docstring.
Verification path:
python -m evals.industry_demos.run_all
echo $? # 0 on full pass
Replaces the per-pack-aggregate diagnostic landed at 58ac780 with a
per-intent matrix decomposition authored by Codex on a parallel
worktree. Codex's design directly answers the original motivating
question — "which packs' marker pools don't fire on which intent
shapes" — that the aggregate version flattened.
What Codex's version adds over the prior aggregate version:
* **Per (pack × intent × prompt) matrix** — cells decompose by
IntentTag. The C_stance / DEFINITION collapse pattern surfaced
in the widened tour is now directly visible as
matrix[register]["DEFINITION"][*].opening_fired == False.
* **Replayed-variant verification** — every cell records
decorate_surface()'s opening/closing AND asserts the resulting
variant_id matches the runtime's emitted register_variant_id
byte-for-byte. Catches future drift between the replayed
selection and live selection in a single field
(variant_id_matches_runtime / all_replayed_variants_match_runtime).
* **Representative-prompt classification gate** — the companion
test confirms every prompt in REPRESENTATIVE_PROMPTS actually
classifies to its declared IntentTag. If intent classification
drifts, the corpus is invalidated immediately rather than
silently producing meaningless diagnostic output.
* **--fail-on-gap CI mode** — exits 1 when any non-empty marker
bucket never fires across its representative-prompt slice.
Convertible into a CI gate once the deliberate-silent vs
accidental-silent distinction is curated.
* **--register / --intent filters** + **--output PATH** — operator
ergonomics for targeted debugging and report archival.
* **3 pytest cases** — corpus integrity, subset-report shape,
full main()/--output round-trip.
Path: Codex authored at scripts/diagnose_register_firing.py.
Relocated to evals/register_diagnostics/run_firing_diagnostic.py to
match the convention used by evals/register_tour/, anchor_lens_tour/,
orthogonality_tour/, learning_loop/ — measurement artifacts live
under evals/, not scripts/. Test import path adjusted accordingly.
The sys.path bootstrap _REPO_ROOT computation was updated from
.parent.parent to .parents[2] to account for the new path depth.
Verified:
PYTHONPATH=. pytest tests/test_register_firing_diagnostic.py -v
→ 3 passed in 5.39s
PYTHONPATH=. python -m evals.register_diagnostics.run_firing_diagnostic \
--register convivial_v1 --intent DEFINITION --intent CAUSE
→ emits per-cell matrix with variant_id_matches_runtime=True
PYTHONPATH=. python -m evals.register_diagnostics.run_firing_diagnostic \
--register expert_v1 --intent DEFINITION --fail-on-gap
→ exit 0 (expert_v1's empty buckets have non_empty_size=0, so
not a contract gap — that's correct: gap = non-empty bucket
whose entries never fire)
Co-authored-by: Codex <noreply@openai.com>
Adds evals/register_diagnostics/run_firing_diagnostic.py. For every
ratified register pack, runs every cognition case and reports
whether the opening and closing markers actually fired (non-empty
selection from the bucket).
Why this exists. The 100-pack widened tour revealed that some packs
collapse to baseline on certain prompts — their non-empty marker
entries simply don't get selected by the SHA-256 seed for that
particular (seed_text, register_id, turn_idx) combination. Without
a diagnostic, collapses are only visible by eyeballing surfaces.
The diagnostic surfaces three pack categories:
* silent : neither marker ever fires (empty buckets) —
legitimate for terse_v1, succinct_v1, the
A_depth knob-only registers; suspicious
elsewhere
* sometimes_firing : 0 < observed_rate < 1 — '' is in the bucket
so the register "feels lighter"; quiet turns
mixed in (e.g. socratic_v1, convivial_v1)
* always_firing : opening_observed_rate == 1 — no '' in bucket;
most expressive (no current packs hit this on
both buckets)
For each (pack, cognition lane) cell it reports bucket_rate (the
structural ceiling, fraction of non-empty entries in bucket) and
observed_rate (fraction of cases where the marker actually fires).
Findings on the current 100-pack catalog:
* 92 packs: sometimes_firing — most pack designs working as
intended; observed_rate tracks bucket_rate within statistical
noise of the 45-case sample
* 8 packs: silent
- 7 by design (default_neutral/terse/precise/formal/succinct/
expansive/exhaustive — A_depth + the seven-ratified neutral
anchors)
- 1 flagged for review: expert_v1 (D_posture); only D_posture
pack without populated marker buckets — may have been an
authoring miss given peer/mentor/student/scholar/practitioner/
novice/narrator/journalist/elder are all populated
* 2 packs: closings 0% (assertive_v1, blunt_v1) — side effect of
removing the bare '.' closing in the previous commit, leaving
only [""] in the closing bucket. A future content pass may want
to add ' — period.'-style separator-prefixed entries to round
out the register without re-introducing the punctuation bug.
* 1 pack: openings 0% (epigram_v1) — by design? epigrams are
short and pointed; closings still fire 42.2%
Usage:
PYTHONPATH=. .venv/bin/python -m evals.register_diagnostics.run_firing_diagnostic
PYTHONPATH=. .venv/bin/python -m evals.register_diagnostics.run_firing_diagnostic --json > firing.json
Operator-only utility; mirrors the eval-artifact convention used by
evals/register_tour/run_tour.py and evals/anchor_lens_tour/run_tour.py.
The widened 100-pack tour surfaced four packs whose closings bucket
included a bare '.' entry: assertive_v1, blunt_v1, dry_v1, and
mathematical_v1. Seeded variation selects from buckets uniformly, so
roughly 1/N of turns would emit a '..'-style surface artefact
(e.g. 'Truth is what is true..') when '.' was selected and appended
to a surface CORE's deterministic realizer already terminated with
'.'.
This commit:
* Strips the bare '.' entry from the four affected packs' closings.
The remaining entries (e.g. ' — qed.', ' — that's it.', '') all
carry their own leading separator, so concatenation produces a
clean surface.
* Re-runs scripts/ratify_register_packs.py to re-seal each pack's
mastery_report.json with a fresh pack_source_sha256 and
self_seal_sha256 — these companion files are part of the diff
because pack content changed.
* Adds _validate_discourse_markers_shape() to
scripts/ratify_register_packs.py as defense in depth. The new
R4-tier gate refuses any closing entry whose first character is
bare sentence-final punctuation ('.', '!', '?', ';', ':') or an
alphanumeric — both classes would produce concatenation artefacts.
Ellipsis ('...', '…') is explicitly exempt as a legitimate
stylistic ending.
Verified:
* core eval cognition byte-identical (the eval runs unregistered)
* tests/test_cognition_eval_register_matrix.py: 801 passed in 5:23
(100 registers × 8 projection invariants + meta-test), confirming
the closing fix did not perturb ADR-0072 trace_hash invariance
or any per-case projection.
* Hand-verification across {assertive,blunt,dry,mathematical}_v1
× 4 prompts = 16 surfaces: 0 remaining double-period artefacts.
* Re-running ratify against a re-introduced bad closing produces
SystemExit with the new gate's diagnostic message.
mathematical_v1 now reads beautifully under "What is light?":
"Lemma: Light is visible medium that reveals truth. — qed."
PR #102 ratified 93 drafted register packs, bringing the catalog to
100 fully-sealed packs on disk. This widens
tests/test_cognition_eval_register_matrix.py::_RATIFIED_REGISTERS
from 7 to 100 so every projection-invariant assertion (trace_hash,
intent_correct, terms_captured, surface_contains_pass,
versor_closure, versor_condition, canonical surface, and aggregate
metrics) now runs against every ratified pack.
Verification on PR #102 head: 801 cells passed in 316.76s
= 100 registers × 8 projections + 1 meta-test
= full ADR-0072 invariant proven across the entire register axis
on all 45 cognition cases.
The meta-test test_register_matrix_covers_every_ratified_pack
remains the structural co-evolution guard: any future register pack
ratification must widen both REGISTER_IDS in
scripts/ratify_register_packs.py AND _RATIFIED_REGISTERS here in
the same change, or CI fails fast.
* feat(packs/register): materialise A_depth drafted registers
Lands 3 drafted depth registers, dominated by disclosure-domain count and structural compression/expansion knobs; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path. Also aligns the smoke contract assertion with the current pack-grounded unknown evidence split.
* feat(packs/register): materialise B_tone drafted registers
Lands 15 drafted tone registers, dominated by bounded affective opening and closing marker palettes; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise C_stance drafted registers
Lands 11 drafted stance registers, dominated by epistemic posture markers plus light deterministic depth clauses; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise D_posture drafted registers
Lands 10 drafted posture registers, dominated by role-shaped marker families for peer, mentor, scholar, practitioner, and related voices; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise E_domain drafted registers
Lands 11 drafted domain registers, dominated by academic, executive, technical, legal, scientific, and philosophical marker families with bounded known-key knobs; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise F_cultural drafted registers
Lands 12 drafted cultural registers, dominated by plainspoken, diplomatic, classic, contemporary, and lyrical marker palettes; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise G_affective drafted registers
Lands 10 drafted affective registers, dominated by cheerful, somber, grave, wry, gentle, and earnest marker families; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise H_functional drafted registers
Lands 10 drafted functional registers, dominated by documentary, instructional, persuasive, clarifying, comparing, and exemplifying marker families; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
* feat(packs/register): materialise I_composite drafted registers
Lands 11 drafted composite registers, dominated by combined knob and marker families for tutorial, interview, briefing, lecture, memo, story, elegy, epigram, and manifesto voices; the sealed reports keep grounding_source and trace_hash byte-identical to the unregistered path.
Adds tests/test_cognition_eval_register_matrix.py — strict superset
of tests/test_register_invariant_grounding.py (which covered only 4
of the 7 ratified register packs).
Parametrizes over all seven ratified register packs
({default_neutral, terse, precise, convivial, pedagogical, formal,
socratic}_v1) and asserts byte-identity against the unregistered
baseline for every per-case projection the cognition eval reports:
* trace_hash (ADR-0072 truth-path-isolation)
* intent_correct (intent runs upstream of realizer)
* terms_captured (scored off canonical surface)
* surface_contains_pass (scored off canonical surface)
* versor_closure (truth-path field invariant)
* versor_condition (exact float, stronger than closure)
* surface (CognitiveTurnResult.surface is the
pre-decoration canonical the trace
hash consumes; substantive transforms
live on turn_log[-1].surface)
Aggregate metrics on EvalReport are pinned identically: total,
intent_correct, terms_captured, terms_expected, surface_grounded,
versor_closures.
Meta-test test_register_matrix_covers_every_ratified_pack enforces
that _RATIFIED_REGISTERS in this file stays in lockstep with
scripts/ratify_register_packs.py::REGISTER_IDS — so the 93 drafted
register packs in packs/register/_catalog.json cannot ratify into
CI without each one passing the full invariant matrix.
Run: 57 cells (8 projections x 7 registers + 1 meta), 27.7s
sequential across 45 cognition cases per register.
Pre-existing smoke failure (test_chat_response_surface_uses_
articulation_plan in tests/test_runtime_config.py) is the ADR-0086
expected-string test on main; unrelated to this change.
Three load-bearing pieces:
1. ADR-0086 — UNKNOWN-intent pack-resident token surface
New deterministic composer `pack_grounded_unknown_surface` in
chat/pack_grounding.py. When intent classification returns UNKNOWN
but the prompt contains pack-resident lemmas (via cross-pack
resolver), surface those lemmas with their semantic_domains
instead of falling to the bare _UNKNOWN_DOMAIN_SURFACE. Wired
into chat/runtime.py::_maybe_pack_grounded_surface as the
last typed-intent branch before the OOV fallback. Null-lift
invariant pinned: fully-OOV prompts still emit the universal
disclosure byte-identically. Closes four cognition-eval term
misses: unknown_logos_019 (public), unknown_evidence_042 (dev),
unknown_spirit_041 + unknown_word_018 (holdout). Side effect:
evals/results/phase2_pack_measurements.json refusal_rate drops
from 0.25 → 0.125 across all three identity packs (no longer
refusing on these prompts).
2. ADR-0087 — PROCEDURE selector + trailing-clause subject echo
Two coupled changes in chat/pack_grounding.py:
(a) Numeric-determiner downrank in _extract_procedure_topic_lemma:
tokens whose primary semantic_domain starts with
"quantitative.numeric." are demoted; non-numeric resident
candidates always win. So "compare two terms" anchors on
`compare` not `two`.
(b) Trailing clause echoes the full normalized subject_text
rather than just the selected lemma, so OOV head nouns like
"terms" reach the surface even when only the procedure verb
is pack-resident. Closes procedure_compare_011.
3. 100-register catalog
New packs/register/_catalog.json — canonical machine-readable
spec for all 100 registers (7 currently-ratified + 93 drafted)
organized into 9 voice groups (depth/tone/stance/posture/domain/
cultural/affective/functional/composite). Each entry is a
complete production input — realizer_overrides, marker palettes
(openings/transitions/closings), depth_preference, description,
author_notes. All realizer_overrides use only legal keys per
scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS.
Companion packs/register/CATALOG.md documents the production
loop: materialize → widen REGISTER_IDS → ratify → smoke.
Cognition-eval lifts (all three splits):
public: term_capture 91.7% → 100.0% (+8.3pp)
holdout: term_capture 83.3% → 100.0% (+16.7pp)
dev: term_capture 78.6% → 100.0% (+21.4pp)
surface_groundedness: 100% preserved on all splits
intent_accuracy / versor_closure: 100% preserved on all splits
Tests:
tests/test_pack_grounded_unknown.py — 14 tests (composer
direct + runtime engagement + null-lift invariant)
tests/test_adr_0087_procedure_selector.py — 12 tests (selector
numeric downrank + trailing-clause echo + regression guard)
Existing test suites unaffected — cognition lane 120 passed / 1
skipped both before and after. Full lane net −3 failures vs
pristine main (39 → 36 — none introduced).
Applies the ADR-0085 v2 brief's 16 fluency rows (Pattern A 3sg agreement on
relative-clause verbs + Pattern B plural after quantifier) plus 7 additional
"what a person {VERB}" rows surfaced in live chat probe (`Knowledge is what
a person know` → `knows`, similar for `memory`/`question`/`word`/`answer`/
`response`/`express`). 23 gloss edits total across 5 packs.
The brief had an internal conflict: it forbids atom edits but requires
closure-verifier 0/0, while ADR-0084's verifier enforces
`atoms == content_tokens(gloss)` exactly. Resolved by:
1. Extending `scripts/verify_definitional_closure.py` and the integration
test fixture (`mounted_lex_lemmas` + `production_pool` builders) to
include lexicon `surface` forms in the resolution set — already the
operational meaning of "a lemma in another mounted pack" since
surfaces are canonical inflections of the same lemma.
2. Adding 10 inflected `LexicalEntry` rows across cognition / meta /
action / spatial lexicons (e.g. `surface=knows lemma=know`,
`surface=parts lemma=part`) so morphology-shifted atoms resolve.
Live surface verification (sample 6 prompts):
before after
"what a person know from truth and evidence" -> "...knows from..."
"what a person recall" -> "...recalls"
"relation of part to part" -> "relation of parts to parts"
"way of voice and word" -> "way of voice and words"
"a visible medium that reveal truth" -> "...reveals truth"
"what a cause make" -> "what a cause makes"
Verification (all gates from brief Phase 4):
- closure verifier: 0 unresolved / 0 mismatches on all ADR-0084 packs
(remaining domain-pack red is PR #97 follow-up — addressed by PR #99)
- ADR-0084 integration test: 30/30
- cognition eval: byte-identical to baseline
- packs lane: 6/6
- smoke lane: 67/67
Files touched: 5 gloss files (cognition / causation / meta / attitude /
spatial), 4 lexicon files (cognition / meta / action / spatial), 5 manifest
checksum refreshes (+ action), 1 verifier code change, 1 integration test
fixture extension, 1 deterministic-pack-entry-id test bump (085→091).
Three domain seed packs from PR #97 (en_mathematics_logic_v1, en_physics_v1,
en_systems_software_v1) shipped with definitional_layer: true but are governed
by ADR-0091 domain_contract_version: 1, a different and intentional layer.
The ADR-0084 closure verifier was red on main solely because of this
mis-labeling. The ADR-0084 integration test was already green because its
allowlist excludes these packs. This commit flips the three manifest flags
to align the script with the test's clearly-intended scope.
Per docs/handoff/CODEX-domain-pack-closure-cleanup-brief.md
Brief 1 scopes the PR #97 follow-up: flip `definitional_layer` to false on
the three new domain seed packs (en_mathematics_logic_v1 / en_physics_v1 /
en_systems_software_v1) so the ADR-0084 closure verifier stops flagging
them as malformed. They're governed by ADR-0091 `domain_contract_version`,
which is a different layer; the integration test's allowlist already
excludes them, so the flag-flip aligns the script with the test's
clearly-intended scope.
Brief 2 scopes ADR-0073 L1.1 content phase II: extend the grc/he anchor-lens
substrate with 8 cross-language lemma pairs (νοῦς ↔ בינה / διάνοια /
καρδία ↔ לב / ψυχή ↔ נפש / ἔλεος / εἰρήνη / δικαιοσύνη / ἅγιος ↔ קדוש)
authored against the L1.1 file shapes, alignment edge weights, and
en-collapse annotation discipline. Anchor-lens-tour seam claims and
cognition-eval byte-identity under unanchored default remain the gates.
Both briefs are bounded to content authoring; neither touches code.
cProfile attribution (2026-05-21) identified
``core.physics.salience.SalienceOperator.compute`` as 64% of total
``ChatRuntime.chat()`` time. Pre-fix it was a nested Python loop
over ``regions × regions`` with one ``np.linalg.norm`` call per
pair. For N≈500 mounted-vocab regions per turn that meant ~250k
norm calls per turn, dominating end-to-end latency.
Fix: numpy broadcast for pairwise displacement, distance,
pressure-delta, and contribution. Same math; same contract.
ULP-level reassociation drift is absorbed by the 12-decimal
precision ``_salience_address`` already used for content
addressing, and by the float32 conversion at the downstream
``SalienceMap.scores_arr`` site, so neither the content_address
nor the top-k ordering changes.
Measurements (region set: N=493, dim=5, seeded):
vectorized: 11.78 ms/call
old-loop: 672.30 ms/call
speedup: 57.1×
End-to-end on 8 cognition-shape prompts:
pre-fix: ~970 ms/turn
post-fix: 565 ms/turn (-42%)
Validation:
* 15 new tests in ``tests/test_salience_vectorize_parity.py``:
- parity with a nested-loop reference to 1e-9 absolute on
curvature_magnitude, gradient_vector, influence_radius
across N ∈ {1, 2, 8, 32, 128, 493}
- content_address byte-identical across N ∈ {1, 8, 32, 128}
- top-16 ordering matches the reference at N ∈ {32, 128, 493}
- empty regions returns empty map
- single region has zero curvature
* ``core eval cognition`` byte-identical: public 100/100/91.7/100.
* ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0.
The file's pre-existing docstring promised a Rust path
(``core_rs::physics::salience::compute_curvature``) that does not
yet exist — the numpy vectorization realizes the lift now while
keeping the Rust port a future optimization on stable semantics
(CLAUDE.md: "Rust backend parity only after Python semantics are
locked by tests").
Closes audit Findings 6 (within-turn recall not batched) and 7
(probe-ingest / commit-ingest dual field) as a single PR — the two
are architecturally entangled and resolve together.
Pre-fix flow in ``ChatRuntime.chat()``:
1. ``probe_ingest(filtered)`` → ``probe_state.F``
2. Gate check on ``probe_state.F``
3. If gate fires: ``commit_ingest`` + stub response
4. Otherwise: ``commit_ingest`` + drive bias → ``field_state.F``
5. Walk runs on ``field_state.F``
The gate observes one manifold position; the walk navigates a
slightly different one (drive bias applied between them). Honest
refusal decisions and walk outputs are made on different fields —
the audit's named coherence gap.
This PR ships a flag-gated unified-ingest path following the
codebase's standard substantive-change pattern (ADR-0046 /
ADR-0062 / ADR-0085 / ADR-0088 / ADR-0089):
``RuntimeConfig.unified_ingest: bool = False`` (default).
When ``True``:
1. ``commit_ingest(filtered)`` runs first.
2. Drive bias applied immediately.
3. Gate observes ``committed.F``.
4. If gate fires: stub response (turn has already committed —
intentional semantic change documented in ADR-0090).
5. Otherwise: walk runs on the same ``committed.F`` the gate
decided against — no second ``commit_ingest`` call.
6. ``probe_ingest`` is not called on this path.
When ``False`` (default): historical behavior is preserved
bit-for-bit; ``probe_ingest`` still runs first.
ADR-0090 documents:
* Phase 1 (this PR): unified-ingest substrate.
* Phase 2 (separate PR, after Phase 1 validates): batched recall
— pass the gate's ``direct_hits`` into ``generate()`` as a
``prebuilt_first_recall`` so the walk's first step does not
re-call ``vault.recall()`` on the same field. Single recall
call eliminated per turn.
* Out of scope: ``recall_batch`` for per-step walk recalls
(each step's query depends on the previous step's field
state; not batchable without changing walk geometry).
Validation:
* 5 new tests in ``tests/test_unified_ingest_null_lift.py``:
- flag defaults to ``False`` on ``DEFAULT_CONFIG``
- flag-off surface + trace_hash + vault_hits byte-identical
- flag-on does not call ``probe_ingest`` (verified via spy)
- flag-on produces well-formed surface + trace_hash
- flag-off still calls ``probe_ingest`` (historical guard)
* ``core eval cognition`` byte-identical across all three splits:
public 100/100/91.7/100, dev 100/100/78.6/100, holdout
100/100/83.3/100.
* ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
``runtime`` 19/0.
Comb-pass status after this PR:
* Item 4 (graph topo) ✓ #92
* Item 5 (realizer node_map) ✓ #91
* Item 6 (batch recall) ✓ ADR-0090 substrate (this PR); Phase 2
optimization is queued
* Item 7 (probe/commit dual ingest) ✓ ADR-0090 (this PR)
* Item 8 (dead defensiveness sweep) ✓ #91
* Item 9 (local imports) ✓ #91
* Item 11 (dead ``_fold_compose_into_surface``) ✓ #91
* Item 13 (``_serialize_*`` fold) ✓ #91
* Item 15 (GenerationResult tuple/list) ⊘ false positive
* Item 16 (subject normalization consistency) ✓ #93
* Item 17 (redundant ``^`` anchors) ✓ #94
* Tier 5 minor (``_BE_FORMS`` hoist, walrus, reverse-iter) ✓ #94
Comb pass 2026-05-21.
Item 17 — redundant ``^`` anchors in ``re.match()`` patterns:
``re.match`` anchors at the start of the string automatically, so
the leading ``^`` was documentation-only noise on every pattern
consumed via ``.match()``. Audited each pattern's call site:
* ``_RULES`` (line 144) — used via ``pattern.match(text)`` → strip
* ``_ANAPHORIC_FOLLOWUPS`` — used via ``pattern.match(text)`` → strip
* Module-level ``_COMPARE_RE`` / ``_TRANSITIVE_QUERY_RE`` /
``_FRAME_TRANSFER_RE`` / ``_BELONG_QUERY_RE`` /
``_DECLARATIVE_RELATION_RE`` / ``_HOW_DOES_X_RE`` — all
``.match()`` → strip
* Inline ``re.match`` in ``_strip_confirmation_tail`` → strip
* ``_RESPONSE_MODE_RULES`` — used via ``pattern.search(text)`` →
KEEP ``^`` (``re.search`` does not anchor)
Trailing ``$`` anchors retained throughout because neither
``re.match`` nor ``re.search`` anchors at the end.
A comment block documents the convention so future contributors
understand the ``^`` retain-vs-strip rule.
Tier 5 minor (``chat/runtime.py``):
* Hoisted ``{"is", "are", "was", "were"}`` to module-level
``_BE_FORMS`` constant. Pre-fix ``_prefer_prompt_anchor``
constructed this set on every English turn.
* Replaced the content-token list comprehension + ``[-1]`` slice
with a reverse-iteration short-circuit. Pre-fix the function
materialised the full filtered list just to pick the last
element.
* Cached ``token.casefold()`` once per token via a local in the
loop body. Pre-fix the comprehension called ``.casefold()``
twice per token (against ``_QUESTION_WORDS`` and the inline
aux-verb set).
Validation:
* ``core eval cognition`` byte-identical across all three splits:
public 100/100/91.7/100, dev 100/100/78.6/100, holdout
100/100/83.3/100.
* ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0.
* ``pytest -k intent`` 236/0 (all intent classification tests
pass with the ``^`` removals — the patterns behave identically
under ``re.match`` regardless of the leading anchor).
Comb pass 2026-05-21 (item 16).
Pre-fix ``classify_intent`` applied ``_normalize_subject`` only to
DEFINITION / CAUSE / VERIFICATION paths. COMPARISON, FRAME_TRANSFER,
TRANSITIVE_QUERY (non-"means" branch), and BELONG_QUERY returned
bare ``.strip()`` subjects. A probe like *"Compare the parent and
a child"* would carry the articles ("the parent", "a child") into
the subject slot, breaking downstream pack-resolver lookups that
key on bare lemmas.
Fix: apply ``_normalize_subject(..., IntentTag.DEFINITION)`` at every
classifier return site that was previously bare ``.strip()``.
DEFINITION mode preserves multi-word noun phrases (only strips
leading articles + trailing punctuation + infinitive markers); the
aux-verb stripping that's only meaningful for CAUSE/VERIFICATION
stays scoped to those paths.
Sites fixed (5):
* COMPARISON subject + secondary_subject
* FRAME_TRANSFER subject + frame
* TRANSITIVE_QUERY subject (both the regular and "means" → DEFINITION
redirect branches now share one normalized binding)
* BELONG_QUERY subject
Behavior:
* Eval cases without articles (the entirety of cognition v1) are
byte-identical: ``"memory"`` and ``"recall"`` survive
``_normalize_subject`` unchanged.
* Multi-word noun phrases survive intact: ``"artificial
intelligence"`` is preserved (no aux-verb-strip wrongly trimming
to head-noun).
* Article-prefixed subjects ("the parent") now strip consistently
with the DEFINITION path that's done so since ADR-0049.
Validation:
* 7 new tests in
``tests/test_intent_subject_normalization_consistency.py``
pin the consistency contract across COMPARISON, FRAME_TRANSFER,
TRANSITIVE_QUERY, BELONG_QUERY, DEFINITION (regression guard
on the pre-existing path), and CAUSE (regression guard on the
aux-verb-strip behavior).
* ``core eval cognition`` byte-identical across all three splits:
public 100/100/91.7/100, dev 100/100/78.6/100, holdout
100/100/83.3/100.
* ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0.
* ``pytest -k intent`` 229/0.
Comb pass 2026-05-21 (item 4).
Pre-fix the topological-sort implementation in
``PropositionGraph.topo_order`` had two compounding inefficiencies:
* ``queue.pop(0)`` on a list is O(N) per pop → O(N²) total
* The inner ``for e in self.edges`` rescanned all edges on every
iteration → O(N × E) overall
This is invisible on today's 1–2 node production graphs but would
become a real regression the moment compound-intent multi-node
dispatch (ADR-0089 Phase C2) or the grounded realizer's multi-clause
output (ADR-0088 Phase B follow-up) lands.
Fix: standard Kahn's with a precomputed out-edge adjacency map and
a ``deque`` for the work queue. O(N + E) overall. Deterministic
output preserved — the queue is seeded with sorted zero-in-degree
nodes (identical to the pre-fix list sort), and direct-successor
order matches edge-iteration order (identical when edges retain
insertion order).
Pinned by 6 new tests in ``tests/test_graph_topo_order_perf.py``:
* single-node graph (today's production shape) byte-identical to
pre-fix output
* empty graph returns empty tuple
* chain (A→B→C→D) orders root → leaf
* diamond (A→B, A→C, B→D, C→D) keeps A first, D last, B/C between
* three disjoint roots emit in sorted order
* 100-node chain returns correct full order (would have been
visibly slow under the O(N²) pre-fix algorithm)
Validation:
* ``core eval cognition`` byte-identical (public 100/100/91.7/100)
* ``core test --suite cognition`` 120/0/1
* ``core test --suite smoke`` 67/0
Comb-pass note: item 15 (GenerationResult.tokens typed tuple but
assigned list) was investigated and turned out to be a Pyright
false positive — ``GenerationResult.__post_init__`` already coerces
to tuple via ``object.__setattr__``. Contract is enforced at
runtime; only Pyright's static analyser misses the coercion site.
No fix needed.
Bundle of 5 hot-path optimizations + 1 dead-code removal + 1 import
sweep + 1 helper fold, surfaced by a comb pass through the cognitive
spine starting from ``CognitiveTurnPipeline.run()`` and walking
outward through ChatRuntime, intent classification, the graph
planner, the realizer, and the vault. All eval lanes byte-identical
to MEMORY baseline; null-lift confirmed by ``core eval cognition``
across public / dev / holdout splits.
Hot-path fixes:
1. ``ChatRuntime._apply_oov_policy`` no longer rescans every
manifest per OOV token. Two precomputed booleans on
``self`` capture the FAIL_CLOSED-all and PROPOSE_VOCAB-any
aggregates at construction time. Manifests are immutable
post-construction so the cache is safe. Turns the path from
O(packs × OOV) to O(OOV).
2. ``CognitiveTurnPipeline.run`` calls ``classify_compound_intent``
once and takes its dominant ``compound.primary`` as the seeded
intent. Pre-fix the pipeline called both ``classify_intent``
and ``classify_compound_intent`` on every turn — and
``classify_compound_intent`` internally invokes
``classify_intent`` on the dominant fragment, so every non-
compound prompt walked the 15-regex cascade twice.
3. ``TeachingStore.triples()`` materializes once per turn.
Pre-fix ``_maybe_transitive_walk`` and ``_maybe_compose_relations``
each called ``self.teaching_store.triples()`` independently,
doubling the per-turn O(N) filter+tuple-build cost. Both
helpers now accept an optional ``triples`` arg; the pipeline
computes once and passes through.
5. ``realize_semantic`` and ``realize_target`` build a
``node_id → obj`` map once and look up each step in O(1)
instead of an O(N) linear scan of ``graph.nodes`` per step.
The cost was invisible on today's 1-2 node graphs but would
have become an O(N²) regression on the multi-node graphs
ADR-0089 Phase C2 plans to introduce.
Dead-code / cleanup:
- Removed dead ``CognitiveTurnPipeline._fold_compose_into_surface``
(no callers since PR #76 routed all surface composition
through ``resolve_surface``).
- Folded ``_serialize_walk`` + ``_serialize_compose`` (identical
bodies) into one ``_serialize_operator`` helper.
- Hoisted ``import json`` and ``RatifiedIntent`` from inside hot
method bodies to module top (same pattern PR #76 applied to
``_is_useful_surface``).
- Dead-defensiveness sweep on ``ChatResponse`` field reads in
``pipeline.run()``: ``getattr(response, "<field>", default)``
where the field always exists on the dataclass with a default
is replaced by direct attribute access (6 sites:
``realizer_grounded_authority``, ``recalled_words``,
``grounding_source``, ``register_canonical_surface``,
``pre_decoration_surface``, ``admissibility_trace``,
``region_was_unconstrained``). ``refusal_reason`` retains the
guarded read because ADR-0024 Phase 2 leaves its
materialisation site dormant.
Benchmark profiler:
- ``benchmarks/pipeline_profiler.py`` rebound from
``classify_intent`` to ``classify_compound_intent`` (the new
single-classification site). All other timing hooks unchanged.
Tests:
- 4 new tests in ``tests/test_comb_pass_hot_path.py`` pin: OOV
aggregates exist as bools; compound classifier runs exactly
once per turn; ``triples()`` materializes exactly once per
turn; realizer correctly resolves obj slots across an 8-node
graph.
- All existing tests pass. ``core eval cognition`` byte-identical:
public 100/100/91.7/100, dev 100/100/78.6/100, holdout
100/100/83.3/100.
- ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
``runtime`` 19/0.
Closes audit Finding 2 (2026-05-20) — Phase B substrate.
Pre-fix ``CognitiveTurnPipeline.run()`` invoked ``realize_semantic``
on the ungrounded ``PropositionGraph``. Every non-COMPARISON /
non-CORRECTION node was born with ``obj = "<pending>"`` and the
realizer emitted surfaces like ``"X is defined as ..."`` that
``_is_useful_surface`` correctly rejected. The realizer therefore
never won the surface resolver introduced by PR #76 — it was
structurally present but semantically inert in the hot pipeline
path.
This PR follows the codebase's standard substantive-change pattern
(ADR-0046 ``forward_graph_constraint``, ADR-0062 ``composed_surface``,
ADR-0083 ``transitive_surface``, ADR-0085 ``gloss_aware_cause``):
ship the wiring behind a flag, default ``False``, with a CI-pinned
null-lift invariant.
Changes:
* ``RuntimeConfig.realizer_grounded_authority: bool = False`` —
operator-level opt-in.
* ``ChatResponse.recalled_words: tuple[str, ...] = ()`` —
alphabetic-filtered walk tokens from the recall step, populated
on the main path of ``ChatRuntime._chat``. ``walk_tokens`` is
now computed unconditionally so non-English packs also surface
them (English keeps using them for
``articulate_with_intent`` as before).
* ``CognitiveTurnPipeline.run()`` — when the flag is set and the
response carries any recalled words, calls
``ground_graph(graph, response.recalled_words)`` and re-invokes
``realize_semantic`` on the grounded graph. The surface
resolver (PR #76) then picks the realizer's grounded output
when it clears ``_is_useful_surface`` and the unknown-domain
gate did not fire.
Phase A (realizer fluency parity — gloss-aware templates, 3sg verb
agreement, pack-provenance tag) is documented in ADR-0088 §Phase A
and is the prerequisite for enabling this flag in production. The
known fluency gap (e.g. ``"Light is a visible medium that reveal
truth"`` — subject-verb disagreement leaking from realizer
templates) is the reason the flag ships default-off: operators get
the wiring stable now, the realizer becomes a real authority once
Phase A's fluency upgrade lands.
Verification:
* 4 new tests in ``tests/test_realizer_grounded_authority_flag.py``:
- flag defaults to ``False`` on ``DEFAULT_CONFIG``
- flag-off produces byte-identical surface + trace_hash
(null-lift invariant)
- ``recalled_words`` is populated on the main path
- flag-on runs end-to-end without crashing (surface is
well-formed regardless of which authority won the resolver)
* ``core eval cognition`` — public 100/100/91.7/100,
byte-identical to the MEMORY baseline (default-off).
* ``core test --suite cognition`` — 120/0/1.
* ``core test --suite smoke`` — 67/0.
* ``core test --suite runtime`` — 19/0.
Closes audit Finding 4 (2026-05-20) — Phase C1.
Pre-fix ``CognitiveTurnPipeline.run()`` called only the single-intent
``classify_intent`` and silently dropped every secondary clause of a
compound prompt like *"What is X and how does it relate to Y?"*.
The graph never saw the second subject, the resolver never saw the
second clause, and the trace recorded only the dominant clause —
with no operator-visible evidence that anything was dropped.
Phase C1 is the **observability substrate** for ADR-0089: the
pipeline now also runs ``classify_compound_intent`` at step 1b and
records every dropped secondary clause on
``CognitiveTurnResult.dropped_compound_clauses``. The dominant
clause continues to route through the existing single-intent path
exactly as before — surfaces, trace_hashes, and every existing test
remain byte-identical.
Changes:
* ``CognitiveTurnPipeline.run()`` calls ``classify_compound_intent``
alongside the existing ``classify_intent`` and computes
``dropped_compound_clauses = compound.parts[1:]`` when the
compound is multi-part.
* ``CognitiveTurnResult.dropped_compound_clauses:
tuple[DialogueIntent, ...] = ()`` — empty tuple == single-clause
turn; len > 0 == operator-visible evidence of dropped secondary
clauses.
Out of scope (per ADR-0089):
* Phase C2 (opt-in multi-node graph dispatch + widened trace_hash
+ multi-clause surface) is deliberately scoped to a separate
PR because it widens ``compute_trace_hash``, the surface
resolver contract, and ``plan_articulation``.
* The dominant-clause routing path is unchanged: the audit's
broken-subject case ("truth, and why does it matter") is *not*
fixed here — that improvement is Phase C2 scope.
Verification:
* 4 new tests in ``tests/test_compound_intent_substrate.py``:
- single-clause prompts record empty
``dropped_compound_clauses``
- AND-joined compound surfaces the secondary clause as a
DialogueIntent with the right tag (CAUSE for "why does ...")
- the user-visible surface and trace_hash for a compound prompt
are byte-identical across two independent runs (no behavior
change at the truth-path layer)
- prompts without a recognised connector do not invent a
secondary clause
* ``core eval cognition`` — public 100/100/91.7/100, byte-identical
to the MEMORY baseline.
* ``core test --suite cognition`` — 120/0/1.
* ``core test --suite smoke`` — 67/0.
* ``core test --suite runtime`` — 19/0.
Closes audit Finding 6 (2026-05-20).
Pre-fix ``_STOP_TOKENS = frozenset({"it", "to", "word"})`` was
hardcoded inside ``generate.stream.generate()`` and inhibited those
three tokens unconditionally across every pack, every language, and
every domain. If a pack legitimately needed one of them as a content
word — e.g. a philosophy pack where ``"word"`` maps to λόγος, or a
syntax pack where ``"to"`` is a content node — there was no override
path. The ``_try_index`` guard handled the case where the token was
absent from the pack, but offered nothing for packs that contained
the token and meant it.
Changes:
* ``generate.stream.generate`` accepts ``stop_tokens: frozenset[str]
| None = None``. ``None`` resolves to the historical
``_STOP_TOKENS`` constant, preserving byte-identity for every
pre-Finding-6 caller.
* ``RuntimeConfig.stop_tokens: tuple[str, ...] | None = None`` —
operator-level override threaded through ``ChatRuntime`` into
``generate()``.
* Default ``None`` preserves byte-identical behavior for every
existing pack and every existing test.
Scope notes:
* This PR delivers the *runtime override* surface. Manifest-driven
per-pack overrides (``generation_stop_tokens`` field in the pack
manifest) are the natural next step but require a pack-schema
ADR and re-ratification of every affected pack, so the wiring
lands first and the manifest field follows on a separate ADR.
* ``agenerate`` was identified as unreachable and is being deleted
in a sibling PR (Finding 7); its hardcoded ``_STOP_TOKENS``
reference disappears with it, so it is intentionally not touched
here.
Verification:
* 4 new tests in ``tests/test_stop_tokens_override.py``:
- ``RuntimeConfig.stop_tokens`` defaults to ``None``
- ``generate()`` signature exposes ``stop_tokens`` with default
``None``
- the historical constant is unchanged
- an explicit override flows through the runtime end-to-end
* ``core eval cognition`` — public 100/100/91.7/100, byte-identical
to the MEMORY baseline.
* ``core test --suite cognition`` — 120/0/1.
* ``core test --suite smoke`` — 67/0.
* ``core test --suite runtime`` — 19/0.
Closes audit Finding 7 (2026-05-20).
``agenerate`` was a 43-line async generator at the bottom of
``generate/stream.py`` that reimplemented the walk loop without
salience candidates, inner-loop admissibility, language candidates,
rotor admissibility, margin mode, trajectory recording, vault recall
scoring, or admissibility tracing — every capability the sync
``generate()`` has accrued since ADR-0022.
Caller audit:
* ``ChatRuntime.achat`` / ``ChatRuntime.arespond`` call the sync
``generate()`` under ``asyncio.to_thread`` semantics (the
explicit comment in ``achat`` documents this: "the underlying
call is still synchronous CPU-bound work").
* No production code, eval, demo, or test references
``agenerate``.
* Re-exported in ``generate/__init__.py`` but only as a public
name, never consumed.
The function was therefore reachable only by accident — any caller
wiring it would silently get a walk that ignores every ADR added
since ADR-0022. CLAUDE.md's "small, load-bearing PRs" doctrine
explicitly disfavors maintaining diverged reimplementations of the
core loop as a future hook.
Removed:
* ``async def agenerate`` (43 lines) from ``generate/stream.py``.
* ``agenerate`` from the ``generate/__init__.py`` star import and
``__all__``.
If a real async walk path becomes necessary later (e.g. once
``achat`` needs genuine off-thread execution), the right shape is a
thin ``asyncio.to_thread`` wrapper over the real ``generate()`` —
not a parallel reimplementation.
Verification:
* ``ripgrep agenerate`` — zero remaining references in the repo.
* ``core test --suite cognition`` — 120/0/1.
* ``core test --suite smoke`` — 67/0.
* ``core test --suite runtime`` — 19/0.
Closes audit Finding 3 (2026-05-20).
Pre-fix ``ratify_intent`` defaulted to ``threshold=0.0``, which admits
anything with non-negative ``cga_inner(prompt, anchor)`` — the field
gate (ADR-0022 §TBD-1) was structurally live but semantically
transparent. RATIFIED was logged on essentially every turn because
the CGA inner product over conformal space is not sign-symmetric.
Measurement (``scripts/calibrate_ratification_threshold.py``):
* Runs every cognition eval prompt (45 cases = 13 public + 13 dev +
19 holdout) through a primed ``CognitiveTurnPipeline``.
* Captures the actual ``cga_inner(prompt, anchor)`` score from the
pipeline's own ``_ratify_intent`` via a temporary spy on the
imported ``ratify_intent`` binding.
Observed distribution:
* 34 RATIFIED: min=+1.1039 p10=+1.1039 median=+2.6820 max=+5.7508
* 11 PASSTHROUGH (no vocab-grounded anchor available; score=0.0)
* 0 DEMOTED at any threshold ≤ 1.10
Threshold = 0.5 chosen as the calibrated default:
* Well below the empirical floor of 1.10 — every currently-passing
case stays RATIFIED, byte-identically.
* Clearly non-trivially positive — random Cl(4,1) inner products
fluctuate around zero, so 0.5 demands genuine correlation with
the anchor rather than passive non-negativity.
* Leaves headroom for the gate to actually demote weakly-aligned
off-corpus / adversarial prompts to UNKNOWN and route them
through the honest-refusal surface.
Verification:
* ``core eval cognition`` — public 100/100/91.7/100, holdout
100/100/83.3/100, dev 100/100/78.6/100 — byte-identical to
MEMORY baselines.
* ``core test --suite cognition`` — 120/0/1
* ``core test --suite smoke`` — 67/0
* ``core test --suite runtime`` — 19/0
* 2 new tests in ``tests/test_ratification_threshold_default.py``
pin both the constant and the signature default so a future
change cannot silently regress to ``0.0``.
Closes audit Finding 5 (2026-05-20).
Pre-fix ``CognitiveTurnPipeline._speculative_subjects`` was a bare
``set[str]`` that only grew over a session. Two correctness gaps:
* A subject promoted to ``EpistemicStatus.COHERENT`` via the teaching
review loop kept appearing with the "(speculative, not yet
reviewed)" marker forever, contaminating reviewed material on
later probes.
* Long teaching sessions widened the per-turn substring scan in
``_should_mark_speculative`` without bound.
Fix:
* Back the cache with ``OrderedDict[str, None]`` (LRU) capped at
``_MAX_SPECULATIVE_SUBJECTS = 64``.
* Introduce ``_remember_speculative_subject`` (insert / refresh) and
``_forget_speculative_subject`` (evict) helpers; route all
SPECULATIVE inserts through them.
* When a proposal lands as ``EpistemicStatus.COHERENT``, evict the
subject and every long-enough non-stopword token derived from it,
so the marker stops appearing on reviewed material.
Iteration order in ``_should_mark_speculative`` is unchanged (keys
view); lookups remain O(1). No surface change for any case the prior
behavior didn't already mishandle, so byte-identical eval surfaces
stay stable (verified locally against ``core eval cognition`` public /
holdout / dev splits — all unchanged from MEMORY baseline).
Tests (7 new, ``tests/test_speculative_subject_lifecycle.py``):
* storage is an OrderedDict and the cap is 64
* remember normalizes (lower+strip) and drops empty input
* remember refreshes LRU position on re-insert
* cache caps at 64 with insertion-order eviction
* forget is case-insensitive and removes the entry
* forget on a missing / empty subject is a no-op
* ``_should_mark_speculative`` triggers after remember and stops
triggering after forget
Audit findings referenced:
https://github.com/AssetOverflow/core/pull/76 (Finding 5, "Unbounded
``_speculative_subjects``")
Comprehensive survey of every pack in the tree across five layers
(primitives → language → teaching → policy → selection axes → style)
with per-pack stats, cross-pack lemma overlap, teaching-chain graph
topology, and a ranked top-leverage-gaps list.
Key findings:
* **Layer 4 (selection axes) is the densest by far** — 24 packs
(17 anchor lenses + 7 registers). Greek and Hebrew lens
families are at parity (8 each).
* **Layer 2 (teaching corpora) is the load-bearing thin layer** —
only 41 reviewed chains across 18 subjects, with just 2 intent
shapes covered (CAUSE / VERIFICATION) and 7 connectives total.
No COMPARISON / PROCEDURE / CORRECTION chains exist; those
intents route through pack composers only.
* **Layer 1 EN is solid** — 354 lemmas across 12 mounted packs
with 93% gloss coverage; 24 ungloss'd lemmas remain.
* **Cross-language is structurally present but content-light** —
grc / he micro-packs ship and mount; the bigger
``*_cognition_v1`` siblings ratify but are not in the default
mount; no glosses exist on any non-EN pack.
* **Three drafted ethics packs are unratified** (legal /
research / engineering) — the highest-leverage low-effort gap.
* **Rhetorical-style axis ships substrate only** — one
null-lift pack; ADR-0087 consumer phase pending.
Top-leverage gaps ordered by ratio of unblocked value to effort
are enumerated in §7 of the doc itself. No code lands here.