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.
Two ADRs that unblock the remaining items from the 2026-05-20 audit
that could not ship as direct PRs.
ADR-0088 — Realizer-Grounded Authority (Finding 2 retry)
========================================================
The first-response audit remedy (wire ``ground_graph`` between
``runtime.chat`` and ``realize_semantic``) was empirically attempted
on ``fix/ground-graph-wiring`` and reverted: the grounded realizer's
template output (e.g. ``"Light is a visible medium that reveal
truth"``) is grammatically and stylistically weaker than the runtime
path's ADR-0085-polished pack-grounded surface, so the realizer wins
the surface resolver (PR #76) and the user-visible surface regresses
on 23 byte-identical tests + ``register_invariant_grounding``.
ADR-0088 reframes Finding 2 as a two-phase rollout:
* Phase A (no behavior change) — realizer fluency parity.
Templates consult the same gloss source ADR-0085 wired into the
CAUSE composer, emit 3sg verb agreement, and carry the same
pack-provenance tag the runtime path emits. Byte-identical
today because the realizer is still gated by
``_is_useful_surface``.
* Phase B (substantive) — ground the graph and let the realizer
compete. Surfaces change exactly once, with a per-case
re-baseline justified by a "fluency ≥ pre-fix runtime surface"
invariant.
The audit's final-draft remedy (hot-path short-circuit only) is
explicitly rejected — pure perf cleanup, no metric lift since
``core eval cognition`` is already at 100% groundedness.
ADR-0089 — Compound-Intent Pipeline Dispatch (Finding 4)
========================================================
``classify_compound_intent`` is implemented but never reaches
``CognitiveTurnPipeline.run()``. Compound inputs like *"What is X
and how does it relate to Y?"* silently drop the second clause.
Naive multi-node dispatch breaks every downstream stage:
PropositionGraph (one root), plan_articulation (single-root),
realize_semantic (one target), surface resolver (one surface per
turn), compute_trace_hash (one intent_tag + articulation_surface),
teaching loop (one correction-source proposal), register / anchor-
lens telemetry (one variant per turn).
ADR-0089 proposes a three-phase rollout:
* Phase C1 (no behavior change) — call ``classify_compound_intent``
in step 1b, record dropped clauses on
``CognitiveTurnResult.dropped_compound_clauses`` for
observability while routing the dominant clause through the
existing single-intent path.
* Phase C2 (opt-in substantive) — flag-gated multi-node graph
dispatch with new ``CompoundEdge`` / ``ConjunctionRelation``,
widened ``compute_trace_hash`` carrying a
``compound_clauses_hash``, and a ``multi_clause_surface`` field
on the resolver. Flag-off preserves byte-identity.
* Phase C3 — telemetry alignment + demo + docs.
Each phase is independently shippable and preserves the existing
null-lift / byte-identity invariants register and anchor-lens
established (ADR-0072, ADR-0073d) as the project's pattern for
substantive runtime behavior changes.
Both ADRs are Proposed; ratification follows the existing pack /
ADR review process. No code lands in this commit.
* fix(cognition): add explicit surface resolution policy
* test(cognition): cover explicit surface resolution policy
* fix(cognition): route pipeline surfaces through resolver
* fix(cognition): address PR #76 review comments
- hoist `_is_useful_surface` import from inside `run()` to module top
- call `_render_walk_surface` / `_render_compose_surface` via the class
name (both are @staticmethod) for consistency with the existing
`_fold_*_into_surface` helpers
- drop redundant `realized_surface` truthiness check in
`resolve_surface` — `realizer_useful` already excludes empty /
placeholder surfaces via `_is_useful_surface`
Tests: tests/test_surface_resolution.py + tests/test_cognitive_turn_pipeline.py
green (16 passed); cognition suite 120/1s, smoke suite 67/0.
Follow-up brief for the cheaper dev agent. Scoped tight: ~18 concrete
row-by-row edits across two patterns the v1 pass (PR #73) deferred or
missed.
Pattern A — 3sg present-tense agreement after relative pronouns
what/who/that/which followed by a bare-form verb where the implied
subject is singular. 10 candidates identified up front by repo scan:
- en_core_causation_v1/effect (with judgment note)
- en_core_cognition_v1/beginning, creation, definition, evidence,
light, reason, symbol
- en_core_meta_v1/example, mind
Brief explicitly clarifies that bare verbs after modals (`can`,
`may`, `should`) are CORRECT and must NOT be changed — flagging
the "who can know and do" case the v1 agent did right.
Pattern B — plural agreement after quantifier / preposition
between/among/of/two/three/many + count-noun-in-singular. 5
confident edits + 3 borderline cases with judgment guidance:
- en_core_attitude_v1/broad
- en_core_cognition_v1/context, order, style
- en_core_spatial_v1/between
Borderline guidance distinguishes count vs mass nouns: `reason`
in "group of reason" is count (apply fix); `reason` in "because
of reason" is mass (leave alone).
Same hard rules as v1 brief:
- no code edits
- definitional_atoms / predicates_invited / pos / lemma /
definition_version must not change
- Greek/Hebrew packs and primitives pack untouched
- closure verifier must exit 0
- cognition eval must stay byte-identical
- draft PR, human review before merge
Estimated effort: ~18 one-character edits. Whole pass should take an
order of magnitude less time than v1 because the candidate rows are
enumerated in the brief itself rather than discovered via heuristic
scan.
Why a v2 brief rather than amending v1's PR:
Plural patterns were not on v1's explicit pattern list (the v1
brief only named verb agreement, missing articles, missing
infinitives, missing copulas). 3sg agreement was named but
required a different tool than the agent had at hand. Scoping
v2 to exactly the rows known to need fixing is cheaper than
re-running the v1 heuristic scan with an expanded ruleset.
Provenance tag for the v2 pass:
adr-0085-style-v2:reviewed:2026-05-22
Pre-work for a writing-curriculum extension to CORE. Two companion
documents, both Proposed status (no code shipped).
docs/decisions/ADR-0087-rhetorical-style-axis.md
Pins rhetorical style as a third selection axis — sibling to anchor
lens (ADR-0073), orthogonal to register (ADR-0070). Substantive
axis: trace_hash DISTINCT across styles (style changes which moves
the composer requires and which frames the realizer emits, which
changes the propositional plan, which changes the trace).
Four anti-patterns explicitly named and rejected:
- style as motor (re-couples realizer to geometry; same shape as
the ADR-0085 fusion-operator rejection)
- style as register variant (conflates substantive with stylistic)
- style as identity axis (bloats identity doctrine)
- style auto-detected from user input (operator-chosen only)
Pack shape mirrors packs/anchor_lens/. default_unstyled_v1 is the
null-lift pack identical to no-style behavior. Three CI invariants
proposed: rhetorical_style_null_lift, schema validation, three-axis
orthogonality.
Substrate-only ADR — no consumer code, no genre packs. Consumer
integration is a follow-up ADR (composer + realizer extensions
that read permitted_frames + required_moves_per_claim +
forbidden_moves).
docs/curriculum/writing-chain-harvester-spec.md
Layer 0 of the writing curriculum. A deterministic tool that
extracts candidate (subject, predicate, object) triples from
reviewed expert prose and surfaces them as proposals to the
existing teaching/review pipeline.
Five stages (segment → classify → extract → propose → audit) —
pure-Python rule-based, no LLM generation, no auto-acceptance.
Trust boundary: reviewer accept/reject via the existing
core teaching propose/review path. No bypass permitted.
The harvester is a proposal PRODUCER, not a proposal CONSUMER.
Plugs into the existing pipeline without inventing a new review
mechanism. Each proposal carries source_id + source_line + the
exact source_clause it came from for reviewer verification.
First-implementation acceptance criteria deliberately tight:
Stage 0+1 with dry-run only. Stages 2-5 are follow-up PRs.
Substrate-first sequencing pattern (ADR-0084 → 0085) reused
throughout. Both documents acknowledge open questions deferred to
implementation phase rather than pre-deciding.
Why now: a writing curriculum is being scoped. Without this ADR,
every downstream PR faces the same "should style be a motor?"
question and the temptation to reach for the geometry will recur
every time the realizer produces a stilted surface. Pinning the
axis up front prevents that recurrence.
Brief for a fluency pass on the 333 ratified gloss entries. Closes the
content-side counterpart of ADR-0085's surface lift:
Before "Light exists as visible medium that reveal truth."
After "Light exists as a visible medium that reveals truth."
Same gloss content, English-correct. Fixes 3sg agreement after
relative clauses, plural agreement after quantifiers, missing
articles before adjective-noun NOUN-frame glosses, and missing 'to'
in VERB-frame glosses.
Hard constraints encoded in the brief (matching the wrapper-prompt
pattern that worked for ADR-0084 content):
- code untouched: only language_packs/data/<pack>/glosses.jsonl edits
- definitional_atoms, predicates_invited, pos, lemma, definition_version
must NOT change
- closure verifier (scripts/verify_definitional_closure.py) must
still exit 0
- cognition eval must stay byte-identical to baseline
- Greek/Hebrew packs untouched (not in definitional layer per ADR-0084
scope limit)
- primitives pack untouched
- draft PR, human review before merge
Includes a Phase-1 inventory script the agent runs first to scope the
work (heuristic pattern matcher across the 13 opted-in packs),
worked-example fluency rules with before/after table, per-pack
checksum-refresh shell snippet, and Phase-4 verification commands.
Estimated effort: ~30-60 lines of JSONL edits. Same handoff format as
docs/handoff/ADR-0084-pack-content-brief.md.
The original "Why does light exist?" complaint that motivated ADR-0084
was specifically about CAUSE-intent surfaces. ADR-0084 (substrate) +
PR #65 (content) already moved DEFINITION/RECALL to gloss-grounded
surfaces ("Light is visible medium that reveal truth."). But CAUSE
still dispatched through the chain-walk path:
Before: light — teaching-grounded (cognition_chains_v1):
cognition.illumination; logos.core.
light reveals truth (cognition.truth).
No session evidence yet.
After: Light exists as visible medium that reveal truth.
pack-grounded (en_core_cognition_v1).
The chain-walk is structurally correct but the wrong SHAPE for a why-
question — it's a graph traversal, not an explanation. ADR-0085 fixes
the shape using the same gloss material that DEFINITION/RECALL already
consume, with no new content authoring.
Additive composer
chat/pack_grounding.py:gloss_aware_cause_surface()
- Resolves gloss via lexicon-residency-checked resolve_gloss().
- Frames POS-aware:
NOUN -> "{Lemma} exists as {gloss}."
VERB -> "To {lemma} is to {gloss}."
ADJ -> "To be {lemma} is to {gloss}."
* -> falls back to _frame_gloss (predicate-identity).
- Threads anchor lens via the existing helper (ADR-0073c parity).
- Returns None when no gloss exists — runtime falls through to the
existing chain-walk path. Additive: no CAUSE case loses its surface.
Runtime dispatch
chat/runtime.py — IntentTag.CAUSE tries gloss path FIRST under the
flag; falls through to teaching_grounded_surface* on None.
Unconditional fallback — never silent.
Opt-in flag
core/config.py — RuntimeConfig.gloss_aware_cause: bool = False
Default off preserves pre-ADR-0085 chain-walk surfaces byte-
identically (null-drop invariant, CI-pinned).
Prompt-diversity classifier update
evals/prompt_diversity/runner.py — _CAUSE_MARKERS widened with the
explanation-frame markers ("exists as", "is to", "to be", "is for",
"purpose of") plus bare-form predicates ("reveal" alongside
"reveals"). Neither composer path is penalised on shape_fit just on
inflection grounds.
v1/public lift (flag OFF vs ON, 26 cases)
intent_accuracy : 65.4% -> 65.4% ( — )
versor_closure_rate : 100.0% -> 100.0% ( — )
response_shape_fit : 57.7% -> 57.7% ( — , both frames recognized)
audit_in_surface_rate : 42.3% -> 42.3% ( — , envelope ADR's job)
gloss_quote_rate : 11.5% -> 23.1% (+11.5pp, structural lift)
Tests (15)
- 5 pure composer (NOUN/VERB frame, unknown/empty None, no chain-
walk artifacts in surface)
- 5 runtime dispatch (flag-off chain-walk, flag-on gloss, parametrized
across glossed subjects, VERIFICATION unchanged under flag, no-
gloss fallback engages)
- 5 cognition lane invariance (aggregate metrics byte-identical
under both flag states; surfaces deliberately shift on the 2 CAUSE
cases with glossed subjects — the structural-change-vs-metric-
invariance both-sides invariant)
Lanes
smoke 67/0, cognition 120/0/1 skipped, packs 6/0, teaching 17/0,
runtime 19/0. core eval cognition byte-identical 100/91.7/100/100
under both flag states.
Scope limits (per ADR §Scope limits)
- CAUSE only; VERIFICATION still chain-walks (different shape).
- English pilot only; Greek/Hebrew packs not opted into definitional
layer yet (ADR-0084 scope limit).
- Single-lemma subjects; compound/anaphoric fall through.
- Opt-in until cognition holdout confirms the lift transfers off-
fixture. Future PR flips default on.
Out of scope
- Surface-vs-envelope cleanup ("pack-grounded (...)" still leaks).
- Predicate licensing (ADR-0086).
- Content style pass (bare lemma forms in glosses — separate brief).
The v1 gloss-quote detector used a 4-token contiguous window of
≥4-char tokens. That heuristic was too strict for the actual ADR-0084
brief gloss style, which is deliberately short and primitive-only:
light "visible medium that reveal truth" 5 tokens ≥4 chars
parent "person with a child" 3 tokens ≥4 chars ← can't window
recall "get memory from before" 3 tokens ≥4 chars ← can't window
wisdom "good use of knowledge" 2 tokens ≥4 chars ← can't window
Result: post-PR #65 baseline showed gloss_quote_rate=0.0% even though
the pack-grounded composer was visibly emitting glosses verbatim:
surface: "Parent is person with a child. pack-grounded (en_core_relations_v1)."
gloss: "person with a child"
window: could not even form
Replace with substring match against the gloss text. The composer
emits the gloss verbatim (no paraphrasing — that's the no-LLM
discipline), so substring is exact, high-confidence, and trivially
correct:
gloss_quoted ⟺ gloss.lower().strip() in surface.lower()
Re-baselined v1/public (26 cases):
gloss_quote_rate: 7.7% (false-positive 4-token window noise)
→ 0.0% (post-#65, broken metric)
→ 11.5% (this PR, real signal)
The other four metrics unchanged. 3/26 cases (DEFINITION on
``evidence``/``recall``/``parent``) are detected as gloss-quoted now,
which matches reality — the pack-grounded composer at
chat/pack_grounding.py:398 has been gloss-aware all along; it just
had no glosses to quote pre-#65.
Why this is just a heuristic refinement, not a contract change:
The contract.md still says v1 has NO pass thresholds beyond
versor_closure_rate==1.00. The lane's job is to establish baseline
distribution. The heuristic was *measuring the wrong thing* — fixing
the measurement is a contract clarification, not a contract change.
Tests added (TestGlossQuote, 4 cases):
- short brief-style gloss detected via substring
- chain-walk surface for same lemma NOT counted as gloss-quoted
- unknown term returns False
- empty terms returns False
Updated the function docstring with the post-#65 context so future
readers understand why v1's contract predicted 0% but reality is ~12%.