Commit graph

144 commits

Author SHA1 Message Date
Shay
ac5fb35d45
fix(identity): harden ADR-0220 reconciliation inputs (follow-up to #774) (#776)
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.
2026-06-15 12:01:29 -07:00
Shay
512453b6fc
feat(identity): split engine identity from build provenance (ADR-0220 PR C) (#774)
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).
2026-06-15 11:38:04 -07:00
Shay
2b32bd2f8f feat(l10): idle backpressure telemetry — observational flywheel pressure gauge
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.
2026-06-15 02:36:44 -07:00
Shay
103def317d feat(engine-state): generation-dir atomic checkpoint (ADR-0219)
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.
2026-06-15 02:01:52 -07:00
Shay
efd280d45f
feat(l10): autonomous idle frontier-contemplation — the always-on life learns on its own (#758)
* 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.
2026-06-14 23:59:49 -07:00
Shay
18e25580f3 feat(l10): always-on daemon/CLI — the process that runs the continuous-life heartbeat
The L10 heartbeat loop (run_continuous) had no process to drive it; `core always-on`
is that process — the T-experience spine made runnable. It ticks idle_tick on a wall-clock
cadence so the engine LIVES and LEARNS with no user turn, persists lived_life.json (the
workbench Lived Life surface) + the checkpoint, and resumes the SAME life on restart.

- chat/always_on.py: run_continuous gains unbounded operation (heartbeats=None, runs until
  stop) + an interruptible inter-beat wait (_sleep_until_stop) so shutdown latency is one
  slice, not the cadence interval. Persists the run's identity pack ids (resume-verdict
  faithfulness). on_heartbeat is now best-effort (a broken log pipe can't kill the life).
- chat/always_on_daemon.py (new): the daemon shell — a single-instance OS lock (fcntl.flock:
  kernel-held, atomic, auto-released on death — no stale window, no PID-reuse, no half-written
  race), SIGINT/SIGTERM -> graceful stop (handlers saved/restored), the continuous-life config
  FORCED on, ephemeral (--no-load-state) writes no durable artifact. Foreground + explicit:
  no hidden background execution (CLAUDE.md); only writes the engine-state dir it was given.
- core/cli.py: `core always-on [--interval --max-beats --no-load-state --quiet]` with per-beat
  + summary logging; validates --interval; reports IdentityContinuityError / IncompatibleEngine
  StateError (the "different life / newer build" cases) as clean refusals, not tracebacks.
- workbench/readers.py: ENGINE_STATE_ROOT now honors CORE_ENGINE_STATE_DIR (= the daemon's
  resolved dir), so the workbench can't be split-brained (reading REPO_ROOT/engine_state while
  the daemon writes elsewhere); the Lived Life resume verdict recomputes from the persisted
  pack config, not a default config (no false substrate_changed for a non-default-pack life).
- Lived Life absence state now points at the real `core always-on` command (loop closed).

Adversarial 4-lens review (lock/concurrency, signals/shutdown, invariants/trust-boundary,
test-vacuity/CLI) caught 16 findings; this fixes all real ones — the HIGH lock races (two
daemons over one life), the env split-brain, the IO-kill, the uncaught identity/schema errors,
the unvalidated interval, the ephemeral-artifact shadow, and the resume-verdict pack-id bug —
and closes the two test-coverage gaps it flagged (real SIGTERM path + config-forcing-at-the-
runtime boundary).

Tests (non-vacuous): 11 daemon (flock live-holder refusal, leftover-lock reclaim, unbounded+
stop, interruptible sleep, forced-config-at-boundary, no-load-state guard, REAL SIGTERM
subprocess) + the reader pack-id discrimination test (fails under the old default-config bug).
245 workbench+invariants+always-on Python green; frontend tsc + vitest green; `core always-on`
verified end-to-end (bounded, real SIGTERM graceful stop, interval rejection).

engine_state/always_on.lock is runtime state (gitignored, ADR-0146 pattern).
2026-06-14 17:33:37 -07:00
Shay
7ead395eeb feat(workbench): Lived Life surface — the always-on heartbeat made felt (read-only, persist-first)
The L10 heartbeat (chat/always_on, #747) holds CORE alive over uptime but had no
surface. This adds the read-only Lived Life route that renders the continuous-life
evidence: the heartbeat over uptime + the resume-as-same-life verdict — both halves
of "one continuous life" on one surface.

Persist-first, never recompute engine-owned values:
- chat/always_on: serialize_report + write_lived_life persist a run deterministically
  (sorted keys) to engine_state/lived_life.json; run_continuous gains an opt-in
  report_path so a real continuous-life run leaves its evidence where the workbench
  reads it. closure_ceiling is persisted so the artifact is self-describing.
- workbench/lived_life.py: projection + a fail-closed validate() gate — the wrong=0
  analogue for the continuity surface: closure_held/closure_observed/totals/converged
  are each re-checked against the per-beat measurements, and resume_status is re-checked
  against identity-vs-current-substrate, so a tampered artifact RAISES rather than
  rendering a false claim (a beat lying about a breached ceiling, an inflated closure,
  a miscounted total, a falsely-claimed resume).
- workbench/readers.lived_life(): reads the artifact, computes the resume verdict from
  the persisted identity vs the canonical engine_identity_for_config (fail-soft ->
  "unknown", like IdentityContinuity). Honest absence when no run has been persisted.
- GET /lived-life + LivedLife/LivedLifeHeartbeat schemas (snapshot regenerated; the
  schema-drift gate proves every field is mirrored in src/types/api.ts).

The resume verdict made felt: would_resume / substrate_changed / unknown IS the L11
reboot guarantee (a reboot recomputes identity and refuses if it differs) — so the
surface shows this life wakes up as ITSELF, not a copy. The per-reboot lineage chain
stays owned by Runs > Identity (honest cross-link).

Frontend: /lived-life route (Evidence section, route count 15 -> 16). Renders the
headline (heartbeats, closure-held-by-construction, learned-while-idle, at-rest, resume
pill), the summary, the heartbeat timeline (closure flat below the ceiling — read, never
repaired), and the resume verdict. Honest absence + ADR-0162 route conformance
(loading/error/empty), fail-closed like Vault/Calibration.

Tests (non-vacuous): 12 backend (every tamper case raises; resume tracks identity vs
substrate; run_continuous(report_path) round-trips a readable+valid artifact) + 4
frontend render tests (recorded life renders; a breached beat shows the warning, never a
false "held"; a changed substrate shows would-refuse, never a false resume; absent shows
honest absence). All workbench Python (167) + affected vitest (167) + tsc + vite build
green; architectural invariants green.

engine_state/lived_life.json is runtime state (gitignored, ADR-0146 pattern).
2026-06-14 16:45:03 -07:00
Shay
32466434c2 feat(l10): always-on heartbeat — the loop that makes the life continuous (T-experience spine)
The 'one continuous life' telos had three built pieces (turn loop, Shape B+ resume,
idle_tick learning) but no process holding the engine alive+learning over uptime. This
adds that heartbeat loop: chat/always_on.run_continuous ticks idle_tick on a cadence so
the engine LIVES and LEARNS even when no one is talking to it, READS (never repairs) the
closure invariant as evidence each beat, and persists so the life survives interruption
and resumes as the SAME life.

Proven by 3 falsifiable soak tests (non-vacuous): lives (closure observed + held, real
field excited, ~7x under the 1e-6 ceiling); learns while idle (Step-D consolidation
DERIVES and STORES member(socrates,mortal) during idle — absent before the heartbeat,
present after); converges (saturated life stops churning); survives interruption as the
SAME life (strict identity guard enforces it on reboot + the consolidated STORED record
survives, not mere re-derivation); never repairs (field byte-identical after the run).

Safety by composition, no new authority: idle_tick stays proposal-only (HITL untouched)
+ sound proof-gated SPECULATIVE session-memory consolidation; closure owned by
algebra/versor.py (heartbeat only reads versor_condition — no hot-path repair).

Adversarial 3-reviewer panel: no-hot-path-repair PASS, wrong=0 PASS; the panel's
proof-quality findings are fixed — removed the tautological identity_continuous claim
(engine identity is config-derived, invariant by construction), reframed the resume test
to prove ACCUMULATED learning survived (stored record, not re-derivation) under the strict
identity guard, surfaced final_checkpoint_ok (no silent swallow), and softened the
docstring (this is the heartbeat LOOP a daemon runs; the daemon shell is a follow-up).

Scope: the reusable loop ships; a production daemon (CLI entry, real cadence, signals)
is a thin follow-up. Purely additive (no existing file touched).
2026-06-14 16:07:41 -07:00
Shay
97ba7a3a17 feat(ask): add runtime-facing ASK helper module
Adds a small chat.ask_runtime helper that consumes the ASK acquisition seam without editing chat/runtime.py.

- Uses honest contemplation_result keyword forwarding
- Preserves fallback behavior when ASK serving is disabled
- Does not call pass_manager or render_question
- Does not change public chat/runtime schema
2026-06-10 03:34:02 -07:00
Shay
c0b00587f6 revert(ask): remove unsafe runtime hook bypass
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.
2026-06-09 13:29:18 -07:00
Shay
2bafceea46 feat(ask): wire default-dark ASK candidate into runtime fallback path
- 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.
2026-06-09 13:26:13 -07:00
Shay
32385d954c feat(runtime): read-only proposal-review sub-pass in idle_tick (IT-b)
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.
2026-06-07 22:32:20 -07:00
Shay
7cb826a548 feat(determine): calibrated disclosed estimation — the engine earns the right to guess (Step E)
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.
2026-06-06 13:49:07 -07:00
Shay
8edafd04ac feat(determine): idle deductive consolidation — the loop learns from determined facts (Step D)
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.
2026-06-06 12:28:09 -07:00
Shay
dc06bd5a3c feat(runtime): surface the determination — the engine answers from accrued knowledge (Step B-2)
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).
2026-06-06 10:52:54 -07:00
Shay
3cdc4faa5f feat(runtime): inline realization — the conversation accrues knowledge (Step B-1)
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).
2026-06-06 10:40:20 -07:00
Shay
1c5bd19a11 feat(learning): continuous learning in idle — idle_tick advances the reviewed-learning flywheel
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.
2026-06-05 14:05:52 -07:00
Shay
f2dac1dc5c feat(identity): L11 identity continuity — same identity across reboot, not just same bytes
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).
2026-06-05 13:52:57 -07:00
Shay
5ed9fbb8e7 feat(persistence): Shape B+ Phase D+E — opt-in lived-state persistence; reboot transparent
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).
2026-06-05 13:17:30 -07:00
Shay
0b19e08306 feat(engine-state): schema-version migration discipline (L10 step-2)
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).
2026-06-05 07:48:46 -07:00
Shay
d5872a2e96 feat(adr-0206): response-governance bridge scaffold (STRICT-only, inert)
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.
2026-06-03 10:39:12 -07:00
Shay
18a679c29d feat(packs): reland en_core_syntax_v1 foundation vocabulary (collision-audited)
First foundation-curriculum substrate pack: 24 reviewed noun lemmas for
syntax/claim/provenance vocabulary (subject, predicate, agent_role,
clause, antecedent, consequent_role, polarity, negation, evidence_span,
…).  Substrate only — no parser, no runtime generation change.

