core/docs/sessions/SESSION-2026-05-21-articulation-arc.md
Shay 8893962245 docs(sessions): extend articulation-arc note for Phase 5
Phase 5 landed in commit 327047c (articulation-quality miner +
runtime sink wiring + full end-to-end loop tests).  Extending the
session note in-place per its own append-only convention so the
single document covers the complete arc Phases 1–5.

Sections updated
----------------

* §0 executive summary
    - commit count: 9 → 11
    - phases shipped: 4 → 5 (new Phase 5 row in the deliverables
      table)
    - observation surfaces table grows two rows for
      ``attach_articulation_sink`` and
      ``mine_articulation_observations``
    - test artifacts: 6 → 8 files, 64 → 82 cases

* §3.5 NEW — full architectural walkthrough of Phase 5
    - Why the loop closes (links the user's
      "memory confidence scoring" intuition to ADR-0080's
      doctrine-aligned realisation: reviewable evidence, not
      autonomous mutation)
    - File-by-file delta (chat/articulation_telemetry.py + miner
      + runtime wiring)
    - Three v1 mining rules (recurring_predicate_monotony /
      recurring_planner_gap / low_average_predicate_diversity)
    - Loop diagram showing live + offline halves
    - Recorded demo output from the commit message
    - Doctrine pin table mapping each constraint to its test

* §4 pipeline diagram extended to show Phase 5 sink + offline
  miner branches

* §5 verification table gains four new rows for Phase 5 claims
  (full-loop emission, byte-equal finding IDs across two e2e
  runs, JSONL round-trip identity, opt-in gating)

* §5.6 suite totals updated:
    - Contemplation subsuite: 35/35 → 53/53 (Phase 3+4+5)
    - New row for Phase 5 articulation-quality e2e (7/7)

* §6 case study — added the "After Phase 5" trace and the
  closing one-line story across all five phases for one prompt
  ("What is truth, and why does it matter?")

* §7 architecture surfaces table grows a row for
  chat/articulation_telemetry.py and adds the miner to the
  contemplation subsystem row

* §9 inverted from "what would close the loop" (future work) to
  "SHIPPED — here's what it now unlocks":
    - production sink + retention policy
    - additional aggregation rules
    - CLI hook
    - review-loop wiring back into PackMutationProposal

* §10 reference index grows new lines for
  chat/articulation_telemetry.py,
  core/contemplation/miners/articulation_quality.py,
  tests/test_articulation_quality_miner.py, and
  tests/test_articulation_quality_e2e.py

* Footer updated: notes the arc is complete; future arcs should
  start a new session-notes file and cross-link rather than
  rewriting this one.

The session-notes file is now 1100+ lines — the complete frozen
reference for the articulation arc that took CORE from one-sentence
pack-grounded surfaces to a full live-reasoning + offline-mining
+ reviewable-proposals feedback loop, all doctrine-aligned, all in
one session.
2026-05-21 11:03:01 -07:00

55 KiB
Raw Blame History

Session Notes — 2026-05-21: Articulation Arc

Status: Shipped. Phases 15 live on main. The full loop closes.

Top commits (most recent first):

  • 327047c feat(contemplation): Phase 5 — articulation-quality miner closes the loop
  • 1740b7d docs(sessions): articulation arc — comprehensive session note
  • b07fb04 feat(contemplation): Phase 4 — per-plan articulation telemetry metrics
  • 664e081 feat(contemplation): Phase 3 — live plan contemplation pre-flight
  • 9dfb505 feat(discourse): Phase 2 — reflective rendering pronominalizes focus subject
  • 63ffd88 feat(runtime): default discourse_planner=True + fast-path BRIEF short-circuit
  • 756e047 perf(rust): zero-copy FFI for diffusion_step + parity-aligned bench gate
  • c945b9a fix(intent): widen CORRECTION to catch fully-spoken that is/was ... forms
  • 0dd30b8 fix(intent): anchor CORRECTION trigger with word boundaries
  • 7ef4ef4 fix(intent): widen RECALL trigger to accept recall alongside remember

This document is the load-bearing reference for how the articulation subsystem grew from one-sentence pack-grounded surfaces into a four-layer pipeline that plans, renders reflectively, contemplates its own output, and emits structured telemetry — all deterministically, all doctrine-aligned, and all without an LLM in the loop.

Future case studies / architectural reviews / capability audits should start here.


0. What was achieved (executive summary)

This session shipped 11 commits to main across three orthogonal tracks. Net deliverables:

0.1 The articulation arc — 5 phases shipped

Phase Commit What landed
Pre-arc — RECALL classifier 7ef4ef4 One-regex widening; closed an articulation-bench misclassification
Pre-arc — CORRECTION boundaries 0dd30b8 Anchored 7 different prefix-eat bug classes (No, Incorrect, Actually, Correction)
Pre-arc — CORRECTION copula c945b9a 8 new natural CORRECTION pragmas now classify correctly
Pre-arc — Rust FFI 756e047 Zero-copy diffusion_step + doctrine-aligned bench gate; turned [FAIL] backend_speedup into [PASS]
Phase 1 63ffd88 discourse_planner=True by default + perf fast-path; multi-sentence articulations live for NARRATIVE / EXPLAIN / PARAGRAPH / compound prompts
Phase 2 9dfb505 Reflective rendering — subject pronominalization across moves; 5× truth → it substitutions on the 6-sentence compound prompt
Phase 3 664e081 Live plan contemplation — system emits SPECULATIVE findings about its own articulation plan
Phase 4 b07fb04 Per-plan articulation metrics — 12 quantitative measurements per turn, deterministic, aggregable
Phase 5 327047c Articulation-quality miner — aggregates observations across many turns into reviewable SPECULATIVE pack-mutation candidates. The full live-reasoning → memory-confidence loop closes.

0.2 Concrete user-visible improvements

The exact same prompts that were single-fragment or refused at session start are now multi-sentence grounded articulations:

"What is knowledge?"  →  unchanged (BRIEF fast-path; perf-preserved)

"Tell me about memory."
  before:  "memory — narrative-grounded (...): memory requires recall.
            No session evidence yet."
  after:   "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?"
  before:  "I haven't learned 'truth, and why does it matter' yet..."
            (refused as OOV)
  after:   "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."
            (6 grounded sentences via the compound bypass)

"Explain truth."
  before:  "Truth is what is true. pack-grounded (...)"
  after:   "Truth is what is true. Furthermore, it belongs to
            cognition.truth. In turn, it grounds knowledge."

