* feat(epistemic): populate normative_detail on TurnEvent and ChatResponse
Adds normative_detail_from_verdicts() to core.epistemic_state and wires
it into both the stub and main ChatResponse/TurnEvent construction sites.
The field carries a sorted comma-separated list of violated boundary or
commitment IDs when normative clearance is VIOLATED or SUPPRESSED; empty
string otherwise.
* docs(ADR-0142): ratify epistemic state taxonomy — 14-state vocabulary + normative clearance axis
Formalises the six-subsystem Framing 1 audit findings into a first-class
decision. Accepts the 14-state taxonomy and companion 4-value normative
clearance axis. Documents Phase 3 deliverables already landed and defers
structured provenance + cross-subsystem transition machinery to ADR-0144.
* feat(epistemic): add first-class state enums
* feat(epistemic): tag TurnEvent with state axes
* feat(epistemic): serialize turn state axes
* feat(packs): tag curated and inferred unit entries
* feat(epistemic): expose word-level state on manifold
* feat(epistemic): expose vault status mapping
* feat(epistemic): preserve pack entry states through compiler
* test(epistemic): cover phase 3 state tagging spine
* feat(runtime): wire epistemic_state + normative_clearance into ChatResponse
Add first-class epistemic_state and normative_clearance fields to
ChatResponse (defaulting to "undetermined"/"unassessable" for backward
compat). Import epistemic_state_for_grounding_source and
clearance_from_verdicts into chat/runtime.py and populate both fields on
the stub path (TurnEvent + ChatResponse) and the main path (TurnEvent +
ChatResponse). Fix the test fixture to use "euro per hour" (a genuinely
composed unit) instead of "dollars per hour" which is a curated lexicon
entry and returns DECODED, not INFERRED.
* test(cognition): update term_capture_rate baseline from 0.9167 to 1.0
unknown_logos_019 now correctly surfaces "light" as a pack-resident
token near the logos versor — producing term_capture_rate 1.0 on both
main and Phase 3. The 0.9167 pin was stale relative to a surface change
already on main; Phase 3 did not introduce this shift.
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.
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
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).
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).
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 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.
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).
Strict superset of ADR-0062's depth-1 composer. `max_depth` is the
number of follow-up hops appended beyond the initial chain:
max_depth=0 → byte-identical to single-chain surface
max_depth=1 → byte-identical to ADR-0062 composed
max_depth=2 → byte-identical to ADR-0062 when no second hop
survives, strict superset when one does
The composer surfaces content the realizer was silently dropping
from chains already ratified in `cognition_chains_v1`. Example
live lift on `"Why does light exist?"`:
composed: "light reveals truth, which grounds knowledge."
transitive(2): "...which grounds knowledge, which requires evidence."
Cycle-safe at every depth via a single visited-set; single-corpus
traversal in v1 (cross-corpus transitive deferred to a follow-up
ADR alongside ADR-0064's cross-pack model).
Both flags default False — every existing surface is preserved
byte-identically. When both `composed_surface` and
`transitive_surface` are True, transitive wins.
Implementation:
- `core/config.py`: `transitive_surface: bool = False`,
`transitive_max_depth: int = 2`.
- `chat/teaching_grounding.py`: `_resolve_followup` shared helper
refactored out of the depth-1 composer (no behavioural change),
plus new `teaching_grounded_surface_transitive(subject,
intent_tag, *, max_depth)`.
- `chat/runtime.py`: dispatch order — transitive > composed > single.
Verification:
- tests/test_transitive_surface.py: 16 new tests covering pure-fn
contract, visited-set cycle guard at every depth, runtime
integration, and the cognition-lane null-drop invariant at
`max_depth=2` (public + holdout splits).
- tests/test_composed_surface.py: 11/11 pass after the helper
refactor (ADR-0062 behaviour preserved).
- `core test --suite smoke`: 67 pass.
- `core test --suite cognition`: 120 pass, 1 skipped.
- `core test --suite teaching`: 17 pass.
- `core eval cognition`: 100 / 91.7 / 100 / 100 (byte-identical).
Wires observational telemetry on the composer-vs-graph atom-set
relationship. Phase 1 is strictly observational: no enforcement,
no surface mutation, no grounding-source change, no trace-hash impact.
New telemetry fields on TurnEvent + ChatResponse:
composer_graph_atom_status ∈ {equivalent, divergent,
graph_unconstrained,
composer_no_atoms,
not_applicable, ""}
composer_atom_set_hash SHA-256 over sorted unique atoms
graph_atom_set_hash SHA-256 over sorted unique atoms
composer_graph_atom_overlap_count int
Composer atoms come from existing pack candidate metadata
(pack_semantic_domains channel through _maybe_pack_grounded_surface).
Graph atoms come from build_graph_from_input + resolve_lemma on
node.subject/predicate/obj — no prose parsing. When a grounded
composer path lacks explicit atom provenance, status is
'composer_no_atoms'.
New pure helper:
chat/atom_equivalence.py — normalize_atoms, hash_atoms,
atoms_for_graph_nodes, compare_atom_sets
Tests (tests/test_composer_graph_atom_equivalence.py):
- Pack DEFINITION path produces observable equivalence
- Divergent atom sets produce distinct hashes
- Register invariance: atom hashes + status identical across
{neutral, terse, convivial}; trace_hash also constant (R5 axis)
- Anchor lens engaged case still ASCII-only on surface
- No prose-parsing helper symbols introduced in runtime.py
(extract_candidate_surface_lemmas, surface_lemma,
parse_surface_atoms) — enforces Phase 1 boundary
Performance note: build_graph_from_input now runs on every warm
English turn (previously only when forward_graph_constraint=True).
Phase 1 accepts this cost to make the telemetry universally
available; Phase 2+ can introduce a feature flag if needed.
Validation:
- Cognition eval byte-identical: 100/100/91.7/100
- Full lane: 2864 passed, 3 skipped, 0 failed (+5 over baseline)
- Targeted lane: 72 passed in tests/test_{graph_constraint,
pack_grounding,register_tour_demo,anchor_lens_tour_demo,
orthogonality_tour_demo,realizer_guard_holdout,
composer_graph_atom_equivalence}.py
R5 (ADR-0072) shipped the register *machinery*; ADR-0074's orthogonality
tour proved the axis was decoratively orthogonal to anchor-lens but
inspection of the cognition-eval surfaces revealed two structural gaps:
* On pack-grounded DEFINITION/RECALL/COMPARISON composers, the only
realizer override any register consumed was `disclosure_domain_count`
— which only fires on the no-gloss disclosure path. Under terse_v1,
every gloss-DEFINITION cell was byte-identical to default_neutral_v1.
* The register-tour's `surfaces_vary_at_least_once` gate could be
satisfied by convivial's decorative wrapper alone, masking that
regression in CI.
R6 closes both:
Layering separation (the load-bearing fix):
* New TurnEvent/ChatResponse field `register_canonical_surface` carries
the composer output BEFORE any register transformation. The pipeline
hashes this field for `trace_hash`, preserving R5's invariant that
per-prompt trace_hash is CONSTANT across registers even while
substantive transforms produce visibly different surfaces.
Substantive transforms (`chat/register_substantive.py`):
* terse_v1 gains 3 bool knobs: `drop_provenance_tag`, `compress_gloss`,
`drop_articles` — all pure regex transforms on the canonical surface.
* convivial_v1 gains `append_semantic_domain_clause` — appends a single
bounded "Related: <atom>." clause using the lemma's pack atoms.
* default_neutral_v1 leaves overrides empty; substantive transform is
byte-identical no-op (preserves `byte_identity_null_lift`).
* C1 (ADR-0075) safety preserved: drop_articles refuses to drop
articles following `not` (avoids R3 violations); no knob combination
trips R2/R3.
Strengthened tour gate (`evals/register_tour/run_tour.py`):
* Replaces `surfaces_vary_at_least_once` with two falsifiable claims:
- `terse_substantively_differs_from_neutral_on_pack_grounded_definition`
- `convivial_substantively_differs_from_neutral_on_pack_grounded_definition`
Both restrict to DEFINITION+pack-grounded cells and require
difference beyond whitespace/punctuation.
* New claim `register_canonical_surfaces_identical` directly proves
the layering separation.
* Preserves R5's `all_grounding_sources_identical` +
`all_trace_hashes_identical`.
Pack ratification:
* Loader widened to accept `bool` for closed-set R6 keys
(drop_provenance_tag / compress_gloss / drop_articles /
append_semantic_domain_clause).
* `_KNOWN_OVERRIDE_KEYS` ratify gate extended with same.
* terse_v1 + convivial_v1 reratified with new knobs; companion
mastery reports re-sealed. default_neutral_v1 unchanged.
Invariants pinned:
* `invariant_register_canonical_surface_constant_across_registers` (new)
* `invariant_terse_substantively_distinct_from_neutral` (new)
* `invariant_convivial_substantively_distinct_from_neutral` (new)
* `invariant_realizer_no_illegal_articulation` (C1, preserved)
* `invariant_realizer_guard_byte_identity_on_currently_passing_cases`
(C1, preserved)
Verification:
* `core eval cognition`: 100.0% / 91.7% / 100.0% / 100.0% — byte-
identical under default_neutral_v1.
* `core demo register-tour`: all 5 claims green, exit 0.
* `core demo anchor-lens-tour`: green (no anchor-lens code touched).
* `core demo orthogonality-tour`: green (5/5 claims).
* Full lane: 2858 passed, 1 pre-existing failure
(test_all_preamble_explains_combined_run, carried forward
unchanged from main). 56 new R6 tests across three files.
C1 coherence floor: a deterministic verifier that runs on every
candidate surface produced by the truth path, before assignment to
ChatResponse.surface. Rejects illegal articulations and routes them
to a bounded disclosure string — admission control with a
deterministic fallback, not normalization.
Active rules (R1 deferred during ratification — see ADR):
R2_aux_neg_requires_verb — "<aux> not <wrong-POS>" rejected
R3_be_neg_requires_predicate — "<be> not <verb>" rejected
Fail-open on unknown POS, fail-closed on explicit wrong POS.
Cognition eval byte-identical (100/91.7/100/100).
Original bug class — "Light reveals truth, right?" → "Right does not
thought." — now routes to "I do not have a reviewed articulation for
that yet." with grounding_source=none, walk_surface preserving the
rejected candidate, and telemetry carrying R2_aux_neg_requires_verb.
Files:
generate/realizer_guard.py NEW — pure verifier
chat/runtime.py hook on stub + main paths
chat/telemetry.py serialize guard fields
core/physics/identity.py TurnEvent +2 fields
evals/realizer_guard/run_holdout.py NEW — 6-prompt cluster
tests/test_realizer_guard_*.py NEW — 46 tests (unit/seam/holdout)
docs/decisions/ADR-0075-*.md NEW — ratified
Invariants pinned:
invariant_realizer_no_illegal_articulation
invariant_realizer_guard_byte_identity_on_currently_passing_cases
Lanes (excluding 1 pre-existing TestDemoPreambles failure unrelated
to C1, already present at 4426f38):
smoke 67/67 cognition 120/120(+1s) teaching 17/17
packs 6/6 runtime 19/19 algebra 132/132 full 2792/2793
L1.3 of the anchor-lens inside-out rollout — first substantive
surface lift on the substantive axis. Two ratified non-trivial
lenses engage on cognition-pack lemmas via the alignment graph,
appending [lens(<id>):<mode>] annotations to the existing
pack-grounded surface.
Two ratified lenses
grc_logos_v1 (Greek substrate)
primary_substrate : "grc"
semantic_domain_preferences: ["logos.episteme.systematic_knowledge"]
cognitive_mode_label : "systematic"
Engages on en "knowledge" via grc-core-cog-021 (ἐπιστήμη) →
en-core-cog-007 alignment edge.
he_logos_v1 (Hebrew substrate)
primary_substrate : "he"
semantic_domain_preferences: ["logos.aletheia.verity"]
cognitive_mode_label : "covenant-verity"
Engages on en "truth" via he-core-cog-002 (אמת) →
en-core-cog-002 alignment edge.
Both ratified under method anchor_lens_lifts_proposition.
Engagement rule (single)
1. Resolve en_lemma → entry_id (cognition pack).
2. For each substrate pack matching lens.primary_substrate, load
alignment.jsonl; find edges where target_id == entry_id.
3. For each such substrate lemma, if any atom in its
semantic_domains ∈ lens.semantic_domain_preferences → engage.
4. No match → None (no annotation; byte-identical surface).
The pivot is shared semantic_domain atoms surfaced via the
alignment graph — exactly the language-neutral commitment from
ADR-0073. Engagement never touches non-English surface text;
entry_ids and atom strings only.
Surface lift
no-lens : "Knowledge is X. pack-grounded (en_core_cognition_v1)."
lens-on : "Knowledge is X. pack-grounded (en_core_cognition_v1) [lens(grc_logos_v1):systematic]."
Annotation between existing provenance and trailing period.
Both metadata fields are ASCII-bounded ≤64 chars at the loader
level, so the annotation can never carry non-ASCII.
Scope deliberately narrow
L1.3 wiring restricted to pack_grounded_surface /
build_pack_surface_candidate (DEFINITION/RECALL only). Other
composers (COMPARISON / CORRECTION / PROCEDURE / NARRATIVE /
EXAMPLE / CAUSE / VERIFICATION) accept the anchor_lens kwarg via
forward-compat default UNANCHORED but do not yet consume it.
L1.3b or later broadens to those intent shapes.
Ratify gate widening
Non-null lenses must:
- have primary_substrate ∈ {grc, he, en}
- have a non-empty cognitive_mode_label
- every preferred atom must exist in at least one lemma of the
named substrate (trust boundary: operators cannot ship a lens
pointing at atoms not on disk).
Method: anchor_lens_lifts_proposition. Null lenses still ratify
under byte_identity_null_lift (L1.2 method).
Seam allow-list widening
Truth-path modules (cognition / trace / pipeline / intent /
propagation / vault / algebra) still refused. Composer-side
imports from chat/pack_grounding.py now permitted — the same way
ADR-0069's R2 widened the register seam.
New invariants pinned (3)
tests/test_anchor_lens_engagement_unit.py (14 tests) — resolver
returns mode label only on intended substrate × en lemma pair;
case-insensitive; engagement None under null lens; synthetic
lens with unmatched atom returns None; annotation is pure ASCII.
tests/test_anchor_lens_lifts_proposition.py (17 tests) — grc
engages on knowledge only, he engages on truth only,
cross-lens isolation, three-way distinctness, replay determinism
per (lens × prompt), register-tour seam holds within each lens
scope (orthogonality CI-pinned, parametrized over 4 lens
choices).
tests/test_anchor_lens_no_glyph_leak.py (5 tests) — hard
block-scoped gate: Greek (U+0370..03FF, U+1F00..1FFF), Hebrew
(U+0590..05FF), Syriac, Arabic. Stylistic punctuation
(em-dash etc.) explicitly allowed; em-dash predates L1.3 by a
wide margin and is not a substrate-leak risk. Tested per-lens
across every cognition case + direct lens-metadata ASCII check.
Lane evidence
74 anchor-lens tests pass (37 from L1.2 + 37 new).
python -m core.cli eval cognition → public 100/100/91.7/100
byte-identical (lens=None / default_unanchored_v1).
core demo register-tour --json → all_claims_supported: True
(R5 seam still holds; L1.3 doesn't perturb presentation axis).
Full lane: 2706 passed / 4 skipped / 1 pre-existing failure
(+37 over L1.2's 2669; the one failure remains
test_all_preamble_explains_combined_run, unrelated).
Files
packs/anchor_lens/grc_logos_v1.json NEW
packs/anchor_lens/grc_logos_v1.mastery_report.json NEW
packs/anchor_lens/he_logos_v1.json NEW
packs/anchor_lens/he_logos_v1.mastery_report.json NEW
scripts/ratify_anchor_lens_packs.py EDIT
LENS_IDS adds grc_logos_v1 / he_logos_v1; gate widened.
chat/pack_grounding.py EDIT
_resolve_anchor_lens_mode, _maybe_append_anchor_lens_annotation,
_substrate_lexicon_by_entry_id, _en_lemma_to_entry_id.
build_pack_surface_candidate + pack_grounded_surface gain
anchor_lens kwarg (default UNANCHORED).
chat/runtime.py EDIT
Thread self.anchor_lens into pack_grounded_surface() call.
tests/test_anchor_lens_pack_seam.py EDIT
Doc-comment updated for L1.3 allow-list.
tests/test_anchor_lens_* NEW (3 files)
docs/decisions/ADR-0073c-anchor-lens-composer-wiring.md NEW
Adds compound-intent decomposition for prompts that ask multiple
things in one turn ("What is X, and why does it matter?",
"Explain X, but how does it work?", "What is X, and what is Y?").
Three landings in one PR (rule says additive; the three pieces
are inseparable for the runtime hook to do anything useful):
1. generate/intent.py
* New ``CompoundIntent`` frozen dataclass — ordered tuple of
``DialogueIntent`` parts + raw_text + ``.primary`` back-compat
accessor + ``.is_compound()`` helper.
* New ``classify_compound_intent(prompt)`` sibling to
``classify_intent``. Pure, deterministic, byte-stable. Splits
on closed connector list (``,\s+(and|but|because|while)\s+``);
anaphoric tails ("why does it matter") get the prior part's
subject substituted ("why does truth matter") then are
classified independently.
* ``classify_intent`` return shape is untouched — every existing
caller still receives ``DialogueIntent``.
* No new ``IntentTag`` introduced. v1 semantic approximation:
"why does X matter" routes to ``CAUSE(X)``; "matter" means
causal/relevance support, not metaphysical importance.
2. generate/discourse_planner.py
* New ``plan_compound_discourse(compound, mode, bundles)`` —
concatenates per-part sub-plans in source order with a
``TRANSITION`` bridge (fact=None) between consecutive parts.
No cross-part re-sorting.
* New private kw-only ``_exclude_facts`` parameter on
``plan_discourse`` so subsequent sub-plans can avoid emitting
the same facts the prior sub-plans already used (prevents
"Truth is X. Truth is X." duplicates on shared-subject
compounds). Public signature ``(intent, mode, bundle)`` is
unchanged.
3. chat/runtime.py
* Helper ``_maybe_apply_discourse_planner`` now consults the
compound classifier first. When the prompt is multi-part it
builds per-part bundles and calls ``plan_compound_discourse``;
otherwise it follows the previous single-intent path.
* Compound bypass: when upstream tagged the surface ``oov`` /
``none`` because the flat classifier saw a polluted subject
(e.g. ``"truth, and why does it matter"``), but the compound
decomposition reveals a pack-resident primary subject, the
planner engages on the decomposed parts. This narrowly widens
the gate exclusively for compound prompts with substrate.
* BRIEF mode upgrades to EXPLAIN for compound prompts —
single-anchor sub-plans on shared subjects would emit duplicate
anchor sentences in BRIEF.
* Return shape widened to ``tuple[str, str] | None`` —
``(rendered_surface, new_source_tag)``. ``new_source_tag`` is
``"teaching"`` when the plan uses any teaching fact, else
``"pack"`` — so downstream labels reflect actual provenance
even on the compound bypass. Both cold and warm call sites
updated to apply both fields.
24 new tests pin: compound decomposition correctness, source-order
preservation across sub-plans, anaphoric-followup rewriting,
deterministic byte-stable plans, no new IntentTag introduced,
fact-dedup across sub-plans, compound-bypass engagement, and
source-tag correction on planner-engaged surfaces.
Lane re-measurement after 3 compound cases added to cases.jsonl
(24 total cases):
flag off: articulate=0.0833, disclosure=0.1667, unarticulate=0.7500
flag on : articulate=0.9167, disclosure=0.0000, unarticulate=0.0833
Note: disclosure flag-on dropped to 0.0 because the source-tag
correction now correctly labels compound-bypass surfaces as
``pack/teaching`` instead of letting the upstream ``oov`` label
inflate disclosure. The two remaining unarticulate cases flag-on
are the walkthrough prompts targeted by the next landing.
Critical gates all green:
* flag off cognition byte-identical: public 100/100/91.7/100
* smoke suite 67/67
* 32/32 planner tests pass (helper + render + compound)
* 18/18 compound classifier tests pass
Pre-cleanup before extending intent classification. Extracts
``ChatRuntime._maybe_apply_discourse_planner(text, source_tag) ->
str | None`` and replaces the two duplicated blocks (cold-start
pack-grounded branch + warm post-walk branch) with single-line
``planned = ...; if planned is not None: assign`` call sites.
Signature locked: takes only the prompt and the already-classified
grounding source tag; returns the replacement surface or None.
Callers own assignment — the helper neither reads nor writes any
surface or articulation state. The warm site additionally does the
``articulation = replace(articulation, surface=planned)`` follow-up
which the cold site does not need.
Gating discipline unchanged (re-pinned in 9 new tests):
* Returns None when ``self.config.discourse_planner`` is False.
* Returns None unless source_tag ∈ {"pack", "teaching"}.
* Returns None when the classified intent has no subject.
* Returns None on single-move plans (BRIEF mode / empty bundle).
* Returns None on empty rendered string.
Behavior is byte-identical to the pre-dedup state — same metrics:
flag off: multi=0.1429, primed_multi=0.0000, conn=0.0769
flag on : multi=0.5238, primed_multi=0.5000, conn=0.2308
cognition eval byte-identical: public 100/100/91.7/100.
smoke suite 67/67.
The two paths now cannot drift; the upcoming intent classifier
extension lifts both branches in lockstep.
Option 2 of the lane-isolation work. Mirrors the existing warm-path
hook into the cold-start branch (``gate_decision.fire`` ⇒ stub
response): after ``_maybe_pack_grounded_surface`` succeeds and the
result is pack- or teaching-grounded, build the same DiscoursePlan
and replace ``pack_surface`` with the rendered plan whenever it has
more than one move.
This closes the gap option 1 exposed: cold-start one-shot prompts
("Tell me about truth.", "Describe wisdom.", "Give me an example of
truth.") now produce deterministic multi-clause output without any
priming setup — the planner becomes the spine for grounded surfaces,
not a warm-only sidecar.
Gating discipline preserved:
* Engages only when pack_source_tag in {"pack", "teaching"}. Cases
routed to vault, none, or the discovery-signal disclosure are
untouched.
* BRIEF mode collapses to a single ANCHOR move which renders
byte-equivalent to the existing pack-grounded composer, so
flag-off cognition byte-identity is preserved.
* Empty bundles → empty plan → no surface change (planner is total).
A/B on multi_sentence_response (21 cases, public/v1):
flag off: multi=0.1429, primed_multi=0.0000, conn=0.0769
flag on : multi=0.5238, primed_multi=0.5000, conn=0.2308
Cold-start lift: multi +38pp, conn +15pp. Primed metric unchanged
(those cases already engaged the warm hook in step 5).
Sample cold-start surfaces flag-on:
* "Tell me about truth."
→ "Truth is a claim or state grounded by evidence and coherent
judgment. Furthermore, truth belongs to cognition.truth. In
turn, truth grounds knowledge."
* "Describe wisdom."
→ "Wisdom is sound judgment informed by knowledge and experience.
Furthermore, wisdom belongs to cognition.wisdom. In turn,
wisdom orders judgment."
* "Give me an example of truth."
→ "Truth is a claim or state grounded by evidence and coherent
judgment. In turn, truth grounds knowledge." (EXAMPLE mode:
anchor + relation, no support)
Gates all green:
* flag off: cognition eval byte-identical
- public 100/100/91.7/100, holdout 100/100/83.3/100
* smoke suite 67/67
* conversational_thread_coherence: 3 unwanted placeholders flag off
and flag on (no regression)
* 136/136 planner + grounding + intent + lane tests pass
Step 5 of the discourse-planner sequencing. Closes the chain:
classify_intent + classify_response_mode
-> grounding_bundle_for(subject)
-> plan_discourse(intent, mode, bundle)
-> render_plan(plan)
-> response_surface
Adds RuntimeConfig.discourse_planner (default False). When True, the
runtime — after the warm pack/teaching-grounded surface is set —
classifies the response mode, assembles a GroundingBundle from the
ADR-style accessors, builds a DiscoursePlan, and replaces the warm
surface with the deterministic multi-clause rendering whenever the
plan has more than one move.
Gating discipline:
* Engages only on warm_grounding_source in {"pack", "teaching"} so
vault/none turns and the discovery-signal CAUSE/VERIFICATION
disclosure are preserved exactly.
* BRIEF mode always collapses to a single ANCHOR move, so flag-on
with BRIEF intent is byte-identical to flag-off.
* Empty bundles produce empty plans; the runtime falls through to
the existing warm surface untouched.
Adds render_plan(plan) to generate/discourse_planner.py — a pure,
deterministic multi-clause renderer with fixed canonical connectives:
ANCHOR : capitalized opening sentence
SUPPORT : "Furthermore, ..."
RELATION : "In turn, ..."
TRANSITION: "Consequently, ..."
CLOSURE : skipped when fact is None
Every visible token is a verbatim pack lexicon entry, gloss, or
reviewed teaching chain string — no synthesis.
13 new tests pin:
* render_plan empty/brief/paragraph shape
* canonical connectives present in paragraph rendering
* deterministic + verbatim-fact invariants
* RuntimeConfig.discourse_planner defaults False
* Flag-off surface has no planner connectives
* Flag-on lifts produce structurally well-formed multi-sentence
output on grounded substrate
Lift measurement (multi_sentence_response public/v1, 15 cases):
* flag off: multi=0.40, connective=0.50, grounded=0.40
* flag on : multi=0.40, connective=0.60, grounded=0.40
-> connective_present_rate +10pp; multi-sentence count flat
because the existing narrative composer's literal "." chars in
tags like "cognition.truth" already trigger sentence splits in
the lane regex. Real lift is form quality: e.g. "Tell me about
truth" now renders as "Truth is a claim or state grounded by
evidence and coherent judgment. Furthermore, truth belongs to
cognition.truth. In turn, truth grounds knowledge." instead of
the prior provenance-laden narrative surface.
Critical gates (all green):
* flag off: cognition eval byte-identical
- public 100/100/91.7/100, holdout 100/100/83.3/100
* smoke suite 67/67
* conversational_thread_coherence: 3 unwanted placeholders flag off
and flag on (no regression)
* planner JSON byte-stable across calls (contract tests)
* grounding source order preserved (sidecar tests)
Three independent hygiene fixes named in the 2026-05-19 design review.
All small, all observable, none architectural.
1. ``RuntimeConfig`` flag drop on pack_id / frame_pack override
chat/runtime.py:306-320 used to enumerate fields by hand when
reconstructing RuntimeConfig under the pack_id / frame_pack
override path. The list stopped at ``admissibility_margin`` and
silently dropped FIVE newer flags: identity_pack, ethics_pack,
forward_graph_constraint, composed_surface, thread_anaphora.
Caller side-effect:
ChatRuntime(pack_id="x", config=RuntimeConfig(composed_surface=True))
.config.composed_surface == False # silently lost
Fix: ``dataclasses.replace(config, input_packs=..., frame_pack=...)``.
Every field on the dataclass survives by construction; future
additions never need a synchronized edit on this path.
2. Stale CAUSE / VERIFICATION docstring
tests/test_intent_classification_extensions.py described a sixth
runtime-side fix (pack_grounded_surface fallback for
CAUSE/VERIFICATION) that was considered, reverted, and the file's
own test classes pin the opposite contract. Docstring now states
the doctrine correctly: no fallback, deliberately, so the discovery
layer can log the teaching-gap signal.
3. Thin convenience wrappers: respond / achat / arespond
tests/test_achat.py and tests/test_language_pack_runtime.py
referenced these public methods since 2026-05-14, but they were
never implemented on ChatRuntime — those 12 tests had been red on
every full-lane run since the rebase. Added as thin wrappers:
respond(text) -> ChatResponse.surface
achat(text) -> async wrapper around chat()
arespond(text)-> async wrapper around respond()
The async wrappers are deliberately NOT genuinely non-blocking —
the underlying CPU-bound walk/recall/composition remains sync.
Docstrings say so explicitly. Callers needing real concurrency
should wrap in asyncio.to_thread at the call site; promoting the
wrappers to true async event-loop integration is a future change
gated by an actual concurrent caller.
Regression coverage:
tests/test_runtime_config_passthrough.py — 4 tests
- all 19 RuntimeConfig fields survive a pack_id override
- all five newer flags survive a frame_pack override
- no-override path preserves caller config by identity (no rebuild)
- the four public methods exist and are callable
Verification:
44/44 affected tests green (was 12 red pre-fix).
Cognition eval byte-identical on both splits.
No surface-format change; this commit is pure plumbing.
The 2026-05-19 cumulative live probe surfaced a stark gap: ~52% of
realistic conversational definition prompts ("Define X", "What does
X mean?", "What is to V?", "How does X work?", "What causes X?")
returned ``grounding_source="none"`` *even though every subject
lemma was pack-resident* across the 9 mounted English packs.
Root cause: the bottleneck was intent classification + subject
extraction, not lexicon coverage. Five patterns either had no rule
or routed to an intent the runtime dispatcher couldn't handle. The
fluency assessment at
``/Users/kaizenpro/.codex/worktrees/6533/core/notes/fluency_assessment_2026-05-19.md``
named these as Root Cause #1 ("public chat path does not use the
cognitive spine") and Root Cause #3 ("proposition graphs are too
thin"). This commit closes the surface-level half of that gap;
the deeper answer-plan layer (gloss propositions, P3 in the
assessment) is the next step.
Patterns fixed in ``generate/intent.py``:
1. ``Define X`` — added ``^define\s+`` rule mapping to
DEFINITION (placed after ``^what is/are``
so multi-word DEFINITION patterns still
prefer the question form).
2. ``What does X mean?`` — was matching TRANSITIVE_QUERY with
relation=``mean``. Now re-routes to
DEFINITION inside ``classify_intent`` so
``pack_grounded_surface`` fires on X.
Other transitive relations (precede,
ground, etc.) remain TRANSITIVE_QUERY.
3. ``What is to V?`` — added infinitive-marker strip to
``_normalize_subject`` for DEFINITION /
RECALL. ``to`` is gated on intent tag so
it never strips a transfer preposition
from CAUSE / VERIFICATION.
4. ``How does X work?`` — added ``_HOW_DOES_X_RE`` (third-person
mechanistic-cause). Distinct from the
first-person PROCEDURE rule ("How do I
X?"). Verbs: work / function / operate /
happen / exist / behave / act / emerge.
5. ``What causes X?`` — added causative-verb rule (causes /
triggers / enables / prevents / drives /
produces / induces / yields) routing to
CAUSE with X as subject.
Deliberate NON-fix: I considered adding a ``pack_grounded_surface``
fallback in the CAUSE / VERIFICATION dispatcher when no teaching
chain matches the subject. Reverted on review — that masks the
"would_have_grounded" discovery-candidate signal the teaching
pipeline uses to identify teaching-content gaps (see
``tests/test_discovery_candidates``). CAUSE on a pack-resident
lemma without a teaching chain stays ``grounding_source=='none'``
so the discovery layer can log the gap honestly.
``chat/pack_grounding.py``:
Extended ``_CORRECTION_TOPIC_STOPWORDS`` to include polarity
markers (no / yes / maybe / perhaps / hardly / indeed / surely /
definitely). Without this the CORRECTION composer would
short-circuit on ``no`` from "No, my parent disagrees" and miss
the topical lemma ``parent``.
Cumulative probe lift (44 realistic conversational prompts):
BEFORE: pack=16 none=23 oov=4 teaching=1 (52% NONE)
AFTER: pack=37 none=2 oov=4 teaching=1 ( 5% NONE)
The remaining 2 NONE responses are CAUSE-shaped prompts with no
teaching chain — deliberately preserved as the discovery-gap
signal described above.
Tests: tests/test_intent_classification_extensions.py — 23 new
tests covering each pattern + the lift invariant.
Verification:
Cognition eval byte-identical on both splits (100/100/91.7/100
public, 100/100/83.3/100 holdout).
All 111 intent-affected tests green:
test_intent_classification_extensions.py (23)
test_intent_proposition_graph.py / test_intent_ratifier.py /
test_intent_subject_extraction.py / test_narrative_example_intents.py
test_procedure_surface.py
test_correction_topic_lemma.py
test_cross_pack_grounding.py (including the polarity-stopword fix)
test_discovery_candidates.py
test_contemplation_wiring.py
test_en_core_polarity_v1_pack.py
Root cause: recalled_words was built from result.tokens (versor walk
neighbours) rather than the pack-resolved proposition slots. The walk
produces nearest-neighbour traversal artifacts; the proposition already
carries the correct subject/predicate/object from realize(). This made
ground_graph() fill <pending> obj slots with stop-word-adjacent tokens
instead of the actual answer content.
Fix — two changes, one new helper:
generate/intent_bridge.py
• build_recalled_words_from_plan(plan, proposition, walk_tokens)
Constructs the grounding tuple in priority order:
1. plan.object (ArticulationPlan — pack-resolved, already a word)
2. proposition.object_ (Proposition — versor-decoded object slot)
3. plan.predicate (descriptive predicate word, richer than walk)
4. plan.subject (subject as last-resort semantic anchor)
5. walk_tokens (result.tokens alpha-filtered — supplemental backfill)
Strips <pending>/<prior>/empty/non-alpha before deduplicating.
Returns a deduplicated tuple in that priority order.
• articulate_with_intent() gains an optional `proposition` param
(typed as object to avoid import coupling at the call site).
When provided, build_recalled_words_from_plan() is called to
replace the raw recalled_words before ground_graph() runs.
When omitted, behaviour is byte-identical to Phase 1 (backward
compatible: all existing callers and tests pass unchanged).
chat/runtime.py
• The single articulate_with_intent() call site now passes
proposition=proposition so the bridge receives the full
pack-resolved proposition for grounding. walk_tokens (the old
recalled_words) are passed through as supplemental backfill.
• No change to ChatResponse, TurnEvent, GenerationResult, or any
ADR-gated schema.
ADR-0066 P3.1 + P3.2. Conversation now reads as a thread: turns
carry structured summaries of their predecessors and (optionally)
prefix new pack/teaching surfaces with deterministic backreferences.
P3.1 — chat/thread_context.py.
TurnSummary(turn_index, intent_tag_name, subject, grounding_source,
chain_id, corpus_id) — frozen, structured-fields-only.
ThreadContext — bounded FIFO (default MAX_THREAD_TURNS=8) with
snapshot(), recent_for_subject(), recent_subjects(), clear().
recent_for_subject() excludes ungrounded tiers (oov/partial/none)
by default — those are not strong-enough anchors.
ChatRuntime.thread_context is owned at construction.
_push_thread_summary runs at end-of-turn on BOTH stub and walk
paths. Teaching-grounded turns carry chain_id + corpus_id so
downstream composers (P3.2) can detect same-chain reference.
Cold-start intent classification now runs unconditionally (was:
gated on sink attachment) so thread context captures subject
regardless of sink state.
P3.2 — chat/anaphora.py.
thread_anaphora_prefix(ctx, subject, intent_name, source) returns
a deterministic prefix when:
- current turn is pack/teaching tier
- a prior pack/teaching turn on the same subject exists
- the prior intent differs from the current intent
Format (structural-fields-only — no prose):
"(Recalling turn N: chain <chain_id>.) " # prior was teaching
"(Recalling turn N: <subject> grounded pack.) " # prior was pack
Opt-in via RuntimeConfig.thread_anaphora=False. Default off keeps
every existing surface byte-identical.
Live verification (with thread_anaphora=True + seeded context):
> What is light? # following a "Why does light exist?" teaching turn
[pack] (Recalling turn 0: chain cause_light_reveals_truth.)
light — pack-grounded (en_core_cognition_v1): cognition.illumination;
logos.core; perception.clarity. No session evidence yet.
32 new tests passed. Curated lanes green. Cognition eval
byte-identical to pre-ADR baseline.
Replaces the flat "I don't know — insufficient grounding" disclosure
with a deterministic gradient that names specific vocabulary gaps
and gives operators concrete next steps.
P2.1 — OOV "teach me" surface (chat/oov_surface.py).
When the intent classifier extracts a clean subject lemma but that
lemma is not resident in any mounted lexicon pack, the runtime now
emits a deterministic learning-invitation surface tagged
``grounding_source="oov"`` instead of the universal disclosure.
Surface format (fixed template):
"I haven't learned '{token}' yet (intent: {intent}).
Mounted lexicon packs: {pack_list}.
Teach me via a reviewed PackMutationProposal."
The OOV token passes through ``core._safe_display.safe_display``
before persistence — user-input sanitization at the trust boundary.
No vocabulary is invented; no domain is inferred. Honours the
ADR-0027 proposal-only invariant: the surface invites a reviewed
pack mutation, never silently mutates any pack.
Refactored ``_maybe_pack_grounded_surface`` so every existing
intent branch (COMPARISON / CAUSE / VERIFICATION / CORRECTION /
PROCEDURE / DEFINITION+RECALL) falls through on a None composer
result instead of early-returning. The OOV invitation is the
deterministic fall-through for any clean-subject prompt whose
subject doesn't resolve.
P2.2 — Partial-grounding tier (chat/partial_surface.py).
When exactly one of two COMPARISON lemmas resolves, the runtime
emits a hedged surface that grounds the known side verbatim and
disclaims the OOV side explicitly:
"Whatever '{oov}' is, I can ground '{known}' — pack-grounded
({pack_id}): {d1}; {d2}. I cannot ground the comparison
without learning '{oov}' — teach me via a reviewed
PackMutationProposal."
Tagged ``grounding_source="partial"``. Falls through to OOV
invitation when both lemmas are OOV, and to full pack-grounded
COMPARISON when both resolve — partial is the middle tier in the
five-tier gradient.
Also normalises trailing sentence punctuation on
intent.secondary_subject at the COMPARISON boundary so prompts
like "Compare A and B." (with the period) still resolve B
correctly.
Five-tier gradient (vault → teaching → pack → partial → oov → none).
Test debt retired: four pre-existing tests asserted "OOV → universal
disclosure", which is exactly the contract P2.1/P2.2 inverted.
Rewritten to the new contract. Plus test_procedure_surface.py
gained a test for the OOV gradient on procedure intents.
Verification:
tests/test_oov_surface.py 22 passed
tests/test_partial_surface.py 16 passed
Cognition eval byte-identical:
public 100% / 100% / 91.7% / 100%
holdout 100% / 100% / 83.3% / 100%
Curated lanes all green.
Pre-ADR-0062, the teaching-grounded composer emitted exactly one
reviewed chain per surface — "light reveals truth" — even when the
corpus already contained an immediate follow-up "truth grounds
knowledge". With 21 active chains after curriculum saturation v2,
many grounded prompts had a corpus-ratified follow-up the composer
silently dropped.
ADR-0062 adds the composed composer + an opt-in config flag:
flag OFF (default):
light — teaching-grounded (cognition_chains_v1): cognition.illumination;
logos.core. light reveals truth (cognition.truth). No session evidence yet.
flag ON:
light — teaching-grounded (cognition_chains_v1): cognition.illumination;
logos.core. light reveals truth (cognition.truth), which grounds
knowledge (cognition.knowledge). No session evidence yet.
Follow-up resolution:
- prefer cause; fall back to verification (deterministic preference)
- cycle guard: 1-step cycles (A→B, B→A) blocked
- pack-residency guard: follow-up's object must be pack-resident
- bounded depth: v1 follows exactly one hop
- degrades to single-chain BYTE-IDENTICALLY when no follow-up
survives the guards (drop-in replacement)
Trust-boundary invariants preserved:
- Every visible non-template token is lemma / pack-domain /
humanize_predicate connective / template constant. Only added
template constant: ", which "
- Deterministic: same chains → same surface bytes
- Default-False flag pattern mirrors ADR-0047/0058
- `versor_condition < 1e-6` invariant untouched (surface composition only)
Cognition lane null-drop invariant CI-pinned:
Composed mode emits a strictly LONGER surface (extra follow-up
clause); every expected_term passing flag-OFF must still pass flag-ON.
Asserted in test_cognition_lane_metrics_unchanged_with_composed_flag
for both public and holdout splits. If a future change drops tokens,
the test fails as a deliberate regression.
public flag OFF: intent 100% / surface 100% / term 91.7% / versor 100%
public flag ON : intent 100% / surface 100% / term 91.7% / versor 100% (identical)
holdout flag OFF: intent 100% / surface 100% / term 83.3% / versor 100%
holdout flag ON : intent 100% / surface 100% / term 83.3% / versor 100% (identical)
Live-prompt lift visible on ~12 of 21 active chains; the rest hit
cycle or pack-residency guards. Saturation v2's clusters were
authored partly with composition in mind (thought→meaning→
understanding, inference→evidence→knowledge, etc.).
- core/config.py — `RuntimeConfig.composed_surface: bool = False`
- chat/teaching_grounding.py — `teaching_grounded_surface_composed`
sibling to `teaching_grounded_surface`
- chat/runtime.py — dispatch branch in `_maybe_pack_grounded_surface`
selects composed vs single-chain based on config flag
- tests/test_composed_surface.py — 11 tests pin: function-level
(None on no chain / degrades when no follow-up / two-clause when
follow-up exists / includes intermediate + final domains /
deterministic / cycle guard / trust label preserved); runtime
integration (default single-chain / flag-on composed / frozen
config); cognition-lane null-drop invariant.
Lanes (regression): smoke 67 / cognition 121 / teaching 17 /
composed-surface 11 — all green.
Pre-ADR-0061 every "How do I X?" question fell through to the
universal disclosure even when X was a pack-resident lemma. The
teaching corpus carries CAUSE/VERIFICATION chains only — procedural
knowledge is fundamentally different in kind from propositional
claims and deserves its own ratification path (deliberately out of
scope; a future parallel `procedure_chains_v1.jsonl` schema is
discussed in the ADR's non-goals).
ADR-0061 adds the honest cold-start fallback: ground the topic in
pack semantic_domains and note explicitly that ratified step-by-step
guidance does not exist yet.
Surface format:
"procedure-grounded ({pack_id}): {lemma} ({d1}; {d2}).
Step-by-step guidance for {lemma} is not yet ratified
in this session."
Selector — **last** pack-resident lemma in the verb-phrase subject:
"define a concept" → concept (object beats verb)
"verify a claim" → verify (verb wins when object is OOV)
"correct an error" → correct
"learn this" → learn
"do stuff" → None (falls through to universal disclosure)
Stopwords: only `be` and `have` (dialogue fillers). Procedure verbs
are deliberately NOT stopworded so the verb-as-fallback rule fires
when the object is OOV — keeps surface coverage.
Trust-boundary invariants:
- Every visible non-template token is lemma / pack-domain / template.
- Deterministic: same subject_text → same bytes.
- Returns None for fully-unknown utterances → universal disclosure
fires. Never fabricates surface from nothing (ADR-0053 contract).
- "not yet ratified" trust-label preserved.
Cognition lane lift:
public : intent 100% / surface 100% / term 91.7% / versor 100% (unchanged)
holdout : intent 100% / surface 94.7%→100.0% / term 79.2%→83.3% / versor 100%
Two cases fixed:
- procedure_define_010 ("How do I define a concept?") — surface +
term `concept` now captured.
- procedure_verify_034 ("How do I verify a claim?") — surface only
(case has no expected_terms; the verb fallback grounds it).
Combined effect: holdout `surface_groundedness` closes to 100%; 4 of
5 architectural holdout misses now resolved (this ADR + ADR-0060 +
the supersede from epistemology v1). Remaining 2 are UNKNOWN-intent
cases (unknown_spirit_041, unknown_word_018) — out of scope; deserve
their own ADR with distinct selector semantics.
- chat/pack_grounding.py — `_extract_procedure_topic_lemma` helper +
`pack_grounded_procedure_surface` composer.
- chat/runtime.py — import + dispatch branch for `IntentTag.PROCEDURE`.
- tests/test_procedure_surface.py — 15 tests pin: extraction
(last-wins / verb-by-elimination / be+have skipped / None on empty /
strips punctuation / case-insensitive); surface (contains lemma /
contains domains / pack_id / "not yet ratified" label / None for
no-pack-lemma / deterministic); end-to-end through ChatRuntime.
Lanes (regression): smoke 67 / cognition 121 / teaching 17 /
procedure 15 — all green.
The non-negotiable field invariant (versor_condition < 1e-6) is
unaffected: this ADR changes surface composition only.
ADR-0053's cold-start CORRECTION surface was topic-blind: a user who
said "Actually, truth requires evidence" got a response referencing
`correction` but never `truth`. The holdout case correction_truth_040
expected `term=['truth']` and missed — one of the architectural gaps
surfaced by the epistemology v1 curriculum unit.
ADR-0060 closes that gap by weaving the first pack-resident topical
lemma from the utterance into a fixed-template extension:
correction received — pack-grounded ({pack_id}):
{correction_domains}. Noted topic: {lemma} ({lemma_domains}).
No prior turn in this session to correct yet.
Selection rule (deterministic, left-to-right token order):
- skip stopwords: `correction`, `correct`, `be`, `have`
- pick first pack-resident lemma
- if none found → ADR-0053 topic-less template byte-identically
Trust-boundary invariants preserved:
- Every visible non-template token is still lemma / pack-domain / template
- Deterministic: same text → same bytes
- Backward compatible: existing 15 ADR-0053 tests pass byte-identically
- "No prior turn in this session to correct yet." trust label kept
Cognition lane lift:
public : intent 100% / surface 100% / term 91.7% / versor 100% (unchanged)
holdout : intent 100% / surface 94.7% / term 75.0%→79.2% / versor 100%
The +4.2pp matches the single-case fix exactly (correction_truth_040).
Remaining 3 holdout misses (procedure_define_010, unknown_spirit_041,
unknown_word_018) are out of scope for this ADR.
- chat/pack_grounding.py — `_extract_correction_topic_lemma` helper +
optional `text` parameter on `pack_grounded_correction_surface`.
- chat/runtime.py — single-line call-site change to pass `text` through.
- tests/test_correction_topic_lemma.py — 14 new tests pin:
extraction (first lemma / skips correction / skips fillers / None on
empty / strips punctuation / case-insensitive); surface (contains
corrected lemma / contains topic domains / degrades to ADR-0053
byte-identically / preserves trust label / deterministic / correct
pack_id); end-to-end (correction_truth_040 emits 'truth' / no-pack-
lemma still grounds).
Why text-level extraction, not intent.subject:
`intent.subject` after ADR-0049 head-noun extraction returns
", truth requires evidence" for the test prompt — the CORRECTION
intent's subject-extractor preserves the post-marker tail. Parsing
the raw text at the surface layer is cleaner; isolates the fix;
doesn't perturb upstream classification logic.
Lanes (regression): smoke 67 / cognition 121 / teaching 17 /
correction tests 29 (15 ADR-0053 backward-compat + 14 ADR-0060 new) —
all green.
The non-negotiable field invariant (versor_condition < 1e-6) is
unaffected: this ADR changes surface composition only.
`ChatRuntime.correct()` propagates a backward perturbation through the
session graph (per session/correction.py): each past turn whose output
versor has non-trivial CGA-alignment with the correction versor is
blended toward it (decayed by graph distance). The forward regen turn
that followed already emitted a TurnEvent — but the backward
perturbation itself was invisible to the telemetry sink.
ADR-0059 closes that gap with a discriminated event line.
- chat/telemetry.py — adds `serialize_correction_event` +
`format_correction_event_jsonl` emitting one JSONL line discriminated
by `"type": "correction"`. Payload: target_turn, records_count,
turns_skipped, turn_idxs_affected, max_delta_norm, mean_delta_norm,
SHA-256 correction_versor_digest, pack ids. No raw versor coordinates.
- chat/runtime.py — `_emit_correction_event` (mirrors
`_emit_turn_event`); called from `correct()` after the graph state
is updated but before the forward regen turn. No-op without sink.
- tests/test_correction_telemetry.py — 7 tests pin: no-op without
sink, emission with sink, payload shape (required keys + types +
ranges), SHA-256 digest shape, trust boundary (no versor
coordinates leaked), determinism (byte-identical lines across
runs), correction event and turn event coexist in the sink.
Trust boundary (per CLAUDE.md):
- Metadata-only: only L2 deltas + SHA-256 digest.
- No implicit wall-clock.
- Deterministic: same CorrectionResult → byte-identical line.
- Sink contract unchanged: `emit(line: str)`.
- `versor_condition < 1e-6` invariant: untouched (telemetry-only).
Verification: smoke 67 / runtime 19 / correction telemetry 7 — green.
ChatRuntime.attach_contemplation(enabled=True) flips an opt-in
flag; when on, each emitted DiscoveryCandidate runs through
teaching.contemplation.contemplate before the sink writes the
JSONL line. Default off ⇒ Phase B raw output preserved byte-
identical.
Trust boundary
- Contemplation is read-only over pack + corpus.
- Without an attached discovery sink the flag is inert (no hidden
work — emission requires an observable destination).
- Active teaching corpus on disk byte-identical pre/post.
Lanes: smoke 67 / runtime 19 / cognition 121 / contemplation-
wiring 6 — all green. Cognition eval unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lands the first deterministic trigger of the discovery → reviewed-
memory loop. Candidates are structured evidence; emission is
opt-in via attach_discovery_sink and NEVER mutates the active
teaching corpus.
- teaching/discovery.py: DiscoveryCandidate dataclass + pure
extract_discovery_candidates(turn_event, intent, subject) rule
firing. Phase B fires only the would_have_grounded trigger:
grounding_source == "none"
AND intent ∈ {CAUSE, VERIFICATION}
AND subject lemma in ratified cognition pack
AND (subject, intent) NOT in active corpus
candidate_id = SHA-256 of canonical JSON payload — replay-stable.
Other DiscoveryTrigger literals (successful_comparison,
hedge_acknowledged, oov_resolved_via_decomp) are reserved for
later phases.
- teaching/discovery_sink.py: DiscoveryCandidateSink protocol,
DiscoveryBufferSink (in-memory), DiscoveryMonthlyFileSink
(append-only JSONL, <root>/<YYYY>/<YYYY-MM>.jsonl rollover,
injectable clock).
- chat/runtime.py: opt-in attach_discovery_sink, post-turn
emission inside _stub_response only when caller threads
classified intent forward (gate-fire fall-through site).
Intent classification at the call site reuses the same
deterministic classifier already invoked by
_maybe_pack_grounded_surface for the empty-vault English path.
Trust boundary: candidates write to a separate sink/file path
only; the active corpus on disk is never touched. Tests
explicitly assert corpus bytes are byte-identical before and
after a candidate-emitting turn.
Tests: tests/test_discovery_candidates.py — 24 tests covering
pure-predicate rule firing, every short-circuit path,
deterministic candidate_id, sink opt-in, runtime parity with no
sink, monthly rollover semantics, append-only behaviour, no
corpus mutation.
Lanes: smoke 67, cognition 121, runtime 19, teaching 17, packs 6
— all green. Cognition eval metrics unchanged on dev / public /
holdout splits. versor_condition < 1e-6 invariant untouched.
Closes both cognition splits at 100% surface_groundedness. Three
parts:
1. Teaching corpus expansion (no code). cognition_chains_v1.jsonl
grows 3→10 chains. 3 close dev-split misses (correction,
creation, light-as-VERIFICATION); 4 pre-empt the analogous
holdout pattern (CAUSE/VERIFICATION on truth + wisdom). Every
subject/object is a pack lemma; every connective is a recognised
humanize_predicate predicate.
2. CORRECTION acknowledgement branch. New
`pack_grounded_correction_surface()` in chat/pack_grounding.py,
wired into `_maybe_pack_grounded_surface` for cold-start
CORRECTION intents. Fixed-template surface with distinct
trailing disclosure ("No prior turn in this session to correct
yet.") — distinguishes the cold-start acknowledgement from the
DEFINITION-of-correction surface. The post-correction reviewed-
teaching path in teaching/correction.py is unchanged.
3. Diagnostic memory. Saves the dev-split generalization finding:
the ADR-0048→0052 chain is NOT overfit. Public/dev gap was
teaching-corpus content coverage, not architecture.
Eval deltas (both splits run, post-ADR-0053):
public dev
intent_accuracy 100% 100% (=)
surface_groundedness 100% 100% SATURATED
term_capture_rate 91.7% 78.6%
versor_closure_rate 100% 100% (=)
Public surface_groundedness: 92.3% → 100% (+7.7 pp)
Dev surface_groundedness: 69.2% → 100% (+30.8 pp)
Tests: tests/test_pack_grounded_correction.py (15 new tests).
Lanes green: smoke (67), cognition (121), runtime (19),
teaching (17), packs (6).
Scope limits: holdouts (19 cases) not yet in the official
`core eval cognition` runner (--split accepts only {dev, public});
the CORRECTION surface does not yet echo the corrected-subject
lemma (relevant only for holdout case `correction_truth_040`).
Sibling to ADR-0048's DEFINITION/RECALL pack-grounded surface for
the COMPARISON intent. `pack_grounded_comparison_surface(a, b)` in
`chat/pack_grounding.py` composes a deterministic side-by-side
surface from both lemmas' pack `semantic_domains`, joined by the
fixed connective "contrasts with":
"{a} (d_a1; d_a2) contrasts with {b} (d_b1; d_b2) — pack-grounded
({pack_id}). No session evidence yet."
`chat/runtime.py:_maybe_pack_grounded_surface` gains a COMPARISON
branch that runs before the DEFINITION/RECALL check. Engages only
when both `intent.subject` and `intent.secondary_subject` are pack
lemmas and differ (identical-lemma comparison defers to disclosure).
Order-sensitive by design — matches the graph-layer's directional
CONTRAST edge.
Cognition eval (13-case public split):
surface_groundedness 61.5% → 69.2% (+7.7 pp)
term_capture_rate 50.0% → 58.3% (+8.3 pp)
intent_accuracy 100.0% (=)
versor_closure_rate 100.0% (=)
Case lifted: comparison_memory_recall_030 ("Compare memory and
recall"). Remaining unlift cases (CAUSE×2, VERIFICATION×1,
CORRECTION×1) need teaching-store chains or operator-driven
inference — pack lookup cannot supply causal explanations,
verifications, or corrections without fabrication.
Tests: tests/test_pack_grounded_comparison.py (15 tests).
Lanes green: smoke (67), cognition (121), runtime (19), algebra
(132), teaching (17), packs (6).
Closes the surface-grounding gap isolated by ADR-0047's
characterisation. Adds the ratified cognition pack as a second
grounding source alongside the session vault.
== chat/pack_grounding.py (new) ==
Loads en_core_cognition_v1's lexicon once (cached; immutable pack)
and exposes:
pack_grounded_surface(lemma) -> str | None
Returns a deterministic, fully pack-sourced surface:
"{lemma} — pack-grounded ({pack_id}): {d1}; {d2}; {d3}. No session evidence yet."
Every visible atom is the lemma or a verbatim semantic_domains
string from the pack. No rewording, no synthesis, no LLM.
== chat/runtime.py ==
_stub_response gains optional pack_grounded_surface= parameter.
_maybe_pack_grounded_surface routes to the pack only when all four
hold: gate_source=="empty_vault", output_language=="en",
intent.tag in {DEFINITION, RECALL}, and intent.subject is a pack
lemma. Safety/ethics refusal still takes priority above this branch.
ChatResponse and TurnEvent gain grounding_source ∈ {vault,pack,none}.
Main walk path tags responses "vault".
== core/cognition/pipeline.py ==
gate_fired detection moved from string equality on the universal
disclosure to provenance:
gate_fired = response.vault_hits == 0 and response.grounding_source != "vault"
Same intent (suppress realizer template on gate-fired turns),
broader stub-path surface set.
== Characterisation (core eval cognition, 13-case public split) ==
Metric Pre Post Δ
intent_accuracy 100.0% 100.0% 0
surface_groundedness 15.4% 46.2% +30.8 pp
term_capture_rate 0.0% 33.3% +33.3 pp
versor_closure_rate 100.0% 100.0% 0
Lift is non-uniform by design: only single-lemma DEFINITION/RECALL
on pack-known English subjects engage. CAUSE/COMPARISON/VERIFICATION
and multi-word OOV subjects still return the universal disclosure —
fabricating those would violate the no-LLM-fallback doctrine.
== Tests ==
tests/test_pack_grounding.py 18 passed
tests/test_semantic_realizer_integration.py (updated) 1 stub-path test
pinned to the broader contract: surface is either universal
disclosure or pack-grounded; never the realizer template.
== Lanes ==
smoke 67 cognition 121 runtime 19 algebra 132
teaching 17 packs 6
versor_condition(F) < 1e-6 invariant unaffected (no algebra changes).
Closes ADR-0046's deferred follow-up: convert the PropositionGraph
into an AdmissibilityRegion BEFORE generate() runs on the live
chat path.
== generate/intent_bridge.py ==
New public helper:
build_graph_from_input(text, plan) -> PropositionGraph
Same internal call as _build_graph_from_intent, without the
post-generation ground_graph step — suitable for forward use.
== chat/runtime.py ==
When the new flag is on and output language is English, build the
graph and the region before generate() and pass it via region=.
Empty / fully OOV graphs return AdmissibilityRegion(allowed_indices=None),
which generate() treats as unconstrained — the change is a true
no-op when the graph carries no in-vocab anchors.
== core/config.py ==
RuntimeConfig.forward_graph_constraint: bool = False
Default False preserves all pre-ADR-0046 behaviour and the ADR-0024
honest-refusal contract. A first attempt wired the constraint
unconditionally; 15 tests failed with InnerLoopExhaustion because the
intent-derived graph's CGA neighbourhood doesn't intersect the walk's
candidate pool with top_k=8 on the current packs. The honest answer
is not to widen top_k until the failure goes away nor to silently
relax — both erase the architectural information that the geometry
of the graph and the geometry of the walk are not yet co-located.
Opt-in preserves ADR-0024 and follows the ADR-0022→0026 transition-
window pattern.
== Characterisation (core eval cognition, 13-case public split) ==
A/B with the flag toggled:
Metric OFF ON Δ
intent_accuracy 100.0% 100.0% 0
surface_groundedness 15.4% 15.4% 0
term_capture_rate 0.0% 0.0% 0
versor_closure_rate 100.0% 100.0% 0
InnerLoopExhaustion 0 0 0
non-trivial constraint n/a 6 / 13 —
Findings:
- Wiring is correct and safe (no exhaustions, closure unchanged).
- Single-token in-vocab subjects engage the constraint
(light/knowledge/meaning/memory/correction).
- Multi-word OOV subject phrases produced by the intent classifier
fall through to unconstrained — this is the existing intent-
classifier contract surfacing into geometry, not a constraint bug.
- Restricting which tokens the walk may visit did not change
surface_groundedness or term_capture_rate on this lane. The
surface-grounding gap therefore lives downstream of propagation
— in the realizer / surface-assembly / dialogue-role path — and is
the next load-bearing pull. This isolates the next ADR's scope.
== tests/test_forward_graph_constraint_wiring.py (5 tests) ==
- DEFAULT_CONFIG.forward_graph_constraint is False
- Default runtime answers without InnerLoopExhaustion
- Opt-in runtime answers on a short benign input
- Graph builder + build_graph_constraint produce a labelled
AdmissibilityRegion ("graph:unconstrained" or "graph:<root_id>")
- Flag is observable on the frozen RuntimeConfig
== docs/decisions/ ==
- ADR-0047 ratifies the wire-up, opt-in rationale, and A/B numbers.
- README index updated; the Pillar 1→2→3 section now reflects both
the primitive (ADR-0046) and the live wiring (ADR-0047), and
names the next pull (realizer / surface assembly) explicitly.
Verification (this branch):
tests/test_forward_graph_constraint_wiring.py 5 passed
tests/test_graph_constraint.py 8 passed
core test --suite smoke 67 passed
core test --suite cognition 121 passed
core test --suite runtime 19 passed
core test --suite algebra 132 passed
core test --suite teaching 17 passed
core test --suite packs 6 passed
core eval cognition metrics unchanged from main
versor_condition(F) < 1e-6 invariant unaffected.
Closes three audit gaps left by the ADR-0035→ADR-0038 pack-layer
surface:
1. TurnVerdicts bundle (chat/verdicts.py) — frozen dataclass
aggregating identity_score + safety_verdict + ethics_verdict +
refusal_emitted + hedge_injected. Attached to both
ChatResponse.verdicts and TurnEvent.verdicts. Fields typed as
object for the same module-coupling reason as
TurnEvent.safety_verdict.
2. Stub-path TurnEvent emission — _stub_response accepts optional
tokens kwarg and appends a TurnEvent to turn_log when invoked
from a real turn. Audit consumers can now iterate turn_log
end-to-end without missing stub paths. Defensive call sites
(correct() fallback) bypass the append by omitting tokens.
3. refusal_emitted / hedge_injected flags — runtime tracks whether
it actually mutated the surface this turn. hedge_injected uses
idempotent-on-prefix semantics (True iff the runtime ADDED a
hedge, not iff a hedge happens to be present).
Test-pattern note: previous "gate on rt.turn_log to detect main vs
stub" pattern is now broken; updated to gate on walk_surface ==
_UNKNOWN_DOMAIN_SURFACE. One existing hedge-injection test gate
updated accordingly.
Back-compat: ADR-0035→0038 per-field accessors
(response.safety_verdict, etc.) still work. New consumers should
read response.verdicts.
Files:
- chat/verdicts.py (new) — TurnVerdicts dataclass
- chat/runtime.py — _stub_response tokens kwarg + stub TurnEvent
append + hedge_injected tracking + bundle construction
- core/physics/identity.py — TurnEvent.verdicts: object = None
- tests/test_turn_verdicts_bundle.py (new) — 16 tests
- tests/test_hedge_injection.py — gate fix for stub detection
- docs/decisions/ADR-0039-audit-completeness.md (new)
Verification:
- Combined pack-layer suite: 170 green (was 154 after ADR-0038)
- CLI suites unchanged: smoke 67, runtime 19, cognition 121
- core eval cognition: intent 100%, versor_closure 100% (baseline)