This is the clean reland of #503 (merged then reverted via #508 for
shipping two failing tests).  Root cause of the revert was lemma
collision: the smoke CI gate does not collect dedicated test files, so
the pack merged green while its own test asserted resolver routings that
were false.

Reland fixes, with the full collision audit done against every on-disk
pack (not just the one the original author checked):

- agent      -> agent_role        (bare `agent` owned by en_core_meta_v1)
- comparison -> comparison_relation(bare `comparison` owned by en_core_cognition_v1)
- consequent -> consequent_role   (bare `consequent` owned by en_core_causation_v1)
- negation   kept bare on purpose: no *mounted* pack owned it
  (en_mathematics_logic_v1 is a domain pack, not in
  DEFAULT_RESOLVABLE_PACK_IDS), so mounting syntax now grounds a word
  that previously returned None.  Renaming would have lost grounding.

The original test also mis-asserted prior resolution: it claimed
`cause` -> en_core_causation_v1, but first-match-wins resolves it to
en_core_cognition_v1 (mounted at index 0).  The test now pins the TRUE
current owner; this pack does not change it.

Resolver: en_core_syntax_v1 mounted after en_core_polarity_v1 and before
en_core_relations_v1 — purely additive, steals no prior resolution.

Evidence:
- tests/test_en_core_syntax_v1_pack.py: 13/13 (was 4 failed, 9 passed)
- pack/resolver/grounding/invariants blast radius: 415 passed
- core test --suite smoke: 67 passed
- core test --suite packs: 1 failed, 140 passed — the single failure
  (test_lilibeth_canary_solves_end_to_end, GSM8K candidate-graph reader)
  is PRE-EXISTING on clean main f79b647, byte-identical, and untouched by
  this PR.