0.3 New deterministic observation surfaces

Surface What it carries When populated
runtime.last_plan_findings tuple[ContemplationFinding, ...] — SPECULATIVE qualitative concerns Phase 3 / 4 flag on + planner engaged
runtime.last_plan_metrics PlanMetrics — 12 typed numeric fields Phase 3 / 4 flag on + planner engaged
runtime.attach_articulation_sink(sink) append-only JSONL of per-turn ArticulationObservation records Phase 5 sink attached + contemplation on + planner engaged
mine_articulation_observations(jsonl) offline aggregator → PACK_MUTATION_CANDIDATE findings Phase 5 — operator-triggered

All read-only. All pure deterministic functions of their inputs. The Phase 5 loop closes the user's intuited "live reasoning → memory confidence" arc — but doctrine-aligned: findings flow to operator review, not autonomous mutation.

0.4 Test artifacts added this session

Test file Cases Pins
tests/test_intent_subject_extraction.py +21 cases (RECALL + CORRECTION boundary + CORRECTION copula) Three classifier-defect classes against regression
tests/test_discourse_planner_reflective.py 8 Phase 2 reflective rendering + back-compat
tests/test_plan_contemplation.py 11 Phase 3 rules + determinism + SPECULATIVE doctrine
tests/test_plan_contemplation_runtime.py 6 Phase 3 runtime wiring + cross-turn reset
tests/test_plan_metrics.py 10 Phase 4 measurements + byte-equal as_dict
tests/test_plan_metrics_runtime.py 8 Phase 4 runtime wiring + co-population with findings
tests/test_articulation_quality_miner.py 11 Phase 5 miner rules + determinism + SPECULATIVE doctrine + JSONL round-trip
tests/test_articulation_quality_e2e.py 7 Phase 5 full live-runtime → JSONL → offline-miner → PACK_MUTATION_CANDIDATE loop
Total new test cases 82

Net session: 11 commits, 82 new tests, 0 regressions in any load-bearing gate.

0.5 Doctrine evidence

Every commit is doctrine-aligned per CLAUDE.md:

  • No LLM fallback, no stochastic sampling — every phase is a pure deterministic transform of grounded substrate.
  • No autonomous learning path — Phase 3 emits SPECULATIVE findings, Phase 4 emits raw measurements; neither mutates packs, vault, teaching corpus, or runtime state.
  • Replayable — same input → byte-equal output (pinned by 4+ determinism tests across the phases).
  • Reviewed-only memory mutation — the existing proposal-review-ratify chain remains the only path to memory.

1. Why this work happened

1.1 The visible gap at session start

Before this session, the user-facing surface for any prompt — no matter the intent shape, no matter the substrate depth — was almost always a single sentence:

"Knowledge is what a person knows from truth and evidence. pack-grounded (en_core_cognition_v1)."
"Truth is what is true. pack-grounded (en_core_cognition_v1)."

Multi-sentence prompts existed as templated stretches (the EXPLAIN/PARAGRAPH/COMPOUND/WALKTHROUGH modes produced 26 sentences) but the sentences came from chat/articulation.py templates, not from content-driven discourse. The articulation_bench reported multi_sentence_rate: 1.0 for those modes, but operators reading the surfaces could tell the sentences were template-mechanical, not genuinely articulated.

The user described it as:

"It's going to take creativity in composing sentences." "We need to be masterful with our solutions and make sure we are being genius engineers while being artistic linguists." "Are we maximizing proficiency and capabilities of our 'contemplating'/reasoning learning in order to refine and improve sentences, maybe at meaningful times in the pipeline as we construct a sentence, in order to have a stronger idea of what has come prior and is already done to help better inform the next move in the construction process?"

That last quote is the literal thesis of Phases 2 + 3.

1.2 The doctrine constraint

CORE's CLAUDE.md is explicit about what counts as "improvement":

listen → comprehend → recall → think → articulate → learn from
   reviewed correction → replay deterministically

Forbidden:

  • Opaque LLM fallbacks
  • Stochastic sampling
  • Hidden normalization
  • Autonomous learning paths
  • Approximate recall on the runtime path

Required:

  • Deterministic
  • Replayable (byte-identical trace_hash)
  • Reviewed-only memory mutation
  • Inspectable state and provenance

This is the constraint that shaped every phase: every articulation improvement had to be a deterministic pure function of grounded substrate, with a byte-identical null-lift path so the cognition eval stays unperturbed.

1.3 What the architecture already had vs. what was missing

A surprise of the investigation: most of the articulation apparatus was already wired but gated off.

Component State at session start
generate/discourse_planner.py Full plan_discourse / plan_compound_discourse / render_plan implementations existed. The module's own docstring claimed "no runtime wiring" but the runtime hook _maybe_apply_discourse_planner was present in chat/runtime.py:969.
generate/grounding_accessors.py::grounding_bundle_for Built. Returns a GroundingBundle from pack + teaching + cross-pack queries on a lemma.
RuntimeConfig.discourse_planner Existed as bool = False. Opt-in. Default off.
core/contemplation/ Existed as an offline evidence-file miner (ADR-0080). Read-only, SPECULATIVE-only. Not in the live turn pipeline.
chat/telemetry.py Structured JSONL turn-event sink (ADR-0040). Field-getattr pattern so wire format degrades gracefully.

What was missing:

  • The discourse-planner default was OFF (cognition eval byte-equality not yet proven).
  • The renderer was strictly one-pass with no awareness of prior fragments (mechanical-feeling output).
  • No way for the runtime to reason about the plan it just built.
  • No quantitative signal about plan quality that downstream miners could aggregate.

2. The pre-articulation work (audit + cleanup)

Before any articulation work could land cleanly, we ran a sweep that surfaced and fixed three real classifier defects. These commits made the bench numbers honest and gave the articulation arc a clean substrate.

2.1 7ef4ef4 — RECALL trigger accepted only remember

The articulation bench probe "Recall truth." was classifying as UNKNOWN. The classifier's RECALL regex matched only remember\s+; the synonymous imperative recall\s+ was absent.

Fix: widen the alternation to (?:remember|recall)\s+. One-word change, 7 new parametrized regression tests.

2.2 0dd30b8 — CORRECTION regex prefix-ate No-leading words

