CORE now decides natural-English deductive arguments end-to-end:
'If it rains then the ground is wet. It rains. Therefore the ground
is wet.' -> ENTAILED, rendered over the user's own clauses. Both of
ADR-0256's documented boundary cases (multiword conditional, nested-
negation contraposition) are now decided; their lane gold updated
declined->entailed.
- generate/proof_chain/english.py (new): closed function-word grammar
over OPAQUE clause-atoms (minted ids); if/then, [either] or, and,
three negation forms (leading not / sentential / copular incl.
contractions); typed refusals; honesty caps. Soundness: positive
verdicts are substitution-closed; UNKNOWN is scoped + guarded
(quantifier-led, is-a membership, unnormalizable negation refuse
out of the opaque band).
- render_entailment_english: verdicts quoted back verbatim; UNKNOWN
phrasing explicitly scoped to the opaque reading.
- chat/deduction_surface.py: v2-EN fallback tier AFTER v1/v1b —
monotone widening, byte-identical on previously-served arguments.
- Four en_* shape-bands EARN SERVE via the ADR-0199 arena (720/band,
wrong=0, reliability 0.99087 >= 0.99); 9-band ledger re-sealed.
- evals/deduction_serve/v2_en: 26 hand-authored real-English cases
(content disjoint from the synthetic lexicon), wrong=0; report
re-pinned.
- realizer guard (ADR-0075): deduction surfaces exempt — quoted
templates are not slot-composed articulations (pack 'open'=VERB
was rejecting an honest quoted 'the door is not open'); pinned
cold+warm in e2e.
[Verification]: deductive 84 passed; smoke 180 passed; cognition 122
passed 1 skipped; arena 9x720 wrong=0 all SERVE; lanes v1 28/28,
v2_en 26/26.
Wires the verified ROBDD entailment engine (generate/proof_chain,
716/716 wrong=0) into core chat serving. A propositional argument
('P1. P2. ... Therefore C.') is now comprehended, projected, and
decided end-to-end -- the first real working basic-logic workflow:
user question -> CORE decides -> articulated deterministic answer.
New: chat/deduction_surface.py (DEDUCTION composer, sibling to the
existing oov/narrative/example composer pattern), generate/proof_chain/
render.py (deterministic EntailmentTrace -> surface templates, no LLM),
tests/test_deduction_surface.py (15 tests incl. full propositional gold
corpus decided end-to-end across a multi-turn session, wrong=0).
Changed: core/config.py (deduction_serving_enabled flag, default off),
generate/intent.py (IntentTag.DEDUCTION, observability-only -- the core
classify_intent rule table is untouched so flag-off stays byte-identical),
chat/runtime.py (one flag-gated block in the existing pack/teaching/
partial/oov dispatcher).
Design notes (full detail in docs/research/deduction-serve-arc-phase1-
turn-spine-2026-07-23.md): the deduction check runs BEFORE generic intent
classification (never touches classify_intent's rule table) and BEFORE
the empty-vault gate (a live multi-turn probe caught turn-12 silently
falling through once the vault warmed -- pack/teaching/oov are cold-start-
only by design, deduction serving isn't). grounding_source='deduction' is
NOT registered in the closed, Workbench-coupled GroundingSource Literal
(cross-stack TS contract, inert for this arc's core-chat-only consumer,
documented as a deferred follow-up). Band v1 stays propositional-only --
categorical/syllogism arguments commit but honestly decline as
out-of-band; evals.syllogism.oracle is never imported (INV-25 intact).
[Verification]: smoke 180 passed; cognition 122 passed/1 skipped;
test_deduction_surface.py 15 passed; test_dispatch_trace.py +
test_oov_surface.py 26 passed; test_intent*.py 145 passed.
CognitiveTurnPipeline emits TurnEvent inside runtime.chat() and only THEN
applies the #96 fail-closed surface resolution (resolve_surface) and the #97
logos-morph override — so an overridden turn's telemetry recorded a surface
the user never saw. warmed_session_consistency telemetry_consistency_rate was
0.9444 on main (fast-lane red since #96e0d1b475): an audit/replay trust break
(Absolute Provenance).
Add ChatRuntime.finalize_turn_surface, a sibling to finalize_turn_trace_hash:
the pipeline owns the final surface decision, so it back-stamps the resolved
surface onto turn_log[-1] immediately after the trace-hash back-stamp.
trace_hash byte-identity is preserved — it folds the pre-decoration surface,
not the served surface.
Scope: telemetry-consistency half of T13 only.
- Decision (2), the goldtether over-refusal, is orthogonal to the red and
OPEN pending ruling (ledger) — recommend AGAINST a query-type cue bypass
(fail-open / ADR-0252-retired cue-table pattern); prefer routing
open-geometry-but-pack-grounded surfaces to the existing hedge arm.
- Durable-sink deferral tracked as R7.
telemetry_consistency_rate 0.9444 -> 1.0 (10/10 warmed_session tests green).
[Verification]: uv run core test --suite smoke -q in core-wt-t13serve @ HEAD — 180 passed in 133.17s, exit 0; warmed_session lane 10/10 green.
Opus 4.8 audit of Fable's slice-1 scaffold + authorized hardening (Step 2).
Stacked on feat/adr-0246-slice1-scaffold. Not a PR, not merged, no status flip.
AUDIT (Step 1) — VERDICT PASS. Re-derived A(F)/d_orth/d_stab/typed channels from
§3.1/3.2/3.6 independently: BIT-EXACT vs impl (0.00e+00). Confirmed in code (not
just comments): H_id={I} locked (singleton hardcoded, no enlargement path); path
composes lawful only, refused=break marker, never raw, never soft-I; no §7 scope
creep; serve byte-identity. Finding F1 (doc, not bug): composition uses the raw
CERTIFIED action (required for §6.2 accumulation, else A_path≡I detects nothing) —
now documented for ADR ratification.
HARDENING (Step 2):
§3.7 pure surface (identity_action.py): AdmissionPolicy (calibrated flag),
evaluate_admission (admit-or-abstain, no corrector); CERTIFIED_GAMMA_ID pinned
== identity._WAVE_LEAKAGE_BOUND; all other bounds UNCERTIFIED placeholders.
§3.7 SERVE WIRING (Steps 2.1/2.2): new default-off flag identity_action_surface
(config); threaded chat/runtime -> check -> _wave_field_score; refusal folds
into flagged -> existing would_violate/conjugate_correct abstains; IdentityScore
gains action_surface_active/d_orth/d_stab (legacy defaults). Flag-off is
byte-identical (D4 gate surfaces green unchanged; smoke 176 post-wiring).
§6.3 DISCRIMINATION REPORT (evals/adr_0246_discrimination) — HONEST numbers:
benign pass 0.00, false refusal 1.00, adversarial detect 1.00, control pass
1.00; d_stab AUC 0.375 (95% CI [0.15,0.62]) — BELOW chance. Benign cognition
sits ~18x farther from the frame (mean d_stab 27.8) than the attacks (1.55).
The gate refuses everything and does NOT discriminate; must stay off; usable
separation needs the §11 grounding work, not threshold tuning. Claims language
enforced: 'lawfulness relative to the declared frozen frame', NOT 'semantic
inalienability'.
Ledger raw-sneak hardening test (Step 2.3): mixed lawful/refused sequence must
equal the lawful sub-product, fails if raw sneaks into A_path_lawful.
Handoff to Sonnet: §4.1 per-turn record, path-ledger serve integration, ADR-0246
body + acceptance packet (Proposed, no self-Accept), §11 grounding-feasibility.
[Verification]: uv run core test --suite smoke -q => 176 passed (post-wiring);
ADR-0246 suites 80 passed; egress wiring + D4 gate surfaces 47 passed
(byte-identity); §6.1/6.2 eval 14/14; discrimination report numbers above.
See docs/audit/adr-0246-slice1-opus-audit-and-hardening.md + run log.
Wires the ADR-0244 §2.2 wave gate into the live turn loop behind a new
opt-in flag, byte-identical when off, plus a normalization fix found in
integration.
core/config.py: RuntimeConfig.identity_wave_gate (bool = False). OFF by
default — the leakage threshold is provisional (reuses alignment_threshold)
until calibrated to gamma_id in Phase 3, and flag-off is byte-identical to
the pre-ADR-0244 advisory path.
chat/runtime.py (main chat() path, two flag-gated touches):
- at the identity check, pass wave_field=result.final_state.F when the
flag is on (so hedging + telemetry both use the wave score); None when
off -> legacy scalar-L2 path.
- after the safety/ethics verdicts exist, intersect their violated
boundaries with the manifold's committed boundary_ids (supplement via
dataclasses.replace) and fold a fail-closed geometric IdentityGateRefusal
into the typed refusal surface (TYPED_REFUSAL_PREFIX). refusal_emitted is
derived once, after the block, preserving the (surface is not None) <=>
emitted invariant.
chat/telemetry.py: serialize_turn_event emits the wave-gate keys
(identity_wave_mode / _leakage_norm / _min_self_alignment /
_boundary_violations) only when identity_score.wave_mode_active — so the
wire format is byte-identical when the gate is off.
core/physics/identity_manifold.py — NORMALIZATION FIX (found in
integration): the live versor carries boost (e5) components, and a boost is
a unit versor (R R~ = 1) that does NOT preserve the Euclidean coefficient
norm, so ‖R a_i R~‖ > 1 and the un-normalized leakage/self-alignment ran
past their ranges (measured leakage 5.16, self_align -4.75 on a real turn).
axis_response now normalizes each measure by the rotated-axis magnitude:
leakage is a fraction in [0,1], self_align a signed cosine in [-1,1]. For a
norm-preserving spatial rotor the rotated axis is unit and normalization is
a no-op (Phase 1 results unchanged). §4a updated to match; new boost test
pins bounded measures.
tests/test_adr_0244_identity_gate_runtime.py (new): flag OFF -> legacy
score + no wave telemetry (byte-identical wire); flag ON -> wave gate active
on the live versor + wave telemetry present + bounded measures; flag-off
determinism. Seeds a short sequence to reach main-path (identity-checked)
turns (a fresh empty-vault runtime routes ungrounded inputs to disclosure).
[Verification]: 74 targeted (manifold+gate+runtime+legacy identity+
telemetry) passed; in-worktree smoke 176 passed; fast lane
(-m 'not quarantine and not slow' -n auto) 11883 passed, 109 skipped —
flag-off byte-identity confirmed across the whole suite.
- Fixes skeptic gaps on empty fields in some smokes and fixture mismatch.
- Targeted tests green post-fix.
- Fresh captures have required non-empty data + documented skips.
* test(proposals): cover derived CLOSE proposal bridge
- Flag-off and emission for member/subset + relational transitive derived facts.
- Stable dedupe, skips for non-derived/malformed/unsupported.
- No status upgrade, no corpus side-effects.
- Runtime flag wiring smoke (consolidate + bridge on → emits after idle_tick).
- Re-runs of PR-1 tests + invariants stay green.
* feat(proposals): emit review-gated proposals from derived CLOSE facts
- New focused emitter generate/determine/derived_close_proposals.py:
- collect eligible derived (member/subset + TRANSITIVE) with entailed Derivation + SPECULATIVE.
- deterministic sort + stable dedupe key (predicate+args+derivation+structure_key).
- write proposal-only artifacts (source=derived_close_fact) to dedicated sink.
- best-effort, counts returned, safe skips.
- RuntimeConfig: review_derived_close_proposals (default False), documented.
- ChatRuntime.idle_tick: after consolidation (when flag on), call emitter best-effort (no did_work, no engine checkpoint).
- IdleTickResult: new field derived_close_proposals_emitted (additive, documented).
- No change to existing proposal_review contract or comprehension-failure sink.
- No serving/determine side-effects.
* docs(proposals): document derived CLOSE proposal bridge
- docs/runtime_contracts.md: new subsection under idle passes describing eligibility,
contract (proposal-only, review-gated, default-off, no corpus/COHERENT/serving change),
and cross-ref to the analysis note.
- New dated analysis note with why (flywheel bridge after PR-1), architecture/boundaries,
artifact schema, dedupe, tests, non-goals, and composition with PR-1/PR-3.
- Tiny high-level pointer not added (no suitable learning-loop section in README at this granularity).
* fix(proposals): harden derived CLOSE proposal emission
The two Gemini robustness nits raced the #774 merge: the patch landed on the
branch after GitHub had merged the pre-patch head, so main shipped the
architecture without the migration-input hardening. This re-lands ONLY the
robustness fixes — no identity-semantics change.
- chat/runtime.py: parse identity_scheme with try/except (TypeError, ValueError)
-> fallback to legacy scheme 1; revision str(... or '') so a null becomes ''
(unverifiable -> conservative DIVERGED), not the literal 'None'.
- workbench/readers.py: stored_revision=written_at_revision or '' so a None
revision is handled identically by the shared reconcile helper.
- tests: malformed identity_scheme does not crash the load guard (migrates);
reader falls back to legacy on malformed scheme; reader missing revision is a
conservative break.
Verified: 50 identity/migration/reader tests pass. No lane/serving path touched.
Removes code_revision from the engine-identity hash: engine_identity is now the
sha256 of the 5 ratified packs ONLY. The build revision is provenance (the
manifest's written_at_revision), not identity — so a behavior-neutral rebuild is
the SAME identity and the always-on daemon no longer flag-day strict-breaks on
every commit (the ADR-0220 defect).
Core:
- core/engine_identity.py: ratified_substrate/compute_engine_identity drop the
git_revision arg (packs-only); add compute_legacy_engine_identity (reproduces
the pre-split packs+rev hash, for migration verification), ENGINE_IDENTITY_SCHEME=2,
IdentityReconciliation enum, and reconcile_loaded_identity — the single source of
truth for the runtime guard AND the workbench reader.
- chat/runtime.py: the load guard reconciles via scheme. Current scheme -> direct
packs-only compare. Legacy (code_revision-folded) stamp -> a VERIFYING migration:
reconstruct the legacy hash from the persisted written_at_revision; a match proves
packs unchanged (warn + re-stamp, resume, no break) — a mismatch means the packs
genuinely changed (DIVERGED -> strict-refuse). Preserves 'distinct packs => refuse'.
- engine_state/save_manifest: stamp identity_scheme alongside engine_identity
(additive-optional; no schema_version bump).
- workbench/readers.py: continuity reader uses the same reconcile (no phantom break
on legacy checkpoints). cli.py break message reworded (packs, not 'build revision').
Callsite cleanup: drop the now-unused git_revision arg + imports across
always_on.py, evals/l10_always_on/runner.py, workbench/readers.py.
Tests:
- test_identity_provenance_split.py (new): 5 reconcile unit proofs + 3 runtime
integration proofs incl. the wrong=identity defense (legacy stamp of DIFFERENT
packs still strict-refuses) and the re-stamp migration.
- test_engine_identity.py: invert the code-revision test (rev no longer changes
identity); assert the substrate is packs-only.
- Restore 3 lineage tests silently red since ADR-0219 (flat-path _manifest helper
now resolves the gen-dir).
- Update remaining callsites/monkeypatches for the new signature.
Hygiene: .gitignore now covers the ADR-0219 gen-dir runtime files
(current, gen-*/, session_state.json, proposals.jsonl).
Verified: 59 identity/migration/lineage/workbench + 32 L10 + 76 invariants/cli +
34 smoke pass; serving lane SHAs unchanged (no derivation/reliability_gate touch).
Adds W1-T: an observational telemetry leaf for idle_tick's learning flywheel,
modeled on core.cognition.leeway (the B4 precedent). Never gates, refuses,
or alters trace_hash.
New core/cognition/backpressure.py:
BackpressureRecord(pending_proposals, candidate_backlog, cap, headroom,
contemplated_this_tick, created_this_tick, at_fixed_point, did_work)
build_backpressure_record(...) — pure function, resolves ADR-0161 cap from
env (CORE_HITL_PENDING_CAP or 256), derives headroom = max(0, cap-pending),
at_fixed_point = (candidate_backlog==0 and created_this_tick==0).
chat/runtime.py:
IdleTickResult gains backpressure: BackpressureRecord | None = None (always
set on a completed tick).
idle_tick builds the record from counts already computed (pending_proposals,
candidate_backlog, contemplated_count, created, did_work) — no new
computation, no serving-path change.
Appends one JSONL line per tick to engine_state/idle_telemetry.jsonl
(best-effort, never crashes the continuous life; accumulates across all
generations so history survives reboot independently of the checkpoint).
tests/test_idle_backpressure_telemetry.py (16 tests):
- field derivation: headroom, cap, at_fixed_point (holds + bites)
- cap resolution: default 256, env override, invalid env fallback
- firewall proof: trace_hash of a served turn is byte-identical whether
idle_tick ran before it or not (the load-bearing invariant)
- integration: record fields on a fresh runtime; telemetry written to
engine_state dir; history accumulates across ticks
All 16 tests pass. ADR-0161 cap and queue_full control logic untouched.
Closes the cross-file checkpoint-atomicity gap in ADR-0156. The four
checkpoint files now live in a committed gen-NNNN/ directory; the single
atomic os.replace of a 'current' pointer file is the commit boundary.
A kill before the pointer swap leaves the prior committed generation
intact; a kill after commits the new generation. Unreferenced gen dirs
are ignored. ADR-0156's deferred parent-dir fsync is also closed.
Key changes:
- engine_state/__init__.py: begin_generation() + commit_generation() +
_resolve_dir() for all load_* methods. Flat-layout legacy checkpoints
migrate into gen-0000 on first begin_generation call. GC retains K=2
committed generations.
- chat/runtime.py: checkpoint_engine_state uses the two-phase commit;
finalize_turn_trace_hash no longer writes discovery_candidates outside
the generation sequence (the second unguarded write path is closed).
- evals/l10_continuity/runner.py: _inject_orphan_tmp updated to inject
the two orphan shapes of the generation model.
- tests/test_adr_0219_generation_checkpoint.py: 18 tests, one per
acceptance-gate bullet + biting mutation variant each.
L10 lane: all_gates_pass=true; versor_condition<1e-6 throughout.
Smoke: 95 passed. Runtime: 20 passed.
* feat(l10): autonomous idle frontier-contemplation — the always-on life learns on its own
Frontier survey move #2, path (c): give the always-on idle life a stream of experience via
SYMBOLIC intake (not the afferent→field seam, which is deferred pending a commensurability
ruling — docs/analysis/afferent-field-ingest-scope-2026-06-15.md). The idle heartbeat
converges (idle_tick drains a finite backlog + saturates is-a closure, then does nothing
forever); this makes it autonomously MINE its frontier with no user turn.
Reuse-heavy (the mechanism largely existed): a gated idle pass wires ADR-0080 contemplation
into the always-on loop.
- core/config.py: contemplate_frontier_during_idle (default off → no behavior change).
- chat/runtime.py: idle_tick pass 2b — when on, runs core.contemplation.run_contemplation()
(mines the checked-in frontier-compare reports) and persists a reviewable ContemplationRun
to <engine_state>/contemplation_runs/idle_<substrate_hash>.json. IDEMPOTENT per frontier
(an already-mined frontier is not re-persisted → the life converges, no churn) and MEMOIZED
per session (the static frontier is mined once, not re-read every beat). SPECULATIVE-only
(ADR-0080: ContemplationFinding.__post_init__ RAISES if not SPECULATIVE → wrong=0 by
construction; never ratified here — the HITL path is untouched). IdleTickResult gains
frontier_findings.
- chat/always_on.py: run_continuous did_work + HeartbeatRecord count frontier work, so the
heartbeat/soak convergence (H3) stays honest.
- core/cli.py: `core always-on --contemplate-frontier` (explicit learning-daemon mode) +
honest per-beat log ("+N findings").
Adversarial 3-lens review (wrong=0/convergence, determinism/trust-boundary, test-vacuity)
found 7; fixed the real ones: the MEDIUM torn-write (write_contemplation_run is now atomic
temp+os.replace — the indefinite-uptime daemon expects SIGKILL mid-write, and the skip-guard
would never repair a torn canonical file), the gitignore breadth (contemplation_runs/ at any
engine-state root), the 64-bit filename truncation (full substrate_hash), and the
re-mine-every-beat cost (session memoization). Deferred + documented: the shared
contemplation miner embeds absolute report paths in finding CONTENT — a portability wrinkle
that does NOT affect wrong=0/convergence/soak determinism (those key on the content-only
substrate_hash) and touches shared code + its test surface; a follow-up.
Tests (non-vacuous): 7 — off-by-default (no behavior change), mines-when-enabled,
findings-SPECULATIVE (wrong=0), converges-idempotent, heartbeat-mines-then-converges,
mines-once-per-session (memoization), atomic-no-orphan. 55 idle/contemplation/always-on +
96 invariants/contemplation + 28 smoke green; `core always-on --contemplate-frontier` mines
end-to-end. engine_state contemplation_runs are per-life runtime state (gitignored).
* fix(l10): isolate idle frontier pass + persist frontier_findings (#758)
Three review blockers on the autonomous idle frontier-contemplation pass:
1. Daemon isolation. idle_tick's pass 2b called run_contemplation /
write_contemplation_run bare inside the always-on loop (run_continuous
wraps idle_tick in nothing but an exit-checkpoint finally), so a malformed
frontier report or a transient FS fault would propagate out and KILL the
continuous life. Wrap the optional pass best-effort (the exit-checkpoint
boundary idiom): degrade to 0 findings + RuntimeWarning, leave did_work
untouched, let a later beat retry.
2. Durable did_work explanation. HeartbeatRecord carried frontier_findings
but serialize_report omitted it and AlwaysOnReport had no run total, so
lived_life.json could show did_work=true with facts=0/proposals=0 and no
persisted cause. Persist frontier_findings per-record + total_frontier_findings.
3. Honest HITL claim. The docstring/CLI/config said findings are "reviewable
via the HITL path", but the existing HITL queue + workbench reader scan
<repo>/contemplation/runs while the pass writes <engine_state>/contemplation_runs/.
Narrow the claim to "persisted reviewable artifact, not yet in the HITL
queue" (wiring that discovery is a follow-up).
Tests: 4 new non-vacuous regressions (mining-failure degrade, write-failure
degrade, always-on loop survives frontier failure, lived_life persists
frontier_findings) — all fail against the unpatched pass. 38 L10/always-on/
workbench tests green.
Backs out the direct main push that added _maybe_apply_served_ask using a string-concatenation workaround to evade an over-broad text guard.
Restores chat/runtime.py to the #678 merge state and removes tests/test_ask_runtime_hook.py so the next runtime hook can land through a reviewed PR with structural tests instead of guard bypasses.
- This is the smallest runtime consumption hook for the ASK acquisition seam.
- It is default-dark.
- It preserves normal runtime behavior when disabled.
- It does not change public `chat(...)` signature.
- It does not change `ChatResponse`, `TurnEvent`, or telemetry schema.
- It does not call/render question prose.
- It does not serve from `proposal_path`.
- It does not retire Q1B carve-out or flip `proposal_allowed`.
- It does not serve VERIFIED.
- It does not move CLAIMS or metrics.
core/config.py: review_pending_proposals: bool = False (opt-in, mirrors consolidate_determinations). chat/runtime.py: IdleTickResult gains an additive optional proposal_review field; idle_tick runs a READ-ONLY sub-pass (after consolidation, gated) that surfaces core.proposal_review.idle_summary(). It does NOT set did_work / checkpoint / mutate / ratify / mount / modify readers; a reporter exception is CAPTURED (safe=False, errors=('proposal_review_failed:<type>',)), never propagated.
docs/runtime_contracts.md documents the sub-pass + field (required for runtime-surface changes). Contract tests: default-off -> proposal_review None (existing shape preserved); enabled -> summary surfaced; exception captured not propagated; read-only review never checkpoints; other passes unperturbed. 91-test smoke green incl. architectural invariants + both existing idle contract suites. Existing idle_tick remains the only idle_tick; NOT L10 always-on.
The final AGI-spine step (A INSTRUMENT → B WIRE → C DEEPEN → D CLOSE → E ESTIMATION).
The engine may now SERVE a DISCLOSED estimate for a query it would otherwise refuse —
but only for a predicate-class that has measured itself reliable, and never as fact.
This executes the ADR-0206 §5 cognition-path widening: the bridge's LICENSE node
(reliability_gate.license_for), previously "built — not yet called from serving", is now
called. govern_response returns APPROXIMATE iff a genuine licensed Action.SERVE
LicenseDecision is passed (STRICT for every other input — so every existing serving call
site is byte-identical); shape_surface DISCLOSES the estimate as "[approximate] …".
Mechanism:
- generate/determine/estimate.py — a BLIND converse-guesser: told p(a,b), asked p(b,a),
it commits the converse. It never reads the pack's symmetry metadata; whether the guess
is right is MEASURED, not assumed.
- evals/determination_estimation/ — the gold lane: run_practice (sealed, ADR-0199) folds
the converse-guesser over symmetric (sibling_of) vs directed (parent_of) cases, scored
against the pack's graph.edge.symmetric truth (gold independent of the solver). The gate
DISCRIMINATES: sibling_of earns SERVE (660 correct → Wilson floor 0.990046 ≥ θ_SERVE),
parent_of does not (660 wrong → 0.0). The license is earned by VOLUME — 657 perfect
commits is the exact θ_SERVE=0.99 threshold (656 is below).
- generate/determine/data/estimation_ledger.json — the ratified committed ledger,
hash-verified on load (a hand-edited ledger raises RatifiedLedgerError); it IS the
deterministic sealed-practice output (a GSM8K-style --check test pins this).
- chat/runtime.py — when a converse query is refused and the class holds a SERVE license,
the disclosed estimate is surfaced through the bridge (gated by config.estimation_enabled,
default OFF; only meaningful with accrue_realized_knowledge).
Invariants:
- wrong=0 by construction — an estimate is ALWAYS disclosed ([approximate]), never a silent
commit (UNVERIFIED_POSSIBLE is never in APPROXIMATE's admissible set), and only a genuine
ratified license widens (a forged {"licensed":True} dict / a PROPOSE license / an
unlicensed SERVE all stay STRICT). Defense-in-depth: type-gate ∧ admissible-set ∧
hardcoded disclosed state.
- never self-authored — ceilings stay at safe defaults (θ_SERVE=0.99); the engine cannot
raise its own bar. The ledger is sealed practice, hash-verified.
- session/serving only — no corpus/pack/identity/proposal/vault mutation; the HITL teaching
path is untouched. Deterministic; no clock/random.
- byte-identical for every non-E turn (the 2643 govern_response call passes no license).
Out of scope (separate ADR-0206 §5 PRs): the math-serving seam (select_self_verified,
touches the sealed metric), SITUATE (stakes), and the live FEED-BACK loop.
Verified green: smoke (90), architectural invariants (56), response_governance (321,
incl. the new license-gated widening test), the determination-estimation lane (12), and
the B/D/determine regression net. Four-lens adversarial review (disclosure/wrong=0,
calibration integrity, byte-identity, boundary/determinism): all held. Design:
docs/analysis/E-estimation-design-2026-06-06.md.
CLOSE the autonomous-loop spine: when idle, the engine consolidates each
soundly-derived determination back into the held self, so the next determine()
reaches it directly and can chain one hop further. The directly-answerable set
climbs monotonically across idle ticks to the deductive-closure fixed point.
Mechanism — generate/determine/consolidate.py::consolidate_once runs ONE
semi-naive layer of the member/subset deductive closure (member∘subset → member,
subset∘subset → subset; NEVER member∘member — instance-of is not transitive).
Each one-hop conclusion not yet realized is VERIFIED by the sound+complete
proof_chain ROBDD (reusing C's single verifier _verify_subsumption) and written
back via generate/realize::realize_derived as a SPECULATIVE realized record
carrying derived-provenance (premise structure_keys + rule + the ENTAILED
verdict). idle_tick gains a consolidation pass gated by the new
config.consolidate_determinations (default OFF); IdleTickResult.facts_consolidated
reports the layer.
Invariants held:
- wrong=0 — every consolidated fact is a sound-rule conclusion confirmed by the
sound+complete decider; member∘member is structurally unreachable (a member fact
is only ever extended by a subset edge). _verify_subsumption now refuses a
mislabeled/wrong-arity path (belt-and-suspenders now that consolidation is a
second caller), so the fallacy cannot be laundered through a corrupted chain.
- honesty — a fact derived from SPECULATIVE premises stays SPECULATIVE / as-told;
the soundness of the inference never upgrades the standing of the premises.
COHERENT is never minted.
- teaching-safety — SESSION memory (immediate), an extension of the realize path;
NOT corpus mutation and NOT coupled to proposals. The HITL path is untouched.
- determinism/replay — pure function of the realized set; sorted write order;
derived structure_key identical to a told fact's; provenance round-trips through
the Shape B+ snapshot (consolidated facts resume the SAME life across reboot).
- no new normalization — writes reuse the INV-21-allowed vault writer;
algebra/versor.py keeps closure.
Falsification — evals/determination_closure: a frozen replay seeds a deep is-a
chain and runs idle ticks; asserts the closure climbs monotonically to a complete
fixed point (no-op final tick), wrong=0 (member∘member canary never derived, no
fabricated membership), all derived facts SPECULATIVE, and every derived record
re-verifies ENTAILED from its recorded premises.
Verified green: smoke, runtime, cognition, architectural invariants, plus the new
D unit + lane tests and the determine/realize/persistence regression net. Five-lens
adversarial review: 4 lenses held; the 5th (normalization) was a misattribution
(pre-existing vault reproject, triggered identically by the merged realize path,
on sanctioned null-vector storage). Design + findings: docs/analysis/
D-close-consolidation-design-2026-06-06.md.
When accrue_realized_knowledge is on and a question turn is Determined over realized
knowledge, the user-facing surface IS that answer (generate.determine.render_determination):
'Yes — as I was told, truth is a concept.' The realizer's articulation_surface is
RETAINED as evidence — the determination is a user-facing SELECTION (like the
unknown-domain gate), not a rewrite. An Undetermined turn keeps the default
articulation surface (the honest 'I don't know'); flag-off never takes this path.
Basis is rendered honestly: SPECULATIVE grounds read 'as I was told', never 'verified'
(D0 only asserts answer=True, so the surface is an affirmation — no fabricated or
asserted-False string). Selection only: no field op, no normalization, proposes no
learning (HITL untouched).
Updates docs/runtime_contracts.md selection policy + adds the contract test in the
same PR (per the surface-contract discipline).
Wires comprehend->realize/determine into the live turn loop: a declarative turn
REALIZES a fact into the held self (session vault, SPECULATIVE/as-told); a question
turn is DETERMINED over realized knowledge (answered as-told, or refused open-world).
The first time a CORE conversation actually accumulates knowledge it can recall and
reason over — the 'one continuous life' telos made real, including across reboot
(the accrued fact is in the Shape B+ snapshot, so it survives the process ending).
Seam: all chat() returns funnel through _checkpointed_response; accrual runs there
BEFORE the checkpoint (so the fact persists this turn), gated by a new
accrue_realized_knowledge flag (default OFF — one-shot/eval runtimes don't accrue;
the production L10 process enables it with persist_session_state).
Discipline: SESSION memory, not ratified learning — proposes nothing, HITL untouched.
Realization writes go through generate.realize (INV-21 allowed writer). Additive: a
failure is a clean no-op, never crashes a turn. Slice B-1 RECORDS the outcome
(last_turn_accrual(), introspectable) and leaves the ChatResponse/surface contract
UNCHANGED; surfacing the determination is the B-2 follow-up (needs runtime_contracts
+ contract-test updates).
Tests: declarative accrues; question determines as_told; untold refuses open-world;
idempotent retell not recreated; flag-off no-ops with identical surface; and the
lived-spine proof — accrued knowledge survives reboot (determined as_told after a
fresh runtime over the same checkpoint).
The second lived-spine half: the engine learns WHILE IT LIVES, not only when
prompted. ChatRuntime.idle_tick() advances the contemplation/proposal flywheel
between turns (no user input):
- contemplates the pending discovery backlog (enrichment), then runs the
replay-gated propose_from_candidate into a persistent, file-backed ProposalLog
(engine_state/proposals.jsonl) held on the runtime.
- PROPOSAL-ONLY: it never ratifies. Raw cold-start candidates are 'undetermined'
and the eligibility gate refuses them outright (the engine won't propose what
it hasn't determined). A determined candidate only reaches 'pending' — moving
to 'accepted'/corpus-append stays HITL via teaching/review. No idle tick emits
an accepted / accepted_corpus_append event.
- The proposal log and the candidate backlog both live in the engine-state dir,
so idle learning persists across reboot and accumulates (CL-2) — building on
Shape B+ resume + L11 identity continuity.
idle_tick returns IdleTickResult(candidates_contemplated, proposals_created,
pending_proposals). None proposal log under no_load_state (ephemeral runtimes
keep no learning lineage).
5 dedicated tests: no-op on empty backlog, contemplates the backlog, refuses
undetermined (safety), proposes-a-determined-candidate-but-never-ratifies,
idle-learning-persists-across-reboot.
Builds the L11 lived-spine half on top of Shape B+ T-resume: prove the
continuous/resumed life is the SAME identity, with a content-derived, hash-chained
lineage and a falsifiable behavioral proof.
- core/engine_identity.py (L11-1): EngineIdentity = sha256 of the ratified
PERSONALITY substrate (identity/safety/ethics/register/anchor-lens pack files)
+ code revision. Content-derived, NOT entropy — same substrate => same identity
(cross-engine portable). The "who am I" hash; bumped by a ratified identity
change, NOT by lived learning (that is experience, carried by Shape B+).
- engine_state + chat/runtime (L11-2): every checkpoint manifest stamps
engine_identity + parent_engine_identity (git-like lineage). Stable substrate
=> identity == parent (one continuous life); a ratified change => the bump.
- chat/runtime + config (L11-3): on reboot, recompute identity and compare to the
stamped one. Mismatch (substrate changed while down) surfaces a warning +
identity_continuity_break flag; strict_identity_continuity (opt-in) refuses
(IdentityContinuityError). Default warns — reboot is recovery, not control flow
(ADR-0157); the operator must not be bricked by a benign ratified pack swap.
- tests (L11-4): the proof. Continuity is SUFFICIENT (byte-identical resume +
no break under a fixed identity), identity is LOAD-BEARING (distinct packs =>
distinct hashes), and the CONTRAPOSITIVE holds (resuming under a different
identity raises the break). Same identity <=> continuous; different => break.
Test hygiene (required by L11's always-on identity stamping): conftest isolates
the default engine_state dir per test; the refusal-calibration cold-start probe
uses no_load_state=True. Both prevent cross-test identity-lineage pollution.
19 dedicated tests; curated smoke green (no spurious break warnings).
The load-bearing L10 milestone: with resume mode enabled, a reboot resumes the
SAME life. Wires SessionContext.snapshot/restore (Phases A-C) into the
engine-state checkpoint and flips the L10 spike's P2b oracle to transparent.
Persistence is OPT-IN (RuntimeConfig.persist_session_state, default False): it is
a deliberate always-on-runtime mode, and per-turn snapshotting has an O(turns)
cost, so demos / evals / one-shot runtimes do NOT pay for resume they don't use.
This keeps every existing ChatRuntime byte-for-byte unchanged (no perf tax, no
pinned-lane SHA drift, no test breakage); only the L10 continuity lane and the
production L10 process enable it.
Phase D (wiring):
- core/config.py: persist_session_state flag (default False).
- engine_state/__init__.py: bump _SCHEMA_VERSION 1->2; add save_session_state /
load_session_state (atomic, ADR-0156). v1 checkpoints still load (1 <= 2) with
no session_state -> fresh session.
- chat/runtime.py: when persist_session_state, checkpoint_engine_state saves the
session snapshot BEFORE the manifest (manifest = the commit marker / WAL force
boundary); _load_engine_state restores it into self._context.
Phase E (flip the oracle):
- evals/l10_continuity/runner.py: the continuity lane forces persist on (it IS
the resume-mode lane).
- tests/test_l10_continuity.py: test_p2b_documents_current_resume_gap ->
test_p2b_reboot_is_transparent (asserts post_reboot_transparent, divergence
None). predicates.py / runner.py / contract.md: P2b is now the
resume-as-same-life guard.
- tests/test_adr_0146_engine_state.py: manifest schema_version 1 -> 2.
Validation: full spike (P1 closure, P2a determinism, P2b NOW TRANSPARENT, P3
bounded, P4 crash-recovery determinism + commit point, P5b/P5c) +
reboot-restores-lived-state + v1 back-compat + ADR-0146, all green;
[run K -> reboot -> run M] byte-identical to [run K+M]. With persistence off
(default), the curated smoke + showcase budget + pinned lanes are unchanged.
Closes the A->E Shape B+ scope (docs/analysis/L10-shapeBplus-persistence-scope-2026-06-05.md).
Versioned additive-optional migration (L10 scoping step-2 ruling): a checkpoint
schema bump is a recorded lineage transition, not death-and-rebirth.
- engine_state.load_manifest() now REFUSES (IncompatibleEngineStateError) a
checkpoint whose schema_version > this build's _SCHEMA_VERSION, and tolerates
<= current (older/equal read any missing newer fields via additive-optional
defaults). Never silently mis-loads newer state.
- chat.runtime._load_engine_state() loads the manifest FIRST so the version
refusal gates before any recognizers/candidates are read.
- DerivedRecognizer.from_json documents the additive-optional convention
(new fields .get-defaulted + omitted-when-default), mirroring DiscoveryCandidate.
Tests (TDD): refuses newer schema_version; tolerates older. Prerequisite for the
L10 continuity spike's P2 byte-identity gate (it may now assume a fixed schema
within a run, with version bumps handled explicitly).
Adds the first consumer of CORE's two built epistemic substrates — the
decode-state taxonomy (core/epistemic_state.py) and the risk-reward
reliability gate (core/reliability_gate/, ADR-0175) — beginning to close
the LABEL-ONLY consumption gap they sat behind.
Ships the scaffold only:
- core/response_governance/policy.py — ReachLevel (STRICT < APPROXIMATE <
EXTRAPOLATE < CREATIVE), ReachPolicy, govern_response (STRICT-only stub),
shape_surface (STRICT = identity transform; higher levels real but
unreachable in production), and the 9/5/1 EpistemicState partition.
- chat/runtime.py — cognition-path seam: the response surface now flows
through shape_surface(govern_response(...)). STRICT = identity, so the
path is byte-identical to pre-bridge. reach_level carried on ChatResponse.
- core/physics/identity.py — reach_level on TurnEvent (default "strict").
wrong==0 untouched: select_self_verified is NOT touched (ADR-0206 §5);
reach_level is NOT added to the telemetry JSONL dict, so pinned lane SHAs
stay byte-identical. Widening, SITUATE/FEED-BACK, the math-serving seam,
and the EVIDENCED reconcile are deferred to their own PRs.
Tests (tests/test_response_governance.py): STRICT-only contract over all
states x license x stakes, STRICT identity, live-wiring proof (forces
APPROXIMATE -> policy-sensitive surface), total+disjoint taxonomy
partition. Each fails loudly under the violation it guards.
* feat(W-024): reboot_event audit trail entry (L10b.3, ADR-0158)
L10 scope §Sub-question 3: a reboot_event analog of TurnEvent, written
to the telemetry JSONL, lets future audit reconstruct when this engine
instance lost and regained its lifetime.
- serialize_reboot_event / format_reboot_event_jsonl in chat/telemetry.py
emit type="reboot" with restored_turn_count, stored/current revisions,
revision_matched, recognizers_count, candidates_count
- ChatRuntime._load_engine_state() buffers the JSONL line in
_pending_reboot_payload (str|None); ChatRuntime.attach_telemetry_sink()
flushes it exactly once when a sink is first attached
- Reboot event precedes all turn events in the session audit stream
- Pinned by 11 tests: serializer structure, determinism, revision_matched
logic, runtime integration (emit-once, no-checkpoint, no-load-state,
revision match, ordering)
Closes L10b: W-022 (atomic writes) + W-023 (revision warning) + W-024
together satisfy ADR-0146's atomic/observable/auditable checkpoint triad.
* fix(W-024): expose cached public git revision helper
TurnEvent had no trace_hash field, so teaching/discovery._trace_hash
always returned "" via getattr default. Every persisted DiscoveryCandidate
had source_turn_trace="" — provenance gap observed in a real 103-turn
session.
- Add trace_hash: str = "" to TurnEvent
- runtime.finalize_turn_trace_hash back-stamps last TurnEvent and
unstamped tail of _pending_candidates, then re-persists
- CognitiveTurnPipeline.process calls finalize_turn_trace_hash after
compute_trace_hash, before constructing CognitiveTurnResult
Invariants: empty hash is a no-op; back-walk halts at first already-
stamped candidate (no overwrite of prior turns); trace_hash bytes are
unchanged for any given turn.
Validated: tests/test_adr_0153_trace_hash_backstamp.py (6 tests),
core test --suite cognition/smoke/runtime/teaching all green.
Out of scope: OOV candidate trace_hash (same root cause, line-streamed
sink requires different fix); telemetry-sink trace_hash exposure.
Wires contemplation-enriched DiscoveryCandidates into the ADR-0057 proposal
gate at _load_engine_state(). Proposals land in ProposalLog with
source.kind="contemplation"; operator ratification via existing
core teaching review path unchanged.
* feat(W-003): wire VaultPromotionPolicy into turn boundary (ADR-0148)
VaultPromotionPolicy had zero callers; vault entries never crystallized
from SPECULATIVE to COHERENT. This PR wires the policy at the turn
boundary so settled entries can promote automatically.
Changes:
- core/config.py: add vault_promotion_enabled flag (default False, null-drop)
- vault/store.py: add promote_eligible_entries(policy) — metadata-only scan,
versors unchanged, _matrix_cache not invalidated
- session/context.py: persist energy_raw/energy_class/coherence_residual in
vault payload inside finalize_turn so the policy has data to decide on
- chat/runtime.py: call promote_eligible_entries after each finalize_turn,
gated on vault_promotion_enabled; import VaultPromotionPolicy
- docs/decisions/ADR-0148-vault-promotion-policy-wiring.md: decision record
- tests/test_adr_0148_vault_promotion.py: 6 tests, all green
Unlocks W-007 (DerivedRecognizer derivation from COHERENT vault entries).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(W-003): resolve Pyright errors on vault promotion wiring
- vault/store.py: add TYPE_CHECKING guard to import VaultPromotionPolicy
only at type-check time, avoiding circular import at runtime while
making the name resolvable to Pyright.
- session/context.py:262: suppress union-attr false positive — self.state
is guarded non-None by the raise at line 256 when input_versor is also
None, but Pyright cannot narrow through the nested ternary structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes W-013 wiring debt. Per Phase 2 operator decision: wire
core.cognition.explain into the live core chat REPL.
Changes:
- core/cognition/explain.py: add explain_from_intent(intent, correction_text)
companion to explain() — same dispatch table, skips the full
CognitiveTurnResult round-trip. Callers with only a DialogueIntent can
use this directly.
- chat/runtime.py: add _last_intent and _last_input_text instance fields;
store intent on every classify_intent_from_input() call (pack-grounded
path and stub/empty-vault path); add explain_last_turn() -> str method
that calls explain_from_intent(_last_intent, correction_text=_last_input_text).
- core/cli.py: in cmd_chat REPL loop, handle "/explain" command — calls
runtime.explain_last_turn() and prints the canonical prompt restatement
(or a "no prior turn" message to stderr if no turn has run yet).
- tests/test_explain_repl.py: 11 tests pinning explain_from_intent dispatch
for all intent tags and the ChatRuntime.explain_last_turn() contract.
Per ADR-0017 (Responsive-with-Axiology): introspection is per-turn and
operator-invoked, never autonomous — the /explain command is correct
placement for this feature.
W-011: recognition refusal_reason now materializes in
CognitiveTurnResult.refusal_reason via RECOGNITION_REFUSED enum value.
Precedence: recognition wins over generation (earlier-fail boundary).
W-012: ChatRuntime.chat() catches InnerLoopExhaustion from generate()
and returns a typed refusal ChatResponse with refusal_reason populated,
instead of propagating as an unhandled exception.
Adds RefusalReason.RECOGNITION_REFUSED to generate/exhaustion.py.
Lane SHAs: 7/7 match (demos don't exercise refusal paths — no re-pin).
Smoke + cognition suites green. Full suite not run to completion.
Closes the gap identified in the L8 audit (PR #250): the four-tier
memory model (ADR-0055) designates T1 (session vault) as a source for
contemplation evidence, but _emit_discovery_candidates was calling
contemplate(c) with no vault_probe, so inline contemplation operated
on pack + reviewed corpus only.
Changes:
- core/config.py: add RuntimeConfig.vault_probe_discoveries (default
False) — opt-in flag that enables the vault probe; default-off
preserves all pre-W-016 discovery output byte-identically.
- chat/runtime.py: add _build_vault_probe(vault, vocab) module helper
that closes over the live session vault and returns a _VaultProbe
callable querying at EpistemicStatus.COHERENT (ADR-0021 §3 — only
reviewed-coherent entries contribute evidence; SPECULATIVE/CONTESTED/
FALSIFIED entries are excluded by vault.recall min_status filter).
_emit_discovery_candidates now passes the probe to contemplate() when
vault_probe_discoveries is True.
- tests/test_discovery_contemplation_vault_probe.py: four contracts
pinned — probe not called by default, probe called when flag on,
probe evidence reachable in emitted JSONL, raising probe does not
crash the loop (defensive: vault unavailability must not block
discovery).
Lane SHAs: 7/7 unchanged (demo_composition, public_demo, et al).
Smoke suite: 67/67. Teaching suite: 17/17. New test: 4/4.
Out of scope: W-017 (automated T1/T2 → T3 promotion) is a separate
ratchet entry. This PR only wires the probe.
* 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.