- lane SHAs: 7/8 content-match; the public_demo miss is a wall-clock
  DemoContractError (>30s budget), not content drift — serving frozen.

PR checklist:
- Capability: foundation-curriculum language substrate vocabulary.
- Field invariant: pack loads through checksum-sealed compiler; no
  algebra/versor path touched.
- Lane: core test --suite packs / smoke; dedicated pack test.
- No hidden normalization, stochastic fallback, approximate recall, or
  unreviewed mutation.
- Trust boundary: language-pack loading only; manifest checksums hash the
  exact bytes on disk (recomputed after the two renames).
2026-05-31 16:18:26 -07:00
Shay
5c75853a0b Revert "Merge pull request #503 from AssetOverflow/feat/en-core-syntax-relations-v1"
This reverts commit 2405e9b06e, reversing
changes made to 95bf0dfa60.
2026-05-31 09:00:15 -07:00
Shay
65eab68080 feat(language): register syntax pack in resolver 2026-05-30 19:15:37 -07:00
Shay
5045700484
feat(W-024): reboot_event audit trail entry (L10b.3, ADR-0158) (#284)
* 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
2026-05-25 20:37:00 -07:00
Shay
5e6a16d473
feat(W-020a): TurnEvent.trace_hash back-stamp (ADR-0153) (#277)
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.
2026-05-25 18:42:35 -07:00
Shay
df6c9a3206
feat(W-017): load-time auto-proposal pipeline from enriched candidates (ADR-0151) (#275)
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.
2026-05-25 12:46:10 -07:00
Shay
81718a0952
feat(W-007): wire DerivedRecognizer registry into CognitiveTurnPipeline (ADR-0149) (#274)
- RecognizerRegistry.first_admitted() — deterministic first-registered selection
- CognitiveTurnPipeline consults runtime registry when no recognizer explicitly passed
- ChatRuntime gains _pending_recognizer_examples + record_recognition_example()
- checkpoint_engine_state() derives and registers recognizer from accumulated examples
- RuntimeConfig.recognition_grounded_graph gate (already existed) controls wiring
- ADR-0149 decision record
2026-05-25 12:24:48 -07:00
Shay
5152719613
feat(W-018): autonomous inter-session contemplation at checkpoint (ADR-0150) (#273) 2026-05-25 12:24:39 -07:00
Shay
0b940674c0
feat(W-003): wire VaultPromotionPolicy into turn boundary (ADR-0148) (#272)
* 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>
2026-05-25 11:57:00 -07:00
Shay
9bbdcc96aa
feat(W-008): L10 Shape B hybrid engine-state persistence (#271)
* ci: re-trigger full-pytest

* docs: ADR-0146 — L10 Shape B hybrid engine-state persistence

* feat(W-008): Shape B engine-state persistence spike (ADR-0146)

* fix(W-008): eval isolation + env-var path + empty-manifest guard

- evals/run_cognition_eval.py: all ChatRuntime() calls pass no_load_state=True
  so parallel eval workers never touch engine_state/ checkpoints
- engine_state/__init__.py: honour CORE_ENGINE_STATE_DIR env var (ADR-0146 spec)
- engine_state/__init__.py: load_manifest() skips empty file instead of crashing
  (defensive against partial writes in concurrent contexts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:45:54 -07:00
Shay
f1d6c49814
[codex] Implement energy-modulated vault surface (#269)
* Implement energy-modulated vault surface

* docs/tests: add ADR-0145 and test suite for energy-modulated vault readback

Adds the decision record and 9 tests pinning the W-005 contract:
- energy_modulated_surface() prefix table (E0–E4)
- pack-grounded paths carry no recall_energy_class
- vault-grounded paths carry recall_energy_class=E2 and prefixed surface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: retrigger after 30m timeout

* ci: raise full-pytest timeout-minutes 30→45

* fix(ci): skip showcase runtime budget on slow CI runners (CORE_SHOWCASE_SKIP_BUDGET)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:33:32 -07:00
Shay
a1a085057e
feat(w013): wire explain_last_turn() into core chat /explain REPL command (#265)
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.
2026-05-25 06:09:49 -07:00
Shay
9d31f80fc8
fix(W-011/W-012): propagate recognition refusal + catch InnerLoopExhaustion (#258)
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.
2026-05-24 20:46:46 -07:00
Shay
db0f34f4d2
fix(W-016): wire vault probe into ChatRuntime discovery contemplation (#257)
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.
2026-05-24 20:30:03 -07:00
Shay
4a8aec7f8f
chore(chat): dispatch trace for grounding-source dispatcher (ADR-0142 debt #2) (#233) 2026-05-24 15:22:02 -07:00
Shay
35c8a1c56b
feat(epistemic): populate normative_detail on TurnEvent and ChatResponse (#223)
* 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.
2026-05-24 11:56:34 -07:00
Shay
9ef609c460
feat: materialize refusal_reason in CognitiveTurnResult when safety/ethics refusal fires (#222) 2026-05-24 11:53:06 -07:00
Shay
ab4c7cb0c3
feat(epistemic): Phase 3 state tagging spine (#220)
* 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.
2026-05-24 11:26:06 -07:00
Shay
327047ce26 feat(contemplation): Phase 5 — articulation-quality miner closes the loop
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.
2026-05-21 10:55:39 -07:00
Shay
b07fb0413c feat(contemplation): Phase 4 — per-plan articulation telemetry metrics
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.
2026-05-21 10:39:39 -07:00
Shay
664e08150c feat(contemplation): Phase 3 — live plan contemplation pre-flight
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.
2026-05-21 10:30:22 -07:00
Shay
9dfb505f06 feat(discourse): Phase 2 — reflective rendering pronominalizes focus subject
The Phase 1 multi-clause renderer (commit 63ffd88) produces grounded
content but reads mechanically because the subject lemma repeats in
every clause:

  "Truth is what is true. Furthermore, truth belongs to cognition.truth.
   In turn, truth grounds knowledge. Truth belongs to epistemic.ground.
   Furthermore, truth belongs to logos.core. In turn, truth requires
   evidence."

This is the literal articulation gap that motivated Phase 2 —
"reasoning at meaningful checkpoints during sentence construction
in order to have a stronger idea of what has come prior and is
already done to help better inform the next move."  Between move
``i`` and move ``i+1`` the renderer now reflects on what subject
has just been established (the "focus") and renders the next clause
with a pronoun when the focus carries forward:

  "Truth is what is true. Furthermore, it belongs to cognition.truth.
   In turn, it grounds knowledge. It belongs to epistemic.ground.
   Furthermore, it belongs to logos.core. In turn, it requires
   evidence."

Rules
-----

* Track ``focus_subject`` across moves (the lemma most recently used
  as a fact subject).
* When the next move's ``fact.subject`` is byte-equal to the current
  focus → swap subject token to ``"it"``.
* When the next move's subject differs → preserve the explicit lemma
  AND update focus.  Topic shifts (TRANSITION moves; compound bridge
  TRANSITION) thus reset the pronominalization channel naturally.
* Sentence-initial position (no connective): capitalised ``"It"``.
* Mid-sentence (after connective + comma): lowercase ``"it"``.

Doctrine alignment
------------------

Pure deterministic transformation of the existing plan; no new
content introduced, no LLM, no stochastic sampling.  Same plan in →
same surface out, always.  trace_hash invariance holds because:

  * BRIEF-mode prompts short-circuit the planner before render
    (commit 63ffd88's fast path) and are unaffected.
  * Multi-move plans render to a deterministically-different string
    that compute_trace_hash already folds in via ``surface``.

Wiring
------

* New ``reflective: bool = False`` parameter on ``render_plan``
  (back-compat default — every existing call site and test pinning
  Phase 1 output continues to work).
* ``_clause_for`` gains optional ``prior_focus_subject`` arg used by
  the reflective path; unchanged default behaviour.
* Runtime hook ``chat.runtime._maybe_apply_discourse_planner``
  passes ``reflective=True`` so the default chat path benefits.

Tests
-----

New ``tests/test_discourse_planner_reflective.py``:

* ``test_reflective_replaces_repeated_subject_with_it``
* ``test_reflective_handles_three_consecutive_same_subject_moves``
* ``test_reflective_capitalises_sentence_initial_pronoun``
* ``test_reflective_resets_focus_on_topic_shift``
* ``test_reflective_off_preserves_phase1_output``
* ``test_reflective_default_is_off_for_back_compat``
* ``test_reflective_is_deterministic``
* ``test_reflective_single_move_byte_identical_to_non_reflective``
  (load-bearing — pins that the cognition eval stays byte-equal
  across the Phase 2 flip because every cognition case is single-
  move).

Verification
------------

  pytest tests/test_discourse_planner_*.py        99/99 pass
                                                  (91 existing + 8 new)
  pytest tests/test_articulation_demo.py          all claims supported
  pytest tests/test_narrative_example_intents.py  pass
  pytest tests/test_runtime_config.py             pass
  cognition eval OFF vs ON                        45/45 surface byte-equal
                                                  45/45 trace_hash byte-equal
                                                  4/4 aggregate metrics
                                                      identical
  core test --suite smoke                         67/67 pass
  core test --suite runtime                       19/19 pass

Live demo (default config):

  "What is knowledge?"  → unchanged (BRIEF, fast-path)
  "Tell me about
    memory."            → "Memory is what a person recalls.
                          Furthermore, it belongs to cognition.memory.
                          In turn, it requires recall."
  "What is truth, and
    why does it matter?"→ "Truth is what is true. Furthermore, it
                          belongs to cognition.truth. In turn, it
                          grounds knowledge. It belongs to
                          epistemic.ground. Furthermore, it belongs
                          to logos.core. In turn, it requires
                          evidence."
  "Explain truth."      → "Truth is what is true. Furthermore, it
                          belongs to cognition.truth. In turn, it
                          grounds knowledge."

Out of scope for this commit (future Phase 2 follow-ons):

* Connective rotation ("Furthermore" → "Also" → "In addition"
  to break the repetitive cascade).
* Cross-clause de-duplication (skip moves whose ``new`` lemmas
  were already introduced by an earlier move).
* Generalised pronoun selection beyond ``it`` (requires gender /
  number / animacy signals the pack lexicon doesn't carry today).
2026-05-21 10:16:12 -07:00
Shay
63ffd88595 feat(runtime): default discourse_planner=True + fast-path BRIEF short-circuit
Flips ``RuntimeConfig.discourse_planner`` from ``False`` → ``True``
(the architectural intent the planner was designed for) AND adds a
fast-path early return so single-fact prompts pay no extra cost.

Why the flip
------------

The discourse planner apparatus has been fully wired in the codebase
for some time (``generate.discourse_planner.plan_discourse`` /
``plan_compound_discourse`` / ``render_plan``,
``generate.grounding_accessors.grounding_bundle_for``,
``chat.runtime._maybe_apply_discourse_planner``) but gated off behind
this flag.  Investigation surfaced that:

  * **Cognition eval (45 cases) is byte-identical OFF vs ON** across
    both surface and trace_hash projections — the planner's
    downstream ``len(plan.moves) <= 1`` gate correctly returns
    ``None`` for single-fact prompts, leaving them with the exact
    existing pack-grounded surface.

  * **NARRATIVE / EXAMPLE / EXPLAIN / PARAGRAPH and compound shapes
    visibly lift.**  ``"Tell me about memory."`` goes from a one-
    fragment disclosure to a 3-sentence grounded discourse.
    ``"What is truth, and why does it matter?"`` — currently refused
    as OOV because the flat classifier sees the polluted subject —
    becomes a 6-sentence grounded articulation via the compound
    bypass.

  * **No quality regression on existing benches.**  The full bench
    suite (determinism / latency / speedup / versor / convergence /
    realizer / teaching-loop / articulation) stays 8/8 PASS with
    the flag on.

Why the fast-path
-----------------

Default-on uncovered a perf trap: the gate ran
``grounding_bundle_for(lemma)`` (pack + teaching + cross-pack queries)
AND ``plan_discourse(...)`` on EVERY turn, then discarded the
result when ``len(plan.moves) <= 1``.  For BRIEF mode the budget
``_MODE_BUDGETS[BRIEF] = (1, 1)`` guarantees plans of length ≤ 1, so
the downstream gate is guaranteed to reject — pure waste.  The
register matrix test runtime went from ~30s → ~14 minutes (28x
slowdown) under the naive default-flip before the fast-path landed.

The new short-circuit:

  if mode is BRIEF and not compound.is_compound():
      return None

skips the bundle query + plan run entirely for the common case.
Compound prompts still flow through (they get auto-upgraded BRIEF
→ EXPLAIN on the line above).  Empirical post-fast-path
measurement on a 45-case eval (workers=1):

  OFF: 23.31s  (1.93 turns/sec)
  ON : 17.74s  (2.54 turns/sec)
  slowdown : 0.76x  (flag-ON is actually 24% FASTER — the bundle
                     work the OFF path also touches downstream is
                     short-circuited cleanly when not needed)
  surface byte-equal: True
  trace_hash byte-equal: True

Test updates
------------

* ``test_discourse_planner_render.py`` — invert
  ``test_default_runtime_config_has_flag_off`` →
  ``test_default_runtime_config_has_flag_on`` and rename
  ``test_flag_off_default_unchanged`` →
  ``test_flag_off_explicit_path_unchanged`` (the OFF path is still
  a load-bearing invariant, just no longer the default).

* ``test_narrative_example_intents.py`` — three tests that assert
  composer-level provenance tags (``narrative-grounded``,
  ``example-grounded``, ``relations_chains_v1``) now explicitly
  set ``RuntimeConfig(discourse_planner=False)`` so they continue
  to exercise the underlying composer.  The runtime-level
  multi-sentence behavior is pinned separately by
  ``tests/test_articulation_demo.py``.

Verified
--------

  cognition eval (45 cases)               OFF ≡ ON byte-identical
  pytest tests/test_discourse_planner_*   132/132 pass
  pytest tests/test_articulation_demo.py  all claims supported
  pytest tests/test_narrative_example_intents.py  pass
  pytest tests/test_runtime_config.py     pass
  core test --suite smoke                 67/67 pass
  core test --suite runtime               19/19 pass
  core test --suite packs                  6/6 pass

Live demo (default config):
  "What is knowledge?"          → single sentence (BRIEF, fast-path)
  "Tell me about memory."       → 3 grounded sentences
  "What is truth, and why does
   it matter?"                  → 6 grounded sentences (was: OOV)
  "Explain truth."              → 3 grounded sentences
2026-05-21 10:06:49 -07:00
Shay
79f1678923 feat: ADR-0086 + ADR-0087 + 100-register catalog — cognition lane closure
Three load-bearing pieces:

1. ADR-0086 — UNKNOWN-intent pack-resident token surface
   New deterministic composer `pack_grounded_unknown_surface` in
   chat/pack_grounding.py.  When intent classification returns UNKNOWN
   but the prompt contains pack-resident lemmas (via cross-pack
   resolver), surface those lemmas with their semantic_domains
   instead of falling to the bare _UNKNOWN_DOMAIN_SURFACE.  Wired
   into chat/runtime.py::_maybe_pack_grounded_surface as the
   last typed-intent branch before the OOV fallback.  Null-lift
   invariant pinned: fully-OOV prompts still emit the universal
   disclosure byte-identically.  Closes four cognition-eval term
   misses: unknown_logos_019 (public), unknown_evidence_042 (dev),
   unknown_spirit_041 + unknown_word_018 (holdout).  Side effect:
   evals/results/phase2_pack_measurements.json refusal_rate drops
   from 0.25 → 0.125 across all three identity packs (no longer
   refusing on these prompts).

2. ADR-0087 — PROCEDURE selector + trailing-clause subject echo
   Two coupled changes in chat/pack_grounding.py:
   (a) Numeric-determiner downrank in _extract_procedure_topic_lemma:
       tokens whose primary semantic_domain starts with
       "quantitative.numeric." are demoted; non-numeric resident
       candidates always win.  So "compare two terms" anchors on
       `compare` not `two`.
   (b) Trailing clause echoes the full normalized subject_text
       rather than just the selected lemma, so OOV head nouns like
       "terms" reach the surface even when only the procedure verb
       is pack-resident.  Closes procedure_compare_011.

3. 100-register catalog
   New packs/register/_catalog.json — canonical machine-readable
   spec for all 100 registers (7 currently-ratified + 93 drafted)
   organized into 9 voice groups (depth/tone/stance/posture/domain/
   cultural/affective/functional/composite).  Each entry is a
   complete production input — realizer_overrides, marker palettes
   (openings/transitions/closings), depth_preference, description,
   author_notes.  All realizer_overrides use only legal keys per
   scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS.
   Companion packs/register/CATALOG.md documents the production
   loop: materialize → widen REGISTER_IDS → ratify → smoke.

Cognition-eval lifts (all three splits):
  public:  term_capture 91.7% → 100.0%  (+8.3pp)
  holdout: term_capture 83.3% → 100.0%  (+16.7pp)
  dev:     term_capture 78.6% → 100.0%  (+21.4pp)
  surface_groundedness: 100% preserved on all splits
  intent_accuracy / versor_closure: 100% preserved on all splits

Tests:
  tests/test_pack_grounded_unknown.py     — 14 tests (composer
    direct + runtime engagement + null-lift invariant)
  tests/test_adr_0087_procedure_selector.py — 12 tests (selector
    numeric downrank + trailing-clause echo + regression guard)
  Existing test suites unaffected — cognition lane 120 passed / 1
  skipped both before and after.  Full lane net −3 failures vs
  pristine main (39 → 36 — none introduced).
2026-05-21 00:08:12 -07:00
Shay
a36b48b198
feat(runtime): opt-in unified-ingest path (ADR-0090, audit Findings 6+7) (#95)
Closes audit Findings 6 (within-turn recall not batched) and 7
(probe-ingest / commit-ingest dual field) as a single PR — the two
are architecturally entangled and resolve together.

Pre-fix flow in ``ChatRuntime.chat()``:

  1. ``probe_ingest(filtered)`` → ``probe_state.F``
  2. Gate check on ``probe_state.F``
  3. If gate fires: ``commit_ingest`` + stub response
  4. Otherwise: ``commit_ingest`` + drive bias → ``field_state.F``
  5. Walk runs on ``field_state.F``

The gate observes one manifold position; the walk navigates a
slightly different one (drive bias applied between them).  Honest
refusal decisions and walk outputs are made on different fields —
the audit's named coherence gap.

This PR ships a flag-gated unified-ingest path following the
codebase's standard substantive-change pattern (ADR-0046 /
ADR-0062 / ADR-0085 / ADR-0088 / ADR-0089):

``RuntimeConfig.unified_ingest: bool = False`` (default).

When ``True``:

  1. ``commit_ingest(filtered)`` runs first.
  2. Drive bias applied immediately.
  3. Gate observes ``committed.F``.
  4. If gate fires: stub response (turn has already committed —
     intentional semantic change documented in ADR-0090).
  5. Otherwise: walk runs on the same ``committed.F`` the gate
     decided against — no second ``commit_ingest`` call.
  6. ``probe_ingest`` is not called on this path.

When ``False`` (default): historical behavior is preserved
bit-for-bit; ``probe_ingest`` still runs first.

ADR-0090 documents:

  * Phase 1 (this PR): unified-ingest substrate.
  * Phase 2 (separate PR, after Phase 1 validates): batched recall
    — pass the gate's ``direct_hits`` into ``generate()`` as a
    ``prebuilt_first_recall`` so the walk's first step does not
    re-call ``vault.recall()`` on the same field.  Single recall
    call eliminated per turn.
  * Out of scope: ``recall_batch`` for per-step walk recalls
    (each step's query depends on the previous step's field
    state; not batchable without changing walk geometry).

Validation:

  * 5 new tests in ``tests/test_unified_ingest_null_lift.py``:
      - flag defaults to ``False`` on ``DEFAULT_CONFIG``
      - flag-off surface + trace_hash + vault_hits byte-identical
      - flag-on does not call ``probe_ingest`` (verified via spy)
      - flag-on produces well-formed surface + trace_hash
      - flag-off still calls ``probe_ingest`` (historical guard)
  * ``core eval cognition`` byte-identical across all three splits:
    public 100/100/91.7/100, dev 100/100/78.6/100, holdout
    100/100/83.3/100.
  * ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
    ``runtime`` 19/0.

Comb-pass status after this PR:

  * Item 4 (graph topo) ✓ #92
  * Item 5 (realizer node_map) ✓ #91
  * Item 6 (batch recall) ✓ ADR-0090 substrate (this PR); Phase 2
    optimization is queued
  * Item 7 (probe/commit dual ingest) ✓ ADR-0090 (this PR)
  * Item 8 (dead defensiveness sweep) ✓ #91
  * Item 9 (local imports) ✓ #91
  * Item 11 (dead ``_fold_compose_into_surface``) ✓ #91
  * Item 13 (``_serialize_*`` fold) ✓ #91
  * Item 15 (GenerationResult tuple/list) ⊘ false positive
  * Item 16 (subject normalization consistency) ✓ #93
  * Item 17 (redundant ``^`` anchors) ✓ #94
  * Tier 5 minor (``_BE_FORMS`` hoist, walrus, reverse-iter) ✓ #94
2026-05-20 21:00:27 -07:00
Shay
3e9c9ce10d
chore: comb-pass closeout — item 17 + Tier 5 minor cleanups (#94)
Comb pass 2026-05-21.

Item 17 — redundant ``^`` anchors in ``re.match()`` patterns:

  ``re.match`` anchors at the start of the string automatically, so
  the leading ``^`` was documentation-only noise on every pattern
  consumed via ``.match()``.  Audited each pattern's call site:

    * ``_RULES`` (line 144) — used via ``pattern.match(text)`` → strip
    * ``_ANAPHORIC_FOLLOWUPS`` — used via ``pattern.match(text)`` → strip
    * Module-level ``_COMPARE_RE`` / ``_TRANSITIVE_QUERY_RE`` /
      ``_FRAME_TRANSFER_RE`` / ``_BELONG_QUERY_RE`` /
      ``_DECLARATIVE_RELATION_RE`` / ``_HOW_DOES_X_RE`` — all
      ``.match()`` → strip
    * Inline ``re.match`` in ``_strip_confirmation_tail`` → strip
    * ``_RESPONSE_MODE_RULES`` — used via ``pattern.search(text)`` →
      KEEP ``^`` (``re.search`` does not anchor)

  Trailing ``$`` anchors retained throughout because neither
  ``re.match`` nor ``re.search`` anchors at the end.

  A comment block documents the convention so future contributors
  understand the ``^`` retain-vs-strip rule.

Tier 5 minor (``chat/runtime.py``):

  * Hoisted ``{"is", "are", "was", "were"}`` to module-level
    ``_BE_FORMS`` constant.  Pre-fix ``_prefer_prompt_anchor``
    constructed this set on every English turn.
  * Replaced the content-token list comprehension + ``[-1]`` slice
    with a reverse-iteration short-circuit.  Pre-fix the function
    materialised the full filtered list just to pick the last
    element.
  * Cached ``token.casefold()`` once per token via a local in the
    loop body.  Pre-fix the comprehension called ``.casefold()``
    twice per token (against ``_QUESTION_WORDS`` and the inline
    aux-verb set).

Validation:

  * ``core eval cognition`` byte-identical across all three splits:
    public 100/100/91.7/100, dev 100/100/78.6/100, holdout
    100/100/83.3/100.
  * ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0.
  * ``pytest -k intent`` 236/0 (all intent classification tests
    pass with the ``^`` removals — the patterns behave identically
    under ``re.match`` regardless of the leading anchor).
2026-05-20 21:00:22 -07:00
Shay
fd48931838
perf(cognition): hot-path comb pass — 5 mechanical-sympathy fixes (#91)
Bundle of 5 hot-path optimizations + 1 dead-code removal + 1 import
sweep + 1 helper fold, surfaced by a comb pass through the cognitive
spine starting from ``CognitiveTurnPipeline.run()`` and walking
outward through ChatRuntime, intent classification, the graph
planner, the realizer, and the vault.  All eval lanes byte-identical
to MEMORY baseline; null-lift confirmed by ``core eval cognition``
across public / dev / holdout splits.

Hot-path fixes:

  1. ``ChatRuntime._apply_oov_policy`` no longer rescans every
     manifest per OOV token.  Two precomputed booleans on
     ``self`` capture the FAIL_CLOSED-all and PROPOSE_VOCAB-any
     aggregates at construction time.  Manifests are immutable
     post-construction so the cache is safe.  Turns the path from
     O(packs × OOV) to O(OOV).

  2. ``CognitiveTurnPipeline.run`` calls ``classify_compound_intent``
     once and takes its dominant ``compound.primary`` as the seeded
     intent.  Pre-fix the pipeline called both ``classify_intent``
     and ``classify_compound_intent`` on every turn — and
     ``classify_compound_intent`` internally invokes
     ``classify_intent`` on the dominant fragment, so every non-
     compound prompt walked the 15-regex cascade twice.

  3. ``TeachingStore.triples()`` materializes once per turn.
     Pre-fix ``_maybe_transitive_walk`` and ``_maybe_compose_relations``
     each called ``self.teaching_store.triples()`` independently,
     doubling the per-turn O(N) filter+tuple-build cost.  Both
     helpers now accept an optional ``triples`` arg; the pipeline
     computes once and passes through.

  5. ``realize_semantic`` and ``realize_target`` build a
     ``node_id → obj`` map once and look up each step in O(1)
     instead of an O(N) linear scan of ``graph.nodes`` per step.
     The cost was invisible on today's 1-2 node graphs but would
     have become an O(N²) regression on the multi-node graphs
     ADR-0089 Phase C2 plans to introduce.

Dead-code / cleanup:

  - Removed dead ``CognitiveTurnPipeline._fold_compose_into_surface``
    (no callers since PR #76 routed all surface composition
    through ``resolve_surface``).
  - Folded ``_serialize_walk`` + ``_serialize_compose`` (identical
    bodies) into one ``_serialize_operator`` helper.
  - Hoisted ``import json`` and ``RatifiedIntent`` from inside hot
    method bodies to module top (same pattern PR #76 applied to
    ``_is_useful_surface``).
  - Dead-defensiveness sweep on ``ChatResponse`` field reads in
    ``pipeline.run()``: ``getattr(response, "<field>", default)``
    where the field always exists on the dataclass with a default
    is replaced by direct attribute access (6 sites:
    ``realizer_grounded_authority``, ``recalled_words``,
    ``grounding_source``, ``register_canonical_surface``,
    ``pre_decoration_surface``, ``admissibility_trace``,
    ``region_was_unconstrained``).  ``refusal_reason`` retains the
    guarded read because ADR-0024 Phase 2 leaves its
    materialisation site dormant.

Benchmark profiler:

  - ``benchmarks/pipeline_profiler.py`` rebound from
    ``classify_intent`` to ``classify_compound_intent`` (the new
    single-classification site).  All other timing hooks unchanged.

Tests:

  - 4 new tests in ``tests/test_comb_pass_hot_path.py`` pin: OOV
    aggregates exist as bools; compound classifier runs exactly
    once per turn; ``triples()`` materializes exactly once per
    turn; realizer correctly resolves obj slots across an 8-node
    graph.
  - All existing tests pass.  ``core eval cognition`` byte-identical:
    public 100/100/91.7/100, dev 100/100/78.6/100, holdout
    100/100/83.3/100.
  - ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
    ``runtime`` 19/0.
2026-05-20 20:31:56 -07:00
Shay
de3f40b549
feat(cognition): opt-in grounded-realizer authority flag (ADR-0088 Phase B) (#88)
Closes audit Finding 2 (2026-05-20) — Phase B substrate.

Pre-fix ``CognitiveTurnPipeline.run()`` invoked ``realize_semantic``
on the ungrounded ``PropositionGraph``.  Every non-COMPARISON /
non-CORRECTION node was born with ``obj = "<pending>"`` and the
realizer emitted surfaces like ``"X is defined as ..."`` that
``_is_useful_surface`` correctly rejected.  The realizer therefore
never won the surface resolver introduced by PR #76 — it was
structurally present but semantically inert in the hot pipeline
path.

This PR follows the codebase's standard substantive-change pattern
(ADR-0046 ``forward_graph_constraint``, ADR-0062 ``composed_surface``,
ADR-0083 ``transitive_surface``, ADR-0085 ``gloss_aware_cause``):
ship the wiring behind a flag, default ``False``, with a CI-pinned
null-lift invariant.

Changes:

  * ``RuntimeConfig.realizer_grounded_authority: bool = False`` —
    operator-level opt-in.
  * ``ChatResponse.recalled_words: tuple[str, ...] = ()`` —
    alphabetic-filtered walk tokens from the recall step, populated
    on the main path of ``ChatRuntime._chat``.  ``walk_tokens`` is
    now computed unconditionally so non-English packs also surface
    them (English keeps using them for
    ``articulate_with_intent`` as before).
  * ``CognitiveTurnPipeline.run()`` — when the flag is set and the
    response carries any recalled words, calls
    ``ground_graph(graph, response.recalled_words)`` and re-invokes
    ``realize_semantic`` on the grounded graph.  The surface
    resolver (PR #76) then picks the realizer's grounded output
    when it clears ``_is_useful_surface`` and the unknown-domain
    gate did not fire.

Phase A (realizer fluency parity — gloss-aware templates, 3sg verb
agreement, pack-provenance tag) is documented in ADR-0088 §Phase A
and is the prerequisite for enabling this flag in production.  The
known fluency gap (e.g. ``"Light is a visible medium that reveal
truth"`` — subject-verb disagreement leaking from realizer
templates) is the reason the flag ships default-off: operators get
the wiring stable now, the realizer becomes a real authority once
Phase A's fluency upgrade lands.

Verification:

  * 4 new tests in ``tests/test_realizer_grounded_authority_flag.py``:
      - flag defaults to ``False`` on ``DEFAULT_CONFIG``
      - flag-off produces byte-identical surface + trace_hash
        (null-lift invariant)
      - ``recalled_words`` is populated on the main path
      - flag-on runs end-to-end without crashing (surface is
        well-formed regardless of which authority won the resolver)
  * ``core eval cognition`` — public 100/100/91.7/100,
    byte-identical to the MEMORY baseline (default-off).
  * ``core test --suite cognition`` — 120/0/1.
  * ``core test --suite smoke`` — 67/0.
  * ``core test --suite runtime`` — 19/0.
2026-05-20 20:00:58 -07:00
Shay
401ae53328
chore(generate): make stop-tokens caller-overridable via RuntimeConfig (#87)
Closes audit Finding 6 (2026-05-20).

Pre-fix ``_STOP_TOKENS = frozenset({"it", "to", "word"})`` was
hardcoded inside ``generate.stream.generate()`` and inhibited those
three tokens unconditionally across every pack, every language, and
every domain.  If a pack legitimately needed one of them as a content
word — e.g. a philosophy pack where ``"word"`` maps to λόγος, or a
syntax pack where ``"to"`` is a content node — there was no override
path.  The ``_try_index`` guard handled the case where the token was
absent from the pack, but offered nothing for packs that contained
the token and meant it.

Changes:

  * ``generate.stream.generate`` accepts ``stop_tokens: frozenset[str]
    | None = None``.  ``None`` resolves to the historical
    ``_STOP_TOKENS`` constant, preserving byte-identity for every
    pre-Finding-6 caller.
  * ``RuntimeConfig.stop_tokens: tuple[str, ...] | None = None`` —
    operator-level override threaded through ``ChatRuntime`` into
    ``generate()``.
  * Default ``None`` preserves byte-identical behavior for every
    existing pack and every existing test.

Scope notes:

  * This PR delivers the *runtime override* surface.  Manifest-driven
    per-pack overrides (``generation_stop_tokens`` field in the pack
    manifest) are the natural next step but require a pack-schema
    ADR and re-ratification of every affected pack, so the wiring
    lands first and the manifest field follows on a separate ADR.
  * ``agenerate`` was identified as unreachable and is being deleted
    in a sibling PR (Finding 7); its hardcoded ``_STOP_TOKENS``
    reference disappears with it, so it is intentionally not touched
    here.

Verification:

  * 4 new tests in ``tests/test_stop_tokens_override.py``:
      - ``RuntimeConfig.stop_tokens`` defaults to ``None``
      - ``generate()`` signature exposes ``stop_tokens`` with default
        ``None``
      - the historical constant is unchanged
      - an explicit override flows through the runtime end-to-end
  * ``core eval cognition`` — public 100/100/91.7/100, byte-identical
    to the MEMORY baseline.
  * ``core test --suite cognition`` — 120/0/1.
  * ``core test --suite smoke`` — 67/0.
  * ``core test --suite runtime`` — 19/0.
2026-05-20 19:59:33 -07:00