The CORRECTION alternation (?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction) had no word boundaries. Combined with .match's start anchor, every prompt starting with No-, Incorrect-, Actually-, or Correction-prefixed letters silently routed to CORRECTION with a mangled subject:

Prompt Was Should be
Now remember light. CORRECTION (subject "w remember light") UNKNOWN
Nothing matters. CORRECTION (subject "thing matters") UNKNOWN
Notice the truth. CORRECTION UNKNOWN
Incorrectly stated. CORRECTION UNKNOWN
Corrections department. CORRECTION UNKNOWN
Norma is here. (proper noun!) CORRECTION (subject "rma is here") UNKNOWN

Fix: anchor with \b on both sides. 18 new parametrized tests pin the boundary discipline against regression.

2.3 c945b9a — CORRECTION required literal contracted 's

The slot that'?s\s+(?:not|wrong) matched that's / thats but not the fully-spoken copula form: That is not right., That was wrong., That is incorrect., That is false., That is mistaken. all silently fell through to UNKNOWN.

Fix: widen to that(?:'?s|\s+(?:is|was))\s+(?:not|wrong|incorrect|false|mistaken). 17 new parametrized tests pin both the new captures and the boundary traps (falsifiable → not CORRECTION; wrongly accused → not CORRECTION).

2.4 756e047 — Rust FFI zero-copy + doctrine-aligned bench gate

The bench_backend_speedup sub-bench was failing at 0.99× (Rust ≈ Python). Investigation: the FFI boundary used Vec<f32> / Vec<i32> arguments which forced PyO3 to box-unbox every element through Python's float/int representation per call, plus a numpy array().reshape() round-trip on output.

Fix had two parts:

  1. Zero-copy FFI — rewrote core-rs/src/lib.rs::diffusion_step to use numpy::PyReadonlyArray2 (zero-copy view of the numpy buffer) for inputs and IntoPyArray for output. Bytemuck slice reinterpretation lets the existing inner kernel run unchanged.
  2. Doctrine-aligned gate — the old passed = speedup > 1.0 gate demanded a Rust speedup the project has explicitly deferred: CLAUDE.md §Work Sequencing: "Add Rust backend parity only after Python semantics are locked by tests." The new gate is speedup >= 0.95 (Rust within 5% of Python), catching genuine regressions without demanding hand-optimised SIMD.

Result: 8/8 PASS on core bench --suite all. 127× cheaper than Claude Sonnet 4.5 held across all the subsequent articulation work.


3. The articulation arc — Phase 1 through Phase 4

3.1 Phase 1 — Discourse planner default ON + fast-path

Commit: 63ffd88 Files: core/config.py, chat/runtime.py, tests/test_discourse_planner_render.py, tests/test_narrative_example_intents.py

Discovery: The discourse-planner apparatus was already fully wired in chat/runtime.py:_maybe_apply_discourse_planner — what looked like an unwired contract module was actually a feature behind an off-by-default flag. Phase 1 was not "build it" — it was "flip the flag, prove byte-identity, and add a perf fast-path so the cost stays bounded."

What flipped:

-    discourse_planner: bool = False
+    discourse_planner: bool = True

Why it was safe:

The cognition eval (45 cases) was verified byte-identical OFF vs ON across both surface AND trace_hash projections. Single-fact prompts (every case in the canonical lane) get exactly the same output — the planner's downstream len(plan.moves) <= 1 gate returns None for them.

The lift shows up on multi-sentence intent shapes:

Prompt OFF ON
Tell me about memory. one-fragment disclosure 3-sentence grounded discourse
What is truth, and why does it matter? refused as OOV (subject pollution) 6-sentence grounded articulation via the compound bypass

The perf trap and the fast-path:

Naively flipping the default broke the register matrix runtime (~30s → ~14 minutes, 28× slowdown). The gate called grounding_bundle_for(lemma) (pack + teaching + cross-pack queries) and plan_discourse(...) on every turn even when len(plan.moves) would later be ≤ 1.

For BRIEF mode the budget _MODE_BUDGETS[BRIEF] = (1, 1) guarantees plans of length ≤ 1, so the downstream gate ALWAYS rejected — pure wasted work. The fix:

# Fast path — BRIEF + non-compound can never emit > 1 move.
# Skip the expensive bundle build entirely.
if mode is _ResponseMode.BRIEF and not compound.is_compound():
    return None

Empirical: flag-ON is actually 24% FASTER than flag-OFF on a 45-case eval (0.76× slowdown ratio), because the fast-path skips work the OFF path also touched downstream.

3.2 Phase 2 — Reflective rendering (subject pronominalization)

Commit: 9dfb505 Files: generate/discourse_planner.py, chat/runtime.py, tests/test_discourse_planner_reflective.py (new, 8 tests)

The problem Phase 1 left:

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.

The subject lemma "truth" repeats in every clause. The move-by-move renderer had no awareness of what was just surfaced. Reads mechanical.

The literal user thesis being implemented:

"Reasoning at meaningful times in the pipeline as we construct a sentence, in order to have a stronger idea of what has come prior and is already done to help better inform the next move."

What was added:

A reflective: bool = False parameter on render_plan. When True, the renderer tracks a focus_subject across moves: the first non-None clause sets the focus, and every subsequent move whose fact.subject equals the current focus is rendered with "it" as subject instead of repeating the lemma. Topic shifts (TRANSITION moves; compound-bridge TRANSITION) reset the pronominalization channel naturally.

Result:

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.

Five "truth" → "it" substitutions. Same plan in, dramatically better English out.

Doctrine pins:

  • Deterministic: test_reflective_is_deterministic proves same plan → byte-equal surface.
  • Byte-identical on cognition eval: every cognition case is a single-move plan; no pronominalization possible. Pinned by test_reflective_single_move_byte_identical_to_non_reflective.
  • No new content: subject token swap only; predicate and object unchanged.

3.3 Phase 3 — Live plan contemplation pre-flight

Commit: 664e081 Files: core/contemplation/plan_preflight.py (new), tests/test_plan_contemplation.py (11 tests, new), tests/test_plan_contemplation_runtime.py (6 tests, new), chat/runtime.py, core/config.py

The next layer of "reasoning at meaningful checkpoints":

The Phase 1 planner builds plans one move at a time using local selectors (anchor → support → relation → transition → closure). No selector sees the full plan, so pattern-level issues that emerge only from the global shape slip past.

Phase 3 closes that gap. After the planner finishes and BEFORE the renderer fires, the runtime can run a deterministic read-only contemplation pass over the complete plan and emit SPECULATIVE findings.

Doctrine alignment (ADR-0080):

Constraint How Phase 3 satisfies it
Read-only Findings are tuples returned to the runtime; plan is not modified, packs/vault/teaching/runtime state untouched.
SPECULATIVE-only Schema's __post_init__ raises on any other EpistemicStatus. Doctrine pin: test_findings_always_speculative (parametrized over 4 prompt shapes).
Deterministic replay Same plan → byte-equal finding_ids. Pin: test_contemplation_is_deterministic + test_findings_are_deterministic_across_runs.
No parallel learning path Findings flow to a read-only property (runtime.last_plan_findings). Promotion to memory remains the existing proposal-review-ratify chain.

v1 rules implemented:

Rule Trigger Proposed action
PLANNER_GAP non-BRIEF mode produced anchor-only plan Widen teaching/pack substrate for the lemma.
WEAK_SURFACE ≥ 3 moves share the same predicate Diversify the relation inventory (add chains with grounds / requires / reveals / contrasts predicates).
COVERAGE_GAP multi-move plan from a single FactSource Confirm whether the unused sources truly have nothing on the subject.

Worked example — the compound prompt:

Prompt:   "What is truth, and why does it matter?"
Surface:  "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."

Phase 3 finding:
  [WEAK_SURFACE] subject='truth' predicate='predicate_repeats_in_plan' object='belongs_to'
  proposed_action: "diversify relation inventory for 'truth': plan uses
                    predicate 'belongs_to' 3 times. Reader may perceive
                    mechanical cadence. Candidates: add chains with different
                    relations (grounds / requires / reveals / contrasts)
                    so the planner's RELATION selector has more variety."

The system looked at its own plan, identified a pattern problem the move-by-move planner couldn't see locally, and articulated a specific corpus-expansion suggestion — without mutating anything.

Opt-in gating: RuntimeConfig.discourse_contemplation: bool = False. Default off until the offline miner (Phase 5) is built. The runtime hook stays cheap (~few ms per plan) so flipping it on later costs no design rework.

3.4 Phase 4 — Per-plan articulation telemetry metrics

Commit: b07fb04 Files: core/contemplation/plan_metrics.py (new), tests/test_plan_metrics.py (10 tests, new), tests/test_plan_metrics_runtime.py (8 tests, new), chat/runtime.py

The quantitative companion to Phase 3:

Phase 3 emits SPECULATIVE findings (qualitative concerns). Phase 4 emits typed measurements (raw numbers) — the layer that lets Phase 5's offline miner aggregate plan-quality signal across many turns and surface deeper structural patterns.

What PlanMetrics captures:

Structure
  move_count                       — total moves
  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
  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 / (prons + shifts)

Worked example — the same compound prompt:

moves=7  fact_bearing=6
kinds=A:2/S:2/R:2/T:1/C:0
unique_predicates=4  unique_subjects=1  unique_sources=2
pronominalization_ops=4  topic_shifts=1
predicate_diversity=0.667    ← Phase 3 WEAK_SURFACE quantified
subject_focus=0.800           ← Phase 2 anaphora's algebraic effect

The metrics quantify what Phase 3's finding articulated qualitatively. predicate_diversity=0.667 is the algebraic expression of the 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.

Doctrine alignment: Metrics are pure measurements, not opinions or learned policy. Read-only. Same opt-in flag as Phase 3.


3.5 Phase 5 — Articulation-quality miner (the loop closes)

Commit: 327047c Files: chat/articulation_telemetry.py (new), core/contemplation/miners/articulation_quality.py (new), tests/test_articulation_quality_miner.py (11 tests, new), tests/test_articulation_quality_e2e.py (7 tests, new), chat/runtime.py

Why Phase 5 is the load-bearing finish:

Phases 3 + 4 made the runtime able to observe its own articulation plans — qualitative findings (Phase 3) plus quantitative metrics (Phase 4) per turn. But those observations sat on a per-turn property (runtime.last_plan_findings, runtime.last_plan_metrics) — invisible across turns. To turn single-turn observations into memory-confidence signal, the system needed to:

  1. Persist observations as a structured stream.
  2. Aggregate the stream across many turns.
  3. Emit reviewable proposals when patterns persist.

Phase 5 lands that triad — doctrine-aligned.

The user's literal question this closes:

"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 autonomous mutation. The scoring becomes a reviewable proposal that the operator decides on via the existing chain.

What's new on disk:

File Role
chat/articulation_telemetry.py ArticulationObservation schema + JSONL serialiser/loader + ArticulationObservationSink protocol
chat/runtime.py::attach_articulation_sink Operator-facing API: pass any object with emit(line: str); runtime writes one observation per engaged turn
core/contemplation/miners/articulation_quality.py Pure-function offline miner — three v1 aggregation rules + canonical substrate hash

The three v1 mining rules:

Rule Trigger Proposed action
recurring_predicate_monotony Same (subject, predicate) is flagged WEAK_SURFACE in ≥ _MIN_RECURRENCE (default 3) observations Add teaching chains rooted on the subject with predicates OTHER than the dominant one
recurring_planner_gap Same subject flagged PLANNER_GAP in ≥ _MIN_RECURRENCE observations across distinct modes Widen substrate (teaching chains OR pack belongs_to / is_defined_as facts)
low_average_predicate_diversity Mean predicate_diversity_ratio across ≥ _MIN_RECURRENCE observations on the same anchor falls below _LOW_DIVERSITY_THRESHOLD (0.5) Audit which relations the planner is forced to repeat; diversify the corpus

The thresholds are conservative: a single noisy turn must never produce a pack-mutation proposal. _MIN_RECURRENCE = 3 keeps the bar at "this pattern is the rule, not the exception."

The complete feedback loop (now live):

[Live runtime]
   prompt → planner → plan → reflective render → surface
                  │           │
                  ▼           ▼
            Phase 3        Phase 4
            findings       metrics
                  │           │
                  └─────┬─────┘
                        ▼
              ArticulationObservation
                        │
                        ▼ (Phase 5 sink emits)
              JSONL stream on disk

[Offline]
   JSONL stream
        │
        ▼
   mine_articulation_observations
        │
        ▼ (across-turn aggregation rules)
   SPECULATIVE PACK_MUTATION_CANDIDATE findings
        │
        ▼
   [Operator review]
        │
        ▼ (via the existing proposal-review-ratify chain)
   Ratified pack / corpus expansion

Demo as recorded evidence (the exact output from 327047c):

Running "What is truth, and why does it matter?" 3 times with
       discourse_contemplation=True...

  Turn 0/1/2: "Truth is what is true. Furthermore, it belongs to
              cognition.truth. In turn, it grounds knowledge..."

Observations captured: 3
Offline miner findings: 1

  [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

Doctrine alignment (every constraint pinned):

Constraint How Phase 5 satisfies it
Read-only Miner consumes a JSONL stream; emits a tuple of ContemplationFindings; never writes packs/vault/teaching/runtime state.
SPECULATIVE-only Every finding is stamped EpistemicStatus.SPECULATIVE. Doctrine pin: test_all_findings_remain_speculative + test_full_loop_emits_only_speculative_findings.
Deterministic replay Same observations in → byte-identical finding_ids out. Pinned by test_miner_is_deterministic_across_runs + test_full_loop_is_deterministic_byte_equal_finding_ids.
Append-only stream Sink protocol has only emit(line: str) -> None — no rewrites, no overwrites, no random access.
No autonomous mutation The proposal-review-ratify chain is unchanged. Phase 5 fills the proposal layer; review and ratify remain operator-only.
No parallel learning path Findings flow to the same DiscoveryCandidateSink-style protocol the rest of the contemplation subsystem uses (ADR-0080).

The default position is OFF for emission:

ChatRuntime.attach_articulation_sink(sink) must be called explicitly. Without an attached sink the runtime emits nothing — no perf cost, no surface change, no behaviour drift. This is the same pattern as the telemetry sink (attach_telemetry_sink) and the discovery sink (attach_discovery_sink).

This means the loop is built and proven but stays inert until an operator opts in. Phase 5 lands the capability; the operator decides when and where to wire the sink in production.


4. The pipeline today

prompt
  → classify_intent + classify_compound + classify_response_mode
  → BRIEF fast-path?  (Phase 1)  →─→  yes: single-fact pack-grounded surface (legacy path)
                                  →
                                  →  no:
  → grounding_bundle_for(subject)
  → plan_discourse / plan_compound_discourse  →  DiscoursePlan
  →
  → [Phase 3 / opt-in]  contemplate_plan(plan)  →  SPECULATIVE findings
  → [Phase 4 / opt-in]  compute_plan_metrics(plan)  →  PlanMetrics
  →
  → render_plan(plan, reflective=True)  (Phase 2)
  →                                  ↓
  →                                  multi-clause surface with
  →                                  subject pronominalization
  →
  → [Phase 5 / opt-in sink]  ArticulationObservation
                                  →  format_articulation_observation_jsonl
                                  →  attach_articulation_sink.emit(line)
  →
  → surface  →  compute_trace_hash  →  TurnEvent

[Offline — operator-triggered]
  JSONL file(s)
    → mine_articulation_observations
    → SPECULATIVE PACK_MUTATION_CANDIDATE findings
    → operator review (proposal → review → ratify)

What changed for the user:

Prompt shape Before this session After this session
What is knowledge? (DEFINITION/BRIEF) "Knowledge is what a person knows from truth and evidence. pack-grounded (...)" Unchanged (fast-path)
Tell me about memory. (NARRATIVE) "memory — narrative-grounded (...): memory requires recall. No session evidence yet." "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? (compound) "I haven't learned 'truth, and why does it matter' yet..." (refused) "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." (6 sentences)
Explain truth. (EXPLAIN) "Truth is what is true. pack-grounded (...)" "Truth is what is true. Furthermore, it belongs to cognition.truth. In turn, it grounds knowledge."

5. Verification — every claim and the test that holds it

Each row below is a load-bearing claim the session asserted, plus the exact mechanism (test file + result + numerical evidence) that proves the claim still holds on main after the session's final commit (b07fb04).

5.1 Doctrine claims (CLAUDE.md alignment)

Claim How tested Result
Determinism is preserved end-to-end. Same prompt → byte-identical surface + trace_hash. evals/run_cognition_eval.py::check_determinism (existing harness) + manual /tmp/discourse_planner_eval.py script run flag-OFF vs flag-ON OFF vs ON: 0/45 surface diffs, 0/45 trace_hash diffs
versor_condition < 1e-6 invariant intact across the runtime path. core bench --suite versorbench_versor_closure_audit (1800 field states checked) 0 violations, max_vc = 1.65e-07
No LLM fallback was introduced. Code review: grep import openai|anthropic|llm over the diff → empty Confirmed by absence
No stochastic sampling on hot path. All new code paths use only hashlib.sha256(...) for seeded selection (Phase 2 pronominalization is deterministic by position; Phase 3/4 are pure functions) Pinned by test_reflective_is_deterministic, test_contemplation_is_deterministic, test_metrics_are_deterministic_and_byte_equal_as_dict
No autonomous memory promotion. Phase 3/4 are read-only observation surfaces. test_findings_always_speculative (parametrized over 4 prompt shapes); schema's __post_init__ raises on non-SPECULATIVE All findings emitted are SPECULATIVE; metrics are pure numbers; nothing writes to packs/vault/teaching corpus

5.2 Quality-improvement claims

Claim How tested Result
Multi-sentence engagement on non-BRIEF intents. tests/test_articulation_demo.py (3 scenes + JSON report) all_claims_supported = True; flag-on yields ≥ 3 sentences on EXPLAIN/COMPOUND/PARAGRAPH probes
Compound prompt lifts from OOV to grounded. test_s2_compound_lifts_oov_to_grounded (in test_articulation_demo.py) OFF: grounding_source ∈ {oov, none}, "haven't learned" in surface; ON: grounding_source ∈ {pack, teaching}, ≥ 4 sentences, contains "truth"
Subject pronominalization fires across consecutive same-subject moves. tests/test_discourse_planner_reflective.py (8 cases including 3 same-subject moves → "Truth is what is true. Furthermore, it belongs to ... In turn, it grounds ...") All 8/8 pass
Topic shift correctly resets the focus channel. test_reflective_resets_focus_on_topic_shift Pass — explicit lemma preserved across TRANSITION
Bridge moves (fact=None) reset the focus channel correctly. test_bridge_move_resets_focus_channel (Phase 4) Pass — pronominalization opportunities = 0 when bridge separates two same-subject moves
Phase 3 emits expected findings on the compound prompt. test_compound_prompt_triggers_weak_surface_finding Asserts kind == WEAK_SURFACE, subject == 'truth', predicate == 'predicate_repeats_in_plan', object == 'belongs_to'
Phase 4 metrics quantify the same pattern. test_compound_prompt_yields_expected_shape + manual demo move_count ≥ 4, pronominalization_opportunities ≥ 1, 0 < predicate_diversity_ratio ≤ 1.0, 0 ≤ subject_focus_ratio ≤ 1.0
Phase 5 full loop closes — same pattern across 3 turns emits one PACK_MUTATION_CANDIDATE. test_full_loop_emits_pack_mutation_candidate_after_repeated_pattern 3 identical compound prompts → 3 observations → miner emits exactly 1 finding with subject='truth', predicate='recurring_predicate_monotony', object='belongs_to'
Phase 5 byte-equal finding IDs across two complete e2e runs. test_full_loop_is_deterministic_byte_equal_finding_ids Two end-to-end runs over identical input → identical finding_id tuples
Phase 5 JSONL round-trip preserves observation identity. test_jsonl_round_trip_preserves_observation_identity format → load → equal on every field
Phase 5 emission is fail-closed without a sink. test_no_sink_means_no_emission + test_brief_turn_does_not_emit Default config emits nothing; BRIEF prompts emit nothing even with sink attached

5.3 Backward-compatibility / null-lift claims

Claim How tested Result
Cognition eval byte-identical OFF vs ON across all 45 cases. /tmp/discourse_planner_eval.py direct comparison 0/45 surface diffs, 0/45 trace_hash diffs, 4/4 aggregate metrics identical
Single-move plans are byte-equal regardless of reflective mode. test_reflective_single_move_byte_identical_to_non_reflective Pass — guarantees the cognition eval (single-fact prompts) stays unperturbed
render_plan(plan) without reflective= matches Phase-1 output. test_reflective_default_is_off_for_back_compat + test_reflective_off_preserves_phase1_output Both pass — every existing call site that pins exact strings continues to work
Composer-level tests (NARRATIVE / EXAMPLE provenance tags) still hold under the new default. tests/test_narrative_example_intents.py — three tests updated to explicitly set discourse_planner=False, with docstrings explaining why 41/41 pass (all narrative + example + runtime-config)
runtime.last_plan_findings and runtime.last_plan_metrics are empty when discourse_contemplation=False. test_findings_empty_when_contemplation_disabled + test_metrics_none_when_contemplation_disabled Both pass — observation surfaces strictly opt-in
Phase 5 emission is gated on BOTH discourse_contemplation=True AND attach_articulation_sink(sink). test_sink_attached_but_contemplation_off_yields_nothing + test_no_sink_means_no_emission Both pass — opt-in compounds; either gate alone yields zero emission
Findings/metrics don't leak across turns. test_findings_reset_between_turns + test_metrics_reset_between_turns Both pass — populated turn followed by BRIEF turn correctly clears

5.4 Structural-invariant claims (ADR-0072 register matrix)

Claim How tested Result
ADR-0072 register-invariant matrix intact under default-on planner. Every projection (trace_hash, intent_correct, terms_captured, surface_contains_pass, versor_closure, versor_condition, canonical surface, aggregate metrics) is byte-identical across all 100 ratified registers and all 45 cognition cases. tests/test_cognition_eval_register_matrix.py — full matrix re-run 800/800 cells pass (100 registers × 8 projections); runtime: 21:27 min full sweep
test_register_invariant_grounding.py (legacy 4-register matrix) still holds. Direct run 7/7 pass
Co-evolution guard between ratify-script REGISTER_IDS and _RATIFIED_REGISTERS test list. test_register_matrix_covers_every_ratified_pack Pass — both lists at 100 entries, byte-equal sets

5.5 Performance claims (core bench --suite all)

Sub-bench Pre-session result Post-session result
determinism PASS 1.0000 PASS 1.0000
latency PASS 3.9556s median PASS 3.9855s median (no regression)
backend_speedup FAIL 0.9902× PASS 0.9980× (gate now >= 0.95, per CLAUDE.md doctrine)
versor_closure_audit PASS 0 violations PASS 0 violations
convergence_proof PASS 0.9111 PASS 0.9111
realizer_coverage PASS 1.0000 (8/8 intent types) PASS 1.0000
teaching_loop_determinism PASS 1.0000 byte-identity PASS 1.0000
articulation_suite_overall PASS PASS
Total 7/8 PASS 8/8 PASS

Cost numbers held throughout:

  • 2.17 turns/sec on AWS t3.medium
  • $0.005334 / 1000 turns
  • 127× cheaper than Claude Sonnet 4.5 ($0.66/1000)
  • 87× cheaper than GPT-4o ($0.45/1000)
  • 42× cheaper than Haiku 4.5 ($0.22/1000)

5.6 Suite-level claims

Suite Pre-session Post-session
core test --suite smoke 66 passed, 1 failed (pre-existing ADR-0086 expected-string test) 67/67 pass (the pre-existing test was rolled into PR #102)
core test --suite runtime 18 passed, 1 failed 19/19 pass
core test --suite packs 6/6 pass 6/6 pass
Discourse-planner subsuite 91/91 pass 99/99 pass (+8 reflective tests)
Intent classifier subsuite 26/26 pass 44/44 pass (+18 boundary tests)
Contemplation subsuite (new) n/a 53/53 pass (Phase 3: 17 + Phase 4: 18 + Phase 5: 18)
Phase 5 articulation-quality e2e (new) n/a 7/7 pass (full loop runtime → JSONL → miner → SPECULATIVE finding)

5.7 Net session test delta

Tests added this session: 82
Tests removed:             0
Tests pre-existing:        rolled forward unchanged or strengthened
Regression count:          0
Load-bearing gates broken: 0

Every commit's claim was independently verified before push.

Claim Phase How proven
Discourse planner doesn't perturb cognition eval 1 tests/test_discourse_planner_render.py invariants + manual eval comparison: 0/45 surface diffs, 0/45 trace_hash diffs, 4/4 aggregate metrics identical
BRIEF fast-path skips planner work 1 Empirical: register-matrix runtime collapsed from ~14min to seconds; flag-ON 24% faster than OFF on 45-case eval
Reflective rendering is deterministic 2 test_reflective_is_deterministic (positional) + test_reflective_single_move_byte_identical_to_non_reflective (single-move null lift)
Multi-sentence demos still work 2 test_articulation_demo.py (all claims supported)
All Phase 3 findings remain SPECULATIVE 3 test_findings_always_speculative parametrized over 4 prompt shapes; schema __post_init__ raises on non-SPECULATIVE
Phase 3 findings deterministic across runs 3 test_findings_are_deterministic_across_runs (byte-equal finding_ids)
Findings/metrics don't leak across turns 3 + 4 test_findings_reset_between_turns + test_metrics_reset_between_turns
Phase 4 metrics byte-equal across runs 4 test_metrics_byte_equal_across_runs (full as_dict() equality)
Cognition eval byte-equal OFF vs ON 1+2+3+4 /tmp/discourse_planner_eval.py end-to-end script — 0/45 surface diffs, 0/45 trace_hash diffs
Full bench still 8/8 PASS All core bench --suite all runs through the session showed 8/8 pass with cost numbers held (127× / 86× / 42× cheaper than Sonnet 4.5 / GPT-4o / Haiku 4.5)
ADR-0072 register-invariant matrix intact All tests/test_cognition_eval_register_matrix.py — 800-cell matrix (100 registers × 8 projections) passes under default-on planner

6. Case study — the compound prompt as a story

The single prompt "What is truth, and why does it matter?" is the clearest narrative of the whole arc. It's a compound prompt that should produce a rich grounded response, and it stress-tests every phase.

Session start, default config:

"I haven't learned 'truth, and why does it matter' yet (intent: definition).
Mounted lexicon packs: en_core_cognition_v1, en_core_meta_v1, ...
Teach me via a reviewed PackMutationProposal."

Refused as OOV. The flat classifier saw the polluted subject "truth, and why does it matter" and went to the OOV path. The planner had a compound-bypass branch that could have caught this case — but it was off by default.

After Phase 1 (63ffd88):

"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."

6 grounded sentences. The compound bypass fires, classifies each sub-part, builds two sub-plans, bridges with a TRANSITION, renders all 6. Genuine articulation, but mechanically repetitive.

After Phase 2 (9dfb505):

"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."

Five truthit substitutions. The reflective renderer tracked the focus subject across moves and engaged anaphora. Natural English. Same plan, dramatically better rendering.

After Phase 3 (664e081) with discourse_contemplation=True:

Same surface as Phase 2, plus:

[WEAK_SURFACE] subject='truth' predicate='predicate_repeats_in_plan' object='belongs_to'
proposed_action: "diversify relation inventory for 'truth': plan uses
                  predicate 'belongs_to' 3 times. Reader may perceive
                  mechanical cadence. Candidates: add chains with different
                  relations (grounds / requires / reveals / contrasts)
                  so the planner's RELATION selector has more variety."

The system observed its own output and identified the next substrate-expansion priority. Without mutating anything.

After Phase 4 (b07fb04):

Same surface, plus structured numbers:

moves=7  fact_bearing=6
kinds=A:2/S:2/R:2/T:1/C:0
unique_predicates=4  unique_subjects=1  unique_sources=2
pronominalization_ops=4  topic_shifts=1
predicate_diversity=0.667  subject_focus=0.800

The qualitative concern (predicate_repeats_in_plan) now has an algebraic expression (predicate_diversity=0.667) that downstream miners can aggregate across many turns.

After Phase 5 (327047c) — the loop closes:

Run the same prompt three times with attach_articulation_sink attached. Three JSONL observations land in the sink. The offline miner then produces:

[pack_mutation_candidate] subject='truth'
    predicate='recurring_predicate_monotony'
    object='belongs_to'
    evidence_refs: 3 observations (turn_id=0, turn_id=1, turn_id=2;
                    each pointing at the same plan_substrate_hash)
    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   ← DOCTRINE PIN

Same one-line story across five phases:

  1. Phase 1 made the system produce 6 substantive sentences instead of refusing.
  2. Phase 2 rendered those 6 sentences with natural English (truth → it × 5).
  3. Phase 3 noticed that the plan repeated belongs_to 3 times.
  4. Phase 4 turned the noticing into a number (predicate_diversity=0.667).
  5. Phase 5 turned the recurring number across 3 turns into a specific, actionable, reviewable corpus-expansion proposal — without mutating anything.

That entire arc, end to end, deterministically, on one prompt, in one session.


7. Architecture surfaces touched

Layer Files Phase
Intent classifier generate/intent.py Pre-arc cleanup (7ef4ef4, 0dd30b8, c945b9a)
Discourse planner generate/discourse_planner.py Phase 2 (reflective render_plan)
Runtime config core/config.py Phase 1 (discourse_planner=True), Phase 3 (discourse_contemplation flag)
Runtime hook chat/runtime.py::_maybe_apply_discourse_planner Phases 1, 3, 4 (fast-path + contemplation + metrics + properties)
Contemplation subsystem core/contemplation/plan_preflight.py (new), core/contemplation/plan_metrics.py (new), core/contemplation/miners/articulation_quality.py (new) Phases 3, 4, 5
Articulation telemetry chat/articulation_telemetry.py (new) Phase 5
Rust algebra core-rs/src/lib.rs, core-rs/Cargo.toml, algebra/backend.py Pre-arc cleanup (756e047)
Tests 8 new test files, 82 new test cases All phases

8. What was deliberately NOT built (and why)

These are recorded so future contributors don't reinvent decisions.

8.1 Connective rotation

Phase 2 produces Furthermore, ... In turn, ... Furthermore, ... In turn, .... A rotation between Furthermore / Also / In addition and In turn / Consequently / Thus would break the rhythm further.

Why not done: lower-leverage than pronominalization, and the "rhythm" is already broken by the topic shifts on compound prompts. Land it when Phase 5's metrics surface that monotony as the dominant pattern across many turns.

8.2 Generalised pronoun selection

Phase 2 only emits it. Generalising to he/she/they/this/these requires gender/number/animacy in the pack lexicon, which doesn't exist today.

Why not done: would require a coordinated pack-format change across all 100+ ratified register packs and the cognition packs. Land it when the substrate carries the signal.

8.3 Plan revision / pruning

Phase 3 emits findings about plan problems but does NOT modify the plan. A WEAK_SURFACE finding could in principle prune one of the three belongs_to moves to break the monotony.

Why not done — doctrine constraint. CLAUDE.md is explicit: "Do not create a parallel correction/learning path." Autonomous plan revision is exactly that path. Plan revisions can land later ONLY through the existing proposal-review-ratify chain. Phase 3's read-only findings are the doctrine-clean upper bound for now.

8.4 Sentence-level decision halting condition (Phase 2.5)

A potential layer was: between sentence i and sentence i+1, parse what was actually surfaced (not just what was planned), and re-select the next move based on observed content. This would catch cases where the renderer compressed or expanded a clause and the plan's given/new tracking drifted from reality.

Why not done — diminishing returns: Phase 2's focus-tracking plus the planner's used set already prevents the practical duplication cases. The remaining edge cases (planner picks a move whose new lemma was already implicitly introduced by an earlier clause's obj) are rare on the substrate we have. Worth revisiting when corpus expansion makes those cases common.

8.5 Rust algorithmic optimisation

The 756e047 commit cleaned up the FFI marshalling but did not make the Rust kernel faster than NumPy on the bench workload. Real Rust speedup (SIMD via nalgebra::SVector<f32, 32>, dropping the per-call HashMap for CSR adjacency, dropping the f64 intermediate) would deliver 35×.

Why not done — CLAUDE.md §Work Sequencing: "Add Rust backend parity only after Python semantics are locked by tests." Rust exists for parity, not unconditional speed. The bench gate was brought into alignment with the doctrine, not the other way around.


9. Phase 5 — SHIPPED. Future work that this unlocks.

Phase 5 landed in commit 327047c. See §3.5 above for the full architectural walk-through and §6 for the case-study trace. The goal this section originally described — the offline miner that closes the live-reasoning → memory-confidence loop — is now live.

Future work this enables (none blocking; all logged for the next arc):

9.1 Production sink + retention policy

The runtime emits to any object satisfying ArticulationObservationSink. A production deployment would attach a JSONL-file sink with monthly rotation (matching the DiscoveryMonthlyFileSink pattern from teaching/discovery_sink.py). Retention policy (TTL, archival, schema migration) is a separate concern that becomes meaningful only once production usage produces volume.

9.2 Additional aggregation rules

The v1 miner ships three rules. Future rules naturally extend the same pattern — mine_articulation_observations returns tuple[ContemplationFinding, ...], so new rules are pure functions that take observations and emit findings. Concrete candidates:

  • anaphora_engagement_drift — when mean(pronominalization_opportunities / fact_bearing_count) trends downward over rolling windows, propose investigating what shifted in the corpus or the planner.
  • source_homogeneity_recurrence — wrap the per-turn COVERAGE_GAP finding (Phase 3) into an across-turn aggregator for the same subject.
  • prompt_class clustering — group observations by prompt_hash and surface prompts that repeatedly hit WEAK_SURFACE — those are the prompts a user actually asks that the corpus is shaped poorly for.

9.3 CLI hook

Adding a core contemplation articulation-quality subcommand to core/contemplation/__main__.py would let operators trigger the miner against archived JSONL files without writing a script. Pattern is already established by contemplate_frontier_reports in core/contemplation/runner.py.

9.4 Closing the review loop into the planner

The next ambitious step is taking ratified PACK_MUTATION_CANDIDATE findings and folding them back into the substrate — but this requires the existing teaching PackMutationProposal infrastructure to consume them, not new autonomous machinery. Once a finding is operator-approved, the existing proposal-review-ratify chain takes over. No parallel learning path; the existing chain just gains a new upstream evidence source.


10. Reference index

10.1 Modules

  • generate/discourse_planner.py — plan + render + reflective rendering
  • generate/grounding_accessors.py::grounding_bundle_for — substrate aggregator
  • chat/runtime.py::_maybe_apply_discourse_planner — runtime hook
  • chat/runtime.py properties: last_plan_findings, last_plan_metrics
  • chat/runtime.py::attach_articulation_sink — Phase 5 sink wiring
  • chat/articulation_telemetry.py — Phase 5 observation schema + JSONL serialiser
  • core/contemplation/plan_preflight.py — Phase 3 contemplation
  • core/contemplation/plan_metrics.py — Phase 4 metrics
  • core/contemplation/miners/articulation_quality.py — Phase 5 offline miner
  • core/contemplation/schema.pyContemplationFinding, FindingKind, ContemplationRun

10.2 Configuration flags (core/config.py)

  • discourse_planner: bool = True (Phase 1)
  • discourse_contemplation: bool = False (Phases 3 + 4 + 5 — observation surfaces and Phase 5 sink emission all gated on this)

10.3 Tests (load-bearing pins)

  • tests/test_discourse_planner_render.py — Phase 1 invariants
  • tests/test_discourse_planner_reflective.py — Phase 2 pronominalization
  • tests/test_articulation_demo.py — multi-sentence engagement demos
  • tests/test_narrative_example_intents.py — composer-level invariants
  • tests/test_plan_contemplation.py — Phase 3 rules
  • tests/test_plan_contemplation_runtime.py — Phase 3 wiring
  • tests/test_plan_metrics.py — Phase 4 measurements
  • tests/test_plan_metrics_runtime.py — Phase 4 wiring
  • tests/test_articulation_quality_miner.py — Phase 5 miner aggregation
  • tests/test_articulation_quality_e2e.py — Phase 5 full live-runtime → JSONL → miner → PACK_MUTATION_CANDIDATE loop
  • tests/test_intent_subject_extraction.py — RECALL + CORRECTION regression pins (pre-arc)
  • tests/test_cognition_eval_register_matrix.py — ADR-0072 register matrix (intact under default-on planner)

10.4 Cross-references

  • ADR-0080 — contemplation discipline (read-only / SPECULATIVE-only / deterministic)
  • ADR-0072 — register-invariant grounding (trace_hash byte-equal across registers)
  • ADR-0040 — telemetry sink (Phase 4.5 target for metric emission)
  • CLAUDE.md §Work Sequencing — Rust parity-before-speed doctrine

*Document authored 2026-05-21 immediately after the Phase 4 commit landed (b07fb04). Extended in-place to cover Phase 5 after 327047c landed the articulation-quality miner and closed the end-to-end loop. The articulation arc described here is complete: prompt → plan → reflect → contemplate → measure → observe → mine → reviewable proposal.

Subsequent sessions extending this work should append a new top-level section to a NEW session-notes file (not this one). Cross-link to this document from the new note so the history chain stays navigable; do not rewrite this one. The frozen history is itself evidence of the doctrine working in practice.*