Compare commits

..

1 commit

Author SHA1 Message Date
Shay
6e0d114726 feat: held-out dev lane + first sound (wrong=0) ratio-chain reader
Lands the honest GSM8K iteration instrument and the first verifiable
reader, both measured on real held-out data + the sealed test.

THE INSTRUMENT (the durable win):
- evals/gsm8k_math/holdout_dev/v1: 500 real GSM8K cases CORE was NOT
  built against. The 50-case train_sample is overfit and hid a sealed
  wrong=0 breach for weeks. This lane is the un-gameable dev signal.
- test_holdout_dev_lane.py: wrong==0 floor (forever) + baseline snapshot.

THE READER (a safe pattern, not yet a sealed lift):
- generate/derivation/ratio_chain.py: structurally-forced, verifiable
  chained-ratio reader. Refuse-preferring.
- Measured: held-out 500 -> 1 correct / 0 wrong; SEALED 1,319 ->
  0 correct / 0 WRONG. It holds the prime directive (zero confab) but
  its held-out hit did not reproduce on the sealed set, so it is a
  proof-of-concept of the verifiable-reader pattern, not a real
  capability gain. Documented as such; not oversold.

EVIDENCE:
- docs/analysis/real-gsm8k-capability-measurement-2026-06-04.md: the
  honest finding (real capability 0%, composer 17% wrong on held-out,
  no separating gate) + why verifiable readers are the path.
- Regression: 851 passed, 0 failed. train_sample wrong=0.

wrong=0 confirmed on the sealed 1,319 before this touched serving.
2026-06-04 07:32:49 -07:00
424 changed files with 508 additions and 46748 deletions

4
.gitignore vendored
View file

@ -25,10 +25,6 @@ frontier_wave1.json
# Claude Code local session artifacts (per-developer, never tracked)
.claude/
# Local macro->micro system map — a per-developer NAVIGATION INDEX (like an IDE
# symbol index), NOT a project artifact. Regenerated on demand; never tracked.
.system-map/
# Runnable audit-passed showcases (ADR-0112 + ADR-0113) are generated on
# demand from the signed claim + on-disk lane result files. The inputs
# are committed; the renders are not.

View file

@ -42,7 +42,6 @@ is a CI failure (`.github/workflows/lane-shas.yml`).
| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `e323adb35ea17987991395424c603ff93bca08c11bc2713bd9f6338e86bb269f` |
| ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d` |
| ADR-0131 | `math_teaching_corpus_v1` | Math teaching corpus replays deterministically; all chains pass exit criterion (correct_rate=1.0, wrong=0) | `evals/math_teaching_corpus/v1/report.json` | `eaf160d145da29f9050ede8d58bf111b0f651dd40aeae9201857d0b97e014dd4` |
| ADR-0206 | `deductive_logic_v1` | Propositional entailment scored against an independent truth-table oracle; dev+holdout+external 716/716 correct, wrong=0, refused=0 | `evals/deductive_logic/report.json` | `97a230949016e38d5e3f37a69e4245b320575ee70e5af92ff7607f7b05f74b5f` |
## Verification

View file

@ -58,16 +58,6 @@ Allowed sites:
- `ingest/gate.py` for raw input injection.
- `language_packs/compiler.py` and vocabulary construction.
- `algebra/versor.py` for algebra-owned sandwich closure.
- `sensorium/*/canonical.py` and pack-governed modality compiler construction
boundaries for pinned signal canonicalization and quantization.
- `session/context.py` for session-scoped **semantic anchoring** of the field
toward the session concept-attractor (the anchor pull, hemisphere
consistency). Allowed ONLY because every such op (1) preserves
`versor_condition` BY CONSTRUCTION — composed from `rotor_power` /
`word_transition_rotor` / `versor_apply` on the Spin manifold, never a
post-hoc `unitize`/grade-projection — AND (2) carries semantic meaning in
the cognitive model. An op that needs a post-hoc closure repair (the
rejected `_slerp_toward`) fails clause (1) and stays forbidden.
Forbidden sites:
@ -80,15 +70,6 @@ Do not add drift repair, grade projection, watchdogs, timers, hot-path
normalizers, or monitoring functions whose only purpose is to repair another
function.
**The bright line — semantic anchoring vs. drift repair.** An op is *semantic
anchoring* (allowed at the sites above) iff it preserves `versor_condition` by
construction AND expresses a relation in the cognitive model. It is *drift
repair* (forbidden) iff its purpose is to restore a numerical invariant a prior
function should have preserved. Closure of field transitions is owned solely
by `algebra/versor.py` (`_close_applied_versor`); no other site may "fix" it.
Naming must not disguise the distinction: an op that anchors semantically must
not be named or documented as a "drift fix".
CGA null vectors are not unit versors. Preserve null vectors as null vectors.
## Core Primitives
@ -123,23 +104,14 @@ runtime path. Vault recall is exact and deterministic.
- `calibration/*` — bounded replay-based calibration.
- `docs/runtime_contracts.md` — response, telemetry, memory, identity, and testing contracts.
### GSM8K math comprehension substrate (sealed; serving `7/43/0`, wrong=0 — moves only via ratified PRs)
### GSM8K math comprehension substrate (sealed; serving `6/44/0`, wrong=0 — moves only via ratified PRs)
- `core/reliability_gate/` — calibrated-learning ledger + gate (ADR-0175): `ClassTally` counts, `conservative_floor` (one-sided Wilson, N_MIN=10), θ ceilings.
- `generate/derivation/` — the comprehension composer: `extract.py` (lexeme quantity extraction, EX-1/4/5 + function-word unit filter), `clauses.py` (GB-1 segmentation), `compose.py` (GB-2a list-sum + GB-3a clause-scoped referent guard), `accumulate.py` (GB-3b.1 single-referent gain/loss chaining), `goal_residual.py` (ADR-0207 R4 goal-residual production), `multistep.py`/`search.py` (bounded search), `verify.py` (the wrong=0 self-verification gate: grounding ∧ cue ∧ unit ∧ completeness ∧ uniqueness).
- `generate/derivation/` — the comprehension composer: `extract.py` (lexeme quantity extraction, EX-1/4/5 + function-word unit filter), `clauses.py` (GB-1 segmentation), `compose.py` (GB-2a list-sum + GB-3a clause-scoped referent guard), `accumulate.py` (GB-3b.1 single-referent gain/loss chaining), `multistep.py`/`search.py` (bounded search), `verify.py` (the wrong=0 self-verification gate: grounding ∧ cue ∧ unit ∧ completeness ∧ uniqueness).
- `generate/cue_precision/``(cue, op, unit_shape)` reliability ledger + trainer (ADR-0177 CP-1/CP-2a); inert (consulted by no serving/gate path yet).
- `evals/gsm8k_math/``train_sample/` (real GSM8K dev sample, currently 7 correct / 43 refused / 0 wrong), `practice/` (sealed attempt-and-eliminate lane + ADR-0163-F additive set), `confusers/` (ADR-0163-F2 discrimination probe — scored by `wrong→0` + pair-consistency, NOT flip-count).
- `evals/gsm8k_math/``train_sample/` (real GSM8K, the capability metric), `practice/` (sealed attempt-and-eliminate lane + ADR-0163-F additive set), `confusers/` (ADR-0163-F2 discrimination probe — scored by `wrong→0` + pair-consistency, NOT flip-count).
- `scripts/verify_lane_shas.py`, `scripts/generate_claims.py --check` — the serving-frozen gate (pinned eval-lane SHAs + `CLAIMS.md`).
### Sensorium / modality compiler substrate (parallel, afferent gates; no broad capability claim)
- `sensorium/compiler/` — shared compiler law for content-addressed afferent compilation units, canonical deltas, local arenas, and trace-safe merge hashes.
- `sensorium/audio/` + `sensorium/adapters/audio.py``audio_core_v1`, deterministic audio compiler substrate, gate closed by default.
- `sensorium/vision/` + `sensorium/adapters/vision.py``vision_core_v1`, tile-first deterministic visual compiler substrate over synthetic eval fixtures, gate closed by default.
- `sensorium/environment/` — ADR-0208 observation-frame contract for bundles of already-compiled afferent units; not late fusion and not a mutable world model.
- `sensorium/sensorimotor/` + `sensorium/adapters/sensorimotor.py` — ADR-0209 afferent proprioception/contact/action-result evidence substrate; no decode path.
- `sensorium/registry.py::decode*` + `AuthorityToken` / `EfferentGate` — ADR-0198 fail-closed efferent governance path. This is not a ratified motor decoder or actuator interface; no real action emission is claimed.
## Efficiency and Performance Doctrine
Performance is part of correctness for this project because slow feedback hides
@ -355,7 +327,7 @@ implicated rather than one.
things the ADR says that didn't actually ship.
2. **Before merging a stacked PR sequence into main.** When 2+ PRs
stack (PR #420 stacked on #416, PR #423 stacked on #420), the
stack (PR #420 stacked on #416, #423 stacked on #420), the
review-each-PR-individually pattern misses cross-PR consistency
issues. Audit the whole stack as one unit before any merge.

View file

@ -1,19 +1,5 @@
from .cl41 import geometric_product, reverse, grade_project, scalar_part, norm_squared, basis_vector
from .versor import versor_apply, normalize_to_versor, versor_condition
from .cga import (
EMBED_EXACT_MAX,
cga_inner,
outer_product,
is_null,
null_project,
embed_point,
read_scalar_e1,
blade_grade,
blade_norm,
graded_wedge,
is_incident,
dual,
meet,
)
from .cga import cga_inner, outer_product, is_null, null_project, embed_point
from .holonomy import holonomy_encode, holonomy_similarity
from .rotor import word_transition_rotor

View file

@ -19,34 +19,13 @@ No cosine similarity. No L2 norm. No approximate indexing.
"""
import numpy as np
from .cl41 import (
geometric_product,
grade_project,
reverse,
scalar_part,
N_COMPONENTS,
)
# The unit pseudoscalar I5 = e1 e2 e3 e4 e5 (the grade-5 blade, component 31).
# In Cl(4,1) with signature (+,+,+,+,-), I5^2 = -1, so I5^{-1} = -I5. Used by
# ``dual`` / ``meet``. Module-level singleton; never mutated.
_PSEUDOSCALAR_INDEX = 31
_I5 = np.zeros(N_COMPONENTS, dtype=np.float64)
_I5[_PSEUDOSCALAR_INDEX] = 1.0
from .cl41 import geometric_product, scalar_part, basis_vector, N_COMPONENTS
# Basis-vector component indices for e4/e5 inside the grade-1 block.
# component 1=e1, 2=e2, 3=e3, 4=e4, 5=e5.
_E4_IDX = 4
_E5_IDX = 5
# Pinned magnitude ceiling for f64-exact embedding + read-back (Phase 0A).
# Below this bound, ``embed_point(..., dtype=np.float64)`` round-trips integer
# coordinates exactly through ``read_scalar_e1`` and the conformal distance metric
# stays exact (proven in tests/test_cga_f64_exactness.py). The field-reasoner reader
# REFUSES any quantity whose magnitude exceeds this bound; the refusal lives in the
# reader — this module only states the bound. Generous vs GSM8K (quantities ~< 1e5).
EMBED_EXACT_MAX: int = 1_000_000
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
"""
@ -59,19 +38,10 @@ def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
def outer_product(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""The antisymmetric (commutator) product ``0.5 * (XY - YX)``.
HONEST CONTRACT: this equals the grade-raising wedge ``X ^ Y`` **only when both
operands are grade 1** (vectors). For higher-grade operands it is the *commutator*
(Lie bracket), which is NOT the wedge in particular it does NOT build a k-blade
by repeated application (a bivector commuted with a vector collapses the grade-3
part to grade 1). Existing callers use the result as an opaque, deterministic
relationship feature (folded into a scalar via :func:`cga_inner`), where the
commutator is well-defined regardless; none read it by grade.
For the true grade-raising exterior product (lines/planes/incidence) use
:func:`graded_wedge`. (Renamed contract only behaviour is unchanged, so every
current caller is byte-identical.)
"""
Outer (wedge) product: X ^ Y.
For a prompt versor X_p and response versor X_r,
X_p ^ X_r is a grade-2 object encoding their geometric relationship.
"""
XY = geometric_product(X, Y)
YX = geometric_product(Y, X)
@ -92,25 +62,18 @@ def null_project(X: np.ndarray) -> np.ndarray:
return embed_point(euclidean)
def embed_point(x: np.ndarray, *, dtype: "np.typing.DTypeLike" = np.float32) -> np.ndarray:
def embed_point(x: np.ndarray) -> np.ndarray:
"""
Embed a Euclidean point x in R^3 into the CGA null cone.
X = x + n_o + 0.5|x|^2 n_inf,
where n_o = 0.5(e5-e4), n_inf = e4+e5.
``dtype`` defaults to ``float32`` so every existing caller is byte-unchanged.
The field-reasoner reader passes ``dtype=np.float64`` to get an exact embedding:
``geometric_product`` already preserves float64 (``np.result_type``), so the
only thing that forced f32 was this construction. f32 silently collapses the
``n_o`` weight past ~1e4 (the ``0.5|x|^2`` terms lose the ``±1``); f64 keeps it
exact up to :data:`EMBED_EXACT_MAX` (see tests/test_cga_f64_exactness.py).
"""
x = np.asarray(x, dtype=dtype)
x = np.asarray(x, dtype=np.float32)
assert len(x) == 3, "embed_point expects a 3D vector"
x_sq = float(np.dot(x, x))
result = np.zeros(N_COMPONENTS, dtype=dtype)
result = np.zeros(N_COMPONENTS, dtype=np.float32)
result[1:4] = x
# n_o + 0.5|x|^2 n_inf
@ -119,108 +82,3 @@ def embed_point(x: np.ndarray, *, dtype: "np.typing.DTypeLike" = np.float32) ->
result[_E4_IDX] = 0.5 * (x_sq - 1.0)
result[_E5_IDX] = 0.5 * (x_sq + 1.0)
return result
def read_scalar_e1(X: np.ndarray) -> float:
"""Projective dehomogenization on the e1 axis — the exact, weight-invariant
read-back of a scalar coordinate from a (possibly dilated) conformal point.
A point at coordinate ``v`` on the e1 number line embeds as
``X = v*e1 + n_o + 0.5 v^2 n_inf``; a uniform conformal dilation by ``k``
scales the whole null vector. The coordinate is recovered as
``e1_coefficient / n_o_weight`` where the n_o weight is ``X[e5] - X[e4]``
(== 1 for an un-dilated point), so any dilation weight divides out. This is
the correct read-back for weight-changing operators; a raw distance-from-origin
is wrong for them.
Raises ``ValueError`` on a degenerate (zero) n_o weight a point at infinity
or an f32 weight-collapse rather than returning a silently wrong value.
"""
no_weight = float(X[_E5_IDX] - X[_E4_IDX])
if no_weight == 0.0:
raise ValueError(
"read_scalar_e1: degenerate n_o weight (point at infinity or f32 collapse)"
)
return float(X[1]) / no_weight
# ---------------------------------------------------------------------------
# Incidence algebra — the corrected grade-raising wedge, dual, and meet.
# These let the inner product operate on RELATIONS among entities (lines, planes,
# incidence) rather than only pairwise point distance. Built only from the existing
# Cl(4,1) primitives (geometric_product, grade_project) + the pseudoscalar; they add
# no normalization, no approximation, and leave the versor_condition path untouched
# (flats are null-cone outer products, not unit versors).
# ---------------------------------------------------------------------------
_MAX_GRADE = 5 # Cl(4,1): grades 0..5
def blade_grade(X: np.ndarray) -> int:
"""The single grade of a homogeneous blade. Raises if X is zero or grade-mixed.
Grade is detected by EXACT nonzero (no tolerance): integer-coordinate embeddings
produce exact integer blades in float64, so a grade block is exactly 0 or not.
"""
grades = [k for k in range(_MAX_GRADE + 1) if np.any(grade_project(X, k))]
if len(grades) != 1:
raise ValueError(f"not a homogeneous blade: nonzero grades {grades}")
return grades[0]
def graded_wedge(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""The true grade-raising exterior product ``X ^ Y`` for homogeneous blades.
``X ^ Y = <X Y>_{grade(X)+grade(Y)}`` the top-grade part of the geometric
product. Unlike :func:`outer_product` (the commutator) this composes correctly:
``graded_wedge(graded_wedge(P, Q), n_inf)`` builds the grade-3 line P^Q^n_inf,
and so on. If the grades sum past the pseudoscalar (>5) the wedge is identically
zero. For two grade-1 vectors it agrees with :func:`outer_product` exactly.
"""
gx, gy = blade_grade(X), blade_grade(Y)
if gx + gy > _MAX_GRADE:
return np.zeros(N_COMPONENTS, dtype=geometric_product(X, Y).dtype)
return grade_project(geometric_product(X, Y), gx + gy)
def blade_norm(X: np.ndarray) -> float:
"""Reversion norm ``sqrt(|<X reverse(X)>_0|)`` — zero iff X is the zero blade."""
return float(np.sqrt(abs(scalar_part(geometric_product(X, reverse(X))))))
def is_incident(point: np.ndarray, flat: np.ndarray) -> bool:
"""Exact incidence test: is ``point`` on ``flat`` (a line/plane OPNS blade)?
True iff ``point ^ flat == 0`` EXACTLY (every component zero) no float
tolerance to admit (the wrong=0 discipline: a near-incident point is REFUSED,
not admitted). Exact for integer-coordinate points within ``EMBED_EXACT_MAX``.
"""
return not bool(np.any(graded_wedge(point, flat)))
def dual(X: np.ndarray) -> np.ndarray:
"""Pseudoscalar dual ``X * I5^{-1}`` (``I5^{-1} = -I5`` since ``I5^2 = -1``).
Maps a grade-k blade to grade ``5-k``. Involutive up to sign:
``dual(dual(X)) == -X``.
"""
return geometric_product(X, -_I5)
def meet(A: np.ndarray, B: np.ndarray) -> np.ndarray:
"""The meet (intersection) ``dual(dual(A) ^ dual(B))`` of two homogeneous blades.
Correct for operands in GENERAL POSITION whose join spans the space e.g. two
non-parallel planes meet in their intersection line. The grade of the result is
``grade(A)+grade(B)-5``.
HONEST ENVELOPE: this full-pseudoscalar meet DEGENERATES for operands that share
a proper subspace (e.g. two coplanar lines, two parallel planes): the inner wedge
``dual(A) ^ dual(B)`` is then identically zero, so ``meet`` returns the **zero
multivector** a detectable signal of "no transversal meet", never a silently
wrong value. The general intersection of such operands (e.g. the point where two
coplanar lines cross) requires the *join-relative* meet, which is deliberately
NOT implemented here; the caller MUST check ``blade_norm(result) == 0`` and treat
zero as degenerate/refuse rather than as a geometric object.
"""
return dual(graded_wedge(dual(A), dual(B)))

View file

@ -4,7 +4,6 @@ from dataclasses import dataclass, replace
import hashlib
import json
import re
import warnings
from collections.abc import Sequence
from typing import Any, List
@ -38,7 +37,6 @@ from core.epistemic_state import (
epistemic_state_for_grounding_source,
normative_detail_from_verdicts,
)
from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary
from core.response_governance import govern_response, shape_surface
from chat.telemetry import (
TurnEventSink,
@ -54,8 +52,7 @@ from teaching.discovery import (
format_candidate_jsonl,
)
from teaching.discovery_sink import DiscoveryCandidateSink
from engine_state import EngineStateStore, get_git_revision
from core.engine_identity import engine_identity_for_config
from engine_state import EngineStateStore
from recognition.anti_unifier import derive_recognizer
from recognition.outcome import FeatureBundle
from recognition.registry import RecognizerRegistry
@ -389,28 +386,6 @@ def _make_trajectory_from_result(result, turn: int):
return operator.build(frames, trajectory_id=f"turn_{turn}")
@dataclass(frozen=True, slots=True)
class TurnAccrual:
"""The inline-realization outcome of one turn (Step B / E).
``kind`` is ``"realized"`` (a declarative fact was accrued into the held self),
``"determined"`` (a question was answered over realized knowledge), ``"estimated"``
(Step E a converse query DETERMINE refused, for which a calibrated converse-guess
exists), or ``"none"`` (nothing comprehensible to accrue/determine). The payload
carries the typed result for introspection. B-1 records, does not surface; B-2 and E
surface only when their flag is on.
"""
kind: str
realized: Any = None # generate.realize.Realized | NotRealized | None
determination: Any = None # generate.determine.Determined | Undetermined | None
# Step E — the converse-guess candidate and its SERVE license, when ``kind`` is
# ``"estimated"``. ``estimate`` is a ``ConverseEstimate``; ``license`` a
# ``LicenseDecision`` (None if the predicate-class is absent from the ratified ledger).
estimate: Any = None
license: Any = None
@dataclass(frozen=True, slots=True)
class ChatResponse:
surface: str
@ -528,29 +503,6 @@ class ChatResponse:
dispatch_trace: DispatchTrace | None = None
@dataclass(frozen=True, slots=True)
class IdleTickResult:
"""Outcome of one ``idle_tick``.
The proposal pass is PROPOSAL-ONLY learning, never ratification (HITL ratifies). The
Step D consolidation pass is SESSION-memory learning: ``facts_consolidated`` derived
facts were written back into the held self so the next ``determine`` reaches them
directly still never corpus mutation, never a proposal. The proposal-review sub-pass
(``proposal_review``) is READ-ONLY: it surfaces pending comprehension-failure proposals for
review and mutates nothing.
"""
candidates_contemplated: int
proposals_created: int
pending_proposals: int
#: Step D — derived facts consolidated into the held self this tick (0 unless
#: ``config.consolidate_determinations`` and the closure had a new layer to add).
facts_consolidated: int = 0
#: IT — read-only proposal-review summary (None unless ``config.review_pending_proposals``).
#: Surfaces pending comprehension-failure proposals for review; mutates nothing.
proposal_review: ProposalReviewIdleSummary | None = None
class ChatRuntime:
def __init__(
self,
@ -709,11 +661,6 @@ class ChatRuntime:
# W-013 — last classified intent, updated each turn for /explain REPL use.
self._last_intent: Any | None = None
self._last_input_text: str = ""
# Step B (inline realization) — the last turn's accrual outcome (what the turn
# realized into the held self, or determined over it). Introspectable; the
# surface contract is unchanged (slice B-1 records, does not surface).
self._last_turn_accrual: TurnAccrual | None = None
self._relational_pack_lemmas: frozenset[str] | None = None
self._engine_state_store: EngineStateStore | None = (
None if no_load_state else EngineStateStore(engine_state_path)
)
@ -727,27 +674,6 @@ class ChatRuntime:
# checkpoint is loaded, flushed to the sink on attach_telemetry_sink.
# None means no reboot was detected this session.
self._pending_reboot_payload: str | None = None
# L11 — the engine's content-derived identity (who am I), and the
# identity stamped in the loaded checkpoint (the lineage parent for the
# next checkpoint). ``_loaded_engine_identity`` stays "" at genesis.
self._engine_identity: str = engine_identity_for_config(
self.config, get_git_revision()
)
self._loaded_engine_identity: str = ""
# CL — the persistent reviewed-learning proposal log. ``idle_tick()``
# advances it during idle (proposal-only); it lives alongside the engine
# state so the learning backlog survives reboot. None for no_load_state
# (ephemeral runtimes don't accumulate a learning lineage).
self._proposal_log: Any | None = None
if self._engine_state_store is not None:
from teaching.proposals import ProposalLog
self._proposal_log = ProposalLog(
path=self._engine_state_store.path / "proposals.jsonl"
)
# L11 — set True on reboot when the stamped checkpoint identity differs
# from the recomputed identity (the ratified substrate changed while down).
self.identity_continuity_break: bool = False
if self._engine_state_store is not None and self._engine_state_store.exists():
self._load_engine_state()
@ -755,46 +681,11 @@ class ChatRuntime:
store = self._engine_state_store
if store is None:
return
# Schema-version compatibility gates the whole load: load_manifest()
# refuses (raises) a checkpoint written by newer code BEFORE we read any
# recognizers/candidates (L10 step-2 migration discipline).
manifest = store.load_manifest() or {}
recognizers = store.load_recognizers()
self._recognizer_registry = RecognizerRegistry.from_recognizers(recognizers)
self._pending_candidates = store.load_discovery_candidates()
manifest = store.load_manifest() or {}
self._turn_count = int(manifest.get("turn_count", 0))
# L11 — the identity this checkpoint was written under becomes the lineage
# parent of the next checkpoint we write. If it differs from the identity
# we recomputed at boot, the ratified substrate changed during downtime:
# we would resume the lived state under a DIFFERENT identity. Surface it
# (warn + flag); refuse only under strict_identity_continuity.
self._loaded_engine_identity = str(manifest.get("engine_identity", ""))
if (
self._loaded_engine_identity
and self._loaded_engine_identity != self._engine_identity
):
self.identity_continuity_break = True
message = (
"engine identity continuity break: checkpoint was written under "
f"{self._loaded_engine_identity[:12]}… but this build computes "
f"{self._engine_identity[:12]}… — the ratified identity substrate "
"changed while the engine was down. Resuming would carry the lived "
"state into a different identity."
)
if self.config.strict_identity_continuity:
from core.engine_identity import IdentityContinuityError
raise IdentityContinuityError(message)
warnings.warn(message, RuntimeWarning, stacklevel=2)
# Shape B+ (schema v2): restore the lived session state into the live
# context so a reboot resumes the SAME life (field/vault/anchor/graph/
# referents/dialogue). Opt-in (config.persist_session_state); None for a
# v1 checkpoint -> fresh session (the historical Shape B behavior), so old
# checkpoints stay loadable.
if self.config.persist_session_state and self._context is not None:
session_snapshot = store.load_session_state()
if session_snapshot is not None:
self._context.restore(session_snapshot)
# W-024 / ADR-0158 — buffer reboot event for emission when sink attaches.
from engine_state import _git_revision
self._pending_reboot_payload = format_reboot_event_jsonl(
@ -828,130 +719,7 @@ class ChatRuntime:
for c in candidates_to_save
]
store.save_discovery_candidates(candidates_to_save)
# Shape B+ (schema v2): persist the lived session state (field, vault,
# anchor, graph, referents, dialogue) BEFORE the manifest, so the
# manifest stays the last durable act — the commit marker for the turn.
# Opt-in (config.persist_session_state): a deliberate resume mode, off by
# default so one-shot runtimes don't pay the per-turn snapshot cost.
if self._context is not None and self.config.persist_session_state:
store.save_session_state(self._context.snapshot())
# L11 — stamp the engine's identity and its lineage parent (the identity
# of the prior checkpoint). Same substrate -> identity == parent (a stable
# life); a ratified substrate change -> identity != parent (the bump).
store.save_manifest(
self._turn_count,
engine_identity=self._engine_identity,
parent_engine_identity=self._loaded_engine_identity,
)
self._loaded_engine_identity = self._engine_identity
def _count_pending_proposals(self) -> int:
if self._proposal_log is None:
return 0
return sum(
1
for entry in self._proposal_log.current_state().values()
if entry.get("state") == "pending"
)
def idle_tick(self) -> "IdleTickResult":
"""Advance learning during idle (NO user turn). Two disjoint passes:
1. PROPOSAL pass (the reviewed-learning flywheel): turn the pending discovery
backlog into reviewable teaching proposals. Contemplate each pending
candidate (enrichment) and run the replay-gated ``propose_from_candidate``,
which leaves a PROPOSAL-ONLY ``pending`` entry in the persistent proposal
log. An idle tick NEVER ratifies ratification (appending to the corpus)
stays HITL via ``teaching/review`` (CLAUDE.md teaching safety). The tick only
*proposes*; the reviewed loop is not bypassed or duplicated.
2. CONSOLIDATION pass (Step D CLOSE): the loop learns from *determined* facts.
Run one semi-naive layer of the member/subset deductive closure over the held
self (``generate.determine.consolidate_once``); each soundly-derived,
proof_chain-verified fact is written back as a SPECULATIVE realized record so
the next ``determine`` reaches it directly. SESSION memory (immediate,
allowed) NOT corpus mutation, NOT a proposal; the HITL path is untouched.
Gated by ``config.consolidate_determinations``. The closure converges (a
saturated tick consolidates nothing ``at_fixed_point``).
The proposal log, the candidate backlog, and (with ``persist_session_state``)
the consolidated facts all live in the engine-state dir, so this learning
progress persists across reboot (CL-2).
"""
contemplated_count = 0
created = 0
facts_consolidated = 0
did_work = False
# 1. Proposal pass — unchanged behavior, runs only with a log + backlog.
if self._proposal_log is not None and self._pending_candidates:
from teaching.contemplation import contemplate
from teaching.proposals import (
ProposalError,
TeachingChainProposal,
_current_revision,
propose_from_candidate,
)
from teaching.source import ProposalSource
vault_probe = (
_vault_probe_for_context(self._context) if self._context else None
)
contemplated = [
contemplate(candidate, vault_probe=vault_probe)
for candidate in self._pending_candidates
]
contemplated_count = len(contemplated)
for candidate in contemplated:
source = ProposalSource(
kind="contemplation",
source_id=candidate.candidate_id,
emitted_at_revision=_current_revision(),
)
try:
result = propose_from_candidate(
candidate, log=self._proposal_log, source=source
)
except ProposalError:
continue
if isinstance(result, TeachingChainProposal):
created += 1
did_work = True
# 2. Consolidation pass (Step D) — runs independently of the backlog.
if self.config.consolidate_determinations and self._context is not None:
from generate.determine import consolidate_once
facts_consolidated = consolidate_once(self._context).consolidated
did_work = True
# 3. Proposal-review sub-pass (IT) — READ-ONLY. Surfaces pending comprehension-failure
# proposals (the contemplation pass's N5 artifacts) for review. It NEVER mutates an
# artifact, NEVER sets ``did_work`` (no state change → no checkpoint), NEVER ratifies
# or mounts. A reporter failure is CAPTURED, not propagated, so it cannot corrupt the
# tick. Gated off by default.
proposal_review = None
if self.config.review_pending_proposals:
try:
proposal_review = idle_summary()
except Exception as exc: # noqa: BLE001 — a reporter failure must not corrupt the tick
proposal_review = ProposalReviewIdleSummary(
safe=False, total=0, review_needed=0, malformed=0, by_family=(),
errors=(f"proposal_review_failed:{type(exc).__name__}",),
)
# Persist the advanced state once (backlog + lineage +, with
# persist_session_state, the consolidated facts). Skipped on a no-op tick so an
# idle engine with nothing to learn does not churn the checkpoint.
if did_work:
self.checkpoint_engine_state()
return IdleTickResult(
candidates_contemplated=contemplated_count,
proposals_created=created,
pending_proposals=self._count_pending_proposals(),
facts_consolidated=facts_consolidated,
proposal_review=proposal_review,
)
store.save_manifest(self._turn_count)
def record_recognition_example(
self,
@ -1004,164 +772,9 @@ class ChatRuntime:
def _checkpointed_response(self, response: ChatResponse) -> ChatResponse:
self._turn_count += 1
# Step B — inline realization, BEFORE the checkpoint so an accrued fact is in
# the snapshot this turn (survives reboot with persist_session_state). Gated;
# off by default. The surface contract is unchanged (the outcome is recorded,
# not surfaced).
if self.config.accrue_realized_knowledge:
self._accrue_in_turn(self._last_input_text)
response = self._maybe_surface_determination(response)
self.checkpoint_engine_state()
return response
def _maybe_surface_determination(self, response: ChatResponse) -> ChatResponse:
"""Step B-2 / E — select the user-facing surface from the turn's accrual.
B-2: when the turn DETERMINED an answer over realized knowledge, select the
rendered determination as ``surface`` (the realizer's ``articulation_surface``
is retained as evidence). An ``Undetermined`` turn keeps the default surface.
E: when the turn is ``estimated`` (a refused converse query with a calibrated
guess) AND ``estimation_enabled``, route the guess through the ADR-0206 bridge
``govern_response`` widens to APPROXIMATE iff the predicate-class holds a genuine
SERVE license, and ``shape_surface`` DISCLOSES it as ``[approximate] ``. An
unlicensed class stays STRICT (the surface is unchanged the honest refusal).
Off-flag turns never reach here. See ``docs/runtime_contracts.md``.
"""
accrual = self._last_turn_accrual
if accrual is None:
return response
if accrual.kind == "determined":
from generate.determine import Determined, render_determination
if not isinstance(accrual.determination, Determined):
return response # Undetermined → keep the default surface
return replace(response, surface=render_determination(accrual.determination))
if accrual.kind == "estimated" and self.config.estimation_enabled:
return self._surface_estimate(response, accrual)
return response
def _surface_estimate(self, response: ChatResponse, accrual: "TurnAccrual") -> ChatResponse:
"""Surface a licensed converse-guess as a DISCLOSED ``[approximate]`` estimate.
The license gates the widening (``govern_response`` returns STRICT for an
unlicensed class surface unchanged); ``shape_surface`` guarantees the
disclosure prefix because a converse guess is ``UNVERIFIED_POSSIBLE``, never in
APPROXIMATE's admissible (fully-grounded) set. So a wrong estimate is always a
DISCLOSED wrong wrong=0 (silent) is preserved.
"""
from core.epistemic_state import EpistemicState
from core.response_governance import ReachLevel, govern_response, shape_surface
from generate.determine import ConverseEstimate, render_estimate
estimate, license_decision = accrual.estimate, accrual.license
if not isinstance(estimate, ConverseEstimate):
return response
policy = govern_response(
epistemic_state=EpistemicState.UNVERIFIED_POSSIBLE,
license_decision=license_decision,
)
if policy.level is ReachLevel.STRICT:
return response # unlicensed → no widening, honest refusal stands
disclosed = shape_surface(
policy,
committed_surface=response.surface,
decode_state=EpistemicState.UNVERIFIED_POSSIBLE,
disclosed_alternative=render_estimate(estimate),
)
return replace(response, surface=disclosed, reach_level=policy.level.value)
def last_turn_accrual(self) -> TurnAccrual | None:
"""The most recent turn's inline-realization outcome (Step B), or None when
accrual is off or did not complete. Introspection only never surfaced."""
return self._last_turn_accrual
def _accrue_in_turn(self, text: str) -> None:
"""Inline realization (Step B): a comprehensible declarative turn accrues a
realized fact into the held self (session vault, SPECULATIVE / as-told); a
comprehensible question turn is determined over realized knowledge. Records the
typed outcome on ``self._last_turn_accrual``.
Never raises into the turn accrual is ADDITIVE, so any failure is a clean
no-op (the turn's response is untouched). This is SESSION memory (immediate),
NOT ratified learning: it proposes nothing and leaves the teaching/review HITL
path untouched. Realization writes go through ``generate.realize`` (the INV-21
allowed vault writer); DETERMINE and the readers are total (typed results, no
raises), so the broad guard is a defensive backstop, not expected control flow.
"""
self._last_turn_accrual = None
if self._context is None or not text or not text.strip():
return
try:
from generate.determine import determine
from generate.meaning_graph.reader import Comprehension, comprehend
from generate.meaning_graph.relational import comprehend_relational
from generate.realize import realize_comprehension
if self._relational_pack_lemmas is None:
from generate.meaning_graph.relational import load_relational_pack_lemmas
self._relational_pack_lemmas = load_relational_pack_lemmas()
readings = (
comprehend(text),
comprehend_relational(text, self._relational_pack_lemmas),
)
comprehensions = [c for c in readings if isinstance(c, Comprehension)]
# A question turn (query-bearing) is DETERMINED over realized knowledge.
for c in comprehensions:
if c.queries:
determination = determine(c, self._context)
self._last_turn_accrual = self._accrue_estimate_if_refused(
c, determination
)
return
# A declarative turn (a single told fact) is REALIZED into the held self.
for c in comprehensions:
if not c.queries and c.meaning_graph.relations:
self._last_turn_accrual = TurnAccrual(
kind="realized", realized=realize_comprehension(c, self._context)
)
return
self._last_turn_accrual = TurnAccrual(kind="none")
except Exception: # additive: accrual must never crash a turn # noqa: BLE001
self._last_turn_accrual = None
def _accrue_estimate_if_refused(self, comprehension: Any, determination: Any) -> "TurnAccrual":
"""Step E: turn a REFUSED converse query into an ``estimated`` accrual.
When DETERMINE refused (``Undetermined``) a single non-negated binary query whose
converse was told (``p(a,b)`` realized, ``p(b,a)`` asked), produce the calibrated
converse-guess + its SERVE license. Off-flag (or any non-converse refusal) returns
the plain ``determined`` accrual unchanged nothing widens. The license is only
*attached* here; the surface decision (and the disclosure) is the bridge's, in
``_maybe_surface_determination``.
"""
from generate.determine import Undetermined
if not (self.config.estimation_enabled and isinstance(determination, Undetermined)):
return TurnAccrual(kind="determined", determination=determination)
queries = getattr(comprehension, "queries", ())
if len(queries) != 1:
return TurnAccrual(kind="determined", determination=determination)
query = queries[0]
if getattr(query, "negated", False) or len(getattr(query, "arguments", ())) != 2:
return TurnAccrual(kind="determined", determination=determination)
from generate.determine import estimate_converse, serve_license
subject, target = query.arguments[0], query.arguments[1]
estimate = estimate_converse(self._context, query.predicate, subject, target)
if estimate is None: # no told converse to generalize from → plain refusal
return TurnAccrual(kind="determined", determination=determination)
return TurnAccrual(
kind="estimated",
determination=determination,
estimate=estimate,
license=serve_license(query.predicate),
)
@property
def session(self) -> SessionContext:
return self._context

View file

@ -25,26 +25,6 @@ from __future__ import annotations
import pytest
import engine_state
@pytest.fixture(autouse=True)
def _isolate_engine_state_default(tmp_path_factory, monkeypatch):
"""Isolate the default engine-state checkpoint dir per test.
A bare ``ChatRuntime()`` (no ``engine_state_path``) falls back to
``engine_state._DEFAULT_DIR`` the shared repo ``engine_state/`` directory.
Tests must not share that mutable dir: one test's checkpoint (recognizers,
candidates, the stamped engine-identity, and under resume mode the lived
session_state) leaks into another test's fresh-state assumptions (and, since
L11, raises spurious identity-continuity-break warnings when a later test
boots under a different identity over the same dir). Point the default at a
fresh per-test temp dir. Tests passing an explicit ``engine_state_path`` are
unaffected; within one test, repeated ``ChatRuntime()`` share this dir.
"""
isolated = tmp_path_factory.mktemp("engine_state_default")
monkeypatch.setattr(engine_state, "_DEFAULT_DIR", isolated)
QUARANTINE: frozenset[str] = frozenset()

View file

@ -1,64 +0,0 @@
"""Bit-exact (de)serialization of numpy arrays for deterministic persistence.
A numpy array encodes to ``{"dtype", "shape", "b64"}`` where ``b64`` is base64 of
the array's raw bytes. This is **bit-exact**: every float round-trips with zero
precision loss, so a restored versor keeps ``versor_condition < 1e-6`` and a
replayed turn keeps its ``trace_hash``.
NEVER serialize field arrays as decimal/JSON floats. Decimal truncates the
mantissa and silently breaks both closure and deterministic replay the Cl(4,1)
float-truncation pitfall. ``dtype`` carries byte order (``'<f4'``/``'<f8'``), so
the encoding is portable, and ``float32`` is never conflated with ``float64``.
This module is a leaf: it imports only numpy + base64, so every layer (field,
vault, session, engine_state) can use it without an import cycle.
Zig-codec follow-up (tagged NOT authorized). This bit-exact codec is the natural
locked **reference contract** (ADR-0196 decision rule 1) for a future Ring-1 Zig
byte-exact serialization component: deterministic buffer ownership, stable layout, and
edge-native build are exactly Zig's profile. It is gated behind the G0G8 ladder and
is **only** worth proposing AFTER (1) persistence becomes incremental/append-only
(O(Δ)/turn the algorithmic fix, in Python), and (2) the edge-budget gate
(``evals/edge_budget/``) proves the bounded per-turn codec is still the device
bottleneck. A Zig rewrite of today's O(n) snapshot would only speed up the wrong
asymptotics. See ``evals/edge_budget/contract.md``.
"""
from __future__ import annotations
import base64
from typing import Any
import numpy as np
def encode_array(arr: np.ndarray) -> dict[str, Any]:
"""Encode a numpy array to a bit-exact, JSON-safe dict."""
contiguous = np.ascontiguousarray(arr)
return {
"dtype": contiguous.dtype.str, # byte-order-aware, e.g. '<f4', '<f8', '<i4'
"shape": list(contiguous.shape),
"b64": base64.b64encode(contiguous.tobytes()).decode("ascii"),
}
def decode_array(payload: dict[str, Any]) -> np.ndarray:
"""Decode a payload produced by ``encode_array`` back to an exact array.
Returns a writable copy (``np.frombuffer`` is read-only) so the restored
array can be composed and mutated like a freshly-built one.
"""
dtype = np.dtype(payload["dtype"])
raw = base64.b64decode(payload["b64"])
flat = np.frombuffer(raw, dtype=dtype)
return flat.reshape(payload["shape"]).copy()
def encode_optional_array(arr: np.ndarray | None) -> dict[str, Any] | None:
"""Encode an array, or return ``None`` for ``None`` (e.g. optional holonomy)."""
return None if arr is None else encode_array(arr)
def decode_optional_array(payload: dict[str, Any] | None) -> np.ndarray | None:
"""Decode an optional-array payload, or return ``None`` for ``None``."""
return None if payload is None else decode_array(payload)

View file

@ -114,29 +114,6 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
"tests/test_versor_condition_rust_parity.py",
"tests/test_versor_apply_rust_parity.py",
),
"sensorium": (
"tests/test_sensorium_compiler_delta.py",
"tests/test_audio_compiler.py",
"tests/test_audio_crdt_merge.py",
"tests/test_audio_eval_gates.py",
"tests/test_audio_pack_manifest.py",
"tests/test_audio_sensorium_mount.py",
"tests/test_vision_compiler.py",
"tests/test_event_vision_compiler.py",
"tests/test_vision_crdt_merge.py",
"tests/test_vision_eval_gates.py",
"tests/test_vision_sensorium_mount.py",
"tests/test_sensorimotor_contract.py",
"tests/test_sensorimotor_pack_manifest.py",
"tests/test_observation_frame_contract.py",
"tests/test_observation_frame_harness.py",
"tests/test_environment_falsification.py",
"tests/test_environment_falsification_eval_cli.py",
"tests/test_witness_log_importer.py",
"tests/test_tabletop_lab_protocol.py",
"tests/test_sensorium_eval_cli.py",
"tests/test_efferent_gate.py",
),
"pulse": (
"tests/test_pulse_integration.py",
"tests/test_graph_diffusion.py",
@ -188,9 +165,6 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
"math": (
"tests/test_adr_0126_train_sample_runner.py",
),
"deductive": (
"tests/test_deductive_logic_entail.py",
),
"full": ("tests/",),
}
@ -2377,8 +2351,6 @@ def cmd_eval(args: argparse.Namespace) -> int:
"""Run an eval lane by name, or list available lanes."""
if getattr(args, "lane", None) == "sensorium":
return cmd_eval_sensorium(args)
if getattr(args, "lane", None) == "environment-falsification":
return cmd_eval_environment_falsification(args)
if getattr(args, "lane", None) == "math-contemplation":
return cmd_eval_math_contemplation(args)
@ -2519,33 +2491,6 @@ def cmd_eval_sensorium(args: argparse.Namespace) -> int:
return 0 if report["failed"] == 0 and report["gate_closed"] else 1
def cmd_eval_environment_falsification(args: argparse.Namespace) -> int:
"""Run deterministic environmental falsification replay reports."""
from evals.environment_falsification import build_environment_falsification_report
report = build_environment_falsification_report()
if getattr(args, "json", False):
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
else:
print(f"lane : {report['lane']}")
print(f"version : {report['version']}")
print(f"cases : {report['total']}")
print(f"passed : {report['passed']}")
print(f"failed : {report['failed']}")
print(f"report_sha256 : {report['report_sha256']}")
if getattr(args, "report", None):
report_path = Path(args.report)
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
)
print(f"\nreport written: {report_path}", file=sys.stderr)
return 0 if report["failed"] == 0 and report["expected_report_hash_ok"] else 1
# ---------------------------------------------------------------------------
# ADR-0172 W3 — math-contemplation CLI lane
# ---------------------------------------------------------------------------
@ -4981,7 +4926,7 @@ def build_parser() -> argparse.ArgumentParser:
eval_cmd.add_argument("--report", metavar="PATH", help="write JSON report to file")
eval_cmd.add_argument(
"--modality",
choices=["audio", "vision", "event-vision", "sensorimotor"],
choices=["audio", "vision", "sensorimotor"],
default="vision",
help="sensorium lane modality to evaluate (default: vision)",
)

View file

@ -22,7 +22,6 @@ from field.state import FieldState
from core.cognition.result import CognitiveTurnResult
from core.cognition.surface_resolution import resolve_surface
from core.cognition.trace import compute_trace_hash, hash_admissibility_trace
from core.reasoning.adapters import evidence_from_entailment_trace
from generate.intent import classify_compound_intent
from generate.intent_bridge import _is_useful_surface
from generate.intent_ratifier import (
@ -48,7 +47,6 @@ from generate.operators import (
multi_relation_walk,
transitive_walk,
)
from generate.proof_chain import EntailmentTrace, evaluate_entailment_with_trace
from teaching.correction import CorrectionCandidate, extract_correction
from teaching.epistemic import EpistemicStatus
from teaching.review import ReviewedTeachingExample, review_correction
@ -124,7 +122,7 @@ class CognitiveTurnPipeline:
) -> None: # runtime: ChatRuntime (no import cycle)
self.runtime = runtime
self._last_node_id: str | None = None
self.teaching_store = teaching_store if teaching_store is not None else TeachingStore()
self.teaching_store = teaching_store or TeachingStore()
if recognizer is not None:
self._recognizer = recognizer
elif hasattr(runtime, "first_admitted_recognizer"):
@ -306,8 +304,6 @@ class CognitiveTurnPipeline:
):
compose_surface = CognitiveTurnPipeline._render_compose_surface(compose_result)
entailment_trace = self._maybe_entailment_trace(intent, triples)
resolved = resolve_surface(
canonical_surface=canonical,
pre_decoration_surface=pre_decoration,
@ -397,14 +393,13 @@ class CognitiveTurnPipeline:
epistemic_status = proposal.epistemic_status.value if proposal is not None else ""
walk_serialised = CognitiveTurnPipeline._serialize_operator(walk_result)
compose_serialised = CognitiveTurnPipeline._serialize_operator(compose_result)
entailment_serialised = CognitiveTurnPipeline._serialize_entailment_trace(
entailment_trace
)
# Deterministic concatenation: walk, compose, then entailment. Empty
# strings are dropped so unaffected turns keep existing trace bytes.
operator_invocation = "|".join(
s for s in (walk_serialised, compose_serialised, entailment_serialised)
if s
# Deterministic concatenation: walk record, then compose record.
# Empty strings are dropped so single-operator turns keep their
# existing trace_hash byte-for-byte.
operator_invocation = (
f"{walk_serialised}|{compose_serialised}"
if compose_serialised
else walk_serialised
)
# ADR-0023 — admissibility trace + ratification provenance.
# Comb pass 2026-05-21 — direct attribute access; the fields
@ -695,39 +690,6 @@ class CognitiveTurnPipeline:
relation=intent.relation,
)
def _maybe_entailment_trace(
self,
intent,
triples: tuple[tuple[str, str, str], ...],
) -> EntailmentTrace | None:
"""Compile exact verification triples into propositional entailment.
Telemetry-only v1: the result is folded into ``operator_invocation`` and
never changes the user-facing surface. Runs only when classification
exposes a precise positive ``subject relation object`` shape.
"""
if intent.tag is not IntentTag.VERIFICATION:
return None
if intent.negated or not intent.relation or not intent.object:
return None
head = self._proof_atom(intent.subject)
tail = self._proof_atom(intent.object)
if not head or not tail:
return None
relation = intent.relation.strip().lower()
premises: list[str] = []
for h, r, t in triples:
if r.strip().lower() != relation:
continue
h_atom = self._proof_atom(h)
t_atom = self._proof_atom(t)
if h_atom and t_atom:
premises.append(f"{h_atom} -> {t_atom}")
if not premises:
return None
return evaluate_entailment_with_trace(tuple(premises), f"{head} -> {tail}")
@staticmethod
def _render_compose_surface(compose: FrameComposeResult) -> str:
"""Render a frame-transfer composition suffix without selecting authority."""
@ -759,19 +721,6 @@ class CognitiveTurnPipeline:
return ""
return json.dumps(op.as_dict(), sort_keys=True, ensure_ascii=False)
@staticmethod
def _serialize_entailment_trace(trace: EntailmentTrace | None) -> str:
if trace is None:
return ""
return f"entailment:{evidence_from_entailment_trace(trace).canonical_json()}"
@staticmethod
def _proof_atom(text: str) -> str:
parts = [p for p in _SUBJECT_SPLIT_RE.split(text.lower()) if p]
if not parts:
return ""
return "atom_" + "_".join(parts)
@staticmethod
def _render_walk_surface(walk: WalkResult) -> str:
"""Render a chain-aware walk suffix without selecting authority."""

View file

@ -1,57 +0,0 @@
"""Shared typed record of a comprehension organ's attempt at a problem (N2).
The normalization layer the contemplation batch (N3 router, N4 registry, N6 pass manager) reasons
over uniform across the R1 and R2 setup compilers. Off-serving; imports no `evals`.
"""
from __future__ import annotations
from core.comprehension_attempt.classify import (
classify_cmb,
classify_r1,
classify_r2,
classify_r3,
cmb_reason,
)
from core.comprehension_attempt.failure_family import (
REGISTRY,
FailureFamily,
enrich_family,
family_by_name,
family_for_reason,
)
from core.comprehension_attempt.model import ComprehensionAttempt, Organ, Outcome
from core.comprehension_attempt.proposal import (
FailureProposal,
build_proposal,
emit_proposal,
)
from core.comprehension_attempt.router import (
RouteResult,
RouteStatus,
cmb_is_authoritative,
route_setup,
)
__all__ = [
"REGISTRY",
"ComprehensionAttempt",
"FailureFamily",
"FailureProposal",
"build_proposal",
"emit_proposal",
"Organ",
"Outcome",
"RouteResult",
"RouteStatus",
"classify_cmb",
"classify_r1",
"classify_r2",
"classify_r3",
"cmb_is_authoritative",
"cmb_reason",
"enrich_family",
"family_by_name",
"family_for_reason",
"route_setup",
]

View file

@ -1,159 +0,0 @@
"""Normalize R1/R2 organ output into a typed ``ComprehensionAttempt`` (N2, setup-level).
`classify_r1` / `classify_r2` run their organ and report **produce-mode** setup outcomes:
`setup_refused` (with the organ's typed reason) or `setup_correct` (an admissible setup was
produced, with a deterministic signature). They do NOT solve, do NOT compare to gold, and import
nothing from `evals` (signatures are computed inline) keeping this a thin, dependency-light
normalizer. Answer-level outcomes are reached downstream (N6) when the solver/verifier run.
"""
from __future__ import annotations
from typing import Any
from core.comprehension_attempt.model import ComprehensionAttempt
from generate.combined_rate_comprehension.model import CombinedRateProblem
from generate.combined_rate_comprehension.reader import read_combined_rate_problem
from generate.constraint_comprehension.model import ConstraintProblem
from generate.constraint_comprehension.reader import read_constraint_problem
from generate.meaning_graph.reader import Refusal
from generate.quantitative_comprehension import comprehend_quantitative, to_relational_metric
from generate.rate_comprehension.model import RateProblem
from generate.rate_comprehension.reader import read_rate_problem
#: CMB reader/solver reasons that are namespaced ``cmb_*`` before the (reason-string-keyed) failure
#: registry sees them, so a CMB boundary never inherits R2/R3's family for the SAME bare string
#: (R3 ``rate_unit_mismatch`` is a growth surface; R2/R3 ``non_integer_solution`` carry other
#: owners). The two ``input_shape`` reasons stay bare so they map to the cross ``input_shape``
#: family (router hygiene). A ``cmb_``-prefixed attempt reason is also the signal that CMB
#: *substantively* recognized the text (used by the CMB-over-R3 precedence rule in the router).
_CMB_BARE_REASONS = frozenset({"not_combined_rate_shaped", "empty"})
def cmb_reason(reason: str) -> str:
"""Namespace a CMB refusal reason for the failure registry (see :data:`_CMB_BARE_REASONS`)."""
return reason if reason in _CMB_BARE_REASONS else f"cmb_{reason}"
def _r1_signature(relations: list[dict[str, Any]]) -> str:
"""Deterministic, order-independent string signature of the projected R1 relations."""
items: list[tuple] = []
for r in relations:
kind = r["kind"]
if kind == "fact":
items.append((kind, r["entity"], int(r["value"])))
elif kind in ("more_than", "fewer_than"):
items.append((kind, r["entity"], r["ref"], int(r["delta"])))
elif kind == "times_as_many":
items.append((kind, r["entity"], r["ref"], int(r["factor"])))
elif kind == "divide_by":
items.append((kind, r["entity"], r["ref"], int(r["divisor"])))
elif kind == "sum_of":
items.append((kind, r["entity"], tuple(sorted(r["parts"]))))
else: # pragma: no cover - defensive
items.append(("unhandled", kind, r.get("entity", "")))
return repr(tuple(sorted(items, key=repr)))
def _r2_signature(problem: ConstraintProblem) -> str:
"""Deterministic, order-independent string signature of an R2 ConstraintProblem setup."""
unknowns = tuple(sorted((u.symbol, u.unit, u.domain) for u in problem.unknowns))
constraints: list[tuple] = []
for c in problem.constraints:
merged: dict[str, int] = {}
for symbol, coeff in c.lhs.terms:
merged[symbol] = merged.get(symbol, 0) + coeff
terms = tuple(sorted((s, v) for s, v in merged.items() if v != 0))
constraints.append((terms, c.relation, c.rhs - c.lhs.constant))
query = (problem.query.symbol, problem.query.unit)
return repr((unknowns, tuple(sorted(constraints, key=repr)), query))
def classify_r1(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
"""Attempt the R1 relational-arithmetic setup compiler on *text*."""
comp = comprehend_quantitative(text)
if isinstance(comp, Refusal):
return ComprehensionAttempt(
"r1_quantitative", "setup_refused", case_id=case_id, refusal_reason=comp.reason
)
projected = to_relational_metric(comp)
if projected is None:
return ComprehensionAttempt(
"r1_quantitative", "setup_refused", case_id=case_id, refusal_reason="unprojectable"
)
relations, _query = projected
return ComprehensionAttempt(
"r1_quantitative", "setup_correct", case_id=case_id, setup_signature=_r1_signature(relations)
)
def classify_r2(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
"""Attempt the R2 two-category constraint setup compiler on *text*."""
problem = read_constraint_problem(text)
if isinstance(problem, Refusal):
return ComprehensionAttempt(
"r2_constraints", "setup_refused", case_id=case_id, refusal_reason=problem.reason
)
return ComprehensionAttempt(
"r2_constraints", "setup_correct", case_id=case_id, setup_signature=_r2_signature(problem)
)
def _r3_signature(problem: RateProblem) -> str:
"""Deterministic string signature of an R3 single-rate setup."""
return repr(
(
(problem.rate_unit.numerator, problem.rate_unit.denominator),
("rate", problem.rate),
("time", problem.time),
("quantity", problem.quantity),
problem.query,
)
)
def classify_r3(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
"""Attempt the R3 single-rate setup compiler on *text*."""
problem = read_rate_problem(text)
if isinstance(problem, Refusal):
return ComprehensionAttempt(
"r3_rate", "setup_refused", case_id=case_id, refusal_reason=problem.reason
)
return ComprehensionAttempt(
"r3_rate", "setup_correct", case_id=case_id, setup_signature=_r3_signature(problem)
)
def _cmb_signature(problem: CombinedRateProblem) -> str:
"""Deterministic string signature of an R4 combined-rate setup. The leading ``"cmb"`` tag
guarantees it never coincides with an R1/R2/R3 signature (cross-organ distinctness). ``sum`` is
commutative, so its two rates are sorted; ``difference`` keeps order (which rate is the drain)."""
rates = (problem.rate_a, problem.rate_b)
if problem.combine_mode == "sum":
rates = tuple(sorted(rates))
return repr(
(
"cmb",
problem.combine_mode,
(problem.rate_unit.numerator, problem.rate_unit.denominator),
rates,
("time", problem.time),
("quantity", problem.quantity),
problem.query,
)
)
def classify_cmb(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
"""Attempt the R4 combined-rate setup compiler on *text* (refusal reasons namespaced ``cmb_*``)."""
problem = read_combined_rate_problem(text)
if isinstance(problem, Refusal):
return ComprehensionAttempt(
"r4_combined_rate", "setup_refused", case_id=case_id, refusal_reason=cmb_reason(problem.reason)
)
return ComprehensionAttempt(
"r4_combined_rate", "setup_correct", case_id=case_id, setup_signature=_cmb_signature(problem)
)
__all__ = ["classify_cmb", "classify_r1", "classify_r2", "classify_r3", "cmb_reason"]

View file

@ -1,250 +0,0 @@
"""Failure-family registry (N4) — the heart of the contemplation batch.
Partitions every typed organ refusal reason (R1 reader/admissibility, R2 reader/solver/
answer-choice) into a named **failure family** that declares:
- ``owner`` which organ surfaces it (``r1`` / ``r2`` / ``cross``)
- ``must_remain_refused`` is this a correct wrong=0 boundary that must stay refused?
- ``proposal_allowed`` is this a genuine coverage gap a proposal may target?
- ``safe_next_action`` the human-readable next step
- ``proposal_target`` what artifact a proposal would suggest (e.g. ``r2_gold_fixture``)
Only three families are growth surfaces (``proposal_allowed = True``): the R2 ``missing_*``
gaps. Everything else is a correct boundary `correct refusal != missing capability`. The
registry is a **partition**: every reachable reason maps to exactly one family (asserted by
test), so ``family_for_reason`` is total and unambiguous. ``answer_key_contradiction`` carries
no refusal reason it is reached from the answer-choice ``contradiction`` *verdict* (N6).
Some R1 reasons are coarse (``unreadable_quantity_clause`` covers both the pronoun and distractor
cases; ``admissibility_refused`` covers both ungrounded and unit-incompatible). v0 folds each to a
single conservative family the *action* (refuse, no proposal) is identical for the folded cases,
so no wrong=0 signal is lost. The reserved families are forward-declared for R3 with no current
reason mapping.
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Literal
from core.comprehension_attempt.model import ComprehensionAttempt
Owner = Literal["r1", "r2", "r3", "r4", "cross"]
@dataclass(frozen=True, slots=True)
class FailureFamily:
"""A named class of comprehension failure with its growth/refusal policy."""
name: str
owner: Owner
must_remain_refused: bool
proposal_allowed: bool
safe_next_action: str
proposal_target: str | None = None
refusal_reasons: tuple[str, ...] = ()
#: The registry. Every reachable organ refusal reason appears in exactly one family.
REGISTRY: tuple[FailureFamily, ...] = (
# --- correct wrong=0 boundaries (no proposal) ---------------------------------------- #
FailureFamily(
"input_shape", "cross", True, False,
"refuse — the text is not a readable problem shape",
refusal_reasons=(
"empty", "no_quantity_template", "non_digit_quantity", "non_identifier_name",
"unreadable_quantity_query", "invalid_binding_graph", "query_target_not_a_category",
"unprojectable", "category_pair_not_found", "query_target_unrecognized", "no_query",
"not_rate_shaped", "not_combined_rate_shaped",
),
),
FailureFamily(
"unsupported_clause_shape", "r1", True, False,
"refuse — compound/pronoun clause the template cannot isolate (subsumes "
"ambiguous_referent + unsupported_distractor_clause until a finer signal exists)",
refusal_reasons=("unreadable_quantity_clause",),
),
FailureFamily(
"ungrounded_base", "r1", True, False,
"refuse — the asked quantity has no grounded anchor (underdetermined)",
refusal_reasons=("no_single_quantity_query",),
),
FailureFamily(
"admissibility_incompatible", "cross", True, False,
"refuse — operands are ungrounded or unit-incompatible (cannot combine across dimensions)",
refusal_reasons=("admissibility_refused", "coefficient_unit_mismatch", "coefficient_conflict"),
),
FailureFamily(
"over_determined", "r1", True, False,
"refuse — structurally incoherent (multiple bases / partition mismatch)",
refusal_reasons=(
"multiple_inverse_bases", "multiple_partitions",
"partition_query_mismatch", "partition_container_mismatch",
),
),
FailureFamily(
"unsupported_system_size", "r2", True, False,
"refuse — more than two categories; needs an n-variable solver (R3)",
refusal_reasons=("too_many_categories",),
),
FailureFamily(
"indistinguishable_system", "r2", True, False,
"refuse — the system is singular/underdetermined; no unique solution",
refusal_reasons=("indistinguishable_weights", "query_target_unsolved", "verification_failed"),
),
FailureFamily(
"non_integer_solution", "r2", True, False,
"refuse — no integer solution exists; never round",
refusal_reasons=("non_integer_solution",),
),
FailureFamily(
"negative_solution", "r2", True, False,
"refuse — a solved count is negative; out of domain",
refusal_reasons=("negative_solution",),
),
FailureFamily(
"answer_choice_unresolved", "r2", True, False,
"refuse — the proven value cannot be tied to exactly one option",
refusal_reasons=(
"no_matching_option", "ambiguous_options", "no_options",
"unknown_provided_label", "unparseable_option",
),
),
# --- growth surfaces (proposal allowed) ---------------------------------------------- #
FailureFamily(
"missing_total_count", "r2", False, True,
"propose a total-count-constraint gold fixture for review",
proposal_target="r2_gold_fixture", refusal_reasons=("missing_total_count",),
),
FailureFamily(
"missing_weighted_total", "r2", False, True,
"propose a weighted-total-constraint gold fixture for review",
proposal_target="r2_gold_fixture", refusal_reasons=("missing_weighted_total",),
),
# --- verdict (not a refusal) --------------------------------------------------------- #
FailureFamily(
"answer_key_contradiction", "r2", False, False,
"report the contradiction — the proven value disagrees with the supplied key",
refusal_reasons=(),
),
# --- reserved / forward-declared for R3 (no current emitter) ------------------------- #
FailureFamily(
"missing_category_pair", "r2", False, True,
"RESERVED — propose a category-pair fixture once the reader distinguishes a partial "
"(one-category) R2 problem from non-R2 text; the raw category_pair_not_found reason is "
"too broad to propose against safely (it fires on any non-R2 text), so it maps to "
"input_shape until that split exists",
proposal_target="r2_gold_fixture",
),
FailureFamily(
"missing_attribute_coefficient", "r2", False, True,
"RESERVED — propose an attribute-coefficient fixture (no emitter yet)",
proposal_target="r2_gold_fixture",
),
# --- R3 single-rate organ (reachable; growth vs boundary by the anti-over-propose rule) -- #
FailureFamily(
"unsupported_rate_duration", "r3", False, True,
"propose a rate fixture for a recognized-but-unsupported rate feature (unit conversion / "
"multi-rate). GROWTH surface: rate_unit_mismatch + combined_rates are emitted ONLY after a "
"rate clause is recognized, so they are always rate-like — never arbitrary text.",
proposal_target="r3_gold_fixture",
refusal_reasons=("rate_unit_mismatch", "combined_rates"),
),
FailureFamily(
"rate_underdetermined", "r3", True, False,
"refuse — a single-rate problem missing a needed value (underdetermined), like ungrounded_base",
refusal_reasons=("missing_rate", "missing_time", "missing_quantity"),
),
FailureFamily(
"unsupported_temporal_state", "r3", True, False,
"refuse — elapsed clock-time is R3.x. NOT a growth surface: the clock-marker detector can "
"fire on non-rate text, so temporal_state is not reliably rate-like.",
refusal_reasons=("temporal_state",),
),
# --- R4 combined-rate organ (reasons namespaced cmb_*; CMB owns them over R3's same-string ---- #
# reasons via the CMB-over-R3 precedence rule in the router) ------------------------------ #
FailureFamily(
"cmb_unit_mismatch", "r4", True, False,
"refuse — the two rates' units are incompatible. must_remain_refused UNTIL a dimension "
"registry exists: CMB v1 cannot distinguish a convertible pair (gallons/min vs gallons/hour) "
"from a dimensionally-incompatible one (rooms/hour vs liters/minute), so a 'try conversion' "
"proposal would be wrong=0-unsafe. NOT 'forever impossible' — a convertible split is future work.",
refusal_reasons=("cmb_rate_unit_mismatch",),
),
FailureFamily(
"cmb_combine_ambiguous", "r4", True, False,
"refuse — two same-unit rates but no licensed sum/difference cue. An ambiguity, not a "
"coverage gap: no fixture can teach which mode the text intends.",
refusal_reasons=("cmb_combine_mode_ambiguous",),
),
FailureFamily(
"cmb_underdetermined", "r4", True, False,
"refuse — combined-shaped but a contributing rate is unstated (under-specified input); no "
"reader enhancement can infer the missing rate.",
refusal_reasons=("cmb_missing_second_rate",),
),
FailureFamily(
"cmb_non_positive_net", "r4", True, False,
"refuse — non-positive net rate; a quantity/time query cannot make progress (solver boundary).",
refusal_reasons=("cmb_non_positive_net_rate",),
),
FailureFamily(
"cmb_non_integer", "r4", True, False,
"refuse — no exact integer answer; never round (solver boundary, exact-integer v1).",
refusal_reasons=("cmb_non_integer_solution",),
),
# --- R4 growth surfaces (proposal allowed) — emitted ONLY after positive combined recognition -- #
FailureFamily(
"cmb_unsupported_rate_count", "r4", False, True,
"propose a combined-rate fixture for ≥3 contributing rates (future capability)",
proposal_target="cmb_gold_fixture", refusal_reasons=("cmb_three_or_more_rates",),
),
FailureFamily(
"cmb_unsupported_reciprocal", "r4", False, True,
"propose a reciprocal work-rate fixture (1/(1/a+1/b); reciprocal rates + rational arithmetic)",
proposal_target="cmb_gold_fixture", refusal_reasons=("cmb_reciprocal_work_rate_deferred",),
),
FailureFamily(
"cmb_unsupported_clock_interval", "r4", False, True,
"propose a clock-interval fixture (elapsed-clock-time duration in a combined-rate problem)",
proposal_target="cmb_gold_fixture", refusal_reasons=("cmb_clock_interval_deferred",),
),
)
_BY_NAME: dict[str, FailureFamily] = {f.name: f for f in REGISTRY}
_BY_REASON: dict[str, FailureFamily] = {
reason: family for family in REGISTRY for reason in family.refusal_reasons
}
#: The verdict-derived family (no refusal reason maps to it).
ANSWER_KEY_CONTRADICTION = _BY_NAME["answer_key_contradiction"]
def family_for_reason(reason: str | None) -> FailureFamily | None:
"""The single failure family a typed organ refusal reason belongs to (or ``None``)."""
if reason is None:
return None
return _BY_REASON.get(reason)
def family_by_name(name: str) -> FailureFamily | None:
return _BY_NAME.get(name)
def enrich_family(attempt: ComprehensionAttempt) -> ComprehensionAttempt:
"""Return *attempt* with its ``family`` resolved from its refusal reason (or unchanged)."""
family = family_for_reason(attempt.refusal_reason)
if family is None:
return attempt
return replace(attempt, family=family.name)
__all__ = [
"ANSWER_KEY_CONTRADICTION",
"FailureFamily",
"Owner",
"REGISTRY",
"enrich_family",
"family_by_name",
"family_for_reason",
]

View file

@ -1,63 +0,0 @@
"""Typed, immutable record of one comprehension organ's attempt at a problem (N2).
A small normalization layer over the R1 (`generate.quantitative_comprehension`) and R2
(`generate.constraint_comprehension`) setup compilers: it turns each organ's heterogeneous
output (a typed setup, or a typed `Refusal`) into one uniform, frozen `ComprehensionAttempt`.
Nothing here changes reader behavior it only *describes* an outcome so the router (N3),
failure-family registry (N4), and contemplation pass manager (N6) can reason over both organs
uniformly.
Outcome semantics. `classify` (N2) produces **produce-mode** outcomes what the organ did on
its own gates, with no gold in hand: `setup_refused` (the organ refused) or `setup_correct`
(an admissible setup was produced). The gold-relative outcomes (`setup_wrong`, `answer_wrong`)
are representable here but are emitted only in **eval mode** by the lanes that hold gold never
fabricated by `classify`. `answer_*` / `contradiction` are reached when the solver / answer-choice
verifier run downstream (N6), not at setup classification time.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from generate.binding_graph.model import SourceSpanLink
Organ = Literal["r1_quantitative", "r2_constraints", "r3_rate", "r4_combined_rate"]
Outcome = Literal[
"setup_correct", # an admissible setup was produced (produce-mode) / matches gold (eval-mode)
"setup_refused", # the organ refused to assemble a setup
"setup_wrong", # eval-mode only: produced setup diverges from gold (a wrong=0 breach)
"answer_correct", # a value was produced and self-verified / matches gold
"answer_refused", # setup produced but the solver/verifier refused
"answer_wrong", # eval-mode only: produced value diverges from gold
"contradiction", # a verified value contradicts a supplied answer key
]
@dataclass(frozen=True, slots=True)
class ComprehensionAttempt:
"""One organ's attempt at one problem. Immutable; carries the outcome, the refusal reason
(if any), a deterministic setup signature (for cross-organ comparison), the answer (if a
value was produced), and source-span evidence. ``family`` is left ``None`` by ``classify``
and resolved later by the N4 failure-family registry."""
organ: Organ
outcome: Outcome
case_id: str | None = None
refusal_reason: str | None = None
family: str | None = None
setup_signature: str | None = None
answer: int | None = None
evidence: tuple[SourceSpanLink, ...] = ()
@property
def is_setup_correct(self) -> bool:
return self.outcome == "setup_correct"
@property
def is_refusal(self) -> bool:
return self.outcome in ("setup_refused", "answer_refused")
__all__ = ["ComprehensionAttempt", "Organ", "Outcome"]

View file

@ -1,148 +0,0 @@
"""Proposal-only failure-artifact emitter (N5) — deliberately toothless.
When the contemplation pass (N6) meets a **growth-surface** failure family (``proposal_allowed
= True``), it may emit a single content-addressed JSON artifact under
``teaching/proposals/comprehension_failures/<hash>.json``. That artifact can *propose* a next
fixture/rule for human review. It can do nothing else:
```text
status is always "proposal_only"
mounted is always false
requires_review is always true
serving never reads these files
no reader/test is modified
```
This routes into the existing proposal-only teaching flywheel (ADR-0055/0056/0057) it is NOT a
parallel correction path (CLAUDE.md teaching-safety). The alignment is
``failure -> classification -> proposal -> review -> ratification``, never ``failure -> self-patch``.
Content-addressing: the filename is ``sha256(failure_family : sha256(problem_text))`` so the
same failure on the same text always writes the same path (idempotent), and the raw problem text
is **hashed, never stored**. Deterministic; no clock, no randomness.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.comprehension_attempt.failure_family import FailureFamily
from core.comprehension_attempt.model import ComprehensionAttempt
_PROPOSAL_STATUS = "proposal_only"
def default_proposal_root() -> Path:
"""``<repo>/teaching/proposals/comprehension_failures`` — the write-only proposal sink."""
return Path(__file__).resolve().parents[2] / "teaching" / "proposals" / "comprehension_failures"
def _sha256(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def _attempt_to_dict(attempt: ComprehensionAttempt) -> dict[str, Any]:
return {
"organ": attempt.organ,
"outcome": attempt.outcome,
"refusal_reason": attempt.refusal_reason,
"family": attempt.family,
"setup_signature": attempt.setup_signature,
"answer": attempt.answer,
}
@dataclass(frozen=True, slots=True)
class FailureProposal:
"""A proposal-only artifact. The invariant fields (``status``/``requires_review``/``mounted``)
are enforced in ``__post_init__`` so even a hand-constructed proposal cannot be made
serving-mountable."""
failure_family: str
problem_text_sha256: str
observed_attempts: tuple[dict[str, Any], ...]
status: str = _PROPOSAL_STATUS
suggested_next_fixture: None = None # v0: always None — a human authors the fixture on review
requires_review: bool = True
mounted: bool = False
def __post_init__(self) -> None:
if self.status != _PROPOSAL_STATUS:
raise ValueError(f"proposal status must be {_PROPOSAL_STATUS!r}; got {self.status!r}")
if self.mounted:
raise ValueError("a proposal can never be mounted")
if not self.requires_review:
raise ValueError("a proposal always requires review")
def content_hash(self) -> str:
"""Deterministic content address: same failure on same text -> same hash."""
return hashlib.sha256(
f"{self.failure_family}:{self.problem_text_sha256}".encode("utf-8")
).hexdigest()
def to_json_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"failure_family": self.failure_family,
"problem_text_sha256": self.problem_text_sha256,
"observed_attempts": list(self.observed_attempts),
"suggested_next_fixture": self.suggested_next_fixture,
"requires_review": self.requires_review,
"mounted": self.mounted,
}
def build_proposal(
text: str, family: FailureFamily, attempts: tuple[ComprehensionAttempt, ...]
) -> FailureProposal | None:
"""Build a proposal for a growth-surface family, or ``None`` for a correct wrong=0 boundary.
A family with ``proposal_allowed = False`` (every must-remain-refused boundary) yields NO
proposal the loop never proposes against a faithful refusal.
"""
if not family.proposal_allowed:
return None
return FailureProposal(
failure_family=family.name,
problem_text_sha256=_sha256(text),
observed_attempts=tuple(_attempt_to_dict(a) for a in attempts),
)
def proposal_path(proposal: FailureProposal, root: Path | None = None) -> Path:
base = root if root is not None else default_proposal_root()
return base / f"{proposal.content_hash()}.json"
def emit_proposal(
text: str,
family: FailureFamily,
attempts: tuple[ComprehensionAttempt, ...],
*,
root: Path | None = None,
) -> Path | None:
"""Write a proposal-only artifact for a growth-surface family; return its path, or ``None``.
Idempotent: the same failure on the same text writes the same content-addressed path with
byte-identical content (``sort_keys``). Creates the sink directory on demand.
"""
proposal = build_proposal(text, family, attempts)
if proposal is None:
return None
path = proposal_path(proposal, root)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(proposal.to_json_dict(), indent=2, sort_keys=True), encoding="utf-8")
return path
__all__ = [
"FailureProposal",
"build_proposal",
"default_proposal_root",
"emit_proposal",
"proposal_path",
]

View file

@ -1,77 +0,0 @@
"""Deterministic multi-organ setup router (N3).
Boring on purpose: attempt the R1 and R2 setup compilers, collect their typed attempts, and
select a setup ONLY when exactly one organ produced an admissible one. No dynamic "best"
scoring, no priority heuristics.
```text
exactly one setup_correct -> routed (use it)
zero setup_correct -> all_refused (classify downstream)
>= 2 setup_correct, signatures agree-> routed (organs concur)
>= 2 setup_correct, signatures differ-> ambiguous (refuse never pick)
```
Cross-organ signatures are produced by different functions and never coincide, so in practice
two admitting organs always resolve to ``ambiguous``. The router never solves and never emits
``setup_wrong`` that is an eval-only outcome; against gold the routed setup must match (the
wrong=0 invariant, asserted by the router tests).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from core.comprehension_attempt.classify import classify_cmb, classify_r1, classify_r2, classify_r3
from core.comprehension_attempt.model import ComprehensionAttempt
RouteStatus = Literal["routed", "all_refused", "ambiguous"]
@dataclass(frozen=True, slots=True)
class RouteResult:
"""The outcome of routing one problem across the organs: every attempt, the selected setup
(or ``None``), and the routing status."""
attempts: tuple[ComprehensionAttempt, ...]
selected: ComprehensionAttempt | None
status: RouteStatus
def cmb_is_authoritative(attempts: tuple[ComprehensionAttempt, ...]) -> bool:
"""True when the R4 combined-rate organ **positively** recognized combined-rate shape — a setup,
or a substantive ``cmb_*`` refusal (NOT the bare ``not_combined_rate_shaped`` / ``empty``
step-aside). When it does, R3's broader single-rate read of the SAME text is inadmissible: a more
specific positive recognition beats a broader partial one ("Anna and Ben paint together; Anna
paints 3 rooms/hour" must not be answered as a single-rate 12). This is the narrow CMB↔R3
instance of a future general domain-specificity adjudication; it does NOT mean "CMB always wins"
(on ``input_shape`` CMB cedes to R3, e.g. a plain single-rate car problem)."""
cmb = next((a for a in attempts if a.organ == "r4_combined_rate"), None)
if cmb is None:
return False
return cmb.is_setup_correct or (cmb.refusal_reason or "").startswith("cmb_")
def route_setup(text: str, *, case_id: str | None = None) -> RouteResult:
"""Route *text* to the single organ that admits an honest setup, or refuse."""
attempts = (
classify_r1(text, case_id=case_id),
classify_r2(text, case_id=case_id),
classify_r3(text, case_id=case_id),
classify_cmb(text, case_id=case_id),
)
# CMB-over-R3 domain precedence: a substantive CMB recognition vetoes R3's single-rate over-read.
vetoed = cmb_is_authoritative(attempts)
correct = tuple(
a for a in attempts if a.is_setup_correct and not (vetoed and a.organ == "r3_rate")
)
if len(correct) == 1:
return RouteResult(attempts, correct[0], "routed")
if not correct:
return RouteResult(attempts, None, "all_refused")
if len({a.setup_signature for a in correct}) == 1:
return RouteResult(attempts, correct[0], "routed")
return RouteResult(attempts, None, "ambiguous")
__all__ = ["RouteResult", "RouteStatus", "cmb_is_authoritative", "route_setup"]

View file

@ -276,74 +276,6 @@ class RuntimeConfig:
# ADR-0151 — generate TeachingChainProposals from enriched candidates on load.
auto_proposal_enabled: bool = False
# Shape B+ (L10 resume-as-same-life) — persist and restore the FULL lived
# session state (field, vault, anchor, graph, referents, dialogue) across
# reboot, not just recognizers/candidates (Shape B). OFF by default: it is a
# deliberate always-on-runtime mode, and per-turn snapshotting has an O(turns)
# cost, so demos/evals/one-shot runtimes must not pay for resume they don't
# use. Enabled by the L10 continuity lane and the production L10 process.
persist_session_state: bool = False
# L11 — on reboot, if the stamped checkpoint identity != the recomputed
# engine identity (the ratified substrate changed during downtime), REFUSE to
# start (raise IdentityContinuityError) rather than the default warn-and-flag.
# OFF by default: reboot is recovery, not control flow (ADR-0157), and the
# operator must not be bricked by a benign ratified pack swap. Deployments
# wanting a hard identity-continuity guarantee opt in.
strict_identity_continuity: bool = False
# Step B (inline realization) — when on, each turn ACCRUES knowledge into the
# held self: a comprehensible declarative turn is realized into the session vault
# (SPECULATIVE, as-told), and a comprehensible question turn is determined over
# realized knowledge. This is the "one continuous life" telos made real — a
# conversation accumulates knowledge it can recall and reason over. OFF by default
# (one-shot/eval runtimes don't accrue); the production L10 process enables it
# alongside persist_session_state so accrued facts survive reboot. Realization is
# SESSION memory (immediate), NOT ratified learning — it proposes nothing; the
# teaching/review HITL path is untouched.
accrue_realized_knowledge: bool = False
# Step D (CLOSE) — when on, idle_tick consolidates soundly-derived determinations
# back into the held self: one semi-naive layer of the member/subset deductive
# closure (member∘subset, subset∘subset — NEVER member∘member) per tick, each
# derived edge proof_chain-verified, written as a SPECULATIVE realized record with
# derived-provenance. This is how the loop "learns from determined facts": derive
# once, remember, reach one hop further next tick — the directly answerable set
# climbs monotonically to the deductive-closure fixed point. OFF by default; the
# production L10 process enables it alongside accrue_realized_knowledge +
# persist_session_state. SESSION memory (immediate), NOT reviewed/corpus learning —
# no proposal coupling, the teaching/review HITL path is untouched. Bounded by the
# same _SUBSUMPTION_SUBSET_FACT_BUDGET; converges (a saturated tick is a no-op).
consolidate_determinations: bool = False
# IT (proposal-review surface) — when on, idle_tick runs a READ-ONLY sub-pass that scans
# the comprehension-failure proposal sink and surfaces a summary in
# IdleTickResult.proposal_review. It NEVER mutates an artifact, sets did_work, checkpoints,
# ratifies, mounts, or modifies readers; a reporter failure is captured, not propagated.
# OFF by default — idle ticks don't pay for the scan unless a deployment wants the surface.
review_pending_proposals: bool = False
# Step E (ESTIMATION) — when on, a converse query the engine would otherwise REFUSE
# (told p(a,b), asked p(b,a)) may be answered with a DISCLOSED [approximate] estimate
# IF the predicate-class has earned the SERVE license on the ratified, committed
# reliability ledger (ADR-0175 license_for, θ_SERVE=0.99) — routed through the
# ADR-0206 govern_response/shape_surface bridge. OFF by default; only meaningful with
# accrue_realized_knowledge (the estimate is computed in the accrual path). wrong=0 is
# preserved by construction: an estimate is ALWAYS disclosed ([approximate]), never
# asserted as fact, and is offered only for a class whose committed track record
# clears the Wilson floor. Absent a cleared license -> STRICT refuse (the safe
# default); the engine never raises its own ceiling.
estimation_enabled: bool = False
# ASK serving gate enable flag. When True, ASK serving is allowed.
# Default False (dark).
ask_serving_enabled: bool = False
# VERIFIED serving gate enable flag. When True, VERIFIED serving is allowed.
# Default False (dark).
verified_serving_enabled: bool = False
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
DEFAULT_ETHICS_PACK: str = "default_general_ethics_v1"

View file

@ -2,10 +2,8 @@
from .contradiction_detection import mine_contradiction_detection_report
from .frontier_compare import mine_frontier_compare_report
from .learning_arena import mine_learning_arena_report
__all__ = [
"mine_contradiction_detection_report",
"mine_frontier_compare_report",
"mine_learning_arena_report",
]

View file

@ -1,111 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from core.contemplation.schema import (
ContemplationEvidenceRef,
ContemplationFinding,
FindingKind,
)
def mine_learning_arena_report(
report_path: str | Path,
*,
substrate_hash: str,
min_coverage: float = 0.25,
) -> tuple[ContemplationFinding, ...]:
"""Convert learning-arena report weaknesses into speculative findings.
Read-only: parses ``report_path`` and returns immutable findings. It never
writes packs, teaching examples, proposals, or runtime state.
"""
path = Path(report_path)
report = json.loads(path.read_text(encoding="utf-8"))
source_id = str(path)
findings: list[ContemplationFinding] = []
for class_name, row in sorted(_per_class(report).items()):
committed = int(row.get("committed", 0) or 0)
coverage = float(row.get("coverage", 0.0) or 0.0)
t2_verified = int(row.get("t2_verified", 0) or 0)
if coverage < min_coverage:
evidence = ContemplationEvidenceRef(
source_type="learning_arena_report",
source_id=source_id,
pointer=f"class={class_name}",
summary=f"coverage={coverage};committed={committed};min={min_coverage}",
)
findings.append(
ContemplationFinding(
kind=FindingKind.COVERAGE_GAP,
subject=class_name,
predicate="weak_coverage",
object=None,
evidence_refs=(evidence,),
proposed_action=(
"Inspect refusal diagnoses for this capability class and add "
"practice or reviewed operators only where the missing piece is named."
),
substrate_hash=substrate_hash,
)
)
if committed > 0 and t2_verified == 0:
evidence = ContemplationEvidenceRef(
source_type="learning_arena_report",
source_id=source_id,
pointer=f"class={class_name}",
summary=f"committed={committed};t2_verified=0",
)
findings.append(
ContemplationFinding(
kind=FindingKind.UNPROVED_RELATION,
subject=class_name,
predicate="missing_tier2_evidence",
object=None,
evidence_refs=(evidence,),
proposed_action=(
"Add convergent self-verification evidence before promoting this "
"class beyond gold-scored practice."
),
substrate_hash=substrate_hash,
)
)
for record in report.get("elimination_records", ()) or ():
if not isinstance(record, dict):
continue
case_id = str(record.get("case_id", "unknown_case"))
class_name = str(record.get("class_name", "unknown_class"))
evidence = ContemplationEvidenceRef(
source_type="learning_arena_report",
source_id=source_id,
pointer=f"case={case_id}",
summary=str(record.get("reason", "wrong attempt")),
)
findings.append(
ContemplationFinding(
kind=FindingKind.BENCHMARK_CASE,
subject=f"{class_name}/{case_id}",
predicate="wrong_attempt",
object=str(record.get("gold", "")),
evidence_refs=(evidence,),
proposed_action=(
"Use this gold-caught wrong attempt as elimination evidence; do not "
"promote the class until the faulty derivation shape is pruned."
),
substrate_hash=substrate_hash,
)
)
return tuple(findings)
def _per_class(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
rows = report.get("per_class", {})
return {str(k): v for k, v in rows.items() if isinstance(v, dict)}
__all__ = ["mine_learning_arena_report"]

View file

@ -1,134 +0,0 @@
"""EngineIdentity — content-derived identity of the ratified substrate (L11).
``EngineIdentity`` is the sha256 of the canonical serialization of the engine's
ratified PERSONALITY substrate the active identity / safety / ethics / register
/ anchor-lens pack files plus the code revision. It is **content-derived, NOT
entropy-based**: two engines with the same ratified substrate compute the SAME
identity, because they ARE the same engine functionally (substrate is shareable).
This is the "who am I" hash. It is bumped only by a ratified change to the
identity substrate (a new identity pack, a safety-axis change), NOT by lived
learning (recall, teaching) that is the engine's *experience*, carried across
reboot by the Shape B+ lived-state lineage, not its identity. The git-like
lineage chain (parent links on ratification) and the reboot identity verification
build on this primitive.
Honest scope (per the EngineIdentity candidate note): this is a *convention*
naming the existing per-pack content hashes, not a new pack format. The ratified
teaching corpus / recognizer-registry head can be folded into the tuple later
(additive a new key changes the identity, which is the correct semantic for a
ratified-substrate change).
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
from core.config import (
DEFAULT_ANCHOR_LENS,
DEFAULT_ETHICS_PACK,
DEFAULT_IDENTITY_PACK,
DEFAULT_REGISTER_PACK,
RuntimeConfig,
)
# The never-swappable safety pack default (packs/safety/loader.py).
DEFAULT_SAFETY_PACK: str = "core_safety_axes_v1"
_PACKS_ROOT = Path(__file__).resolve().parents[1] / "packs"
#: role -> packs/ subdirectory holding ``<pack_id>.json``.
_ROLE_DIRS: dict[str, str] = {
"identity": "identity",
"safety": "safety",
"ethics": "ethics",
"register": "register",
"anchor_lens": "anchor_lens",
}
#: The ratified roles that constitute the engine's identity, in canonical order.
RATIFIED_ROLES: tuple[str, ...] = (
"identity",
"safety",
"ethics",
"register",
"anchor_lens",
)
class EngineIdentityError(RuntimeError):
"""A ratified pack named by the identity tuple could not be resolved."""
class IdentityContinuityError(RuntimeError):
"""A reboot found the stamped checkpoint identity != the recomputed identity.
The ratified substrate changed while the engine was down, so it would resume
the lived state under a DIFFERENT identity than the checkpoint was written
under. Raised only under strict identity continuity; the default surfaces a
warning and a queryable break flag (reboot is recovery, not control flow).
"""
def _pack_content_hash(role: str, pack_id: str) -> str:
path = _PACKS_ROOT / _ROLE_DIRS[role] / f"{pack_id}.json"
if not path.exists():
raise EngineIdentityError(
f"ratified {role} pack not found: {_ROLE_DIRS[role]}/{pack_id}.json"
)
return hashlib.sha256(path.read_bytes()).hexdigest()
def ratified_substrate(pack_ids: dict[str, str], git_revision: str) -> dict[str, Any]:
"""The canonical, auditable identity tuple.
``{role: {"pack_id", "sha256"}}`` for every ratified role plus
``"code_revision"``. ``pack_ids`` must name a pack for every role in
``RATIFIED_ROLES``.
"""
substrate: dict[str, Any] = {}
for role in RATIFIED_ROLES:
pack_id = pack_ids[role]
substrate[role] = {
"pack_id": pack_id,
"sha256": _pack_content_hash(role, pack_id),
}
substrate["code_revision"] = git_revision
return substrate
def compute_engine_identity(pack_ids: dict[str, str], git_revision: str) -> str:
"""The sha256 EngineIdentity over the ratified substrate tuple."""
canonical = json.dumps(
ratified_substrate(pack_ids, git_revision),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def _resolve_pack_ids(config: RuntimeConfig) -> dict[str, str]:
"""Resolve each ratified role to its active pack id (config override or DEFAULT)."""
return {
"identity": config.identity_pack or DEFAULT_IDENTITY_PACK,
"safety": DEFAULT_SAFETY_PACK, # never-swappable
"ethics": config.ethics_pack or DEFAULT_ETHICS_PACK,
"register": config.register_pack_id or DEFAULT_REGISTER_PACK,
"anchor_lens": config.anchor_lens_id or DEFAULT_ANCHOR_LENS,
}
def engine_identity_for_config(config: RuntimeConfig, git_revision: str) -> str:
"""EngineIdentity for a runtime config (resolving every role to its active pack)."""
return compute_engine_identity(_resolve_pack_ids(config), git_revision)
def ratified_substrate_for_config(
config: RuntimeConfig, git_revision: str
) -> dict[str, Any]:
"""The auditable identity tuple for a runtime config."""
return ratified_substrate(_resolve_pack_ids(config), git_revision)

View file

@ -1,87 +0,0 @@
"""The Epistemic Disclosure spine (P0-1+).
CORE's served-surface governance: comprehension produces evidence, the limitation
pass classifies *what kind of gap* is blocking resolution, and (in later slices) a
disposition + disclosure-claim decide what reaches the user and under what epistemic
claim. This package is the *owner* of that machine ``generate/derivation/verify.py``
and other serving sites may eventually *consume* it, but must never *define* it.
Shipped so far (all off-serving nothing here imports ``generate.derivation`` /
``core.reliability_gate``):
* :mod:`~core.epistemic_disclosure.limitation` (P0-1) the typed limitation
vocabulary, a CONSOLIDATING VIEW over the shipped failure-family registry +
contemplation terminals (no fourth taxonomy).
* :mod:`~core.epistemic_disclosure.disclosure_claim` (P0-2) the ``DisclosureClaim``
axis (the epistemic claim a response makes), kept SEPARATE from ``ReachLevel``.
* :mod:`~core.epistemic_disclosure.disposition` (P0-3) ``ServedDisposition`` and
``choose_served_disposition``: the pure mapping
``EpistemicState × LimitationAssessment × DisclosureClaim ServedDisposition``.
Mapping scaffold only no rendering, no bus, no ``verify.py``; nothing consumes
it yet.
* :mod:`~core.epistemic_disclosure.ask_serving` a narrow Q1-D served-ASK artifact
adapter. It validates already-rendered question artifacts and returns a typed
decision; it does not render prose and does not acquire runtime contemplation.
* :mod:`~core.epistemic_disclosure.verified_contract` (P1-A) the VERIFIED contract:
the obligation, the proof shape, the validator, and the single sanctioned route to
``EpistemicState.VERIFIED`` / ``DisclosureClaim.VERIFIED``. Contract only no
producer; a faithful solve of a WRONG read must not verify.
"""
from __future__ import annotations
from core.epistemic_disclosure.ask_serving import (
ServedAskDecision,
evaluate_served_ask,
)
from core.epistemic_disclosure.disclosure_claim import (
DEFAULT_DISCLOSURE_CLAIM,
DisclosureClaim,
)
from core.epistemic_disclosure.disposition import (
ServedDisposition,
choose_served_disposition,
)
from core.epistemic_disclosure.limitation import (
Q1B_ASK_CARVE_OUT,
LimitationAssessment,
LimitationKind,
MissingSlot,
ResolutionAction,
assess_from_attempt,
assess_from_family,
terminal_for_action,
)
from core.epistemic_disclosure.verified_contract import (
VERIFICATION_OBLIGATION,
VerificationObligation,
VerificationProof,
VerificationResult,
VerificationVerdict,
disclosure_for_verification,
evaluate_verification,
)
__all__ = [
"DEFAULT_DISCLOSURE_CLAIM",
"Q1B_ASK_CARVE_OUT",
"VERIFICATION_OBLIGATION",
"DisclosureClaim",
"LimitationAssessment",
"LimitationKind",
"MissingSlot",
"ResolutionAction",
"ServedAskDecision",
"ServedDisposition",
"VerificationObligation",
"VerificationProof",
"VerificationResult",
"VerificationVerdict",
"assess_from_attempt",
"assess_from_family",
"choose_served_disposition",
"disclosure_for_verification",
"evaluate_served_ask",
"evaluate_verification",
"terminal_for_action",
]

View file

@ -1,171 +0,0 @@
"""Stage 2 ASK served-surface artifact adapter.
This module is intentionally narrow: it validates a pre-rendered Q1-D
``DeliveredQuestion`` artifact and decides whether that artifact is eligible to
be exposed as a served ASK/QUESTION_NEEDED surface. It does not acquire
contemplation results from runtime and does not render question prose.
Validation enforces the Q1-D artifact contract:
- top-level JSON object only;
- ``status == "question_only"``;
- ``requires_review is True``;
- ``served is False``;
- ``answer_binding`` is absent or ``None``;
- ``question`` is an object;
- ``question.text`` is a non-empty string;
- ``question.slot_name`` is a non-empty string;
- ``question_path`` exists on disk and differs from ``proposal_path``.
Any validation failure fails closed to the caller's fallback surface and
standing disposition. The served text is consumed from the artifact exactly; no
runtime prose construction or mutation happens here.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.epistemic_disclosure.disposition import ServedDisposition, choose_served_disposition
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_questions.serving_gate import ask_serving_enabled
from core.epistemic_state import EpistemicState
_MISSING = object()
@dataclass(frozen=True, slots=True)
class ServedAskDecision:
"""The adapter's served-ASK decision."""
served: bool
terminal: str
surface: str
disposition: ServedDisposition
def _terminal_value(contemplation_result: Any) -> str:
terminal = getattr(contemplation_result, "terminal", None)
if terminal is None:
return "NO_PROGRESS"
return str(getattr(terminal, "value", terminal))
def _fallback_disposition(terminal: str) -> ServedDisposition:
if terminal == "PROPOSAL_EMITTED":
return ServedDisposition.PROPOSE
if terminal == "SOLVED_VERIFIED":
return ServedDisposition.COMMIT
return ServedDisposition.REFUSE
def _fallback_decision(contemplation_result: Any, fallback_surface: str) -> ServedAskDecision:
terminal = _terminal_value(contemplation_result)
return ServedAskDecision(
served=False,
terminal=terminal,
surface=fallback_surface,
disposition=_fallback_disposition(terminal),
)
def _validate_question_artifact(data: Any, *, question_path: Path, proposal_path: Any) -> str | None:
"""Return the valid question text, or ``None`` for any contract violation."""
if not isinstance(data, dict):
return None
if data.get("status") != "question_only":
return None
if data.get("requires_review") is not True:
return None
served = data.get("served", _MISSING)
if served is _MISSING or served is not False:
return None
answer_binding = data.get("answer_binding", _MISSING)
if answer_binding is not _MISSING and answer_binding is not None:
return None
question = data.get("question")
if not isinstance(question, dict):
return None
text = question.get("text")
if not isinstance(text, str) or not text.strip():
return None
slot_name = question.get("slot_name")
if not isinstance(slot_name, str) or not slot_name.strip():
return None
if proposal_path is not None and str(question_path) == str(proposal_path):
return None
return text.strip()
def evaluate_served_ask(
config: Any,
contemplation_result: Any,
fallback_surface: str,
) -> ServedAskDecision:
"""Evaluate whether a Q1-D question artifact may be surfaced as ASK.
This is a bus/disposition adapter, not a renderer and not the runtime
acquisition path. The caller supplies a contemplation result that already
points to a delivered question artifact. When the gate is disabled or any
artifact invariant fails, the adapter returns the fallback surface and the
standing fallback disposition.
"""
if not ask_serving_enabled(config):
return _fallback_decision(contemplation_result, fallback_surface)
if _terminal_value(contemplation_result) != "QUESTION_NEEDED":
return _fallback_decision(contemplation_result, fallback_surface)
question_path_value = getattr(contemplation_result, "question_path", None)
proposal_path_value = getattr(contemplation_result, "proposal_path", None)
if not question_path_value or question_path_value == proposal_path_value:
return _fallback_decision(contemplation_result, fallback_surface)
question_path = Path(question_path_value)
if not question_path.is_file():
return _fallback_decision(contemplation_result, fallback_surface)
try:
payload = json.loads(question_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return _fallback_decision(contemplation_result, fallback_surface)
question_text = _validate_question_artifact(
payload,
question_path=question_path,
proposal_path=proposal_path_value,
)
if question_text is None:
return _fallback_decision(contemplation_result, fallback_surface)
limitation = LimitationAssessment(
limitation_kind="missing_information",
resolution_action="ask_question",
epistemic_state=EpistemicState.UNDETERMINED,
owner_organ=payload.get("owner_organ"),
blocking_reason=str(payload.get("blocking_reason", "")),
)
disposition = choose_served_disposition(
epistemic_state=EpistemicState.UNDETERMINED,
limitation=limitation,
)
return ServedAskDecision(
served=True,
terminal="QUESTION_NEEDED",
surface=question_text,
disposition=disposition,
)
__all__ = ["ServedAskDecision", "evaluate_served_ask"]

View file

@ -1,65 +0,0 @@
"""P0-2 — the DisclosureClaim axis (the epistemic claim a served response makes).
A served response carries two ORTHOGONAL governance properties:
* ``ReachLevel`` (``core/response_governance/policy.py``) how far PAST
fully-grounded fact the response reaches (STRICT < APPROXIMATE < EXTRAPOLATE
< CREATIVE).
* ``DisclosureClaim`` (here) the EPISTEMIC CLAIM the response makes about its
own truth status.
These are deliberately separate axes. ``verified`` is **not** a reach level it is
a claim about *proof state*, not about how far the response speculates. Conflating
them (e.g. a hypothetical ``ReachLevel.VERIFIED``) would let a proven answer inherit
an "approximate" surface, or an approximation inherit a "verified" badge. Keeping the
axes distinct is the architectural commitment behind the Stage-2 lockfile
(``docs/analysis/stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08.md`` §0).
**Discipline no claim without a producer** (the spine enforces on itself what it
enforces on answers). Every member has a real or imminent emitter:
* ``NONE`` every response today (the baseline; STRICT + NONE).
* ``APPROXIMATE`` active: the cognition Step-E disclosed estimate
(``estimation_enabled`` / ADR-0206 §5).
* ``PROPOSAL_ONLY`` active: ``teaching/proposals`` emits review-only proposals.
* ``VERIFIED`` the imminent frontier: its producer is Phase 1 (P1-A..),
declared because it is the v1 target the bus is built around.
Two claims are intentionally ABSENT, because nothing can emit them and the spine
will not declare a label it cannot earn:
* ``PROVEN`` a claim stronger than VERIFIED; no plan to build a producer.
* ``ESTIMATED`` a *future* split of ``APPROXIMATE`` into a distinct
numeric-estimate claim, added ONLY once a real estimator producer
exists. Until then the cognition estimate is ``APPROXIMATE``.
P0-2 ships ONLY the axis + its default. No bus behaviour, no mapping to a disposition
(that is P0-3 / ServedDisposition). Off-serving: this module imports nothing.
"""
from __future__ import annotations
from enum import Enum, unique
@unique
class DisclosureClaim(str, Enum):
"""The epistemic claim a served surface makes about its own truth status.
Orthogonal to ``ReachLevel``. ``str``-valued for stable serialization into
telemetry / disposition records (the same convention as ``EpistemicState``).
"""
NONE = "none" # no epistemic claim beyond the plain surface (the default)
VERIFIED = "verified" # independently confirmed under a canonical-comparison contract
APPROXIMATE = "approximate" # a disclosed best-estimate from incomplete evidence
PROPOSAL_ONLY = "proposal_only" # offered as a proposal for review, not asserted
#: The default claim: a surface asserts nothing about its truth status unless a
#: producer upgrades it. ``STRICT`` reach + ``NONE`` claim is today's every-response
#: baseline.
DEFAULT_DISCLOSURE_CLAIM: DisclosureClaim = DisclosureClaim.NONE
__all__ = ["DEFAULT_DISCLOSURE_CLAIM", "DisclosureClaim"]

View file

@ -1,110 +0,0 @@
"""P0-3 — ServedDisposition: the served-surface decision (mapping scaffold only).
The third axis of the Epistemic Disclosure bus. Given (a) the epistemic state, (b)
the limitation assessment (if resolution is blocked), and (c) the disclosure claim,
:func:`choose_served_disposition` decides WHAT KIND OF MOVE the served surface makes:
commit / disclose / ask / propose / report / explain / refuse / step-aside.
This is a PURE MAPPING. No rendering, no bus behaviour, no ``verify.py``, no
``govern_response`` mutation nothing consumes the result yet. P0-3 only fixes the
decision table so a later slice / Phase 1 can wire it.
The load-bearing rule (the Phase-1 guard): a ``DisclosureClaim.VERIFIED`` discloses
ONLY when the epistemic state is actually ``EpistemicState.VERIFIED``. An unbacked
verified claim degrades to a plain ``COMMIT`` it is NEVER served as verified before
the producer exists. This protects the whole VERIFIED lane from accidental
"verified-looking" serving.
Off-serving: imports no ``generate.derivation`` / ``core.reliability_gate``.
"""
from __future__ import annotations
from enum import Enum, unique
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_state import EpistemicState
@unique
class ServedDisposition(str, Enum):
"""What kind of move the served surface makes.
``str``-valued for stable serialization (the ``EpistemicState`` /
``DisclosureClaim`` convention).
"""
COMMIT = "commit" # serve a fully-grounded answer as-is, no epistemic caveat
DISCLOSE = "disclose" # serve under a disclosure claim ([verified] / [approximate])
ASK = "ask" # ask the user for the missing/ambiguous datum (Q1 tenant)
PROPOSE = "propose" # offer a review-only capability proposal, do not assert
REPORT = "report" # report a contradiction with a supplied answer key
EXPLAIN = "explain" # explain that the ask is outside the current envelope (scope)
REFUSE = "refuse" # hard-stop refusal (impossible / unreadable / known boundary)
STEP_ASIDE = "step_aside" # not this organ's domain — cede
def choose_served_disposition(
*,
epistemic_state: EpistemicState,
limitation: LimitationAssessment | None,
disclosure_claim: DisclosureClaim = DisclosureClaim.NONE,
) -> ServedDisposition:
"""Decide the served disposition. Pure, deterministic, total.
A blocking limitation dominates you do not serve an answer, you ask / propose /
report / explain / refuse / step aside per its resolution action. (A limitation
whose action is ``answer`` is non-blocking and falls through to the serve
decision.) With no blocking limitation, the disclosure claim + epistemic state pick
the serve mode, under the Phase-1 ``VERIFIED`` guard.
NOTE (scaffold trust boundary): a blocking epistemic state (CONTRADICTED,
AMBIGUOUS, UNDETERMINED) reaches this function AS a limitation (e.g. a contradiction
arrives as ``report_contradiction``), not as ``limitation=None``. The serve branch
trusts ``limitation=None`` to mean "servable".
"""
if limitation is not None:
match limitation.resolution_action:
case "ask_question":
return ServedDisposition.ASK
case "emit_proposal":
return ServedDisposition.PROPOSE
case "report_contradiction":
return ServedDisposition.REPORT
case "step_aside":
return ServedDisposition.STEP_ASIDE
case "refuse_known_boundary":
# scope_boundary is the governed "outside the current envelope"
# disposition — it may render as a refusal later, but must NOT collapse
# into a hard boundary here.
if limitation.limitation_kind == "scope_boundary":
return ServedDisposition.EXPLAIN
return ServedDisposition.REFUSE
case "answer":
pass # non-blocking — fall through to the serve decision
return _serve_disposition(epistemic_state, disclosure_claim)
def _serve_disposition(
epistemic_state: EpistemicState, disclosure_claim: DisclosureClaim
) -> ServedDisposition:
"""The no-blocking-limitation branch: claim + state pick the serve mode."""
match disclosure_claim:
case DisclosureClaim.VERIFIED:
# The Phase-1 guard: disclose-as-verified ONLY with the backing state.
if epistemic_state is EpistemicState.VERIFIED:
return ServedDisposition.DISCLOSE
return ServedDisposition.COMMIT
case DisclosureClaim.APPROXIMATE:
return ServedDisposition.DISCLOSE
case DisclosureClaim.PROPOSAL_ONLY:
return ServedDisposition.PROPOSE
case DisclosureClaim.NONE:
return ServedDisposition.COMMIT
case _: # pragma: no cover - exhaustive over the 4-member enum; loud if extended
raise AssertionError(f"unhandled disclosure_claim: {disclosure_claim!r}")
__all__ = ["ServedDisposition", "choose_served_disposition"]

View file

@ -1,378 +0,0 @@
"""P0-1 — the pre-question limitation pass, as a CONSOLIDATING VIEW (session §1.5).
The intake gate of the Epistemic Disclosure spine: before contemplation chooses a
served disposition, it classifies *what KIND of limitation* is blocking resolution.
Asking a question is only one possible resolution, and asking is wrong unless the
limitation is specifically the kind a question resolves (missing/ambiguous external
information). Mis-classify and intake breaks two ways refuse-when-should-ask (lose
the unlocking datum) or ask-when-should-propose (waste the channel).
This module is the first slice: the typed vocabulary (:data:`LimitationKind` /
:data:`ResolutionAction` / :class:`LimitationAssessment`) and the *mapping* that
DERIVES each from the already-shipped failure-family registry
(:data:`core.comprehension_attempt.failure_family.REGISTRY`) and contemplation
:class:`~generate.contemplation.findings.Terminal` set.
**Hard invariant (no fourth taxonomy).** Every assessment is mechanically derived
from an existing ``FailureFamily``; the *only* genuinely new resolution action is
``ask_question`` (the Q1/ASK tenant). Through Q1-C it was the one action with no
shipped terminal; Q1-D adds :attr:`~generate.contemplation.findings.Terminal.QUESTION_NEEDED`
(sibling of ``PROPOSAL_EMITTED``), so :func:`terminal_for_action` is now total every
action maps onto a shipped terminal, which is what makes this a consolidating view.
**Q1-B transitional carve-out (this slice).** Two shipped families
``missing_total_count`` and ``missing_weighted_total`` are classified here as
``missing_information`` / ``ask_question``: the user *could state the total* and
unblock solving, so they are missing data, not capability gaps. The shipped
:data:`REGISTRY` still flags them ``proposal_allowed = True`` so that existing
consumers (:mod:`core.comprehension_attempt.proposal` /
:mod:`generate.contemplation.pass_manager`) keep emitting proposal-only artifacts
to the pile until Q1-C/Q1-D wire ASK delivery to a served surface there is
nowhere for an ``ask_question`` to go, and silently dropping the proposal signal
would be a capability regression with no compensating intake. Once ASK is serving,
the REGISTRY flag flips to ``False`` and the carve-out
(:data:`Q1B_ASK_CARVE_OUT`) retires. The carve-out is named, enumerated, and
covered by an explicit test (``tests/test_limitation_assessment.py``) so its
removal is a conscious act, not a silent re-key.
**Off-serving.** Imports no ``generate.derivation`` / ``core.reliability_gate``; it
cannot move the sealed GSM8K metric. Nothing consumes ``resolution_action`` to change
serving yet this slice only *classifies* and *describes residue*.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
from core.comprehension_attempt.failure_family import (
FailureFamily,
family_for_reason,
)
from core.comprehension_attempt.model import ComprehensionAttempt
from core.epistemic_state import EpistemicState
from generate.contemplation.findings import Terminal
#: Why resolution is blocked. The split from :data:`ResolutionAction` keeps *state*
#: ("what is true / missing") distinct from *action* ("what to do about it").
LimitationKind = Literal[
"missing_information", # a needed datum is absent — could be supplied → ask
"ambiguous_structure", # data present, relationship unclear → ask (when user-resolvable)
"scope_boundary", # exceeds the current capability/evidence envelope → refuse/explain
"capability_gap", # info present, CORE lacks the transform → propose (or refuse if signal too coarse)
"hard_boundary", # mathematically/logically impossible or undefined → refuse
"contradiction", # evidence conflicts with a claimed answer → report
"renderability_gap", # asking is right but terms aren't grounded enough to name safely → ask-generic
"input_shape", # not this organ's domain → step aside
]
#: What to do about a limitation. ``ask_question`` is the genuinely new action this
#: spine introduces; the other five each correspond to a shipped contemplation
#: terminal (:func:`terminal_for_action`).
ResolutionAction = Literal[
"answer",
"ask_question",
"emit_proposal",
"refuse_known_boundary",
"report_contradiction",
"step_aside",
]
@dataclass(frozen=True, slots=True)
class MissingSlot:
"""One typed slot the ASK limitation identifies as missing.
A *structural* description (NOT user-renderable prose): ``slot_name`` is the
family-defined slot identifier (e.g. ``"total_count"``); ``expected_unit_or_type``
is the family-typed expectation a future bound answer must satisfy (e.g.
``"count_int"``); ``binding_target`` is the structural role the slot fills in
the organ's setup (e.g. ``"collective_unit_total"``), which Q2 answer-binding
re-enters the gate against (scoping §4). Renderable, user-facing strings come
later from grounded text spans (:attr:`LimitationAssessment.grounded_terms`),
NEVER from these snake_case identifiers that split is what keeps the renderer
wrong=0-safe (scoping §2).
"""
slot_name: str
expected_unit_or_type: str
binding_target: str
@dataclass(frozen=True, slots=True)
class LimitationAssessment:
"""One typed classification of the limitation blocking a comprehension attempt.
``epistemic_state`` (what is true) and ``resolution_action`` (what to do) are
deliberately distinct: e.g. ``UNDETERMINED`` + ``ask_question`` for a missing
datum. ``blocking_reason`` is the failure-family key (the partition key), so the
assessment is back-traceable to the shipped registry.
``missing_slots`` and ``grounded_terms`` are the ASK *typed residue* populated
only for ``ask_question`` resolutions by :func:`assess_from_attempt`. Both
default to empty so existing P0-1 callers using :func:`assess_from_family`
continue to work unchanged. The wrong=0 invariant (scoping §2): a slot here
carries family-typed structural identifiers only; renderable prose must come
from ``grounded_terms`` (verbatim text spans), never fabricated.
"""
limitation_kind: LimitationKind
resolution_action: ResolutionAction
epistemic_state: EpistemicState
owner_organ: str | None
blocking_reason: str
missing_slots: tuple[MissingSlot, ...] = field(default_factory=tuple)
grounded_terms: tuple[str, ...] = field(default_factory=tuple)
# --- The consolidating mapping (derived from the shipped registry, not invented) ---
#: ``EpistemicState`` each kind asserts. ``scope_boundary`` lights up the RESERVED
#: ``SCOPE_BOUNDARY`` state with a real producer; ``contradiction`` / ``ambiguous``
#: reuse the ACTIVE states. The refuse/propose kinds share ``UNDETERMINED`` — the
#: *kind* carries the distinction, the *state* only says "no answer determined".
_KIND_TO_STATE: dict[LimitationKind, EpistemicState] = {
"missing_information": EpistemicState.UNDETERMINED,
"ambiguous_structure": EpistemicState.AMBIGUOUS,
"scope_boundary": EpistemicState.SCOPE_BOUNDARY,
"capability_gap": EpistemicState.UNDETERMINED,
"hard_boundary": EpistemicState.UNDETERMINED,
"contradiction": EpistemicState.CONTRADICTED,
"renderability_gap": EpistemicState.UNDETERMINED,
"input_shape": EpistemicState.UNDETERMINED,
}
#: The shipped contemplation ``Terminal`` each action corresponds to. Through Q1-C
#: ``ask_question`` mapped to ``None`` (the one action the spine added with no terminal
#: yet); Q1-D ships ``QUESTION_NEEDED`` (sibling of ``PROPOSAL_EMITTED``), so the map is
#: now TOTAL — all six actions correspond to shipped terminals. This totality is the
#: proof the limitation pass is a consolidating view, not a new universe.
#:
#: NOTE: this is the action's *home* terminal "in principle". The terminal a Q1-D
#: *delivery* actually emits depends on renderability — an unrenderable ``ask_question``
#: falls back to the family's standing disposition (the D2 guard in
#: :mod:`core.epistemic_questions.delivery`), it does NOT emit a contentless
#: ``QUESTION_NEEDED``.
_ACTION_TO_TERMINAL: dict[ResolutionAction, Terminal] = {
"answer": Terminal.SOLVED_VERIFIED,
"emit_proposal": Terminal.PROPOSAL_EMITTED,
"refuse_known_boundary": Terminal.REFUSED_KNOWN_BOUNDARY,
"report_contradiction": Terminal.CONTRADICTION_DETECTED,
"step_aside": Terminal.NO_PROGRESS,
"ask_question": Terminal.QUESTION_NEEDED,
}
#: **Transitional carve-out (Q1-B).** Families this slice classifies as
#: ``ask_question`` in the limitation layer while their shipped
#: :data:`REGISTRY` entries keep ``proposal_allowed = True`` so the contemplation
#: pass and proposal pile keep working unchanged. This is an honest migration seam:
#: the disclosure layer speaks the truthful classification now; the operational
#: layer keeps the current signal so nothing regresses before Q1-C/Q1-D wires ASK
#: delivery. Retirement: once ASK is serving, flip ``proposal_allowed = False`` on
#: these two families in :mod:`core.comprehension_attempt.failure_family`, drop
#: this set, and amend the ``proposal_allowed`` invariant in tests.
Q1B_ASK_CARVE_OUT: frozenset[str] = frozenset(
{"missing_total_count", "missing_weighted_total"}
)
#: family name → (LimitationKind, ResolutionAction). Keys must equal the REGISTRY's
#: family names exactly (asserted total by test). Classification rationale per family:
#: - growth surfaces (``proposal_allowed``) → ``capability_gap`` / ``emit_proposal``
#: EXCEPT :data:`Q1B_ASK_CARVE_OUT` — see that constant's docstring
#: - underdetermined / missing-datum refusals → ``missing_information`` / ``ask_question``
#: - same-unit-but-no-cue ambiguity the user could resolve → ``ambiguous_structure`` / ask
#: - math/logic impossibility, incoherence, coarse-signal parse gaps → ``hard_boundary`` / refuse
#: - beyond-current-solver-envelope → ``scope_boundary`` / refuse
#: - the answer-key verdict → ``contradiction`` / ``report_contradiction``
#: - foreign text → ``input_shape`` / ``step_aside``
_FAMILY_TO_LIMITATION: dict[str, tuple[LimitationKind, ResolutionAction]] = {
# cross-organ
"input_shape": ("input_shape", "step_aside"),
"admissibility_incompatible": ("hard_boundary", "refuse_known_boundary"),
# R1
"unsupported_clause_shape": ("capability_gap", "refuse_known_boundary"),
"ungrounded_base": ("missing_information", "ask_question"),
"over_determined": ("hard_boundary", "refuse_known_boundary"),
# R2 boundaries
"unsupported_system_size": ("scope_boundary", "refuse_known_boundary"),
"indistinguishable_system": ("hard_boundary", "refuse_known_boundary"),
"non_integer_solution": ("hard_boundary", "refuse_known_boundary"),
"negative_solution": ("hard_boundary", "refuse_known_boundary"),
"answer_choice_unresolved": ("ambiguous_structure", "refuse_known_boundary"),
# R2 growth surfaces — Q1-B reclassification (see Q1B_ASK_CARVE_OUT):
# disclosure says ask; REGISTRY still emits proposals to the pile.
"missing_total_count": ("missing_information", "ask_question"),
"missing_weighted_total": ("missing_information", "ask_question"),
"missing_category_pair": ("capability_gap", "emit_proposal"),
"missing_attribute_coefficient": ("capability_gap", "emit_proposal"),
# R2 verdict
"answer_key_contradiction": ("contradiction", "report_contradiction"),
# R3
"unsupported_rate_duration": ("capability_gap", "emit_proposal"),
"rate_underdetermined": ("missing_information", "ask_question"),
"unsupported_temporal_state": ("scope_boundary", "refuse_known_boundary"),
# R4 combined-rate boundaries
"cmb_unit_mismatch": ("hard_boundary", "refuse_known_boundary"),
"cmb_combine_ambiguous": ("ambiguous_structure", "ask_question"),
"cmb_underdetermined": ("missing_information", "ask_question"),
"cmb_non_positive_net": ("hard_boundary", "refuse_known_boundary"),
"cmb_non_integer": ("hard_boundary", "refuse_known_boundary"),
# R4 growth surfaces
"cmb_unsupported_rate_count": ("capability_gap", "emit_proposal"),
"cmb_unsupported_reciprocal": ("capability_gap", "emit_proposal"),
"cmb_unsupported_clock_interval": ("capability_gap", "emit_proposal"),
}
#: family name → typed slots an ASK assessment for that family identifies as missing.
#: Only families with a *known* slot structure appear here; an ask-mapped family without
#: an entry yields an empty residue (the "minimal" stance — never fabricate a slot the
#: family's contract has not pinned down yet). Slot semantics, per family:
#: - ``missing_total_count`` : the collective-unit total count (R2 constraint C7);
#: ``binding_target`` matches the equation slot the augmented input would fill
#: (Q2 re-entry, scoping §4).
#: - ``missing_weighted_total``: the measured-unit weighted total (R2 constraint C8).
#: Other ask-mapped families (``ungrounded_base``, ``rate_underdetermined``,
#: ``cmb_underdetermined``, ``cmb_combine_ambiguous``) get slots in later Q1 sub-PRs
#: once their per-family slot signatures are pinned with tests.
_FAMILY_TO_MISSING_SLOTS: dict[str, tuple[MissingSlot, ...]] = {
"missing_total_count": (
MissingSlot(
slot_name="total_count",
expected_unit_or_type="count_int",
binding_target="collective_unit_total",
),
),
"missing_weighted_total": (
MissingSlot(
slot_name="weighted_total",
expected_unit_or_type="measured_unit_int",
binding_target="weighted_total_value",
),
),
}
# A conservative refusal for any family/reason that is not in the mapping. The total
# mapping (asserted by test against REGISTRY) means this is dead in practice; if a new
# family ever lands unmapped, it refuses — it NEVER silently becomes an answerable
# question (the wrong=0-safe default).
_CONSERVATIVE_DEFAULT: tuple[LimitationKind, ResolutionAction] = (
"hard_boundary",
"refuse_known_boundary",
)
def assess_from_family(family: FailureFamily) -> LimitationAssessment:
"""The limitation a failure family expresses, as a typed assessment.
Total over :data:`REGISTRY` (asserted by test). An unmapped family falls to the
conservative refuse default never ``ask_question``. Residue defaults to empty;
populating typed slots / grounded terms requires a specific attempt (use
:func:`assess_from_attempt`).
"""
kind, action = _FAMILY_TO_LIMITATION.get(family.name, _CONSERVATIVE_DEFAULT)
return LimitationAssessment(
limitation_kind=kind,
resolution_action=action,
epistemic_state=_KIND_TO_STATE[kind],
owner_organ=family.owner,
blocking_reason=family.name,
)
def _residue_from_attempt(
attempt: ComprehensionAttempt,
family: FailureFamily,
action: ResolutionAction,
) -> tuple[tuple[MissingSlot, ...], tuple[str, ...]]:
"""Typed ASK residue for an ask-mapped attempt — empty for any other action.
Wrong=0 invariant (scoping §2): ``grounded_terms`` carries only verbatim text from
:attr:`ComprehensionAttempt.evidence` SourceSpanLinks. If the attempt carries no
evidence (today: every refused attempt :mod:`core.comprehension_attempt.classify`
leaves ``evidence = ()``), ``grounded_terms`` is empty, NEVER fabricated from the
family or the refusal reason. ``missing_slots`` is family-derived (snake_case
structural identifiers, not renderable prose) so it is always safe to emit; absent
from :data:`_FAMILY_TO_MISSING_SLOTS` no slots (minimal stance never invent a
slot the family contract has not pinned down).
"""
if action != "ask_question":
return ((), ())
slots = _FAMILY_TO_MISSING_SLOTS.get(family.name, ())
grounded = tuple(link.text for link in attempt.evidence)
return (slots, grounded)
def assess_from_attempt(attempt: ComprehensionAttempt) -> LimitationAssessment | None:
"""Classify the limitation a comprehension attempt expresses, or ``None``.
- ``contradiction`` outcome report it (no refusal reason carries this; it is the
answer-choice verdict).
- a refusal classify via its failure family; an *unclassified* refusal reason
falls to the conservative refuse default (never ``ask_question``).
- any non-limitation outcome (solved/admissible setup, or eval-only ``*_wrong``)
``None``: there is no resolvable limitation to act on.
For ask-mapped refusals, the returned assessment carries typed residue
(:attr:`~LimitationAssessment.missing_slots` / ``grounded_terms``) per
:func:`_residue_from_attempt`'s wrong=0 invariant.
"""
if attempt.outcome == "contradiction":
return LimitationAssessment(
limitation_kind="contradiction",
resolution_action="report_contradiction",
epistemic_state=EpistemicState.CONTRADICTED,
owner_organ=attempt.organ,
blocking_reason="answer_key_contradiction",
)
if not attempt.is_refusal:
return None
family = family_for_reason(attempt.refusal_reason)
if family is None:
kind, action = _CONSERVATIVE_DEFAULT
return LimitationAssessment(
limitation_kind=kind,
resolution_action=action,
epistemic_state=_KIND_TO_STATE[kind],
owner_organ=attempt.organ,
blocking_reason=attempt.refusal_reason or "unclassified",
)
base = assess_from_family(family)
missing_slots, grounded_terms = _residue_from_attempt(
attempt, family, base.resolution_action
)
if not missing_slots and not grounded_terms:
return base
return LimitationAssessment(
limitation_kind=base.limitation_kind,
resolution_action=base.resolution_action,
epistemic_state=base.epistemic_state,
owner_organ=attempt.organ,
blocking_reason=base.blocking_reason,
missing_slots=missing_slots,
grounded_terms=grounded_terms,
)
def terminal_for_action(action: ResolutionAction) -> Terminal:
"""The shipped contemplation ``Terminal`` an action corresponds to — total.
Every action maps to a shipped terminal; ``ask_question`` resolves to
``QUESTION_NEEDED`` (added by Q1-D, sibling of ``PROPOSAL_EMITTED``). That totality
is what makes this a consolidating view rather than a new taxonomy. This is the
action's *home* terminal; the terminal a Q1-D delivery actually emits may differ
when the question is unrenderable (the D2 fallback see
:mod:`core.epistemic_questions.delivery`).
"""
return _ACTION_TO_TERMINAL[action]
__all__ = [
"Q1B_ASK_CARVE_OUT",
"LimitationAssessment",
"LimitationKind",
"MissingSlot",
"ResolutionAction",
"assess_from_attempt",
"assess_from_family",
"terminal_for_action",
]

View file

@ -1,33 +0,0 @@
"""VERIFIED serving gate helper — default-dark, no served-surface wiring.
This module centralizes the future kill-switch predicate for VERIFIED serving.
It is default-dark / fail-closed: if the config field is missing or malformed,
it must evaluate to False.
This helper only centralizes the future kill-switch predicate so that future serving code
has one audited predicate. It does not wire any served-surface or implement served
VERIFIED behavior. Missing field means False.
Note that eval-gold-backed producers (such as the verification producer in
evals/constraint_oracle/verified_producer.py) are not serving-eligible.
"""
from __future__ import annotations
from typing import Any
from core.config import DEFAULT_CONFIG, RuntimeConfig
def verified_serving_enabled(config: RuntimeConfig | Any | None = None) -> bool:
"""Return whether served VERIFIED delivery is explicitly enabled.
Missing attribute means False. This is the load-bearing dark-gate invariant:
the served VERIFIED path cannot light merely because the helper exists or because
an older RuntimeConfig instance lacks the future field.
"""
cfg = DEFAULT_CONFIG if config is None else config
return bool(getattr(cfg, "verified_serving_enabled", False))
__all__ = ["verified_serving_enabled"]

View file

@ -1,211 +0,0 @@
"""P1-A — the VERIFIED contract (the meaning of "verified", before any producer).
This module defines WHAT IT MEANS for a result to earn ``DisclosureClaim.VERIFIED`` /
``EpistemicState.VERIFIED`` the soundnesscorrectness gate the whole VERIFIED lane
rests on. It ships ONLY the contract: the obligation, the proof SHAPE a producer must
fill, the validator that enforces the obligation, and the single sanctioned route from
a validated proof to the verified state/claim.
It does NOT produce proofs. There is no reader, no solver, no back-substitution
computation, no serving, no ``verify.py`` call, no ``verified_serving_enabled``. P1-B+
fill :class:`VerificationProof` with real digests; P1-A only fixes the rules a proof
must satisfy and, above all, the rule that makes the lane safe:
A faithful solve of a WRONG read must NOT verify.
**The mechanism.** Verification requires TWO INDEPENDENT reads (distinct reader
lineages) that CONVERGE on the same canonical structure:
* a *wrong* primary read is caught because the independent read **disagrees**
back-substitution alone cannot catch a read error (it only proves the solver was
faithful to whatever structure it was handed);
* a single reader run twice ("same answer twice") is rejected as **not independent**.
Neither gold-agreement, nor absence-of-refusal, nor a second solver over ONE read can
earn VERIFIED; only this contract can. (See [[VERIFIED-canonical-comparison-scoping-2026-06-06]]:
"independence must be in the READING, not the solving".)
**Discipline.** ``EpistemicState.VERIFIED`` must be reached ONLY via
:func:`disclosure_for_verification` over a VERIFIED :class:`VerificationResult` never
constructed directly by producer/serving code. A future architectural invariant can
scan for direct emission; P1-A establishes the route.
Off-serving: imports no ``generate.derivation`` / ``core.reliability_gate``.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum, unique
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_state import EpistemicState
@unique
class VerificationVerdict(str, Enum):
"""Whether a result earns the VERIFIED claim. There is no middle state — a result
either survives every obligation or it does not verify (refuse-preferring)."""
VERIFIED = "verified"
NOT_VERIFIED = "not_verified"
@dataclass(frozen=True, slots=True)
class VerificationObligation:
"""The declarative contract: which checks a proof must survive to be VERIFIED.
A schema naming proof obligations (CLAUDE.md "Schema-Defined Proof Obligations").
The canonical :data:`VERIFICATION_OBLIGATION` sets every flag ``True``; the flags
exist so a test can prove each obligation is LOAD-BEARING relax exactly one and
the corresponding poison case slips through (see ``test_verified_contract.py``).
"""
requires_independent_read: bool # two DISTINCT reader lineages — not the same read twice
rejects_wrong_read_even_if_solved: bool # the two reads must CONVERGE — catches a wrong read
requires_bound_slots: bool # the answer binds to the STATED slots (query/unknowns), not phantom ones
requires_back_substitution: bool # the answer back-substitutes into the canonical structure
requires_boundary_clear: bool # no organ boundary fired in the chain
#: The canonical, fully-strict obligation. VERIFIED requires ALL of it.
VERIFICATION_OBLIGATION: VerificationObligation = VerificationObligation(
requires_independent_read=True,
rejects_wrong_read_even_if_solved=True,
requires_bound_slots=True,
requires_back_substitution=True,
requires_boundary_clear=True,
)
@dataclass(frozen=True, slots=True)
class VerificationProof:
"""The replayable proof shape a producer (P1-B+) must fill. P1-A defines the shape
and the rules; it computes none of these digests.
The two reads are kept separate ON PURPOSE: ``primary_reader_lineage`` /
``independent_reader_lineage`` must DIFFER (independence), and ``primary_read_digest``
/ ``independent_read_digest`` must MATCH (convergence on one canonical structure).
Independence + convergence together are what reject a faithful solve of a wrong read.
The "from the stated quantities" obligation is split into THREE separable digests
(P1-C hardening for replay, audit, and failure localization):
``derivation_digest`` (a solve happened from the structure), ``bound_slots_digest``
(the answer binds to a STATED slot the asked unknown not a phantom), and
``back_substitution_digest`` (the answer satisfies the constraints). A complete
derivation does NOT imply the answer bound to a declared slot, so the two are
distinct obligations (``test_derivation_digest_alone_is_insufficient_without_bound_slots``).
"""
source_problem_digest: str # provenance: hash of the problem text
primary_reader_lineage: str # identity of the primary reader
independent_reader_lineage: str # identity of the independent cross-check reader
primary_read_digest: str # canonical structure the primary read produced
independent_read_digest: str # canonical structure the independent read produced
derivation_digest: str # the derivation (solve) from the STATED quantities
bound_slots_digest: str # the answer binds to the STATED slots (asked unknown), not a phantom
back_substitution_digest: str # back-substitution into the canonical structure
boundary_clear: bool # no organ boundary fired
contradiction_clear: bool # no contradiction family fired
# Reason codes for a failed obligation — each names exactly one violated rule.
REASON_READS_NOT_INDEPENDENT = "reads_not_independent" # same reader lineage twice
REASON_READS_DISAGREE = "reads_disagree" # the wrong-read catcher
REASON_NO_BOUND_SLOTS = "no_bound_slots" # answer did not bind to a stated slot
REASON_NO_BACK_SUBSTITUTION = "no_back_substitution"
REASON_BOUNDARY_FIRED = "boundary_fired"
REASON_CONTRADICTION_PRESENT = "contradiction_present"
REASON_INCOMPLETE_PROOF = "incomplete_proof_digest"
REASON_UNRESOLVED_LIMITATION = "unresolved_limitation"
@dataclass(frozen=True, slots=True)
class VerificationResult:
"""The verdict plus the specific obligations that failed (empty iff VERIFIED)."""
verdict: VerificationVerdict
failed_checks: tuple[str, ...]
def evaluate_verification(
proof: VerificationProof,
*,
limitation: LimitationAssessment | None,
obligation: VerificationObligation = VERIFICATION_OBLIGATION,
) -> VerificationResult:
"""Apply the VERIFIED contract to a proof. Refuse-preferring: VERIFIED only if
EVERY obligation survives; otherwise NOT_VERIFIED with the failed checks.
Pure logic over the proof fields it does NOT compute or trust any digest, it only
enforces the rules a producer's proof must satisfy. ``limitation`` is the
contemplation outcome: a verified answer cannot coexist with an unresolved
limitation (a contradiction, an ambiguity, a missing datum).
"""
failed: list[str] = []
if obligation.requires_independent_read and (
proof.primary_reader_lineage == proof.independent_reader_lineage
):
failed.append(REASON_READS_NOT_INDEPENDENT)
if obligation.rejects_wrong_read_even_if_solved and (
proof.primary_read_digest != proof.independent_read_digest
):
failed.append(REASON_READS_DISAGREE)
if obligation.requires_bound_slots and not proof.bound_slots_digest:
failed.append(REASON_NO_BOUND_SLOTS)
if obligation.requires_back_substitution and not proof.back_substitution_digest:
failed.append(REASON_NO_BACK_SUBSTITUTION)
if obligation.requires_boundary_clear and not proof.boundary_clear:
failed.append(REASON_BOUNDARY_FIRED)
# Non-negotiable checks (NOT gated by the obligation flags):
if not proof.contradiction_clear:
failed.append(REASON_CONTRADICTION_PRESENT)
if not (
proof.source_problem_digest
and proof.primary_read_digest
and proof.derivation_digest
):
failed.append(REASON_INCOMPLETE_PROOF)
if limitation is not None:
failed.append(REASON_UNRESOLVED_LIMITATION)
verdict = (
VerificationVerdict.VERIFIED if not failed else VerificationVerdict.NOT_VERIFIED
)
return VerificationResult(verdict=verdict, failed_checks=tuple(failed))
def disclosure_for_verification(
result: VerificationResult,
) -> tuple[EpistemicState, DisclosureClaim]:
"""The ONLY sanctioned route to a verified state/claim.
VERIFIED verdict (``EpistemicState.VERIFIED``, ``DisclosureClaim.VERIFIED``);
anything else (``UNDETERMINED``, ``NONE``). Producer/serving code must reach
``EpistemicState.VERIFIED`` THROUGH this gate, never by constructing it directly
that is what keeps "gold agreement" / "same answer twice" / "no refusal" from ever
masquerading as verified.
"""
if result.verdict is VerificationVerdict.VERIFIED:
return EpistemicState.VERIFIED, DisclosureClaim.VERIFIED
return EpistemicState.UNDETERMINED, DisclosureClaim.NONE
__all__ = [
"VERIFICATION_OBLIGATION",
"VerificationObligation",
"VerificationProof",
"VerificationResult",
"VerificationVerdict",
"disclosure_for_verification",
"evaluate_verification",
]

View file

@ -1,40 +0,0 @@
"""The epistemic question organ — render (Q1-C) and off-serving delivery (Q1-D).
Q1-C (:mod:`core.epistemic_questions.render`) turns an ASK
:class:`~core.epistemic_disclosure.limitation.LimitationAssessment` into an
:class:`~core.epistemic_questions.render.EpistemicQuestion` under the wrong=0
grounded-rendering invariant (scoping §2) render only, no fabrication.
Q1-D (:mod:`core.epistemic_questions.delivery`) routes that rendered question onto
the contemplation bus as the ``QUESTION_NEEDED`` tenant and writes a proposal-only
artifact to the ``teaching/questions`` sink consuming the renderer verbatim, never
rendering. Off-serving: no served surface, no ``ask_serving_enabled`` yet.
"""
from __future__ import annotations
from core.epistemic_questions.delivery import (
AnswerBinding,
DeliveredQuestion,
DeliveryOutcome,
default_question_root,
deliver_ask,
emit_question,
question_path,
)
from core.epistemic_questions.render import (
EpistemicQuestion,
render_question,
)
__all__ = [
"AnswerBinding",
"DeliveredQuestion",
"DeliveryOutcome",
"EpistemicQuestion",
"default_question_root",
"deliver_ask",
"emit_question",
"question_path",
"render_question",
]

View file

@ -1,272 +0,0 @@
"""Q1-D — off-serving ASK delivery: route a rendered question onto the bus.
The fourth and final Q1 rung. Given an ``ask_question``
:class:`~core.epistemic_disclosure.limitation.LimitationAssessment`, decide the
contemplation :class:`~generate.contemplation.findings.Terminal` and, when a question
can honestly be asked, produce a :class:`DeliveredQuestion` artifact for the
review-gated, **question-only** ``teaching/questions`` sink. A question is an *intake
request*, NOT a capability proposal ``question_only`` is its own lane, distinct from
the ``proposal_only`` lane of :mod:`core.comprehension_attempt.proposal`. This is the
ASK *analogue* of that emitter (and just as toothless: it never serves, never mounts,
always requires review), but the artifacts must never be conflated.
**The rung separation (the steer).** Q1-D *consumes* the Q1-C
:class:`~core.epistemic_questions.render.EpistemicQuestion` it does NOT render.
:func:`render_question` is called exactly once (in :func:`deliver_ask`) and its
result is wrapped verbatim; Q1-D constructs no user-facing prose of its own, so the
Q1-C grounded-rendering wrong=0 guard is never bypassed by a second surface.
Q1-B: typed residue (what is missing, as typed slots)
Q1-C: renderability (can it be asked without fabricating? EpistemicQuestion)
Q1-D: delivery (route the rendered question onto the bus) here
**D2 the delivery-side wrong=0 guard.** A contentless ``QUESTION_NEEDED`` is worse
than useless: it is a *false intake surface* (the user is invited to answer a question
that names nothing). So when :func:`render_question` returns ``unrenderable``
(``renderability_gap`` / ``multi_slot_not_supported`` / ``no_missing_slot`` / the
fabrication backstop), :func:`deliver_ask` does NOT emit ``QUESTION_NEEDED``. It falls
back to the family's *standing disposition*:
family still proposes (``proposal_allowed``) ``PROPOSAL_EMITTED``
otherwise ``NO_PROGRESS``
This guard is enforced twice: in :func:`deliver_ask`'s branch, and structurally — a
:class:`DeliveredQuestion` *cannot wrap an unrenderable question* (its
``__post_init__`` refuses), so ``QUESTION_NEEDED`` is unreachable without a real,
rendered question.
**D3 the carve-out stays.** Q1-D delivery is OFF-SERVING. It does not flip the
``Q1B_ASK_CARVE_OUT`` (``missing_total_count`` / ``missing_weighted_total`` keep
``proposal_allowed = True`` in the registry, so the proposal pile keeps working). Both
signals coexist during the carve-out the off-serving question artifact AND the
existing proposal so no operational signal is lost. The flip to ``ask`` waits on a
future ``ask_serving_enabled`` gate, not on this rung.
**D5 single-slot only.** Q1-C refuses multi-slot rendering, so Q1-D delivers at most
one ``DeliveredQuestion`` per assessment; a multi-slot assessment is ``unrenderable``
(``multi_slot_not_supported``) and takes the D2 fallback. No fan-out here.
**Off-serving.** Imports nothing from ``generate.derivation`` / ``core.reliability_gate``;
it cannot move the sealed GSM8K metric, and there is NO served surface no
``ask_serving_enabled``, no ``chat/runtime.py`` wiring. The artifacts land in the
review-gated teaching sink and nowhere else.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.comprehension_attempt.failure_family import family_by_name
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_questions.render import EpistemicQuestion, render_question
from generate.contemplation.findings import Terminal
#: The sink status — the ASK analogue of the proposal emitter's ``"proposal_only"``.
#: A delivered question is review-gated intake, never a served answer.
_QUESTION_STATUS = "question_only"
@dataclass(frozen=True, slots=True)
class AnswerBinding:
"""RESERVED for Q2 — the typed seat a future answer round-trip binds into.
Q1-D produces NO ``AnswerBinding`` (every :class:`DeliveredQuestion` carries
``answer_binding = None``; see that class's ``__post_init__``). The seat exists so
the Q2 binder which must re-enter the limitation gate with augmented input, never
mutate the model mid-flight (scoping §4) is wireable without reshaping the
artifact. ``target_slot`` / ``binding_target`` mirror the
:class:`~core.epistemic_disclosure.limitation.MissingSlot` the answer fills.
"""
target_organ: str
target_slot: str
binding_target: str
parser: str
unit: str | None = None
@dataclass(frozen=True, slots=True)
class DeliveredQuestion:
"""A review-gated, question-only ASK artifact wrapping a rendered Q1-C question.
``question_only`` is its OWN lane (an intake request), not the ``proposal_only``
lane the analogy to :class:`~core.comprehension_attempt.proposal.FailureProposal`
is structural (toothless, review-gated, never served), never semantic.
The invariant fields are enforced in ``__post_init__`` so even a hand-constructed
instance cannot become a contentless question, a served question, or an
answer-bound (Q2) question illegal states are unrepresentable:
- it can never wrap an ``unrenderable`` question (the D2 guard, structurally)
so a ``QUESTION_NEEDED`` terminal always carries real, rendered text;
- it can never be ``served`` (off-serving; ``ask_serving_enabled`` does not exist);
- it always ``requires_review``;
- its ``answer_binding`` is always ``None`` in Q1-D (the Q2 seat is reserved, unbound).
"""
question: EpistemicQuestion
owner_organ: str | None
blocking_reason: str
answer_binding: AnswerBinding | None = None
status: str = _QUESTION_STATUS
requires_review: bool = True
served: bool = False
def __post_init__(self) -> None:
if self.question.unrenderable or self.question.text is None:
raise ValueError(
"a DeliveredQuestion cannot wrap an unrenderable question "
f"(reason={self.question.reason!r}); the D2 fallback handles it"
)
if self.status != _QUESTION_STATUS:
raise ValueError(
f"question status must be {_QUESTION_STATUS!r}; got {self.status!r}"
)
if self.served:
raise ValueError("a Q1-D delivered question is never served")
if not self.requires_review:
raise ValueError("a delivered question always requires review")
if self.answer_binding is not None:
raise ValueError("answer_binding is reserved for Q2; Q1-D emits None")
def content_hash(self) -> str:
"""Deterministic content address: same question on the same blocking family and
slot always yields the same hash (idempotent sink writes). No clock, no
randomness. The rendered ``text`` is included so a template change re-addresses.
"""
slot_name = self.question.slot.slot_name if self.question.slot else ""
payload = f"{self.blocking_reason}:{slot_name}:{self.question.text}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def to_json_dict(self) -> dict[str, Any]:
slot = self.question.slot
return {
"status": self.status,
"blocking_reason": self.blocking_reason,
"owner_organ": self.owner_organ,
"question": {
"text": self.question.text,
"reason": self.question.reason,
"slot_name": slot.slot_name if slot else None,
"expected_unit_or_type": slot.expected_unit_or_type if slot else None,
"binding_target": slot.binding_target if slot else None,
},
"answer_binding": None, # reserved (Q2)
"requires_review": self.requires_review,
"served": self.served,
}
@dataclass(frozen=True, slots=True)
class DeliveryOutcome:
"""The result of routing one ``ask_question`` assessment.
``question`` is the artifact iff ``terminal is QUESTION_NEEDED`` (a renderable ask);
otherwise it is ``None`` and ``fallback_reason`` carries the unrenderable reason that
triggered the D2 standing-disposition fallback.
"""
terminal: Terminal
question: DeliveredQuestion | None
fallback_reason: str | None
def _standing_disposition(blocking_reason: str) -> Terminal:
"""The D2 fallback terminal for an unrenderable ask: the family's standing move.
A family that still proposes (``proposal_allowed``) falls back to
``PROPOSAL_EMITTED`` its existing operational signal, preserved (D3). Anything
else (an unknown reason, or a family that does not propose) falls back to
``NO_PROGRESS``. Never a contentless ``QUESTION_NEEDED``.
"""
family = family_by_name(blocking_reason)
if family is not None and family.proposal_allowed:
return Terminal.PROPOSAL_EMITTED
return Terminal.NO_PROGRESS
def deliver_ask(assessment: LimitationAssessment) -> DeliveryOutcome:
"""Route an ``ask_question`` assessment to a terminal — render via Q1-C, never here.
Renderable ``QUESTION_NEEDED`` + a :class:`DeliveredQuestion` wrapping the Q1-C
result verbatim. Unrenderable the D2 standing-disposition fallback (no artifact).
Raises ``ValueError`` if called on a non-ASK assessment: the bus routes only
``ask_question`` resolutions here, so any other action is a wiring error, not a
runtime input fail loudly rather than silently mis-deliver.
"""
if assessment.resolution_action != "ask_question":
raise ValueError(
"deliver_ask is only valid for ask_question assessments; got "
f"{assessment.resolution_action!r}"
)
question = render_question(assessment)
if question.unrenderable:
return DeliveryOutcome(
terminal=_standing_disposition(assessment.blocking_reason),
question=None,
fallback_reason=question.reason,
)
delivered = DeliveredQuestion(
question=question,
owner_organ=assessment.owner_organ,
blocking_reason=assessment.blocking_reason,
)
return DeliveryOutcome(
terminal=Terminal.QUESTION_NEEDED,
question=delivered,
fallback_reason=None,
)
def default_question_root() -> Path:
"""``<repo>/teaching/questions`` — the write-only, review-gated ASK sink.
A sibling of ``teaching/proposals`` (D4): questions are intake requests, not
capability proposals, so they do not overload the proposal pile.
"""
return Path(__file__).resolve().parents[2] / "teaching" / "questions"
def question_path(delivered: DeliveredQuestion, root: Path | None = None) -> Path:
base = root if root is not None else default_question_root()
return base / f"{delivered.content_hash()}.json"
def emit_question(
assessment: LimitationAssessment, *, root: Path | None = None
) -> Path | None:
"""Deliver an ask assessment and, iff it renders, write the artifact to the sink.
Returns the artifact path for a ``QUESTION_NEEDED`` delivery, or ``None`` when the
ask was unrenderable and fell back (D2) no contentless artifact is ever written.
Idempotent: the same question writes the same content-addressed path with
byte-identical content (``sort_keys``). Creates the sink directory on demand.
"""
outcome = deliver_ask(assessment)
if outcome.question is None:
return None
path = question_path(outcome.question, root)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(outcome.question.to_json_dict(), indent=2, sort_keys=True),
encoding="utf-8",
)
return path
__all__ = [
"AnswerBinding",
"DeliveredQuestion",
"DeliveryOutcome",
"default_question_root",
"deliver_ask",
"emit_question",
"question_path",
]

View file

@ -1,195 +0,0 @@
"""The grounded-only question renderer (Q1-C) — wrong=0-safe by construction.
This is the renderer rung of the Epistemic Disclosure ASK spine: given a
:class:`~core.epistemic_disclosure.limitation.LimitationAssessment` whose
``resolution_action == "ask_question"`` and which carries at least one typed
:class:`~core.epistemic_disclosure.limitation.MissingSlot`, produce an
:class:`EpistemicQuestion` a rendered user-facing question, or an explicit
``question_unrenderable`` verdict. Nothing here delivers, serves, or chooses a
disposition (that is Q1-D); this is *only* surface realization of the residue.
**The wrong=0 invariant (scoping §2 / session §1.5.7) the whole point.**
A question may name an entity, slot, unit, or relation only if it appears
*verbatim* in the assessment's ``grounded_terms``. When the grounded terms
lack what a question needs, degrade to a generic question or emit
``question_unrenderable`` never a named guess.
Two substrate facts force the conservative policy below:
1. ``grounded_terms`` is empty for every assessment produced today the readers
do not yet emit verbatim evidence on refusal (scoping §3, the substrate gap
Q1 must close first). So there is no grounded problem-entity to name.
2. A *missing* slot's referent is, by definition, absent from the comprehension
trace the missing thing can never appear in ``grounded_terms`` even once
readers do emit evidence. ``grounded_terms`` can only supply *context*
entities, and binding a slot to its context entity needs an alignment step
that Q1-C does not have (a later rung).
**Chosen rendering policy generic-structural, names zero problem entities.**
Because Q1-C can neither (today) read grounded context nor (ever, for the slot
itself) name the missing referent from grounded text, the only wrong=0-safe
artifact it can render is a *generic* question whose sole variable content is a
controlled English phrase for the slot's structural *type*
(``expected_unit_or_type``), drawn from the closed, audited
:data:`_CLOSED_TYPE_PHRASES` map below. Concretely the renderer:
- NEVER surfaces ``slot_name`` or ``binding_target`` these are snake_case
structural identifiers, and user-facing prose must never come from them
(limitation.py ``MissingSlot`` docstring; session §1.5.7). ``slot_name`` is
also the field most likely to *read* like a fabricated entity (``ben_rate``
the forbidden "Ben"), so it is never touched.
- NEVER prettifies a snake_case identifier into a natural-language entity no
capitalization, no possessive, no splitting on underscores.
- Translates ``expected_unit_or_type`` through the closed map only; an unmapped
type degrades to ``question_unrenderable`` (a ``renderability_gap``) rather
than dumping raw snake_case or guessing.
- Names no problem-specific entity at all. The closed type phrases ("a
whole-number count") are generic structural descriptors that assert nothing
about *this* problem distinct from problem-specific names, which would need
grounding. This is the line scoping §2 draws ("generic, all terms grounded"
vs. the fabricated "Ben").
A post-render fabrication guard (:func:`_names_only_grounded`) re-checks that
every word in the rendered text is closed-vocabulary scaffold or appears in
``grounded_terms`` defense in depth so a fabricated token can never escape even
if the template were later edited carelessly.
**Off-serving.** Imports nothing from ``generate.derivation`` or
``core.reliability_gate``; it cannot move the sealed GSM8K metric.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from core.epistemic_disclosure.limitation import (
LimitationAssessment,
MissingSlot,
)
#: Closed, audited map from a family-pinned ``expected_unit_or_type`` to a
#: controlled English phrase. Keys are exactly the slot types that ship today
#: (``core.epistemic_disclosure.limitation._FAMILY_TO_MISSING_SLOTS``). A slot
#: whose type is absent here is NOT rendered — the renderer refuses with a
#: ``renderability_gap`` rather than surfacing raw snake_case. New slot types
#: earn a phrase here (with a test) when their family lands, never by guessing.
_CLOSED_TYPE_PHRASES: dict[str, str] = {
"count_int": "a whole-number count",
"measured_unit_int": "a whole-number quantity",
}
#: The fixed question scaffold. The only hole is the closed type phrase; every
#: other word is constant, problem-independent English. It names no entity.
_TEMPLATE = (
"To answer this, one more value is still needed — {phrase} — that the "
"problem does not state. What is it?"
)
#: Machine reasons for an :class:`EpistemicQuestion`. Closed set.
_REASON_RENDERED = "rendered"
_REASON_NOT_ASK = "not_ask"
_REASON_NO_SLOT = "no_missing_slot"
_REASON_MULTI_SLOT = "multi_slot_not_supported"
_REASON_RENDERABILITY_GAP = "renderability_gap"
_REASON_FABRICATION_GUARD = "fabrication_guard"
def _tokens(text: str) -> set[str]:
"""Lowercased maximal alphabetic runs — the unit the fabrication guard checks."""
return set(re.findall(r"[a-z]+", text.lower()))
#: Every word the renderer is allowed to emit *without* grounding: the scaffold
#: words plus the words of every closed type phrase. Built once at import.
_ALLOWED_SCAFFOLD_WORDS: frozenset[str] = frozenset(
_tokens(_TEMPLATE.replace("{phrase}", " "))
| {w for phrase in _CLOSED_TYPE_PHRASES.values() for w in _tokens(phrase)}
)
@dataclass(frozen=True, slots=True)
class EpistemicQuestion:
"""The rendered ASK artifact, or an explicit unrenderable verdict.
``slot`` is the bound :class:`MissingSlot` the question is about present
whenever a slot was selected, ``None`` when the assessment carried no slot to
bind (non-ASK, or zero slots). ``text`` is the rendered question, or ``None``
when ``unrenderable``. ``reason`` is a closed-set machine string (one of the
``_REASON_*`` constants) explaining the verdict; for a renderable question it
is :data:`_REASON_RENDERED`.
"""
slot: MissingSlot | None
text: str | None
unrenderable: bool
reason: str
def _unrenderable(reason: str, slot: MissingSlot | None = None) -> EpistemicQuestion:
"""A ``question_unrenderable`` verdict with no text."""
return EpistemicQuestion(slot=slot, text=None, unrenderable=True, reason=reason)
def _names_only_grounded(text: str, grounded_terms: tuple[str, ...]) -> bool:
"""True iff every word in ``text`` is closed-vocab scaffold or grounded.
The wrong=0 guard, enforced post-render as defense in depth: a fabricated
entity (a word neither in the closed scaffold/phrase vocabulary nor verbatim
in ``grounded_terms``) makes this return ``False``, and the renderer refuses.
With today's empty ``grounded_terms`` and a fully closed-vocab template this
holds by construction; the guard exists so that can never silently change.
"""
allowed = _ALLOWED_SCAFFOLD_WORDS | _tokens(" ".join(grounded_terms))
return _tokens(text) <= allowed
def render_question(assessment: LimitationAssessment) -> EpistemicQuestion:
"""Render a single-slot generic ASK question, or refuse to render.
Strictly single-slot: Q1-C renders only when the assessment carries
*exactly one* missing slot. The fixed template asserts "one more value is
still needed" — a globally-quantified claim that exactly one value is
missing so rendering the first of several slots and ignoring the rest
would make that sentence subtly false (it would imply the single rendered
value closes the gap when others remain). Rather than weaken the template to
an honest-but-vaguer plural, a multi-slot assessment refuses with
``multi_slot_not_supported`` (slot ``None``); one-question-per-slot fan-out
is a later rung. The renderer also refuses (``question_unrenderable``) when
the assessment is not an ASK, carries no slot, or the slot's structural type
is outside the closed phrase map. It NEVER fabricates a natural-language
entity name see the module docstring for the policy and the wrong=0
rationale.
"""
if assessment.resolution_action != "ask_question":
return _unrenderable(_REASON_NOT_ASK)
if not assessment.missing_slots:
return _unrenderable(_REASON_NO_SLOT)
if len(assessment.missing_slots) > 1:
# Strictly single-slot: the template claims exactly one value is
# missing, which is false when several slots remain. Refuse rather than
# render the first and silently drop the rest.
return _unrenderable(_REASON_MULTI_SLOT, slot=None)
slot = assessment.missing_slots[0]
phrase = _CLOSED_TYPE_PHRASES.get(slot.expected_unit_or_type)
if phrase is None:
# Unknown structural type: refuse rather than surface raw snake_case.
return _unrenderable(_REASON_RENDERABILITY_GAP, slot=slot)
text = _TEMPLATE.format(phrase=phrase)
if not _names_only_grounded(text, assessment.grounded_terms):
# Unreachable by construction; the guard is the wrong=0 backstop.
return _unrenderable(_REASON_FABRICATION_GUARD, slot=slot)
return EpistemicQuestion(
slot=slot, text=text, unrenderable=False, reason=_REASON_RENDERED
)
__all__ = [
"EpistemicQuestion",
"render_question",
]

View file

@ -1,32 +0,0 @@
"""ASK serving gate helper — default-dark, no served-surface wiring.
This module is the first code slice after the ASK serving-integration scoping brief.
It intentionally does **not** call ``deliver_ask``/``emit_question``, does not import
``chat.runtime``, and does not expose any user-facing surface. It only centralizes the
kill-switch read so future serving code has one audited predicate.
The planned config field is ``RuntimeConfig.ask_serving_enabled``. During this dark-gate
slice the predicate is conservative: absent field == ``False``. That lets the helper land
without widening behavior and preserves the current default for every existing
``RuntimeConfig`` instance.
"""
from __future__ import annotations
from typing import Any
from core.config import DEFAULT_CONFIG, RuntimeConfig
def ask_serving_enabled(config: RuntimeConfig | Any | None = None) -> bool:
"""Return whether served ASK delivery is explicitly enabled.
Missing attribute means ``False``. That is the load-bearing dark-gate invariant:
the served ASK path cannot light merely because the helper exists or because an
older ``RuntimeConfig`` instance lacks the future field.
"""
cfg = DEFAULT_CONFIG if config is None else config
return bool(getattr(cfg, "ask_serving_enabled", False))
__all__ = ["ask_serving_enabled"]

View file

@ -15,7 +15,6 @@ from core.learning_arena.protocols import (
DomainSolver,
GoldTether,
Problem,
Tier2Verifier,
)
from core.learning_arena.report import (
REFUSAL_DIAGNOSES,
@ -32,7 +31,6 @@ __all__ = [
"DomainSolver",
"GoldTether",
"Problem",
"Tier2Verifier",
"REFUSAL_DIAGNOSES",
"EliminationRecord",
"PracticeReport",

View file

@ -15,20 +15,14 @@ Invariants (the load-bearing ADR-0199 mandates, enforced structurally here):
never touches a serving path or the active teaching corpus. Promotion is the
caller's separate ``propose_from_ledger`` step into the reviewed corridor.
- **L-4 (determinism).** Pure fold over the input order; identical
(problems, solver, tether, diagnose, tier2_verifier) -> identical report.
(problems, solver, tether, diagnose) -> identical report.
"""
from __future__ import annotations
from typing import Callable, Sequence
from core.learning_arena.protocols import (
Attempt,
DomainProblem,
DomainSolver,
GoldTether,
Tier2Verifier,
)
from core.learning_arena.protocols import Attempt, DomainProblem, DomainSolver, GoldTether
from core.learning_arena.report import EliminationRecord, PracticeReport
from core.reliability_gate import ClassTally
@ -49,7 +43,6 @@ def run_practice(
tether: GoldTether,
*,
diagnose: Callable[[str], str] = _default_diagnose,
tier2_verifier: Tier2Verifier | None = None,
) -> PracticeReport:
"""Sealed practice: attempt -> gold-tether score -> per-class ledger.
@ -67,36 +60,21 @@ def run_practice(
for problem in problems:
cls = problem.class_name
attempt: Attempt = solver.attempt(problem)
gold_correct = False
if not attempt.committed:
verdict = "refused"
elif tether.is_correct(attempt, problem):
verdict = "correct"
else:
gold_correct = tether.is_correct(attempt, problem)
verdict = "correct" if gold_correct else "wrong"
verdict = "wrong"
counts[verdict] = counts.get(verdict, 0) + 1
tally = ledger.get(cls) or ClassTally(cls)
t2_verified = 0
t2_agrees_gold = 0
if attempt.committed and tier2_verifier is not None:
t2_verdict = tier2_verifier.verify(attempt, problem)
if t2_verdict.verified:
t2_verified = 1
t2_agrees_gold = 1 if gold_correct else 0
if verdict == "correct":
tally = tally.record(
correct=1,
t2_verified=t2_verified,
t2_agrees_gold=t2_agrees_gold,
)
tally = tally.record(correct=1)
elif verdict == "wrong":
tally = tally.record(
wrong=1,
t2_verified=t2_verified,
t2_agrees_gold=t2_agrees_gold,
)
tally = tally.record(wrong=1)
elims.append(
EliminationRecord(
case_id=attempt.case_id,

View file

@ -21,8 +21,6 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
from core.reasoning.evidence import Tier2Verdict
@runtime_checkable
class DomainProblem(Protocol):
@ -88,15 +86,6 @@ class GoldTether(Protocol):
def gold_answer(self, problem: DomainProblem) -> Any: ...
@runtime_checkable
class Tier2Verifier(Protocol):
"""Optional convergent self-verifier for a domain attempt."""
domain_id: str
def verify(self, attempt: Attempt, problem: DomainProblem) -> Tier2Verdict: ...
@dataclass(frozen=True, slots=True)
class Problem:
"""Concrete :class:`DomainProblem` a domain adapter can build directly."""

View file

@ -1,9 +1,9 @@
"""ADR-0199 / ADR-0175 — the domain-agnostic practice report.
Extracted from ``evals/gsm8k_math/practice/v1/runner.py`` so every subject's
arena emits the same report shape. The report now also exposes the existing
Tier-2 ledger counts when a domain supplies convergent self-verification
evidence; domains without a Tier-2 verifier report zeros.
Extracted verbatim (schema-preserving) from
``evals/gsm8k_math/practice/v1/runner.py`` so every subject's arena emits the
same report shape. ``PracticeReport.as_dict`` is byte-stable with the original
GSM8K report so existing goldens and ``report.json`` are unaffected.
The three refusal-diagnosis axes are the universal ADR-0175 §8 router
(skill / knowledge / ambiguity), not a domain quantity so they live here.
@ -59,9 +59,6 @@ class PracticeReport:
"committed": t.committed,
"reliability": t.reliability,
"coverage": t.coverage,
"t2_verified": t.t2_verified,
"t2_agrees_gold": t.t2_agrees_gold,
"t2_precision": t.t2_precision,
}
for cls, t in sorted(self.ledger.items())
},

View file

@ -1,38 +0,0 @@
"""Read-only proposal review reporter (RPT) — surfaces comprehension-failure proposals for review.
Observes ``teaching/proposals/comprehension_failures/*.json`` (emitted by the contemplation pass,
N5), validates them, and reports pending review obligations. It is **read-only**: it does not
advance the teaching loop, ratify, mount, modify readers, or affect serving. It is **not** an
``idle_tick`` (``ChatRuntime.idle_tick`` remains the only one) and **not** L10 it is the review
surface that keeps proposal artifacts from becoming inert files. A future PR may call this reporter
from ``idle_tick`` as a read-only sub-pass.
"""
from __future__ import annotations
from core.proposal_review.model import MalformedArtifact, PendingProposal
from core.proposal_review.report import (
ProposalReviewReport,
build_report,
report_json,
report_text,
)
from core.proposal_review.safety import SafetyVerdict, dry_check
from core.proposal_review.scan import DEFAULT_SINK, default_sink, scan
from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary
__all__ = [
"DEFAULT_SINK",
"MalformedArtifact",
"PendingProposal",
"ProposalReviewIdleSummary",
"ProposalReviewReport",
"SafetyVerdict",
"build_report",
"default_sink",
"dry_check",
"idle_summary",
"report_json",
"report_text",
"scan",
]

View file

@ -1,60 +0,0 @@
"""CLI for the read-only proposal review reporter (RPT-c).
python -m core.proposal_review # text report + safety dry-check
python -m core.proposal_review comprehension-failures
python -m core.proposal_review --json # machine-readable
python -m core.proposal_review --root <path> # override the sink
Read-only: scans, reports, and dry-checks the comprehension-failure proposal sink. **Mutates
nothing.** Exit 0 iff the safety dry-check passes (every artifact inert + serving unconsumed).
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from core.proposal_review.report import build_report, report_text
from core.proposal_review.safety import dry_check
from core.proposal_review.scan import scan
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="python -m core.proposal_review",
description="Read-only comprehension-failure proposal review (observes; never mounts/ratifies).",
)
parser.add_argument(
"target", nargs="?", default="comprehension-failures", choices=["comprehension-failures"]
)
parser.add_argument("--json", action="store_true", help="emit JSON instead of text")
parser.add_argument("--root", default=None, help="override the sink path")
args = parser.parse_args(argv)
root = Path(args.root) if args.root else None
proposals, malformed = scan(root)
report = build_report(proposals, malformed)
verdict = dry_check(proposals, malformed, root=root)
if args.json:
print(
json.dumps(
{
"report": report.to_json_dict(),
"safety": {"ok": verdict.ok, "violations": list(verdict.violations)},
},
indent=2,
sort_keys=True,
)
)
else:
print(report_text(report))
print(f" safety: {'OK' if verdict.ok else f'VIOLATIONS ({len(verdict.violations)})'}")
for v in verdict.violations:
print(f" ! {v}")
return 0 if verdict.ok else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,39 +0,0 @@
"""Typed records for the read-only proposal review reporter (RPT-a).
A ``PendingProposal`` is the parsed view of one ``teaching/proposals/comprehension_failures/
<hash>.json`` artifact (emitted by the contemplation pass, N5). A ``MalformedArtifact`` is a file
that could not be parsed into one. Pure value data the reporter never mutates the sink.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True, slots=True)
class PendingProposal:
"""A parsed proposal artifact awaiting human review. ``content_hash`` is the filename stem
(the content address). Safety fields (``status`` / ``mounted`` / ``requires_review``) are
carried verbatim so the dry-check (RPT-c) can verify them independently of the emitter."""
path: str
content_hash: str
failure_family: str
status: str
mounted: bool
requires_review: bool
problem_text_sha256: str
observed_attempts: tuple[dict[str, Any], ...]
@dataclass(frozen=True, slots=True)
class MalformedArtifact:
"""A file under the sink that is not a parseable proposal (bad JSON / missing or mistyped
fields). Surfaced so a human notices corruption rather than the reporter silently skipping it."""
path: str
reason: str
__all__ = ["MalformedArtifact", "PendingProposal"]

View file

@ -1,80 +0,0 @@
"""Deterministic review report over the scanned proposals (RPT-b).
A pure projection of the scan results into a summary: total pending, counts by failure_family,
counts by status, the malformed count, and the review-needed list. Deterministic given the sink
contents (no clock): counts are sorted, the review-needed list is sorted by content hash.
Time-based "oldest/newest" is intentionally **omitted**: the proposal artifacts are
content-addressed and carry no timestamp (the emitter is clock-free for idempotence), so an honest
temporal ordering is not available from the data only from non-deterministic filesystem mtime,
which would make this report non-deterministic. A human can sort the sink by mtime if needed.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
from core.proposal_review.model import MalformedArtifact, PendingProposal
@dataclass(frozen=True, slots=True)
class ProposalReviewReport:
"""A deterministic snapshot of the review obligations in the proposal sink."""
total: int
by_family: dict[str, int]
by_status: dict[str, int]
malformed: int
review_needed: tuple[str, ...]
def to_json_dict(self) -> dict[str, Any]:
return {
"total": self.total,
"by_family": self.by_family,
"by_status": self.by_status,
"malformed": self.malformed,
"review_needed": list(self.review_needed),
}
def build_report(
proposals: list[PendingProposal], malformed: list[MalformedArtifact]
) -> ProposalReviewReport:
by_family: dict[str, int] = {}
by_status: dict[str, int] = {}
review_needed: list[str] = []
for p in proposals:
by_family[p.failure_family] = by_family.get(p.failure_family, 0) + 1
by_status[p.status] = by_status.get(p.status, 0) + 1
if p.requires_review:
review_needed.append(p.content_hash)
return ProposalReviewReport(
total=len(proposals),
by_family=dict(sorted(by_family.items())),
by_status=dict(sorted(by_status.items())),
malformed=len(malformed),
review_needed=tuple(sorted(review_needed)),
)
def report_json(report: ProposalReviewReport) -> str:
"""Deterministic JSON (sorted keys)."""
return json.dumps(report.to_json_dict(), indent=2, sort_keys=True)
def report_text(report: ProposalReviewReport) -> str:
"""Human-readable summary."""
lines = [
f"comprehension-failure proposals: {report.total} pending · {report.malformed} malformed",
" by family:",
*(f" {fam}: {n}" for fam, n in report.by_family.items()),
" by status:",
*(f" {status}: {n}" for status, n in report.by_status.items()),
f" review-needed: {len(report.review_needed)}",
]
return "\n".join(lines)
__all__ = ["ProposalReviewReport", "build_report", "report_json", "report_text"]

View file

@ -1,102 +0,0 @@
"""Safety dry-check over the proposal sink (RPT-c) — the load-bearing part of the reporter.
The reporter's value is not just visibility; it is **independent safety verification**. The
dry-check confirms without trusting the emitter that every artifact in the sink is inert:
```text
status == "proposal_only"
mounted == false
requires_review == true
content-address consistent: filename == sha256(failure_family : problem_text_sha256)
path under the sink
no malformed artifact (an unverifiable file in a safety-critical sink is a violation)
serving never imports/reads the sink
```
Any failure is a violation; the CLI exits non-zero. **Pure read** verifies, never repairs.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from pathlib import Path
from core.proposal_review.model import MalformedArtifact, PendingProposal
from core.proposal_review.scan import DEFAULT_SINK
#: Serving-path roots that must never read the proposal sink (CLAUDE.md forbidden/serving sites).
_SERVING_TARGETS = (
("generate", "stream.py"),
("field", "propagate.py"),
("vault", "store.py"),
("generate", "derivation"),
("core", "reliability_gate"),
)
_SINK_MARKER = "comprehension_failures"
@dataclass(frozen=True, slots=True)
class SafetyVerdict:
"""The outcome of the dry-check: ``ok`` iff there are no violations."""
ok: bool
violations: tuple[str, ...]
def _repo_root() -> Path:
return Path(__file__).resolve().parents[2]
def _serving_references_sink(repo_root: Path) -> list[str]:
violations: list[str] = []
for parts in _SERVING_TARGETS:
target = repo_root.joinpath(*parts)
if not target.exists():
continue
files = [target] if target.is_file() else sorted(target.rglob("*.py"))
for f in files:
try:
if _SINK_MARKER in f.read_text(encoding="utf-8"):
violations.append(f"serving reads the sink: {f.relative_to(repo_root)}")
except (UnicodeDecodeError, OSError): # pragma: no cover - defensive
continue
return violations
def dry_check(
proposals: list[PendingProposal],
malformed: list[MalformedArtifact],
*,
root: Path | None = None,
repo_root: Path | None = None,
) -> SafetyVerdict:
"""Verify every artifact is inert and the sink is serving-unconsumed. Returns a SafetyVerdict."""
base = (root if root is not None else DEFAULT_SINK).resolve()
violations: list[str] = []
for p in proposals:
tag = p.content_hash
if p.status != "proposal_only":
violations.append(f"{tag}: status={p.status!r} (must be 'proposal_only')")
if p.mounted:
violations.append(f"{tag}: mounted=True (must be False)")
if not p.requires_review:
violations.append(f"{tag}: requires_review=False (must be True)")
expected = hashlib.sha256(
f"{p.failure_family}:{p.problem_text_sha256}".encode("utf-8")
).hexdigest()
if p.content_hash != expected:
violations.append(f"{tag}: content-address mismatch (expected {expected})")
if not str(Path(p.path).resolve()).startswith(str(base)):
violations.append(f"{tag}: path outside the sink")
for m in malformed:
violations.append(f"malformed (unverifiable): {m.path}{m.reason}")
violations.extend(_serving_references_sink(repo_root if repo_root is not None else _repo_root()))
return SafetyVerdict(not violations, tuple(violations))
__all__ = ["SafetyVerdict", "dry_check"]

View file

@ -1,85 +0,0 @@
"""Read-only scanner over the comprehension-failure proposal sink (RPT-a).
Reads ``teaching/proposals/comprehension_failures/*.json`` into typed ``PendingProposal`` records;
any file that does not parse into one is reported as a ``MalformedArtifact`` (never silently
dropped). **Pure read** opens files, never writes/moves/deletes. Deterministic: results are
sorted by filename. The sink location is computed here independently of the emitter, so the
reporter verifies the artifact contract without importing the writer.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from core.proposal_review.model import MalformedArtifact, PendingProposal
#: The proposal sink the contemplation pass (N5) writes to — known independently of the emitter.
DEFAULT_SINK = (
Path(__file__).resolve().parents[2] / "teaching" / "proposals" / "comprehension_failures"
)
#: Required fields and their JSON types for a well-formed proposal artifact.
_REQUIRED: tuple[tuple[str, type | tuple[type, ...]], ...] = (
("status", str),
("failure_family", str),
("problem_text_sha256", str),
("mounted", bool),
("requires_review", bool),
("observed_attempts", list),
)
def default_sink() -> Path:
return DEFAULT_SINK
def _parse(path: Path) -> PendingProposal | MalformedArtifact:
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
return MalformedArtifact(str(path), f"invalid_json: {exc}")
if not isinstance(raw, dict):
return MalformedArtifact(str(path), "not_a_json_object")
for key, typ in _REQUIRED:
if key not in raw:
return MalformedArtifact(str(path), f"missing_field: {key}")
# bool is a subclass of int; check bools explicitly so 0/1 don't pass as bool.
if typ is bool and not isinstance(raw[key], bool):
return MalformedArtifact(str(path), f"bad_type: {key}")
if typ is not bool and not isinstance(raw[key], typ):
return MalformedArtifact(str(path), f"bad_type: {key}")
attempts: tuple[dict[str, Any], ...] = tuple(
a for a in raw["observed_attempts"] if isinstance(a, dict)
)
return PendingProposal(
path=str(path),
content_hash=path.stem,
failure_family=raw["failure_family"],
status=raw["status"],
mounted=raw["mounted"],
requires_review=raw["requires_review"],
problem_text_sha256=raw["problem_text_sha256"],
observed_attempts=attempts,
)
def scan(root: Path | None = None) -> tuple[list[PendingProposal], list[MalformedArtifact]]:
"""Scan the sink (default: the comprehension-failure sink). Returns ``(proposals, malformed)``,
each sorted by path. A missing sink yields two empty lists (nothing to review yet)."""
base = root if root is not None else DEFAULT_SINK
if not base.exists():
return [], []
proposals: list[PendingProposal] = []
malformed: list[MalformedArtifact] = []
for path in sorted(base.glob("*.json")):
parsed = _parse(path)
if isinstance(parsed, PendingProposal):
proposals.append(parsed)
else:
malformed.append(parsed)
return proposals, malformed
__all__ = ["DEFAULT_SINK", "default_sink", "scan"]

View file

@ -1,52 +0,0 @@
"""Pure idle-use summary of the proposal sink (IT-a).
``idle_summary`` composes the landed read-only pieces ``scan`` (RPT-a) + ``build_report``
(RPT-b) + ``dry_check`` (RPT-c) into one small, JSON-safe value the runtime's ``idle_tick``
can surface (IT-b) without importing the reporter's internals. Pure read: no mutation, no clock.
The summary is deliberately primitives-only (no paths, no raw file content, no mutable dicts) so
it is trivially serializable if ``IdleTickResult`` is ever persisted. ``errors`` carries the
reason the sink is not ``safe`` the dry-check violations here; IT-b additionally uses it to
record a captured reporter exception (``proposal_review_failed:<type>``).
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from core.proposal_review.report import build_report
from core.proposal_review.safety import dry_check
from core.proposal_review.scan import scan
@dataclass(frozen=True, slots=True)
class ProposalReviewIdleSummary:
"""A JSON-safe snapshot of the proposal sink for idle surfacing. ``safe`` is the dry-check
verdict; ``errors`` is empty iff safe (or carries ``proposal_review_failed:<type>`` when the
reporter itself raised, set by the runtime sub-pass)."""
safe: bool
total: int
review_needed: int
malformed: int
by_family: tuple[tuple[str, int], ...]
errors: tuple[str, ...] = ()
def idle_summary(root: Path | None = None) -> ProposalReviewIdleSummary:
"""Scan → report → dry-check the proposal sink into a JSON-safe idle summary. Pure read."""
proposals, malformed = scan(root)
report = build_report(proposals, malformed)
verdict = dry_check(proposals, malformed, root=root)
return ProposalReviewIdleSummary(
safe=verdict.ok,
total=report.total,
review_needed=len(report.review_needed),
malformed=report.malformed,
by_family=tuple(report.by_family.items()),
errors=verdict.violations,
)
__all__ = ["ProposalReviewIdleSummary", "idle_summary"]

View file

@ -1,35 +0,0 @@
"""Shared deterministic reasoning evidence contracts."""
from __future__ import annotations
from core.reasoning.evidence import (
COMMITMENT_DISAGREEMENT,
DUPLICATE_STRUCTURAL_SIGNATURE,
INSUFFICIENT_EVIDENCE,
MISSING_COMMITMENT,
SAME_READER_LINEAGE,
TIER2_VERIFIED,
EvidenceBundle,
OperatorEvidence,
Tier2Verdict,
verify_tier2_agreement,
)
from core.reasoning.adapters import (
evidence_from_entailment_trace,
evidence_from_math_solution,
)
__all__ = [
"COMMITMENT_DISAGREEMENT",
"DUPLICATE_STRUCTURAL_SIGNATURE",
"INSUFFICIENT_EVIDENCE",
"MISSING_COMMITMENT",
"SAME_READER_LINEAGE",
"TIER2_VERIFIED",
"EvidenceBundle",
"OperatorEvidence",
"Tier2Verdict",
"verify_tier2_agreement",
"evidence_from_entailment_trace",
"evidence_from_math_solution",
]

View file

@ -1,119 +0,0 @@
"""Adapters from existing operator traces into shared reasoning evidence."""
from __future__ import annotations
import hashlib
import json
from typing import Any
from core.reasoning.evidence import OperatorEvidence
from generate.math_problem_graph import MathProblemGraph
from generate.math_solver import SolutionTrace
from generate.proof_chain import Entailment, EntailmentTrace
def evidence_from_entailment_trace(trace: EntailmentTrace) -> OperatorEvidence:
"""Convert propositional entailment trace evidence to the shared contract."""
query_key = trace.query_key or ""
check_keys = tuple(
key for key in (
trace.conjunction_key,
trace.entailment_check_key,
trace.refutation_check_key,
)
if key
)
if trace.outcome is Entailment.REFUSED:
commitment_key = ""
else:
commitment_key = f"entailment:{trace.outcome.value}:{query_key}"
structural_signature = _sha256_text(
json.dumps(
{
"operator": "propositional_entailment",
"premise_keys": list(trace.premise_keys),
"check_keys": list(check_keys),
},
sort_keys=True,
separators=(",", ":"),
)
)
return OperatorEvidence(
domain="mathematics_logic",
operator="propositional_entailment",
outcome=trace.outcome.value,
reason=trace.reason,
input_keys=(*trace.premise_keys, query_key),
check_keys=check_keys,
commitment_key=commitment_key,
structural_signature=structural_signature,
reader_lineage="proof_chain.entail",
payload={"entailment_trace": trace.as_dict()},
)
def evidence_from_math_solution(
*,
graph: MathProblemGraph,
trace: SolutionTrace,
reader_trace: tuple[str, ...] = (),
operator: str = "math_problem_graph_solve_verify",
reason: str = "solver_verifier_passed",
) -> OperatorEvidence:
"""Convert a verified MathProblemGraph solution to shared evidence."""
graph_hash = hashlib.sha256(graph.canonical_bytes()).hexdigest()
trace_hash = hashlib.sha256(trace.canonical_bytes()).hexdigest()
operation_kinds = tuple(step.operation_kind for step in trace.steps)
commitment_key = json.dumps(
{
"answer_entity": trace.answer_entity,
"answer_unit": trace.answer_unit,
"answer_value": trace.answer_value,
},
sort_keys=True,
separators=(",", ":"),
)
structural_signature = _sha256_text(
json.dumps(
{
"graph_hash": graph_hash,
"operation_kinds": list(operation_kinds),
"operator": operator,
"pack_id": trace.pack_id,
},
sort_keys=True,
separators=(",", ":"),
)
)
return OperatorEvidence(
domain="mathematics_logic",
operator=operator,
outcome="verified",
reason=reason,
input_keys=(graph_hash,),
check_keys=(trace_hash, trace.graph_canonical_hash),
commitment_key=commitment_key,
structural_signature=structural_signature,
reader_lineage="math_problem_graph.solve_verify",
payload={
"answer_entity": trace.answer_entity,
"answer_unit": trace.answer_unit,
"answer_value": trace.answer_value,
"graph_hash": graph_hash,
"operation_kinds": list(operation_kinds),
"pack_id": trace.pack_id,
"reader_trace": tuple(_reader_event(ev) for ev in reader_trace),
"trace_hash": trace_hash,
},
)
def _reader_event(event: str) -> Any:
try:
return json.loads(event)
except json.JSONDecodeError:
return event
def _sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()

View file

@ -1,250 +0,0 @@
"""Domain-neutral reasoning evidence and Tier-2 agreement checks.
This module is deliberately pure data plus deterministic serialization. It is
the shared evidence shape for proof, reconstruction, contemplation, and sealed
learning arenas; it does not authorize serving behavior by itself.
"""
from __future__ import annotations
import hashlib
import json
from collections import Counter
from collections.abc import Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any, Final
TIER2_VERIFIED: Final[str] = "tier2_verified"
INSUFFICIENT_EVIDENCE: Final[str] = "insufficient_evidence"
DUPLICATE_STRUCTURAL_SIGNATURE: Final[str] = "duplicate_structural_signature"
COMMITMENT_DISAGREEMENT: Final[str] = "commitment_disagreement"
MISSING_COMMITMENT: Final[str] = "missing_commitment"
SAME_READER_LINEAGE: Final[str] = "same_reader_lineage"
TIER2_REASONS: Final[frozenset[str]] = frozenset({
TIER2_VERIFIED,
INSUFFICIENT_EVIDENCE,
DUPLICATE_STRUCTURAL_SIGNATURE,
COMMITMENT_DISAGREEMENT,
MISSING_COMMITMENT,
SAME_READER_LINEAGE,
})
def _freeze_json_value(value: Any) -> Any:
"""Recursively freeze JSON-like payloads for immutable evidence storage."""
if isinstance(value, Mapping):
frozen = {str(k): _freeze_json_value(v) for k, v in value.items()}
return MappingProxyType(frozen)
if isinstance(value, list | tuple):
return tuple(_freeze_json_value(v) for v in value)
if value is None or isinstance(value, str | int | float | bool):
return value
raise TypeError(f"unsupported evidence payload value: {type(value).__name__}")
def _json_value(value: Any) -> Any:
"""Return a JSON-serializable copy of a frozen payload value."""
if isinstance(value, Mapping):
return {str(k): _json_value(v) for k, v in value.items()}
if isinstance(value, list | tuple):
return [_json_value(v) for v in value]
if value is None or isinstance(value, str | int | float | bool):
return value
raise TypeError(f"unsupported frozen evidence value: {type(value).__name__}")
def _canonical_json(payload: Mapping[str, Any]) -> str:
return json.dumps(
_json_value(payload),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
@dataclass(frozen=True, slots=True)
class OperatorEvidence:
"""Replayable evidence for one deterministic operator invocation."""
domain: str
operator: str
outcome: str
reason: str
input_keys: tuple[str, ...]
check_keys: tuple[str, ...]
commitment_key: str
structural_signature: str
# reader_lineage — the mechanistic identity of the decoding PATHWAY that
# produced this evidence (e.g. "proof_chain.entail"). The Tier-2 gate keys
# independence on this, not on the cosmetic structural_signature string, so a
# single reader cannot self-verify by relabeling. Deliberately EXCLUDED from
# ``as_dict``/``canonical_json`` (and therefore the evidence/trace hash): it is
# gate-routing provenance, not replayable evidence content, and including it
# would perturb the entailment trace_hash with no replay benefit. The
# static guarantee that distinct lineages are import-disjoint (cannot share a
# decoding pathway) is enforced by the architectural reader-disjointness
# invariant, not by this string alone.
reader_lineage: str = ""
payload: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
for field_name in (
"domain",
"operator",
"outcome",
"reason",
"structural_signature",
"reader_lineage",
):
value = getattr(self, field_name)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"OperatorEvidence.{field_name} is required")
object.__setattr__(self, field_name, value.strip())
if not isinstance(self.commitment_key, str):
raise ValueError("OperatorEvidence.commitment_key must be a string")
object.__setattr__(self, "input_keys", tuple(str(k) for k in self.input_keys))
object.__setattr__(self, "check_keys", tuple(str(k) for k in self.check_keys))
object.__setattr__(self, "payload", _freeze_json_value(dict(self.payload)))
def as_dict(self) -> dict[str, Any]:
return {
"domain": self.domain,
"operator": self.operator,
"outcome": self.outcome,
"reason": self.reason,
"input_keys": list(self.input_keys),
"check_keys": list(self.check_keys),
"commitment_key": self.commitment_key,
"structural_signature": self.structural_signature,
"payload": _json_value(self.payload),
}
def canonical_json(self) -> str:
return _canonical_json(self.as_dict())
@property
def evidence_hash(self) -> str:
return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest()
@dataclass(frozen=True, slots=True)
class EvidenceBundle:
"""Ordered collection of operator evidence with stable serialization."""
evidences: tuple[OperatorEvidence, ...]
def __post_init__(self) -> None:
object.__setattr__(self, "evidences", tuple(self.evidences))
if not all(isinstance(ev, OperatorEvidence) for ev in self.evidences):
raise ValueError("EvidenceBundle.evidences must contain OperatorEvidence")
def as_dict(self) -> dict[str, Any]:
return {"evidences": [ev.as_dict() for ev in self.evidences]}
def canonical_json(self) -> str:
return _canonical_json(self.as_dict())
@property
def evidence_hash(self) -> str:
return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest()
@dataclass(frozen=True, slots=True)
class Tier2Verdict:
"""Result of a domain-neutral convergent self-verification check."""
verified: bool
reason: str
commitment_key: str = ""
evidence_hash: str = ""
structural_signatures: tuple[str, ...] = ()
reader_lineages: tuple[str, ...] = ()
def __post_init__(self) -> None:
if self.reason not in TIER2_REASONS:
raise ValueError(f"unknown Tier2Verdict.reason: {self.reason!r}")
object.__setattr__(
self,
"structural_signatures",
tuple(str(s) for s in self.structural_signatures),
)
object.__setattr__(
self,
"reader_lineages",
tuple(str(s) for s in self.reader_lineages),
)
def as_dict(self) -> dict[str, Any]:
return {
"verified": self.verified,
"reason": self.reason,
"commitment_key": self.commitment_key,
"evidence_hash": self.evidence_hash,
"structural_signatures": list(self.structural_signatures),
"reader_lineages": list(self.reader_lineages),
}
def verify_tier2_agreement(
evidences: tuple[OperatorEvidence, ...] | list[OperatorEvidence],
) -> Tier2Verdict:
"""Require two **independently-read** structures converging on one commitment.
Independence is keyed on ``reader_lineage`` the decoding PATHWAY not on the
cosmetic ``structural_signature``. The same reader cannot self-verify by emitting
two differently-labeled signatures for one case: that is refused with
``SAME_READER_LINEAGE``. The static guarantee that distinct lineages are
import-disjoint (so distinct lineage no shared decoding pathway) is enforced
by the architectural reader-disjointness invariant; this gate enforces the
runtime half (distinct producing pathways + distinct structure + one commitment).
"""
bundle = EvidenceBundle(tuple(evidences))
if len(bundle.evidences) < 2:
return Tier2Verdict(False, INSUFFICIENT_EVIDENCE)
if any(not ev.commitment_key for ev in bundle.evidences):
return Tier2Verdict(False, MISSING_COMMITMENT, evidence_hash=bundle.evidence_hash)
lineages = tuple(ev.reader_lineage for ev in bundle.evidences)
signatures = tuple(ev.structural_signature for ev in bundle.evidences)
# Mechanistic independence firewall: at least two DISTINCT decoding pathways.
if len(set(lineages)) < 2:
return Tier2Verdict(
False,
SAME_READER_LINEAGE,
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)
if len(set(signatures)) < 2:
return Tier2Verdict(
False,
DUPLICATE_STRUCTURAL_SIGNATURE,
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)
commitments = Counter(ev.commitment_key for ev in bundle.evidences)
shared = [key for key, count in commitments.items() if count >= 2]
if len(shared) != 1 or len(commitments) != 1:
return Tier2Verdict(
False,
COMMITMENT_DISAGREEMENT,
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)
return Tier2Verdict(
True,
TIER2_VERIFIED,
commitment_key=shared[0],
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)

View file

@ -26,7 +26,6 @@ from __future__ import annotations
from core.response_governance.policy import (
ACTIVE_STATES,
APPROXIMATE_POLICY,
RECONCILE_STATES,
RESERVED_STATES,
STRICT_POLICY,
@ -38,7 +37,6 @@ from core.response_governance.policy import (
__all__ = [
"ACTIVE_STATES",
"APPROXIMATE_POLICY",
"RECONCILE_STATES",
"RESERVED_STATES",
"STRICT_POLICY",

View file

@ -121,19 +121,7 @@ STRICT_POLICY: ReachPolicy = ReachPolicy(
license_ratio=0.0,
)
# Step E (ADR-0206 §5) — the first widening rung. APPROXIMATE keeps the SAME admissible
# set as STRICT ({DECODED}): a fully-grounded surface commits verbatim, but anything less
# grounded (a converse GUESS is ``UNVERIFIED_POSSIBLE``) is surfaced by ``shape_surface``
# as a DISCLOSED ``[approximate]`` alternative. So a licensed estimate is never committed
# silently — admitting its state here would defeat the disclosure the rung exists for.
APPROXIMATE_POLICY: ReachPolicy = ReachPolicy(
level=ReachLevel.APPROXIMATE,
admissible_states=_STRICT_ADMISSIBLE,
rationale="license-gated widening (ADR-0206 §5 / Step E — SERVE earned on a committed ClassTally)",
license_ratio=1.0,
)
# Disclosure prefixes for the widening levels. Real
# Disclosure prefixes for the (currently unreachable) widening levels. Real
# code so the higher-level branch of shape_surface is genuinely
# policy-sensitive, exercised by the live-wiring test.
_DISCLOSURE_PREFIX: dict[ReachLevel, str] = {
@ -143,25 +131,6 @@ _DISCLOSURE_PREFIX: dict[ReachLevel, str] = {
}
def _serve_licensed(license_decision: object | None) -> bool:
"""True iff ``license_decision`` is a GENUINE, licensed ``Action.SERVE`` decision.
Strict by type on purpose: only a real :class:`~core.reliability_gate.LicenseDecision`
that the gate marked ``licensed`` for ``Action.SERVE`` widens. A ``None``, a bare
object, or a forged dict (``{"licensed": True}``) is NOT a ratified license and stays
STRICT the wrong=0 guard is that widening rests on the gate's verdict over a
committed ledger, never on a caller's say-so.
"""
from core.reliability_gate import Action
from core.reliability_gate.gate import LicenseDecision
return (
isinstance(license_decision, LicenseDecision)
and license_decision.action is Action.SERVE
and license_decision.licensed
)
def govern_response(
*,
epistemic_state: EpistemicState | None = None,
@ -170,19 +139,17 @@ def govern_response(
) -> ReachPolicy:
"""Decide the reach policy for a response.
Step E (ADR-0206 §5) the first license-gated widening. Returns
:data:`APPROXIMATE_POLICY` IFF ``license_decision`` is a genuine licensed
``Action.SERVE`` decision (a predicate-class that earned SERVE on the committed
reliability ledger); otherwise :data:`STRICT_POLICY`. Every current serving call
site passes no ``license_decision`` STRICT byte-identical to the pre-E path.
SCAFFOLD (ADR-0206 §3): returns :data:`STRICT_POLICY` unconditionally.
The inputs are accepted now so the call site is the final shape, but
the stakes-weighing and license-gated widening they will drive is
*designed, not built*. Every response is therefore governed at STRICT
commit-only-when-grounded, else the existing refuse/disclose path
which is exactly the pre-bridge behavior.
The STRICT default remains the load-bearing line for ``wrong == 0``: nothing
widens without a ratified license, and even APPROXIMATE only ever surfaces a
DISCLOSED estimate (``shape_surface`` adds ``[approximate]``), never a silent
commit. ``stakes``-weighing (SITUATE) stays designed-not-built (ADR-0206 §1).
This single return value is the load-bearing line for ``wrong == 0``:
the live-wiring tests prove that the *only* thing keeping the response
path strict is this STRICT return, not the absence of a consumer.
"""
if _serve_licensed(license_decision):
return APPROXIMATE_POLICY
return STRICT_POLICY

View file

@ -1,236 +0,0 @@
# Roadmap: the autonomous-improvement engine (path to AGI-candidacy)
**Date:** 2026-06-05 · **Status:** ROADMAP (the hyperfocus design plan) · **Telos:** [[project-core-is-one-continuous-life]] — `listen → comprehend → recall → think → articulate → learn → replay`, as one continuous, ever-improving life.
## The bar (what we are actually building toward)
A **serious AGI candidate**: an engine that is at least as **book-smart as an LLM**,
**keeps up with the world**, and **forever gets smarter autonomously under human
supervision** — by **taking in inputs (literature, told facts, world inputs,
experiences), comprehending them, and *realizing* them as structured grounded
memory it can recall** — rather than the LLM move of bulk-absorbing the whole
corpus indiscriminately and compressing it into weights.
> **Clarification — "intake" vs "ingestion".** The engine absolutely *ingests*:
> it must take in literature, knowledge, and experience to learn anything, and
> intake is first-class (Phase 3). The distinction from an LLM is *what is kept
> and how*: CORE keeps **selectively-realized, comprehended, provenance- and
> status-tagged knowledge + remembered experiences** (the vault + corpus *are* its
> memory, with exact recall), and never realizes unverified content as true — vs
> indiscriminately swallowing everything (junk included) and lossily averaging it
> into weights. "We don't need the world's *data*" means we get smart from
> comprehended structure + high-signal told facts, *not* that we don't take input.
What the bar is **not**: mass indiscriminate absorption, statistical
pattern-matching, or confident guessing (the LLM trick — and a different identity
config could even make our own models behave that way; it is not the point).
Determinism / `wrong=0` / auditability are the **necessary baseline**, not the
achievement.
## The strategic key: grounded honesty is the efficient-learning mechanism
An LLM must swallow the entire internet — junk, lies, contradictions — and average
its way past them. CORE **only realizes what it can ground** (told-and-evidenced,
comprehended, or reasoned), so it never absorbs garbage and never has to unlearn
it. The junk-filter *is* the learning advantage: we don't need the world's data,
we need the world's **true, comprehended structure**, accumulated forever. So
grounding is load-bearing for the *capability*, not just for trust. (`wrong=0` is
the high-stakes gear of this honesty — see the epistemic foundation below — not a
universal law that forces the engine to refuse everything it can't prove.)
## The loop (the autonomous-improvement engine)
```
open question / discovery / TOLD fact
→ COMPREHEND (arbitrary input → structured meaning)
→ REALIZE (make it real: integrate into the held self with an epistemic status)
→ REASON / GROUND / RECALL
→ RESPOND in the honest gear:
ASSERT (verified / realized)
ESTIMATE (evidence-grounded likelihood — ONLY where taught it is apt)
REFUSE (no grounding, or stakes forbid an estimate)
→ PROPOSE (idle_tick, proposal-only)
→ HITL ratify (reviewed, supervised)
→ ACCUMULATE into the one continuous life
→ MEASURABLY more capable → repeat, autonomously
```
When this loop demonstrably climbs a **general capability curve** over time, on its
own, under supervision — that is the AGI candidate.
## The epistemic foundation (honesty designed, estimation learned)
This corrects an earlier over-emphasis on `wrong=0` as a universal law. The right
frame has three commitments:
1. **Honesty is designed in; confabulation is impossible by construction.** The
engine's native stance is grounded: ASSERT what it has realized, REFUSE what it
has not. It has **no organ that fabricates** — no statistical token-soup, no
manufactured confidence. That cannot emerge by accident; it could only be
*deliberately built*, and we will not build it. This is the absolute floor, not
a policy defended turn by turn.
2. **Estimation is a LEARNED, ratified competence — never a designed-in default.**
There is a season for a calibrated assessment (*"on the evidence, most likely
X"*). The engine may acquire the competence to give one — **but only through
human ratification and deliberate guidance**, realized as knowledge like any
other. We do NOT design a "guess mode" with a risk knob; the engine never
self-authorizes a guess. `wrong=0` is therefore **demoted to one gear**
(high-stakes / verified assertion), not deleted.
3. **All confidence is evidence-grounded, so even uncertainty is honest.** A CORE
"likelihood" attaches to the deterministic confidence primitives we already
have — the calibrated-learning ledger, one-sided Wilson floors, cue-precision
reliability counts, the `EpistemicStatus` taxonomy. It means *"seen N times, M
coherent → confidence M/N with a hard lower bound"* — a counted fact about the
engine's own realized experience, not a vibe. This is the exact inverse of an
LLM (softmax over absorbed text) and is **why it can offer graded answers
without ever confabulating**.
**The measured invariant is calibration + grounding, not "never wrong":** every
confidence the engine states must trace to counted evidence, and it offers graded
answers only where it was taught that is appropriate. Being *honestly uncertain*
is success; being *dishonestly confident* is the only failure — and the substrate
makes the latter impossible without intentional design.
> **"Being told" is first-class.** Most knowledge arrives as *told facts* ("these
> are facts"); the engine realizes them and earns the why/how (coherence /
> evidence) over time. Determination does NOT mean proof-from-first-principles —
> intake → realize-with-evidence → build coherence is a primary growth path. The
> seed packs are the told bootstrap; the engine comprehends the new by relating it
> to what it has already realized, and grows.
## What is already built (compose, don't rebuild)
- **The continuous self** — Shape B+ resume ([[milestone-shape-b-plus-persistence]]),
L11 identity continuity + the idle learning mechanism
([[milestone-l11-identity-and-continuous-learning]]). The life that accumulates.
- **Verified reasoning substrate** — sound+complete propositional entailment
(`deductive_logic`, wrong=0, independent gold), `generate/proof_chain/`
(proof-tree builder/entail/rules), `generate/binding_graph/` (the universal-
structure interlingua DAG).
- **Determination pieces**`core/reliability_gate/` (gold-tether, ledger,
calibrated propose) determines correctness in the math lane; the wrong=0
self-verification gate in `generate/derivation/verify.py`.
- **Comprehension front door**`generate/derivation/` (extract → clauses →
compose), the question layer.
- **Measurement raw material** — independent-gold lanes (`deductive_logic`,
`relational_metric`, `dimensional`, `cold_start_grounding`, `symbolic_logic`)
+ the Perplexity-surveyed adoptables (ProntoQA, ProofWriter-CWA, CLUTRR, FOLIO —
all with independently-checkable gold + a refuse class).
## The bottleneck that gates everything
The flywheel can only **propose what is already determined**`idle_tick` refuses
`undetermined` candidates. The engine can *learn a fact it is handed*; it cannot yet
autonomously **figure one out**. The missing organ is **general determination**:
comprehend an open question, reason/ground it to a *verified* conclusion (or refuse),
and feed *that* to the flywheel. The math lane does a narrow version; nothing does
it generally. **Closing comprehend → determine → learn, measured on a general
capability curve, is the load-bearing arc.**
## Phased roadmap (entry → exit gates; wrong=0 is structural throughout)
| Phase | Build | Exit gate / measurement |
|-------|-------|-------------------------|
| **0 — the yardstick** | A **general capability index**: compose the independent-gold reasoning lanes (+ adopt ProntoQA/ProofWriter-CWA/CLUTRR/FOLIO) into one report with two axes — **correctness (wrong=0, never fabricate)** and **coverage (determined vs honestly-refused)**. Frozen-gated. | A single reproducible capability number the engine must climb; `wrong=0` enforced; a baseline measured. *You cannot improve what you cannot measure.* |
| **1 — the determination organ** | A general `determine(question) → {determined: conclusion refused}` path composing comprehension (`derivation`/`binding_graph`) + reasoning (`proof_chain`/`deductive`) + the reliability gate. Commits ONLY verified conclusions; refuses the rest. | On the Phase-0 yardstick: coverage rises with **wrong still 0**; every committed conclusion is independently checkable. |
| **2 — close the autonomous loop** | Wire `determine` → the `idle_tick` flywheel: take open questions, determine what it can (wrong=0), propose, HITL-ratify, accumulate. | The capability index **rises across loop iterations**, autonomously, under supervision — falsifiably (a frozen replay shows monotonic, junk-free improvement). |
| **3 — autonomous curriculum** | The engine drives its own agenda: identifies its determination frontier (what it can't yet determine), proposes what to learn next, under HITL guidance. | "Forever getting smarter autonomously under supervision" — the engine's self-chosen curriculum measurably advances the index. |
| **4 — breadth / generality** | Expand comprehension + reasoning across domains so the index is genuinely GENERAL (book-smart breadth), acquired via the loop — intake → comprehend → realize, not bulk indiscriminate absorption. | The capability index spans enough domains to credibly claim general book-smarts — every gain via comprehension+determination over realized knowledge, none via indiscriminate corpus absorption or per-domain matchers. |
## Invariants (non-negotiable across all phases)
- **`wrong=0` is structural** — the engine commits only verified conclusions; it
refuses rather than fabricates. This is the learning filter, not just a gate.
- **Reviewed learning** — ratification stays HITL (`teaching/review`); the loop
*proposes*, the human *ratifies*. Autonomy is supervised, not unmoored.
- **Determinism / replay** — every capability gain is reproducible; improvement is
a replayable curve, not a vibe.
- **Identity continuity** — the improving engine stays one continuous self
(L11); a smarter CORE is the *same* CORE, grown.
## Execution order — logical necessity × technical priority
Not arbitrary phases: each step is gated by what it *logically depends on*, then
ordered within that by leverage × risk. The dependency DAG:
```
MEASURE ───────────────────────────────────┐ (gates every "improved" claim)
│ │
COMPREHEND ──► REALIZE ──► DETERMINE/RESPOND ─┼─► AUTONOMOUS LOOP ──► CURRICULUM
(NL → universal (hold (assert / refuse │ (idle_tick + BREADTH
interlingua) with over realized) │ climbs the curve,
status) │ │ autonomously)
└─ LEARNED ESTIMATION ◄── needs MEASURE(calibration)
(ratified, evidence-grounded)
```
**Step 1 — MEASURE: the cross-domain capability yardstick.** *Logical necessity:*
nothing can be called "more capable" without it; it is prior to all improvement.
*Technical priority:* HIGH leverage (north-star instrument + the anti-self-
deception guard — a per-domain hack moves one lane and breadth stays flat,
exposing it), MODERATE effort (compose the existing independent-gold lanes +
adopt ProntoQA/ProofWriter-CWA/CLUTRR/FOLIO). Measures **assert-correctness +
grounding + coverage + calibration** under a configurable risk budget. **Build
first.**
**Step 2 — COMPREHEND: NL/prose → the universal interlingua.** *Logical
necessity:* it is the wall (GSM8K refuses 92% on comprehension coverage, not
arithmetic; prose/exams are ~0); every downstream step needs structure to operate
on. *Technical priority:* HIGHEST leverage (unlocks all breadth) AND HIGHEST
risk/effort (open-ended; the overfit trap lives here). The discipline: it must
emit the **general** binding-graph / universal-structure, never per-domain parses
— and the Step-1 yardstick is what proves it generalized rather than gamed. **The
make-or-break.**
**Step 3 — REALIZE: integrate comprehended/told structure into the held self**
with an epistemic status (`EpistemicStatus`), persisted via Shape B+. *Necessity:*
needs COMPREHEND. *Priority:* MODERATE effort (vault/corpus/persistence exist),
HIGH leverage — this is what makes knowledge *accumulate* (told facts become
realized; the engine grows). Intake ("being told") lands here.
**Step 4 — DETERMINE / RESPOND: reason over realized structure → the honest
gear** (assert verified / refuse ungrounded). *Necessity:* needs COMPREHEND +
REALIZE. *Priority:* MODERATE effort (compose `proof_chain` / `deductive` /
binding-graph entail onto comprehension output), HIGH leverage — coverage rises
on the yardstick with grounding intact. **No estimation yet — assert/refuse only.**
**Step 5 — AUTONOMOUS LOOP: wire comprehend→realize→determine→idle_tick→ratify→
accumulate.** *Necessity:* needs Steps 14. *Priority:* MODERATE effort (idle_tick
exists), HIGH leverage — this is the step that makes "forever improving" real and
falsifiable (the yardstick curve climbs autonomously, under supervision).
**Step 6 — LEARNED ESTIMATION: the calibrated likelihood competence.**
*Necessity:* needs DETERMINE (the honest floor) + MEASURE-calibration + the
teaching loop. *Priority:* MODERATE effort, MODERATE leverage — deliberately
LATE: only after the assert/refuse floor and the calibration measurement are
solid do we teach (HITL-ratified) when/how to offer evidence-grounded likelihoods.
Never a designed-in default.
**Step 7 — AUTONOMOUS CURRICULUM + BREADTH.** *Necessity:* needs the loop. The
engine drives its own determination frontier under supervision; breadth expands
across domains via the loop (intake → comprehend → realize), never via
indiscriminate corpus absorption or per-domain matchers.
**Critical-path summary:** `MEASURE → COMPREHEND → REALIZE → DETERMINE → LOOP`,
with ESTIMATION grafted after DETERMINE+MEASURE and CURRICULUM after LOOP. The
single highest-risk step is **COMPREHEND** (Step 2); the single highest-necessity
"do-first" is **MEASURE** (Step 1), because it is the only thing that keeps every
later step honest.
## Cross-cutting invariants (hold at every step)
The 8 foundation commitments above, plus the standing CLAUDE.md invariants:
`versor_condition < 1e-6` (math floor), no forbidden-site repair/normalization,
reviewed learning stays HITL, exact CGA recall (no approximation), deterministic
replay. Every step is TDD + mutation-verified-to-bite + curated-smoke + CI-lane-SHA
gated, the way the L10→L11 spine was built.
## Honest scope boundary
This is the multi-phase arc to AGI-candidacy, not one PR. AGI is the destination;
this roadmap is the **critical path** and the **measurement** that makes progress
toward it real and falsifiable. **Phase 0 (the yardstick) is the first build**
without a general capability curve, "getting smarter" is unfalsifiable, and we'd
be doing exactly the unmeasured hand-waving the LLM world runs on.

View file

@ -1,153 +0,0 @@
# Step D — CLOSE: idle deductive consolidation of soundly-derived facts
**Date:** 2026-06-06
**Branch:** `feat/loop-learns-determined`
**Sequence:** A INSTRUMENT (#598/#599) → B WIRE (#600/#601) → C DEEPEN (#602) → **D CLOSE** → E ESTIMATION (last)
**Telos:** [[project-core-is-one-continuous-life]] — the loop *learns from determined facts*, not just the partial discovery chains it emits today.
## The scoped seam (read, not assumed)
The autonomous loop today is `extract_discovery_candidates → _pending_candidates →
idle_tick(contemplate → propose_from_candidate) → ProposalLog (pending, HITL)`. Two
findings from reading the source fix what D actually is:
1. **Today's loop is a within-corpus generalizer.** `extract_discovery_candidates`
emits an *intentionally partial* chain `{subject, intent, connective:None,
object:None}`; `contemplate` completes it *only* by enumerating objects the
reviewed corpus already used, and `check_eligibility` **Gate 2** requires a
`source="corpus"` evidence pointer. So it can only re-propose a shape the corpus
already contains. It cannot learn a fact that arose in lived conversation.
2. **The HITL ProposalLog never reaches serving.** D's falsification — "the capability
index climbs across loop iterations" — therefore *cannot* be met by emitting
proposals (the index is a static breadth×accuracy eval, hard-gated to 0 on any
wrong; proposals are HITL-gated and never feed serving). For the index to climb,
a *determined fact must feed back into what the engine can answer.*
So the inevitable D is **session-memory consolidation**, not proposal emission:
> When idle, the engine consolidates each soundly-derived determination
> (`member∘subset`, `subset∘subset` — the transitive is-a reasoning C built) back
> into the realized-knowledge vault as a new realized record, 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.
Emitting determined facts as HITL teaching-chain proposals is a *distinct* capability
that collides with Gate 2 (the corpus-evidence floor, a teaching-safety / wrong=0
surface). It is **deliberately deferred** to its own evidence-floor-touching PR rather
than bolted onto D — the falsifiable core stays clean.
## Mechanism (semi-naive deductive closure, one layer per tick)
`idle_tick` (gated by `config.consolidate_determinations`, default OFF) runs
`consolidate_once(ctx)`:
1. Recall realized `member` + `subset` facts.
2. Compute every **one-hop** extension under the two SOUND is-a rules:
- `member(s,b) ∧ subset(b,t) → member(s,t)`
- `subset(a,b) ∧ subset(b,t) → subset(a,t)`
- **`member ∘ member` is never an edge** — instance-of is not transitive
("Socrates is a man" + "man is a species" ⊬ "Socrates is a species"). The
reader's member/subset split is exactly what makes the included rules sound.
3. For each derived edge not already realized, **verify with the sound+complete
proof_chain ROBDD** (reusing C's single verifier `_verify_subsumption` — no
duplicate proof logic), then write it via `realize_derived`.
Each tick adds exactly one hop-layer; the closure saturates after `diameter` rounds;
re-running at the fixed point is a no-op. This is textbook semi-naive evaluation, and
it is precisely what produces the monotone "climbs across iterations" signal.
## Invariants held (the wrong=0 / honesty boundary)
- **Soundness (wrong=0).** Every consolidated fact is the conclusion of a sound rule
over realized premises, *confirmed by the sound+complete decider* — defense in depth
beyond the one-hop rule. The `member∘member` fallacy is structurally unreachable (no
member→member edge). A consolidated `member(s,t)` can only be extended by a *subset*
edge, never a member edge, so the fallacy stays unreachable across iterations.
- **Epistemic honesty.** A fact derived from SPECULATIVE premises is SPECULATIVE,
`basis="as_told"`. The soundness of the *inference* never upgrades the *standing* of
the premises. **COHERENT is never minted.**
- **Session memory, not reviewed learning.** Consolidation is the immediate session
tier (allowed), an extension of the `generate.realize` path — **not** corpus
mutation and **not** coupled to proposals. The teaching/review HITL path is
untouched; no parallel learning path is created.
- **Sanctioned write path.** Writes reuse `_realize_structured` (the INV-21 allowed
vault writer). No new normalization; no closure/repair — `algebra/versor.py` keeps
closure. The derived record reuses the subject's `probe_ingest` placement, identical
to a told fact about the same subject.
- **Idempotency / determinism.** Dedup on the span-free `structure_key` (identical to
a told fact's, so a later told duplicate collapses). Deterministic order; no clock,
no LLM, no metric call. Bounded by the existing `_SUBSUMPTION_SUBSET_FACT_BUDGET`.
## Provenance — the replayable proof obligation (Fork 2)
`Determined` already carries `grounds: tuple[RealizedRecord, ...]` — the premise
records that entailed it. The consolidated record records, as derived-provenance, the
premise `structure_key`s + the `rule` + the `verdict` (always `entailed`). This makes
the soundness claim **meaningfully fail** (per the Schema-Defined Proof Obligations
rule): a replay re-fetches the premises by `structure_key` and re-runs the rule +
proof_chain — if a consolidated fact were ever unsound, re-verification fails loudly.
## Falsification — `evals/determination_closure/`
A frozen replay seeds a deep is-a chain (`member(x,c0)`, `subset(c0,c1)…subset(cₙ₋₁,cₙ)`)
and runs idle consolidation ticks. Asserts:
- **Monotone climb:** the directly-realized `member(x, ·)` closure grows by exactly one
per tick (each layer), strictly increasing until the fixed point.
- **Convergence:** after `n` ticks the closure is saturated; a further tick is a
**no-op** (`at_fixed_point=True`, 0 consolidated).
- **wrong=0:** no `member(x, y)` is ever consolidated for `y` outside the chain's
reachable set (no fabricated fact); the `member∘member` canary derives nothing.
- **Provenance replay:** every consolidated record's recorded premises re-verify as
`ENTAILED`.
## Adversarial verification (5 independent skeptics, refute-the-claim)
A panel re-read the shipped source under five distinct lenses, each tasked to *refute*.
- **wrong=0 / soundness** — held. `member ∘ member` is structurally unreachable (member
facts are only ever extended by subset edges); every write is proof_chain-`ENTAILED`;
no cross-subject leakage. **Acted on its note:** `_verify_subsumption` now *refuses* a
mislabeled/wrong-arity path (a member fact smuggled into `subset_path`), converting
soundness-by-caller-discipline into soundness-by-construction now that consolidation is
a second caller.
- **epistemic honesty** — held. `realize_derived` writes SPECULATIVE unconditionally;
`_basis` returns `as_told` for SPECULATIVE grounds; `promote_eligible_entries`
(SPECULATIVE→COHERENT) requires energy metadata derived facts never carry and is
disjoint from `idle_tick`.
- **teaching-safety boundary** — held. Single write path (`ctx.vault.store`, `tier=
"session"`); zero `teaching/`/proposal/pack/identity coupling; the two `idle_tick`
passes are orthogonal (the proposal pass's vault probe filters to COHERENT, excluding
these SPECULATIVE facts).
- **determinism / replay / persistence** — held. Sorted write order; derived
`structure_key` identical to a told fact's; `Derivation` round-trips through the
snapshot; the lane's `reverify_derived` meaningfully fails on a non-entailing record.
- **normalization / closure invariant** — flagged `high`, assessed a **misattribution**.
The cited `vault.store → reproject → null_project` is pre-existing (`aadaf116`,
2026-05-13, ~3 weeks before D), triggered *identically* by the already-merged
`realize_comprehension` path, operates on vault **content** null-vectors (which
CLAUDE.md says to preserve as null vectors — sanctioned), and is a different object
from the runtime field `versor_condition(F)<1e-6` invariant the claim referenced. The
lane's high `vault_reproject_interval` is the established determine/realize test idiom,
not a D-specific sidestep. D adds zero normalization code and reuses the INV-21-allowed
writer; the claim ("consolidation *adds* no forbidden normalization") stands.
## Out of scope (follow-ups)
- **Promotion firewall (defensive).** If a future change ever called
`promote_eligible_entries` inside `idle_tick` or attached energy metadata to derived
facts, SPECULATIVE derived facts could promote to COHERENT and bootstrap standing. No
live path (the separation is architectural); a structural `derived ⇒ never-promote`
marker would harden it.
- **Runtime vs. lane proof obligation.** The provenance replay (`reverify_derived`) runs
in the eval lane, not per-write at runtime — matching the repo's "wrong=0 proven by
lanes, not runtime asserts" pattern (consolidation already verifies each write *before*
writing). Noted, not a gap.
- Determined-fact → HITL teaching proposal (touches Gate 2 / evidence floor — its own PR).
- Incremental frontier (semi-naive with a delta set) instead of recompute-and-dedup per
tick — an O() optimization once the closure substrate is proven.
- Order/containment transitivity (`less_than`, `before_event`, `inside_of`) — the C-2
predicate widening; consolidation generalizes to them once C-2 lands.

View file

@ -1,83 +0,0 @@
# Step E — ESTIMATION: calibrated, disclosed estimation via the ADR-0206 reach bridge
**Date:** 2026-06-06
**Branch:** `feat/learned-estimation`
**Sequence:** A INSTRUMENT → B WIRE → C DEEPEN → D CLOSE → **E ESTIMATION** (last)
**Executes:** ADR-0206 §5 (cognition-path widening) — the `LICENSE` node ("built — not yet called from serving") finally called from serving.
## What E is (and is not)
E lets the engine **commit a disclosed estimate** for a class it has *measured itself
reliable on*, instead of always refusing past proof. It is **not** generic guessing,
not a probabilistic model, and it does **not** touch the sealed GSM8K serving metric
(that is a separate, riskier ADR-0206 §5 PR — `select_self_verified`).
The whole step is one wire: `govern_response` consults `reliability_gate.license_for(…,
Action.SERVE)`; a licensed class reaches `ReachLevel.APPROXIMATE`; `shape_surface`
**discloses** the estimate with an `[approximate]` prefix.
## Why it is wrong=0-safe by construction
`shape_surface(APPROXIMATE)` never commits an estimate silently — it prefixes
`[approximate]`. So an estimate that is wrong is a **disclosed**-wrong, categorically
different from the silent/asserted wrong the `wrong=0` invariant forbids. The reliability
gate (θ_SERVE=0.99 on a committed `ClassTally`) governs *when* the disclosed estimate is
even offered. Two independent guards: disclosure (honesty) + license (calibration).
## The estimator — a blind converse-guesser
Given a realized `p(a, b)` and a query `p(b, a)`, the estimator commits the **converse**
as a candidate. It is **blind** — it never reads the pack's symmetry metadata. Therefore:
- on a **symmetric** predicate (`sibling_of`, `spouse_of`, `equal_to`, `distinct_from`,
`adjacent_to`, `overlaps_event``graph.edge.symmetric`) the converse is **true**;
- on a **directed** predicate (`parent_of`, `less_than`, `before_event`, … —
`graph.edge.directed`) the converse is **false**.
The engine does not *know* which is which. It **measures** its converse-guess precision
per predicate-class over a gold lane and earns a SERVE license only where the measured
floor clears θ. That is calibrated learning (ADR-0175): reliability is commitment
precision, earned by volume. The symmetry metadata is the **gold** (the `GoldTether`'s
truth), never a serving-time shortcut.
## The committed ledger (real, sealed, HITL-ratified)
- `evals/determination_estimation/gold.py` deterministically generates the gold cases:
657+ symmetric converse cases for the licensed class (gold=true) and a directed class
(gold=false). 657 is the Wilson volume floor: a perfect record clears θ_SERVE=0.99 at
`n/(n+z²) ≥ 0.99`, `z=2.576``n ≥ 657`. Reliability is earned by volume, never a
lucky streak — so the gold lane is sized to that bar, not the bar to the lane.
- `core.learning_arena.run_practice` folds a `DomainSolver` (the converse-guesser) +
`GoldTether` (symmetry-as-truth) over the cases → `dict[str, ClassTally]`.
- The resulting ledger (per-class committed counts) is frozen as a **ratified artifact**
(`evals/determination_estimation/ratified_ledger.json`) with an expected-hash. Committing
it via a reviewed PR **is** the HITL ratification. Ceilings stay at safe defaults
(θ_SERVE=0.99) — no override, so the engine never raises its own bar (invariant #4).
## The wire (E-3, the delicate part)
In `chat/runtime.py`, gated by a new config flag (default OFF): when a turn is a
**converse query** (`p(b,a)` asked, `p(a,b)` realized, `p(b,a)` not directly
determinable) whose predicate-class holds a committed `license_for(SERVE).licensed`,
pass that real `LicenseDecision` into `govern_response` → it emits `APPROXIMATE`
`shape_surface` discloses the converse estimate as `[approximate] …`. Every other turn,
and every unlicensed class, stays `STRICT` (byte-identical, wrong=0 untouched). Never a
designed-in default: absent a cleared committed tally, refuse.
## Falsification — `evals/determination_estimation`
A frozen replay asserts:
- **Discriminating gate:** the symmetric class is SERVE-licensed; the directed class is
not (its converse-guess reliability is ~0 over the committed lane).
- **Disclosed estimate:** a licensed converse query yields an `[approximate]`-prefixed
surface; an unlicensed one stays STRICT-refuse, byte-identical to pre-E.
- **No silent estimate:** every reach > STRICT carries the disclosure prefix.
- **wrong=0 (silent):** zero silently-committed wrong answers — every estimate is disclosed.
- **Volume floor:** below 657 committed the symmetric class is NOT licensed (the bar binds).
- **Determinism:** the ledger + verdicts reproduce byte-identically (frozen expected hash).
## Out of scope (separate PRs)
- The **math-serving seam** (`select_self_verified`) — ADR-0206 §5, touches the sealed metric.
- **SITUATE** (stakes/gravity) and the live **FEED-BACK** loop (serving outcome → ledger) —
ADR-0206 §1 "designed, not built". E uses an offline, sealed, ratified ledger.
- `EXTRAPOLATE` / `CREATIVE` reach levels (need `VERIFIED` / novelty capabilities).

Some files were not shown because too many files have changed in this diff Show more