Local deterministic public evidence for the ratified PCCP capability:
proposer submits claim + proof candidate + status/confidence garbage; CORE
ignores proposer authority, fresh-reads curator-certified store state,
recomputes the proof under the pinned engine, and promotes/refuses only
through VaultStore.apply_certified_promotion. The trace proves the decision.
- demos/proof_carrying_promotion/{schema.json,authority.py,run_demo.py}:
closed recursive schema (output fields unsuppliable at the root), the REAL
decider (teaching.proof_promotion.certify_promotion) and the REAL owner —
no demo-local promotion reimplementation. The local arena is rebuilt via
VaultStore.from_dict over dict-literal metadata: demos/ is scanned by
INV-21/INV-24/INV-29, and the demo adds no vault writer, no recall site,
and no status-transition site (proven in tests with the invariants' own
detectors).
- 8 fixtures + pinned expected artifacts: entailed-promotes,
proposer-status-ignored, non-coherent-premise, uncertified-reading,
tampered-certificate (fails byte replay), stale-premise-status (same
honest certificate digest as the promoting run — live state is what
differs), non-sequitur, invalid-state-smuggling (rejected before
evaluation, authority_evaluated=false, null digest).
- Hardened runner per the #688/#690 pattern: default out/ dir, unsafe
output roots refused, --write-expected explicit, double-run byte-identity
enforced per fixture, scenario pass/fail summary.
- tests/test_proof_carrying_promotion_demo.py (26): expected-artifact
conformance, tamper detection, proposer-garbage decision-invariance (incl.
on the promotable setup), refused-cases-mutate-nothing, REFUTED-never-
demotes, trace-hash folds certificate digest, forbidden-import/call scan
(incl. uuid/random/time), INV-21/INV-29 allowlists pinned unchanged, no
named-company/strategy terms.
No runtime turn integration; promotion remains SPECULATIVE->COHERENT only;
README carries the full honesty ledger.
The ratified capability, through the existing mutation owner only:
- teaching/proof_promotion.py — pure decider. Fresh-reads every premise AND
the claim from the store (claim_entry_index; forms never proposer-supplied),
strict status compares (no parse-defaulting), reading_certified must be the
boolean True, engine certification via the pinned deductive engine,
proposer_payload accepted and provably never read (del before use; poisoned-
mapping test), certificate digest emitted for D4 trace-hash folding. Zero
mutation; zero vault.store calls; zero status writes (proven with the
INV-21/INV-29 detectors themselves).
- vault/store.py::apply_certified_promotion — the single transition site.
Independently re-verifies: byte-for-byte replay under DEDUCTIVE_ENGINE_PIN,
promotion_positive, live claim form/reading/status cross-checks, live
premise form/reading/status cross-checks (staleness refuses). Fabricated or
tampered certificates flip nothing; authority is live store state plus
recomputation, never the artifact.
- generate/proof_chain/engine_pin.py — DEDUCTIVE_ENGINE_PIN mirrors the
deductive_logic_v1 lane SHA; sync pinned by AST test against
scripts/verify_lane_shas.py.
- Consistency fix found in lookback: both promotion sites now stamp
epistemic_state alongside epistemic_status (the ADR-0148 site left the
stored state tag stale; recall recomputed it, but the stored key lied).
Obligations: all six strict-xfail markers retired and live (O1/O2
strengthened to prove the transition through the vault owner); the
module-existence pin deleted per its own docstring; the two remaining
honesty pins kept. INV-21 allowlist unchanged; INV-29 allowlist unchanged
({vault/store.py}); INV-29's 29c visibility floor raised 2 -> 3 to cover the
new site.
No runtime turn path calls promotion; the deterministic demo is PR D.
Validation: new suite 49 passed; obligations 8 passed (0 xfail remaining);
invariants 66 passed; certificate 27 passed; 0148 6 passed; epistemic 20
passed; smoke files green; lane SHAs verified.
Pure evidence substrate for proof-carrying coherence promotion. No
promotion, no vault import, no status transition, no teaching-path
change; ADR-0218 stays Proposed.
- generate/proof_chain/certificate.py: frozen PromotionCertificate
(version/claim_form/premise ids+forms+statuses/entailment_trace/
engine_pin/decision/reason, canonical_json with sort_keys+compact
separators); build_certificate runs evaluate_entailment_with_trace
over entry_id-sorted premises; verify_certificate rebuilds from the
embedded forms and accepts iff byte-identical — tampered forms/claim/
trace/decision/reason/ordering/version all fail replay, structural
breakage fails closed as malformed_certificate.
- promotion_positive is necessary-not-sufficient: entailed over a
non-empty all-coherent recorded premise set; REFUTED/UNKNOWN/REFUSED
verify as themselves but are never positive; zero-premise tautology
certificates fail-closed non-positive in v1.
- engine_pin is recorded provenance; pin tampering is caught only via
caller-supplied expected_engine_pin (P3 must pass the lane SHA) —
limit pinned by dedicated tests.
- entail.py: docstring-only fix removing the incorrect ADR-0206
attribution (numbering collision); now cites ADR-0218. Zero behavior
change. generate_claims.py:74 still carries the label (serving-frozen
surface, flagged for follow-up).
- No xfail markers retire: every PR-A obligation binds to the P3
promoter, which must not exist before ratification. The certificate-
shaped halves of O1/O7 are proven for real in
tests/test_proof_chain_certificate.py (27 tests). ADR phasing bullet
and obligations docstring record the reconciliation.
Validation: certificate 27 passed; obligations 3 passed + 6 xfailed;
architectural invariants 61 passed (INV-21/INV-29 green, allowlists
unchanged); entail/builder/rules 61 passed; smoke files 95 passed.
INV-29 is the load-bearing contribution of #693, so its detector must match
its claim. The assignment-only scan missed common write indirections; the
widened _status_transition_writes (renamed from _status_assignment_targets)
now also catches:
- m.update({"epistemic_status": ...}) / update(epistemic_status=...) /
update(**{...}) / update(dict(epistemic_status=...))
- m.setdefault("epistemic_status", ...)
- bound and unbound __setitem__ with the literal key
- setattr(entry, "epistemic_status", ...)
- writes hidden in tuple-unpacking assignment targets
29b now proves all 13 shapes are flagged (per-shape, so one regression names
itself); new 29b' proves reads, other keys, and dict-literal construction do
NOT flag (false positives would train reflexive allowlisting). Honest scope
note added: variable keys / exec / C-level aliasing are statically
undetectable and belong to review.
Pre-scan confirmed zero production call-shape writes exist, so the widened
detector flags nothing new; ALLOWED_STATUS_TRANSITION_SITES stays
{"vault/store.py"}. No runtime change; ADR-0218 still Proposed.
Validation:
- tests/test_architectural_invariants.py: 61 passed
- tests/test_proof_carrying_promotion_obligations.py: 3 passed, 6 xfailed
PR A of the stack specified by docs/issues/proof-carrying-coherence-promotion.md.
No runtime change; no promotion behavior; ADR-0218 is Proposed, not ratified.
- ADR-0218 (Proposed): rulings the issue Section 9 demands — extend the
existing mutation owner (policy decides in teaching/proof_promotion.py at
P3, only vault/store.py mutates), reading stays curator-certified in the
first cut, exact fail-closed admissibility predicate, certificate folds
into trace_hash. Records two substrate corrections: proof_chain Phase 2
(2.1-2.4) is already implemented, and ADR-0148 already automates an
energy-arm promotion.
- INV-29 (passing): epistemic-status transition sites are allowlisted.
ADR-0148's in-place promotion proved INV-21's AST scan cannot see
status transitions (they are not store() calls) — the boundary the
issue calls highest-risk did not actually cover the riskiest shape.
Allowlist pinned to {vault/store.py} BEFORE any promoter exists;
detector mutation-proven non-vacuous (29b/29c).
- Obligations O1-O5, O7 as strict xfail (executable spec; ImportError today;
strict=True forces the P3 PR to retire markers and pass for real) + three
honesty pins that flip red if promotion is wired without review.
Validation (worktree off main@187b008b, python -m pytest):
- tests/test_proof_carrying_promotion_obligations.py: 3 passed, 6 xfailed
- tests/test_architectural_invariants.py: 60 passed (56 prior + 4 INV-29)
- smoke file set: 94 passed
- proof_chain/entail/binding-graph/ADR-0148 set: 84 passed
- epistemic/teaching set: 40 passed
- scripts/verify_lane_shas.py: 8/9 pinned lanes match incl. deductive_logic_v1;
public_demo missed on the known env wall-clock budget flake (47.6s > 30s),
pre-existing and unrelated (diff is docs+tests only).
This demo assigns epistemic truth-state only and runs no normative/
safety/ethics clearance pass, so it has no basis to positively clear
a claim. Drop the verified->cleared mapping: verified now reports
normative_clearance=unassessable like every other non-invalid output.
Updates authority.py, regenerates the affected expected trace hashes,
tightens the test (verified asserts unassessable) and adds an invariant
test that no non-invalid output is ever cleared, and corrects the README
honesty ledger.
A model-style proposer submits a claim, evidence bundle, and optional
bounded-inference block; CORE alone assigns the typed epistemic state
drawn from the canonical core.epistemic_state taxonomy (no parallel
enum) and emits a deterministic, replayable evidence artifact.
The proposer cannot set assigned_state, status, trace_hash,
authority_path, evidence_ledger, or normative_clearance: the closed
recursive schema rejects any root-level injection before state
evaluation runs, and proposer-supplied proposed_state/trace_hash are
recorded as ignored and never read.
Adds closed schema, six fixtures, pinned expected artifacts, a
double-run byte-identical runner with hardened output-dir policy, and a
22-test suite. No network, subprocess, eval/exec, model API, or side
effects beyond demo output.
A narrow, auditable proof path for the hybrid-mode boundary: a
Claude/Fable-style System 1 proposer submits an MCP-shaped tool call
(core.semantic_derivation.verify); CORE re-derives via the ADR-0184
derivation lane, keeps sole acceptance authority, refuses or asks when
it cannot honestly answer, and emits a deterministic replay/provenance
trace for every decision.
Boundary properties (each pinned by a failing-if-removed test):
- the proposer has no commit path: additionalProperties:false schema,
and the demo modules import nothing from generate.* (AST-scanned with
a planted-bypass self-test) — the lane is consumed only through the
ADR-0184 S4b audited trace facade;
- 'verified' requires pool commit AND clean commit-law/faithfulness
audits AND a gold-audited envelope.json entry AND byte-match to the
pinned derivation trace (measured: 118 of the off-serving pool's 231
corpus commits disagree with lane gold, so a bare pool commit is
never served — scenario s4 shows the refusal on a commit that even
matches gold);
- the ask leg is the real Q1 stack (router -> limitation -> Q1-D
producer -> carried handle -> gated serving), dark by default,
tamper-fail-closed on the content hash;
- authority_path names only consulted authorities; responses are
deterministic (double-run byte-identical) and pinned under expected/.
No serving/runtime change; no CLAIMS/metrics/telemetry/lane-pin
movement; no network or Anthropic API dependency (System 1 payloads are
clearly-labeled static fixtures).
ADR-0184 S4b — the replay/provenance equivalence boundary, the
precondition for any semantic-primary path. After #685 the in-repo
legacy path delegates to the semantic boundary, so old-vs-new is
vacuous; the durable reference is a pinned canonical trace artifact
(evals/gsm8k_math/equivalence/v1/, 937 problems) whose authority is
the #684/#685 cross-tree differentials (documented provenance chain).
- state/provenance.py: replay_is_faithful — the replay bridge's law as
a checkable structural property (no verify/pool import; covered by
the #685 authority scan).
- equivalence/trace.py: canonical traces (worlds, candidates, order,
multiplicity, classifications, resolutions) + authority_violations
re-deriving the pool commit law from trace content.
- scripts/verify_semantic_equivalence.py: check / --update gate
mirroring verify_lane_shas.py.
- tests/test_adr_0184_s4b_replay_equivalence.py: full-corpus live-vs-
pinned equality, corpus-wide authority + faithfulness, and 17
single-mutation non-vacuity proofs.
Pure additions; verify.py/pool.py untouched; no serving, CLAIMS,
metrics, or lane pins moved.
ADR-0184 §7 S4: add generate/derivation/state/source.py — the single
boundary through which semantic-ledger worlds become pool candidates —
and swap pool.py's accumulation source to semantic_state_candidates().
accumulate.py keeps its public surfaces as thin compatibility wrappers
(ADR-0184 §10). Byte-identical over the 937-problem differential;
verify.py / pool.py remain the sole commit authority (structural
no-authority-import scan over state/, non-vacuous).
Also: docs/analysis design note for the S3–S5 boundary stack, and a
drive-by ruff F541 fix in r1_reconstruction.py (rf->r, zero behavior).
Compare resolved canonical Path values, not raw string spellings, when
detecting a question_path / proposal_path collision. A relative
proposal_path and an absolute question_path that name the same file now
fail closed with reason "path_collision" instead of resolving.
Adds a regression test for the absolute-vs-relative same-file collision
and keeps the existing exact-string collision test. The new test is
non-vacuous: it fails under the prior raw-string comparison.
Scope: ask_handle.py seam + its test only. No runtime.py, CLAIMS,
metrics, telemetry, schema, or lane-pin changes.
ADR-0184 S1 (state/bind.py, state/change.py) landed the reusable referent and
change-cue helpers. S2 adds the first explicit state-transition substrate and
routes both accumulation readings through it:
- state/model.py frozen SET/GAIN/LOSS transition model over one entity/unit
StateKey, with structural validation (closed op set; unit is a
str with "" = unitless, mirroring the extractor's Quantity.unit
contract; entity optional, mirroring leading_subject_token).
- state/ledger.py build_accumulation_ledger() — the proven single-referent
gain/loss reading, expressed as a SemanticLedger.
- state/replay.py replay_accumulation_ledger() — the only bridge to
GroundedDerivation; refuses any non-accumulation ledger shape
(non-SET start, cross-key mutation).
accumulate.py's _build_accumulation / _build_accumulation_anchor_skip now build a
ledger and replay it instead of constructing the arithmetic chain inline; the
public compose_accumulation()/accumulation_candidates() surfaces are unchanged.
Behavior-equivalent: a byte-for-byte differential over 937 real GSM8K problems
(accumulation_candidates, compose_accumulation, pooled_candidates, resolve_pooled)
shows 0 differences vs main. Semantic worlds never commit directly — replay emits
GroundedDerivation and verify.py / pool.py remain the sole authority. Sealed: no
serving/runtime/chat import; no CLAIMS or eval-lane-SHA movement.
Refs ADR-0184 section 7 (S2).
The missing-boundary doc (#682) established that chat/runtime.py has no
lawful in-turn provider for a QUESTION_NEEDED ContemplationResult: the
only producer is the off-serving contemplation pass, and scanning
teaching/questions/ would be new, unspecified acquisition logic. This
slice builds the recommended unblocking seam instead of the forbidden
runtime shortcut: carried-handle acquisition.
core/epistemic_disclosure/ask_handle.py:
- AskArtifactHandle — an explicit, caller-carried reference to one
already-produced Q1-D DeliveredQuestion artifact. Provenance is the
producer's own content address (sha256 of blocking_reason:slot_name:
text, also the artifact filename stem) — the only identity field the
Q1-D artifact carries today. No turn/case provenance exists yet; that
boundary is documented, not invented.
- resolve_served_ask_handle — gate-first (zero filesystem access while
ask_serving_enabled is dark), structural handle validation, single
explicit-path artifact read (no glob/iterdir/scan), and content-hash
re-derivation so a stale/replaced/tampered artifact fails closed.
Emits a ResolvedAskCandidate duck-typed to what evaluate_served_ask
reads (terminal/question_path/proposal_path) — no question prose is
ever constructed or carried.
- acquire_served_ask_from_handle — composes resolution into the
existing acquire_served_ask_candidate / evaluate_served_ask stack.
All Q1-D artifact policy (status/review/served/answer-binding/text/
slot/collision) stays delegated; the seam owns identity only.
Not changed: chat/runtime.py, chat(...) signature, ChatResponse,
TurnEvent, telemetry schema, Q1B_ASK_CARVE_OUT, proposal_allowed,
VERIFIED serving, CLAIMS/metrics/lane pins. The seam imports neither
generate.contemplation (pass_manager) nor core.epistemic_questions
render/delivery — enforced by AST tests.
Tests (tests/test_ask_handle.py, 18 cases): gate-dark no-I/O proof,
fallback preservation, valid resolution served end-to-end through the
existing stack, proposal-artifact / missing / malformed / invalid-Q1-D
/ path-collision / stale-hash / non-addressed-filename / structurally-
invalid-handle fail-closed paths, no-discovery-without-exact-handle,
and AST guards for scan primitives, forbidden imports, and prose.
Verification: focused ASK+governance suites 396 passed; architectural
invariants 56 passed; smoke-equivalent suite 90 passed; lane SHAs 8/9
content-match (the 9th is the documented local public_demo wall-clock
flake, 48.3s vs the 30s ADR-0099 budget — no content drift, no re-pin).
Adds a small chat.ask_runtime helper that consumes the ASK acquisition seam without editing chat/runtime.py.
- Uses honest contemplation_result keyword forwarding
- Preserves fallback behavior when ASK serving is disabled
- Does not call pass_manager or render_question
- Does not change public chat/runtime schema
Backs out the direct main push that added _maybe_apply_served_ask using a string-concatenation workaround to evade an over-broad text guard.
Restores chat/runtime.py to the #678 merge state and removes tests/test_ask_runtime_hook.py so the next runtime hook can land through a reviewed PR with structural tests instead of guard bypasses.
- This is the smallest runtime consumption hook for the ASK acquisition seam.
- It is default-dark.
- It preserves normal runtime behavior when disabled.
- It does not change public `chat(...)` signature.
- It does not change `ChatResponse`, `TurnEvent`, or telemetry schema.
- It does not call/render question prose.
- It does not serve from `proposal_path`.
- It does not retire Q1B carve-out or flip `proposal_allowed`.
- It does not serve VERIFIED.
- It does not move CLAIMS or metrics.
Adds the Stage 2 served ASK artifact adapter and adapter-only tests.
- Validates Q1-D DeliveredQuestion artifacts before ASK eligibility
- Uses ask_serving_enabled as a default-dark gate
- Uses the disclosure bus disposition mapping
- Fails closed to fallback surface/disposition on invalid artifacts
- Does not wire chat/runtime, telemetry, TurnEvent, ChatResponse, CLAIMS, or metrics
- Does not retire Q1B_ASK_CARVE_OUT or flip proposal_allowed
Adds dedicated ContemplationResult.question_path for off-serving ASK artifacts and stops storing QUESTION_NEEDED question paths in proposal_path. Proposal artifacts remain under proposal_path. No chat/runtime wiring, served ASK surface, Q1B carve-out retirement, registry flip, or CLAIMS/metric movement.
Adds off-serving pass_manager integration for ASK delivery through deliver_ask behind explicit exercise_ask. Preserves boundary-first ordering, writes question artifacts to the question sink via question_root, avoids double delivery, and keeps chat/runtime, served ASK, Q1B carve-out, registry, CLAIMS, and metrics untouched.
Adds explicit default-false RuntimeConfig ask_serving_enabled and verified_serving_enabled fields while preserving default-dark helper behavior. Includes focused ASK/VERIFIED gate tests and cross-gate independence coverage. No pass_manager wiring, runtime wiring, verify.py wiring, served behavior, or CLAIMS/metric movement.
Adds a default-dark verified_serving_enabled helper with focused tests. The helper is unwired: no runtime integration, no verify.py wiring, no eval producer imports, no served VERIFIED behavior, and no CLAIMS/metric movement.
Adds a default-dark ask_serving_enabled helper with focused tests. The helper is unwired: no pass_manager integration, no runtime surface, no carve-out retirement, and no served question text.
Per review: 'proposal-only' described the Q1-D sink/artifact in two docstrings,
conflating two distinct lanes. A question is an intake request, not a capability
proposal; question_only is its own lane. The analogy to the proposal emitter is
structural (toothless, review-gated, never served), never semantic. Tighten the
sink/artifact docstrings and rename the sink test to *question_only* to match
_QUESTION_STATUS. No behavior change — docstrings + one test name only.
The fourth and final Q1 rung: route the Q1-C rendered question onto the
contemplation bus as the QUESTION_NEEDED tenant (sibling of PROPOSAL_EMITTED),
off-serving. Consumes render_question VERBATIM — Q1-D constructs no prose, so the
Q1-C grounded-rendering wrong=0 guard is never bypassed by a second surface.
- generate/contemplation/findings.py: add Terminal.QUESTION_NEEDED.
- core/epistemic_disclosure/limitation.py: ask_question's home terminal is now
QUESTION_NEEDED, making terminal_for_action total (all six actions map onto
shipped terminals — the consolidation proof is now complete, not 'five of six').
- core/epistemic_questions/delivery.py: DeliveredQuestion (proposal-only artifact,
cannot wrap an unrenderable question / cannot be served / answer_binding reserved
None — illegal states unrepresentable), DeliveryOutcome, deliver_ask, the
teaching/questions sink emitter, and AnswerBinding (reserved for Q2).
Rulings honored:
- D1: off-serving only — no served surface, no ask_serving_enabled, no chat wiring.
- D2: an unrenderable ASK falls back to the family's standing disposition
(PROPOSAL_EMITTED if it still proposes, else NO_PROGRESS) — never a contentless
QUESTION_NEEDED. Enforced twice: in deliver_ask and structurally in __post_init__.
- D3: Q1B_ASK_CARVE_OUT untouched — registry keeps proposal_allowed=True; both
signals coexist off-serving so no operational signal is lost.
- D4: sink under teaching/questions/ (sibling of teaching/proposals/).
- D5: strictly single-slot — multi-slot is unrenderable, takes the D2 fallback.
Tests: 16 delivery tests (happy path verbatim, both D2 fallback branches,
structural guards, idempotent sink, off-serving AST) + updated the two limitation
consolidation tests for the now-total terminal map. smoke 90/0, affected suites
128/0.
The template asserts 'one more value is still needed' (exactly one). Rendering
the first of several missing slots and ignoring the rest makes that claim
subtly false. Refuse multi-slot assessments with multi_slot_not_supported
(slot=None) rather than render-first-drop-rest; one-question-per-slot fan-out
is a later rung. Replaces the now-contradicting first-of-many test with a
test that pins the honest refusal.
Add core/epistemic_questions/ — the renderer rung of the ASK spine.
render_question(LimitationAssessment) -> EpistemicQuestion turns ASK typed
residue into a user-facing question or an explicit question_unrenderable verdict.
Render only: no bus delivery, no served disposition, no serving (that is Q1-D).
Policy (scoping §2 / session §1.5.7 wrong=0 invariant): a question may name a
problem entity only if it appears verbatim in grounded_terms. Two substrate facts
force a generic-structural policy today: grounded_terms is empty everywhere (the
readers do not yet emit verbatim evidence), and a *missing* slot's referent is by
definition absent from the trace. So the renderer:
- renders a generic question whose only variable content is a controlled English
phrase for the slot's expected_unit_or_type, from a closed audited map;
- never surfaces slot_name or binding_target (snake_case), never prettifies an
identifier into a natural-language entity, never names a problem entity;
- degrades unmapped types to question_unrenderable (renderability_gap) rather
than dumping raw snake_case;
- single-slot (first slot only); non-ASK / zero-slot -> unrenderable;
- post-render fabrication guard re-checks every token is closed-vocab scaffold or
grounded (defense in depth).
Off-serving: imports no generate.derivation / core.reliability_gate (AST-tested).
Tests: 9/9 green; smoke 90/0.
Adds bound_slots_digest as a distinct VerificationProof obligation before any serving wiring. Keeps the VERIFIED lane off-serving while separating derivation, stated-slot binding, and back-substitution.
Reclassify missing_total_count / missing_weighted_total in the limitation layer
from (capability_gap, emit_proposal) to (missing_information, ask_question) —
the user CAN state the total, so these are missing data, not capability gaps.
Add typed ASK residue (MissingSlot, grounded_terms) on LimitationAssessment;
defaults to empty so existing P0-1 callers continue to work.
Q1B_ASK_CARVE_OUT: the shipped failure_family REGISTRY still flags both as
proposal_allowed=True so existing consumers (proposal pile, contemplation
pass_manager) keep emitting proposal artifacts until Q1-C/Q1-D wires ASK
delivery. Flipping the registry flag before ASK is served would create a
no-proposal/no-question dead zone — a capability-regression window with no
compensating intake. The carve-out is named, enumerated, and explicit; once
ASK delivery is live the REGISTRY flag flips and the carve-out retires.
Wrong=0 invariant on residue (scoping §2): MissingSlot carries family-typed
structural identifiers only (snake_case); grounded_terms is verbatim text from
ComprehensionAttempt.evidence SourceSpanLinks — never fabricated from family
or refusal_reason. Today classify_* leaves evidence empty for refusals, so
grounded_terms is empty everywhere; the wiring is ready for the residue
upgrade to readers later.
Off-serving: limitation.py imports no generate.derivation / core.reliability_gate
(AST-guarded by test_limitation_module_is_off_serving). No renderer, no bus
delivery — those are Q1-C/Q1-D.
Tests:
tests/test_limitation_assessment.py — 39 passing
+ test_proposal_allowed_iff_emit_proposal_with_q1b_ask_carveout (amended INV-A)
+ test_missing_{total,weighted}_count_limitation_is_ask_oriented
+ test_q1b_ask_carveout_is_explicit_and_enumerated
+ test_registry_proposal_allowed_preserved_until_ask_delivery
+ test_no_signal_loss_before_question_bus_is_serving (live pass_manager check)
+ test_missing_{total,weighted}_count_has_typed_missing_slot_residue
+ test_grounded_terms_populated_only_from_evidence_spans
+ test_residue_never_contains_ungrounded_terms
+ test_residue_empty_for_non_ask_attempts
+ test_missing_slots_default_empty_for_non_ask_families
+ test_missing_slot_keys_are_subset_of_ask_families
+ test_limitation_module_is_off_serving (AST guard)
Suites:
tests/test_epistemic_*.py, test_disclosure_claim.py, test_served_disposition.py,
test_verified_contract.py, test_failure_family.py, test_failure_proposal.py,
test_r3_router_contemplation.py, test_proposal_review_*.py,
test_idle_proposal_review.py — 135 passing
core test --suite smoke -q — 90 passing
The first consumer of the P1-A VERIFIED contract. Given an R2 problem (prose) +
the INDEPENDENTLY hand-authored gold SETUP, it builds a VerificationProof and runs it
through the contract. NOT a serving-time VERIFIED producer: it depends on the eval-lane
gold setup (off-serving only), cannot move any serving metric, cannot emit served
[verified]. Serving-time independence (a non-gold second reader) is a separate, larger
rung, explicitly deferred.
evals/constraint_oracle/verified_producer.py (placed in evals/, not core/, to avoid a
backwards core->evals import: it needs the reader+solver (generate), the canonical
signature + gold (evals), and the contract (core) — clean direction evals->core+generate):
- verify_r2(text, gold_setup) -> R2VerificationOutcome.
- Two independent reads: primary = read_constraint_problem(text) (lineage
constraint_reader_v1); independent = the gold-authored setup (lineage
constraint_gold_author_v1). Convergence = equal constraint_setup_signature digests
(the SAME function for both reads). The gold ANSWER never enters — only the gold
STRUCTURE, through the signature.
- A wrong reader structure diverges from the gold-authored structure -> reads_disagree
-> NOT_VERIFIED (the poison). Reader refusal -> no proof + a limitation. Solver
boundary -> boundary_clear False -> NOT_VERIFIED. VERIFIED state/claim reached ONLY
via disclosure_for_verification.
On the real r2_gold lane (13 records): VERIFIED=7 (exactly the 7 'solved'),
NOT_VERIFIED=6 (3 solver_refuses + 3 reader_refuses) — wrong=0.
11 tests: integration over real gold (verifies >=1, never on divergence); all 7 solved
verify; the poison (divergent independent read -> reads_disagree); reader-refusal -> no
proof + limitation; solver-boundary -> not verified; distinct/named lineages; the
sanctioned route; all digests populated; gold answer structurally cannot enter; off-serving.
NOTE (bound_slots_digest): the P1-A VerificationProof (merged, unchanged per scope) has
no bound_slots_digest field; the "bound to stated quantities" obligation is carried by
derivation_digest (hashed over the canonical constraints + solution). Flag to extend.
Off-serving; smoke 90/0.
Defines what it MEANS for a result to earn DisclosureClaim.VERIFIED /
EpistemicState.VERIFIED — the soundness≠correctness gate the whole VERIFIED lane
rests on. Contract ONLY: no producer, no reader, no solver, no serving, no
verify.py, no verified_serving_enabled, no claim emission in any serving path.
core/epistemic_disclosure/verified_contract.py:
- VerificationObligation — the declarative contract (4 load-bearing flags);
VERIFICATION_OBLIGATION is fully strict.
- VerificationProof — the replayable proof SHAPE P1-B+ will fill (two reads kept
separate: distinct lineages for independence, matching digests for convergence).
- evaluate_verification(proof, *, limitation, obligation) -> VerificationResult —
refuse-preferring: VERIFIED only if EVERY obligation survives.
- disclosure_for_verification(result) — the ONLY sanctioned route to
(EpistemicState.VERIFIED, DisclosureClaim.VERIFIED); anything else -> UNDETERMINED/NONE.
The mechanism that makes the lane safe: VERIFIED requires TWO INDEPENDENT reads
(distinct lineages) that CONVERGE on one canonical structure. A faithful solve of a
WRONG read is caught because the independent read disagrees (back-substitution alone
cannot catch a read error); "same answer twice" is rejected as not independent.
Neither gold-agreement nor no-refusal nor a second solver over one read can verify.
15 tests, all non-vacuous. THE central test rejects a faithful solve of a wrong read;
one proves the wrong-read obligation is load-bearing (relax it -> poison slips the
check); the full chain composes proof -> contract -> (VERIFIED, VERIFIED) -> P0-3
DISCLOSE, and the poison degrades to COMMIT. Off-serving; smoke 90/0.
The third axis of the bus: choose_served_disposition composes
EpistemicState × LimitationAssessment × DisclosureClaim → ServedDisposition.
Pure mapping — no rendering, no bus behaviour, no verify.py, no govern_response
mutation. Nothing consumes it yet.
core/epistemic_disclosure/disposition.py:
- ServedDisposition(str, Enum): commit | disclose | ask | propose | report |
explain | refuse | step_aside.
- choose_served_disposition(*, epistemic_state, limitation, disclosure_claim=NONE):
a blocking limitation dominates (its resolution_action -> ask/propose/report/
explain/refuse/step_aside; scope_boundary -> EXPLAIN, NOT a hard REFUSE);
otherwise the disclosure claim + state pick the serve mode.
The Phase-1 VERIFIED guard (load-bearing): a DisclosureClaim.VERIFIED discloses
ONLY when EpistemicState.VERIFIED backs it; an unbacked verified claim degrades to
a plain COMMIT — never served as verified before the producer exists.
30 tests: the load-bearing rules via REAL P0-1 assessments; the VERIFIED guard
parametrized across all 14 non-VERIFIED states; limitation-dominates-claim;
answer-action fall-through; totality over both axes; off-serving. Smoke 90/0.
A served response carries two ORTHOGONAL governance properties: ReachLevel (how
far past grounded fact it reaches) and DisclosureClaim (the epistemic claim it
makes about its own truth status). "verified" is NOT a reach level — it is a
proof-state claim. Keeping the axes separate is the locked Stage-2 decision (no
ReachLevel.VERIFIED; a proven answer never inherits an [approximate] surface).
core/epistemic_disclosure/disclosure_claim.py:
- DisclosureClaim(str, Enum): none | verified | approximate | proposal_only.
Default = none. str-valued for stable serialization (EpistemicState convention).
- Discipline "no claim without a producer": every member has a real (APPROXIMATE
= cognition Step E; PROPOSAL_ONLY = teaching/proposals) or imminent (VERIFIED =
Phase 1 target) emitter. PROVEN and ESTIMATED are intentionally ABSENT — nothing
emits them; ESTIMATED is a future split of APPROXIMATE, added only when a real
estimator producer exists (docs-note only).
Also folds the P0-1 review nit: _KIND_TO_STATE / _ACTION_TO_TERMINAL tightened
from dict[str, ...] to their Literal key types.
9 tests, non-vacuous: default is NONE; verified is not a ReachLevel (structural);
claim axis disjoint from ResolutionAction; PROVEN and ESTIMATED absent; exactly the
approved four; str-enum serialization; module imports nothing (off-serving).
No bus behaviour, no ServedDisposition mapping (that is P0-3). Off-serving; smoke
green (90 passed) locally.
The intake gate of the Epistemic Disclosure spine: classify WHAT KIND of
limitation blocks resolution before choosing a served disposition. Asking is
one resolution among six; mis-classifying breaks intake (refuse-when-should-ask
loses the unlocking datum; ask-when-should-propose wastes the channel).
New off-serving package core/epistemic_disclosure/:
- LimitationKind (8) / ResolutionAction (6) / LimitationAssessment
- the mapping DERIVED from the shipped failure_family REGISTRY + contemplation
Terminal set — a consolidating VIEW, not a fourth taxonomy. The only genuinely
new action is ask_question (the one action with no shipped terminal); every
other action maps onto an existing Terminal (terminal_for_action proves it).
- assess_from_family / assess_from_attempt; conservative refuse default
(an unclassified reason NEVER becomes an answerable question — wrong=0-safe).
The missing_total_count / missing_weighted_total reclassification
(capability_gap -> missing_information) is DEFERRED to Q1-B with its own tests,
not made here; PENDING_Q1B_RECLASSIFICATION + a tripwire test document it.
27 tests, all non-vacuous (each fails under a single mis-keyed entry): totality
over REGISTRY, flag-consistency invariants (proposal<->propose; contradiction
verdict reports; foreign text steps aside), the wrong=0 invariant (hard_boundary
never asks), load-bearing specific mappings, the consolidation proof, and the
attempt-level paths.
Off-serving: imports no generate.derivation / core.reliability_gate (AST-checked);
cannot move the sealed GSM8K metric. Nothing consumes resolution_action yet.
Smoke green (90 passed) locally.
CMB (r4_combined_rate) now participates in route_setup and bounded contemplation. R1/R2/R3 readers/
solvers, the CMB reader/solver/oracle, and serving are all UNCHANGED — pure shared-infra integration,
no new comprehension capability (that was CMB-c).
- model.py: Organ += r4_combined_rate.
- classify.py: classify_cmb + _cmb_signature (organ-tagged, cross-organ-distinct) + cmb_reason —
namespaces CMB reasons cmb_* before the reason-string-keyed 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). not_combined_rate_shaped/empty stay bare -> the
cross input_shape family (router hygiene).
- failure_family.py: Owner += r4; input_shape += not_combined_rate_shaped; 8 CMB families
(must_remain_refused: cmb_unit_mismatch [until a dimension registry exists — NOT forever],
cmb_combine_ambiguous, cmb_underdetermined, cmb_non_positive_net, cmb_non_integer; proposal_allowed
-> cmb_gold_fixture: cmb_unsupported_rate_count/_reciprocal/_clock_interval).
- pass_manager.py: _solve_and_verify_cmb (solver Refusal -> REFUSED_KNOWN_BOUNDARY, never a proposal).
CMB-over-R3 domain-precedence rule (cmb_is_authoritative): when CMB POSITIVELY recognizes
combined-rate shape (a setup, or a substantive cmb_* refusal — NOT the input_shape step-aside), R3's
broader single-rate over-read of the SAME text is inadmissible — for BOTH routing (veto R3
setup_correct) and family attribution (suppress R3 so CMB's sharper diagnosis owns the
terminal/proposal). This came from a pre-build cross-organ pressure-test: R3 reads 'Anna and Ben
paint together; Anna paints 3 rooms/hour. How many do THEY paint in 4 hours?' as a single rate and
would confidently answer a wrong 12. The rule fixes that (-> REFUSED) without touching R3. It does
NOT mean 'CMB always wins': on input_shape CMB cedes (a plain single-rate car problem routes to R3
and solves 180). Narrow CMB<->R3 instance of a future general domain-specificity adjudication.
Tests: new test_cmb_router_contemplation (full terminal matrix; cmb-11 veto-no-wrong-answer; cmb-15
cede-to-R3; solver-refusal-never-proposes; CMB-owned proposal-only artifacts mounted:false; no gold
ambiguous; off-serving AST). Extended router-organ-hygiene to CMB x R1/R2/R3 (0 breaches).
test_setup_router/test_r3_router_contemplation attempt-count 3->4; test_failure_family ALL_REASONS +
growth set. 97 CMB-d tests; 597 broad-net pass (1 unrelated pre-existing red); CMB organ lanes green.
The mandatory lookback before CMB-d (3+ PRs on one surface; next-phase boundary). A 5-dimension
read-only fan-out audit over the 10-item checklist found the CMB-a/b/c substrate composes cleanly:
25 solid / 4 drift / 8 gap / 1 reader hazard. Doc:
docs/analysis/cmb-lookback-review-2026-06-08.md.
Fixes the one real hazard (H3): a query attributing the answer to a SINGLE agent ('how many words
does Alice type' with a distractor second rate) wrongly claimed the substantive
combine_mode_ambiguous — a hygiene over-claim that would corrupt the shared router in CMB-d. The
combined-query gate now excludes single-agent 'does <Agent>' attribution (_SINGLE_AGENT_QUERY) and
steps aside as not_combined_rate_shaped; the genuinely-combined forms ('do they' / 'are produced' /
'does it') still yield combine_mode_ambiguous. Pinned by gold cmb-16 + a reader test.
The review also resolves the CMB-d failure-family classification (input_shape:
not_combined_rate_shaped; must_remain_refused incl. rate_unit_mismatch — DECISIVELY, the reader has
no dimension representation so it cannot tell convertible from incompatible units; proposal_allowed:
combine_mode_ambiguous, missing_second_rate) and enumerates the live CMB-d registry preconditions
(not_combined_rate_shaped mapping, rate_unit_mismatch/non_integer_solution string collisions,
non_positive_net_rate family, Organ/ALL_REASONS extension). Provenance false-alarm (stale local main
checkout) noted + verified against origin/main.
gold 19/19 (6/5/8); reader 11/0/0 + 8 refused-correct; solver 6/0 + 5/0; 0 hygiene breaches; 123
tests; serving + R1/R2/R3 unchanged.
Third rung of the CMB ladder. Reads explicit combined-rate prose into a canonical
CombinedRateProblem or a closed reader refusal; graded by the CMB-a ruler + CMB-b solver via a new
run_reader lane. Off-serving; no router/contemplation/serving/ledger. Claim: 'English prose reads
into a canonical CMB setup or a closed reader refusal.'
Enforces the CMB-a 2x2 domain-entry grid + the deferral taxonomy (rate_unit_mismatch,
combine_mode_ambiguous, missing_second_rate, three_or_more_rates, reciprocal_work_rate_deferred,
clock_interval_deferred, not_combined_rate_shaped). Non-positive-net / non-integer stay the SOLVER's
refusals (the reader parses them). Router-organ hygiene: foreign R1/R2/R3 text steps aside as
not_combined_rate_shaped (the input_shape family), never a substantive boundary (0 breaches on all
36 R1/R2/R3 gold).
THREE adversarial verification rounds found 11 real wrong=0/hygiene hazards a curated-gold lane
cannot surface; all fixed + regression-tested. Root-cause fixes (not point-patches):
- cues are whole-word; difference = a CLEAN fill-vs-drain OPPOSITION classified per rate clause by
the verb in its own lead-in; rate_a=fill/rate_b=drain BY ROLE (a drain listed first still
subtracts); incidental drain nouns / drain-verbs-between-clauses no longer flip the mode;
- query duration + time-query quantity are read only from AFTER the 'how many' question, so neither
a premise number nor a transitional time marker is mistaken for them;
- the rate regex rejects decimals; substantive refusals are gated behind a real combination cue;
combine_mode_ambiguous requires a genuine COMBINED query (a compared pair steps aside);
- sequential segments (each rate carrying its own duration, tolerant of 'also for') step aside.
reader lane 11/0/0 + 7 refused-correct; gold 18/18; 19+17+3 adversarial triggers across 3 rounds;
126 tests (CMB + router/hygiene + R1/R2/R3 readers); serving + siblings unchanged.
Next (ladder): CMB-d router/contemplation wiring -> CMB-e ledger.
solve_combined_rate(CombinedRateProblem) -> int | Refusal over effective_rate = rate_a +/- rate_b.
effective_rate query returns the net (even <=0); quantity/time queries need a positive net rate
(non_positive_net_rate) and exact division (non_integer_solution). Pure integer arithmetic, no
float/Fraction (CMB v1 crosses no units). Off-serving. Graded by a new run_solver lane in
evals/combined_rate_oracle against the committed gold.
Adversarial 5-lens(+adjudicator) verification returned fix_first; both real hazards fixed:
- wrong=0 breach: negative known-slot inputs (time<0/quantity<0) bypassed the eff<=0 guard and
produced negative answers. FIXED UPSTREAM in model.py __post_init__ — rate_a/rate_b and the known
time/quantity are now positive ints, so the illegal state is unrepresentable and the solver can
never receive a negative-yielding path. Added model + gold (cmb-07d eff<0/time) + lane coverage.
- doc overclaim: the solver lane does NOT grade 'two independent paths' (both solver and the
oracle's _canonical_outcome delegate to model.effective_rate). Corrected both docstrings to name
the true anchor (committed gold + inline-computed literal tests) and added a difference-mode
inline-computed test.
R3-vac came back SOLID (separate PR). gold 18/18 (6/5/7); solver lane 6/0 + 5/0; 36 CMB tests;
router-hygiene + serving unchanged.
Closes the sibling vacuity CMB-a surfaced: R3's evals/rate_oracle solver_refuses branch
accepted any fixture labelled non_integer_solution without checking the arithmetic, and the
solved branch never recomputed the gold. A curator could certify an arithmetically-impossible
refusal or a wrong gold (with a self-consistent answer key) as valid — the 'obligation that
can't meaningfully fail = decoration' case CLAUDE.md forbids.
Adds _canonical_outcome (independent reimplementation of the single-rate algebra WITH R3.2
conversion — NOT a call to generate.rate_comprehension.solver, which would be circular). The
validator now cross-checks every solved gold (== computed) and every solver_refuses reason
(genuine) against it, with dedicated meaningful-fail tests anchored on hand-written literals.
Also removes the dead determinism self-comparison.
R3 gold stays 13/13 valid (7/2/4); reader 9/0/4 unchanged; 26 oracle/reader tests pass.
Off-serving; serving + other organs untouched.
First rung of the combined-rate ladder, off-serving (imports no generate.derivation /
core.reliability_gate). Claims exactly: the combined-rate setup ruler is defined and
gold-valid. No reader/solver/wiring yet — no capability claim.
- generate/combined_rate_comprehension/: local RateUnit (decoupled from R3), CombinedRateProblem
(two-explicit-rates + per-query-slot guard; effective_rate = rate_a +/- rate_b; non-positive net
left to the solver, not the model).
- evals/combined_rate_oracle/: span-free signature (sum commutative / difference ordered) + a
NON-VACUOUS validator. 17 gold fixtures: 6 solved (full combine_mode x query grid), 4
solver_refuses, 7 reader_refuses (the complete refusal taxonomy + the 2x2 domain-entry grid).
Adversarial 5-lens verification returned fix_first; the validator now cross-checks every solved
gold and solver_refuses reason against the canonical arithmetic (_canonical_outcome) so a
mislabelled / arithmetically-impossible fixture is rejected (meaningful-fail per CLAUDE.md), with
dedicated tests. Added eff<0 and eff=0/time coverage fixtures; removed a dead determinism guard.
gold 17/17 valid; 25 oracle tests; R1/R2/R3 + router-hygiene + serving all unchanged.
Doc: docs/analysis/cmb-a-combined-rate-ruler-2026-06-08.md
Option A (text-faithful): RateProblem gains time_unit (the duration's ORIGINAL unit; time stays int; defaults to the rate denominator). The SOLVER converts via convert_time -> exact Fraction -> int-or-refuse(non_integer); Fraction never leaves the solver, no floats. The reader accepts a convertible (minute<->hour) duration mismatch (keeps original time_unit); a non-convertible one still refuses rate_unit_mismatch. Signature includes time_unit ('30 minutes' != '30 hours').
Gold: r3-09 flips reader_refuses -> SOLVED (60 mph for 30 min = 30 mile, exact 1/2 hour; distractor 1800 rejected); new r3-13 non-convertible (3 gallons) stays rate_unit_mismatch. R3 gold 13/13; reader 9/0/0 -> answers 7/0/6 (setup_correct 8->9). Failure-family: rate_unit_mismatch now non-convertible-only -> still unsupported_rate_duration proposal surface; router-hygiene invariant stays green; R1 7/0/3, R2 10/0/0 unchanged. 119-test smoke green incl. invariants. Off-serving, no float path.
generate/rate_comprehension/conversion.py: convert_time(value, from_unit, to_unit) -> Fraction for known time units (minute<->hour), exact rational (fractions.Fraction), NO floats. is_convertible gates it. 30 minute -> Fraction(1,2) hour; non-time/non-convertible (minute<->mile, dollar<->hour) raises ConversionError so the caller still refuses rate_unit_mismatch. Tiny: only minute<->hour in v1 (length/currency/compound deferred). 6 tests incl. never-a-float guard.
No organ may claim a substantive boundary on text outside its domain — on every OTHER organ's gold, a refusal must map to the non-substantive input_shape family (not-my-domain), never a substantive boundary/growth surface. This is what lets one organ suppress another's legitimate proposal via boundary-first (caught at N6, R3e, R3.1). MUST-PASS gate before any new organ joins route_setup. Currently green for R1/R2/R3. + standing-rule doc.
classify_r3 normalizes the rate reader into a ComprehensionAttempt; route_setup now tries R1/R2/R3 (same exactly-one-setup_correct rule); the pass manager solves+verifies a routed r3_rate setup (solve_rate + reused answer-choice verifier); unsupported_temporal_state dropped from the unsupported-family set so temporal_state -> REFUSED_KNOWN_BOUNDARY. Organ gains r3_rate.
Terminal matrix (verified): 6 solved->SOLVED_VERIFIED; rate_unit_mismatch+combined_rates->PROPOSAL_EMITTED(unsupported_rate_duration); non_integer/missing_time/temporal->REFUSED_KNOWN_BOUNDARY; exactly 2 proposals (the rate-like gaps only).
REGRESSION CAUGHT+FIXED: R3's reader returned missing_rate (a substantive boundary) on non-rate R2 text, blocking r2-011's legitimate missing_total_count proposal via boundary-first. Fix: missing_rate only when a duration is present; else not_rate_shaped -> input_shape (not-my-domain) — same discipline as the N6 category_pair_not_found fix. r2-011 proposes again; R1/R2 unaffected. R1 7/0/3, 15-case 15/0/0, R2 10/0/0, R3 8/0/0 unchanged; off-serving; 121-test smoke green incl. invariants + idle contract suites.
docs/analysis/r3-rate-inventory-ledger: R3 v1 state (reader 8/0/0 + 4 refused -> answers 6/0/6; gold 12/12), per-fixture table, the compound-unit substrate, covered families + R3.2/R3.3 deferrals.
core/comprehension_attempt/failure_family.py: makes unsupported_rate_duration REACHABLE as a growth surface (proposal_allowed) mapping rate_unit_mismatch + combined_rates — both emitted ONLY after a rate clause is recognized, so always rate-like (anti-over-propose discipline, same as the N6 fix). missing_rate/time/quantity -> rate_underdetermined boundary; temporal_state -> unsupported_temporal_state boundary (clock detector can fire on non-rate text, NOT a safe growth surface); query_target_unrecognized/no_query -> input_shape. Owner gains 'r3'. 107-test smoke green incl. partition + contemplation + invariants; R1 7/0/3 + R2 10/0/3 unchanged; off-serving.
generate/rate_comprehension/solver.py: solve_rate computes the one unknown — quantity=rate×time (always integer), rate=quantity÷time, time=quantity÷rate (exact division or REFUSE non_integer_solution, never rounds). Confirms compound-unit composition via the R3a algebra (defensive; a RateProblem's units compose by construction). answer_unit gives the answer's unit. No floats.
Ties to the gold: solves all 6 solved fixtures to gold; refuses both solver_refuses with non_integer_solution. Meaningful-fail: 100÷3 refuses (not 33), 99÷3=33. Off-serving. 5 tests.
generate/rate_comprehension/units.py: BaseUnit + RateUnit (numerator/denominator) + the three single-rate compositions — rate_times_time (mile/hour × hour = mile; refuses mile/hour × minute), rate_from_quantity_over_time (mile ÷ hour = mile/hour), time_from_quantity_over_rate (mile ÷ mile/hour = hour; refuses dollar ÷ mile/hour). A non-composing op raises UnitError (the wrong=0 dimensional gate), never fabricates a unit.
Deliberately tiny: bare-name units, no generic dimensional universe, no semantic unit-class typing, no unit conversion, no compound-of-compound. Fresh off-serving organ (generate/rate_comprehension/). 10 tests incl. all three refusals + consistent composition.
core/config.py: review_pending_proposals: bool = False (opt-in, mirrors consolidate_determinations). chat/runtime.py: IdleTickResult gains an additive optional proposal_review field; idle_tick runs a READ-ONLY sub-pass (after consolidation, gated) that surfaces core.proposal_review.idle_summary(). It does NOT set did_work / checkpoint / mutate / ratify / mount / modify readers; a reporter exception is CAPTURED (safe=False, errors=('proposal_review_failed:<type>',)), never propagated.
docs/runtime_contracts.md documents the sub-pass + field (required for runtime-surface changes). Contract tests: default-off -> proposal_review None (existing shape preserved); enabled -> summary surfaced; exception captured not propagated; read-only review never checkpoints; other passes unperturbed. 91-test smoke green incl. architectural invariants + both existing idle contract suites. Existing idle_tick remains the only idle_tick; NOT L10 always-on.
core/proposal_review/summary.py: idle_summary(root=None) -> ProposalReviewIdleSummary composes the landed scan/build_report/dry_check into one JSON-safe primitives-only value (safe, total, review_needed, malformed, by_family tuple-of-pairs, errors). Pure read — no mutation, no clock, no runtime touch. errors carries the dry-check violations (why not safe); IT-b reuses it for a captured reporter exception. 7 tests incl. read-only + deterministic + json-serializable.
core/proposal_review/safety.py: dry_check INDEPENDENTLY verifies (without trusting the emitter) that every artifact is inert — status==proposal_only, mounted==false, requires_review==true, content-address consistent (filename == sha256(family:problem_text_sha256)), path under the sink, no malformed (unverifiable) file — and that no serving-path module reads the sink. Any failure is a violation.
__main__.py: python -m core.proposal_review [comprehension-failures] [--json] [--root] — read-only report + dry-check; exit 0 iff safe. Mutates nothing. 8 tests, each safety assertion proven meaningful-fail; serving-unconsumed verified on the real tree.
core/proposal_review/report.py: build_report projects scan results into a deterministic summary — total pending, counts by failure_family + by status (sorted), malformed count, review-needed list (sorted by content hash). report_json (sort_keys) + report_text. Deterministic given sink contents; no clock.
Time-based oldest/newest is intentionally OMITTED: the artifacts are content-addressed with no timestamp (the emitter is clock-free for idempotence), so an honest temporal order isn't in the data — only non-deterministic filesystem mtime, which would break report determinism. 5 tests.
core/proposal_review/{model,scan}.py: scan reads teaching/proposals/comprehension_failures/*.json into typed PendingProposal records (path, content_hash, failure_family, status, mounted, requires_review, problem_text_sha256, observed_attempts); any unparseable file becomes a MalformedArtifact (never silently dropped). Pure read — never writes/moves/deletes; deterministic (sorted by path); missing sink -> empty. The sink path is computed independently of the emitter (the reporter verifies the contract, doesn't import the writer).
Read-only reporter, NOT an idle_tick (ChatRuntime.idle_tick stays the only one), NOT L10. 4 tests incl. read-only invariant + malformed flagging.
generate/contemplation/{findings,pass_manager}.py: a single bounded pass — route (N3) -> classify+enrich (N4) -> terminal -> maybe emit proposal-only (N5). No loops/daemon/L10. Seven terminals: SOLVED_VERIFIED / REFUSED_KNOWN_BOUNDARY / REFUSED_UNSUPPORTED_FAMILY / CONTRADICTION_DETECTED / PROPOSAL_EMITTED / AMBIGUOUS_ORGAN / NO_PROGRESS. R2 solved+verified end-to-end; R1 routed setup is SOLVED_VERIFIED (numeric answer stays the eval lane in v0).
Hazard found+fixed: N6 exposed that category_pair_not_found fires on ANY non-R2 text, so mapping it to a growth surface made gibberish falsely PROPOSAL_EMITTED. Reclassified it to input_shape and made missing_category_pair reserved — only the PRECISE missing_total_count/missing_weighted_total remain reachable growth surfaces. Classification is boundary-first (input_shape non-blocking), so a problem one organ recognizes as a substantive boundary never proposes against the other's broad refusal.
Acceptance proven: known correct refusal -> no proposal; unsupported gap -> proposal-only artifact (exactly the 2 missing_* gaps over the gold); answer-key contradiction -> CONTRADICTION_DETECTED; organ conflict -> AMBIGUOUS_ORGAN. 188 tests green incl. architectural invariants; R1 7/0/3 + R2 reader 10/0/0 unchanged; off-serving.
core/comprehension_attempt/proposal.py: emit_proposal writes a content-addressed teaching/proposals/comprehension_failures/<hash>.json ONLY for growth-surface families (proposal_allowed). Deliberately toothless: status always proposal_only, mounted always false, requires_review always true (enforced in __post_init__); the problem text is hashed (sha256), never stored; filename = sha256(family:text_sha) so the same failure is idempotent.
No proposal for a correct wrong=0 boundary. Serving never reads the sink (scanned: generate/stream, field/propagate, vault/store, generate/derivation, core/reliability_gate). Routes through the existing proposal-only flywheel (ADR-0055/0056/0057), never a parallel correction path. README pins the contract. 6 tests.
core/comprehension_attempt/failure_family.py: partitions every reachable organ refusal reason (R1 reader/admissibility, R2 reader/solver/answer-choice) into a named FailureFamily declaring owner / must_remain_refused / proposal_allowed / safe_next_action / proposal_target. family_for_reason is total + unambiguous (partition asserted by test); enrich_family resolves an attempt's family.
Only THREE families are growth surfaces (proposal_allowed=True): the R2 missing_category_pair / missing_total_count / missing_weighted_total gaps. All other reasons are correct wrong=0 boundaries (refuse, no proposal) — correct refusal != missing capability. answer_key_contradiction is a report verdict (no reason). Reserved R3 families forward-declared. 9 tests incl. whole-surface coverage + partition.
core/comprehension_attempt/router.py: route_setup tries R1 then R2, collects both attempts, and selects a setup ONLY when exactly one organ admits one. Zero->all_refused; >=2 with differing signatures->ambiguous (refuse, never pick); cross-organ signatures never coincide so two admitting organs always resolve to ambiguous. No scoring, no solving.
wrong=0 invariant asserted: every routed R2 setup is byte-equal to the independent gold signature. Each gold problem routes to exactly its owning organ (R2 well-formed->r2; R1 admitted->r1; refused->all_refused); none is ambiguous. 4 router tests.
core/comprehension_attempt/{model,classify}.py: a small frozen ComprehensionAttempt (organ, outcome, case_id, refusal_reason, family, setup_signature, answer, evidence) and classify_r1/classify_r2 that normalize each organ's heterogeneous output (typed setup | Refusal) into a uniform produce-mode setup outcome — setup_refused (with the organ's reason) or setup_correct (with a deterministic inline signature).
No solving, no gold comparison, no evals import (signatures computed inline) — runtime never depends on the harness. Ties to both gold corpora: R2 solved/solver_refuses->setup_correct, reader_refuses->setup_refused with the gold reason; R1 7 admitted / 3 refused. family left None (resolved by the N4 registry). 6 tests.
Per review: (H1) exactly-two-category binding already has its meaningful-fail twin; (H2) a weighted total whose unit does not match the coefficient unit refuses missing_weighted_total (never a cross-unit sum); (H3) answer-choice ties EXACTLY or refuses no_matching_option (never nearest). All three were already handled by construction; these pin them so they stay handled.
generate/answer_choices/{parse,verify}.py: parse_options normalizes {label:value} to {label:int} (int or single-integer string; ambiguous/empty refuse). verify_answer_choice ties a PROVEN value to exactly one option -> ChoiceVerdict(consistent); a disagreeing key -> ChoiceVerdict(contradiction) naming both the consistent answer and the wrong key (truth discipline, not a refusal). Refuses no_matching_option / ambiguous_options / unknown_provided_label.
End-to-end with C2 gold + C3 solver: every solved fixture solves, ties to its labeled answer, confirms consistent. Off-serving. 9 tests incl. contradiction-flag meaningful-fail.
generate/constraint_comprehension/solver.py: solve_two_var_linear (order-independent 2x2 integer Cramer's rule over typed constraints), the solve_two_var_count_weight specialization, and solve/answer_constraint_problem driving it from a ConstraintProblem. Four typed refusals: indistinguishable_weights (det==0), non_integer_solution (numer%det!=0, never rounds), negative_solution, verification_failed (identity backstop).
Ties to the C2 gold: solves all 7 solved fixtures to their gold value and refuses all 3 solver_refuses fixtures with EXACTLY the gold-claimed reason (the gold's reason is now solver-verified, not just annotation). Per-refusal meaningful-fail + positive re-substitution. Off-serving. 9 tests.
evals/constraint_oracle: span-free canonical setup signature (unknowns/facts/constraints/query; terms merged+sorted, lhs constant folded into rhs), a 13-fixture gold (7 solved / 3 solver_refuses / 3 reader_refuses) with a closed expect+refusal taxonomy, and a gold-validation runner. No reader yet — this proves the ruler is coherent (13/13 valid), not capability.
ADR-0211 ratifies the R2 finite-integer two-category constraint compiler: the IR, the canonical signature, the closed taxonomy, the four wrong=0 gates (setup oracle / reader refusal / exact solver / answer-choice contradiction flag), off-serving disjointness, and the C1-C9 ladder. Cross-refs ADR-0207/0175/0083/0055-0057; reuses the R1 setup_oracle pattern. Off-serving (no generate.derivation / reliability_gate). 13 oracle tests incl. per-branch meaningful-fail.
r1-07 now reads setup-correct and answers 6 — 'Nia has 9 more beads than Omar. Nia has 15. -> Omar = 6'. The reader binds the unknown base's unit FROM the relation when its subject is a known fact and its referent is the otherwise-ungrounded query target, so the equation is admissible; the answer oracle reverse-solves it (PR-7a). Bounded: single base == query target (no chains), known subject value, base not otherwise grounded, <=1 inverse (multiple_inverse_bases else), never over times/divide.
R1 setup 6/0/4 -> 7/0/3; R1 answers -> 7 correct / 0 wrong; 15-case 15/0/0; setup_wrong stays 0. Off-serving. Refreshes the R1 ledger to 7/0/3 (R1 closed; the 3 remaining refusals are wrong=0 boundaries).
Pin the EXACT reverse-solve semantics in the independent answer oracle before the
reader learns the inverse frame (PR-7b). A single more_than/fewer_than whose entity
is grounded and whose ref is the (otherwise ungrounded) query target inverts to the
base: base = entity -/+ delta (always integer-exact for +/-).
Refuses everything wider (each a meaningful-fail oracle test): more than one inverse
constraint, an inverse base that is not the query target (a chain), an already-grounded
base, a negative count result, or an inverse over times/divide. The duplicate-definition
guard moved into the per-kind branches so a grounded entity on a more/fewer_than is read
as an inverse constraint, not a redefinition; forward more/fewer/times/divide/sum paths
are unchanged (regression-guarded).
Contract only — the reader is NOT touched: r1-07 still refuses (admissibility_refused),
so R1 stays 6 / 0 / 4 and 15-case 15 / 0 / 0. r1-07 gold drops the non-consumed ask_base
(aligning with the reader's future projection) and gains gold=6, dormant until PR-7b.
Off-serving (only evals/tests import the oracle). SHA gate 8/9 (public_demo env flake).
Widen the aggregate-query recognizer so a multi-part total may be asked with a
trailing qualifier after 'have': 'How many X do A and B have altogether?' and
'... in total?'. The qualifier is stripped and honored ONLY for the multi-part
(sumquery) form — a single-entity query carrying it refuses, guarded by
'not aggregate'.
Phrasing-only: no new arithmetic, no new relation kind, no inverse solving, no
distractor/pronoun handling. The parts still flow through the existing sum_of;
an ungrounded part (unit unbound) or a unit-incompatible part (unit mismatch) is
refused downstream by the REAL admissibility check, so the recognizer cannot
over-read. Off-serving organ only (no generate.derivation / reliability_gate).
Flips r1-03 (more+altogether -> 25) and r1-04 (fewer+in total -> 34):
R1 setup 6 / 0 / 4 R1 answers 6 / 0 / 4 (setup_wrong 0, gold_error 0)
15-case 15 / 0 / 0 29 quant tests, 102 affected-file tests green
Tests are meaningful-fail: the single-entity-qualifier, ungrounded-part, and
unit-incompatible-part refusals each fail loudly if their guard is removed.
PR-6d adds the partition frame: combine all parts into a total, then split that
total equally into N containers. r1-06-subtotal-reused moves refused → correct —
the FIRST case where the divisor applies to a DERIVED symbol (the total), not a
directly given fact. That is real progress toward GSM8K setup comprehension,
where intermediate quantities are the norm.
Scope (kept narrow on purpose):
No new relation kind.
No new arithmetic operation.
No rational support.
No rounding/flooring.
No serving path touched.
The frame reuses the already-ratified pieces — SumOf(parts) + Div(Symbol(total),
Literal(N)) → divide_by — so this PR is reader-only (no IR / admissibility /
oracle / signature change).
Frame grammar:
"They combine their <unit> and split them equally into N <containers>."
+ "How many <unit> are in each <container>?"
-> total = sum(all facts); per_<container> = total / N; ask per_<container>.
wrong=0 boundaries:
- Exact-divisibility still gates the ANSWER, now over a derived total: 5+6=11,
11/3 is non-exact -> the setup reads correctly but the answer REFUSES (never
floors). Setup comprehension and answer exactness are cleanly separated.
- Partition/query coherence: a partition is read ONLY together with its
"in each <container>" query (and vice versa); container mismatch (box vs jar)
refuses. Prevents over-reading a story detail into an unused derived value.
Meaningful-fail verified: disabling the guard makes a dangling partition
wrongly comprehend.
Gates:
R1 setup: 4 correct / 0 wrong / 6 refused
R1 answers: 4 correct / 0 wrong / 6 refused / setup_wrong 0 / gold_error 0
15-case setup: 15 / 0 / 0
97 PR-6d tests + 99 relational/invariant tests green. Reader is off-serving
(no generate.derivation / core.reliability_gate import).
PR-6c adds the divisive comparative frame: "half as many" read as EXACT INTEGER
DIVISION. It is the divisor twin of PR-5c's multiplicative frame, and moves the
independent R1 gold's r1-02-half from refused → correct.
No serving path touched. No rational/fractional answer support added. Non-exact
division refuses.
Design (ADR-0134 amended — divide made symmetric with multiply):
- `_check_divide` now admits a SINGLE-DEP divide-by-dimensionless-literal
(item / dimensionless = item), the exact twin of single-dep multiply. The
2-dep rate-divide path is untouched. This keeps the IR's "literal operands
are not deps" invariant (proven in PR-6a) uniform across Mul AND Div, so the
reader builds both without a per-op special case and WITHOUT synthesizing a
divisor symbol that would pollute the setup-oracle's unit signature.
- `Div(Symbol, Literal)` IR node: "ref / divisor", operation_kind "divide",
projects to `divide_by`. Divisor-only contract mirrors the scalar-only one.
- Reader: `_DIVISOR_WORDS={half:2}` slots into the same 8-token "<WORD> as many"
template as the factor words; graph carries only the two entities.
- Gold reconciliation: r1-02 placeholder `times_as_many factor 0.5` → exact
`divide_by divisor 2` (gold 4). Makes the INDEPENDENT gold integer-faithful.
The wrong=0 boundary — exact divisibility:
the oracle admits `divide_by` only when `base % divisor == 0`. An odd base
halved REFUSES (gold_error), never floors to a wrong integer. Divisor must be
a nonzero int (0, 0.5, 1.5, bool all refuse); divisor=1 is intentionally the
identity (pinned). admissibility proves DIMENSION; the oracle proves EXACT VALUE.
Meaningful-fail (CLAUDE.md Schema-Defined Proof Obligations), both verified red:
- drop the `% divisor` guard → test_oracle_refuses_non_exact_division fails (returns 3).
- disable the single-dep divide branch → the admissibility test AND the reader's
`half` test fail (admissibility refuses → reader refuses → half stays refused).
Gates:
R1 setup: 3 correct / 0 wrong / 7 refused
R1 answers: 3 correct / 0 wrong / 7 refused / setup_wrong 0 / gold_error 0
15-case setup: 15 / 0 / 0
91 PR-6c tests + 60 relational lanes + 56 architectural invariants + 502
binding-graph/proof-chain/adapter tests green. All 8 SHA-content lanes match
(serving unmoved; admissibility has no generate.derivation/reliability_gate consumer).
The multiplicative comparative frame (PR-5c) admits exactly one shape —
Mul(Symbol, Literal), a unit-bearing symbol times a dimensionless integer
(count × scalar = count). That contract was held by OMISSION: to_relation's
`case _: return None` refused every other Mul shape, but no test would fail
if the guard were loosened, and no doc stated where the guarantee lives.
This makes the obligation meaningfully-failing (CLAUDE.md Schema-Defined
Proof Obligations), with no runtime logic change:
- test_mul_projection_admits_only_symbol_times_literal — Mul(Symbol, Symbol)
(a count×count product), a commuted factor, and compound factors all REFUSE
(to_relation → None). Verified to go red when a Mul(Symbol, Symbol) projection
arm is injected.
- test_literal_factor_is_dimensionless_by_construction — Literal has exactly
one field (value); a unit-bearing literal multiplication is unrepresentable,
not merely unchecked.
- test_scalar_only_guard_is_load_bearing — check_admissibility's `multiply`
dispatch products operand units generally (count×count → count², no refusal),
so it would NOT catch the masquerade. The projection arm is the sole boundary.
Docstrings on Mul and to_relation now state the scalar-only contract and that
it is enforced at the projection boundary, not in the dimensional checker.
Gates unchanged: setup-oracle 15-case 15/0/0 and R1 2/0/8 (setup_wrong=0);
77 expr/admissibility/reader/setup-oracle tests + 56 architectural invariants
green. No serving path touched.
The first capability slice on the R1 arc, gated by the setup-oracle: turn the
"twice / N times as many" reading from REFUSED into a correct setup, without a single
misread. Builds on the typed IR (PR-4) and the R1 gold (PR-5b).
- IR: a Mul(symbol, literal-factor) node — to_canonical_string "ref * factor",
operation_kind "multiply", dependencies {ref}, to_relation -> times_as_many. The
product keeps the symbol's unit (count * scalar = count), admitted by the REAL
check_admissibility multiply path (the literal factor is dimensionless, not a dep).
- Reader: a multiplicative template "Y has <factor> as many <unit> as X" (factor word:
twice/double/triple/quadruple) and "Y has <N> times as many <unit> as X", checked
BEFORE the digit gate (the factor may be a word). 'half' (a /2) is deliberately
deferred — divide-by-literal is a separate admissibility path.
- setup-oracle: relation_signature now canonicalizes times_as_many.
Setup-oracle R1 result: 2 setup_correct (r1-01 twice; r1-05 the multi-step chain
ivy/jon=3*ivy/kim=jon+2), 0 setup_WRONG, 8 setup_refused. Every hard negative stays a
safe refusal: missing-base (Rosa ungrounded), ambiguous referent, distractor, inverse,
partition, 'altogether'/'in total' phrasings, and 'half' (divide). wrong=0 held through
the first capability addition.
Gates green: setup-oracle R1 setup_wrong=0; 15-case setup gate 15/15 setup_wrong=0;
relational_metric answer lane 15/15 wrong=0; binding-graph admissibility + realize +
architectural invariants + chat-runtime + pipeline (122+). No serving path touched (this
reader feeds the relational_metric / setup-oracle lanes, not the candidate-graph serving).
Pure-gold. Defines what "correct" means for the R1 (comparative / derived-symbol /
multi-step) shapes the reader cannot read yet, and verifies the current reader REFUSES
them rather than misreading them. NO frame extraction, no parser change, no serving.
- evals/setup_oracle/r1_gold.jsonl — 10 INDEPENDENT, self-contained fixtures (id, text,
relations, expected_units, query, notes) covering: twice/half-as-many (multiplicative),
more/fewer-than -> total, a multi-step derived chain (jon=3*ivy; kim=jon+2), a derived
subtotal reused downstream (total=lee+mae; per_box=total/3), an inverse target, and the
hard negatives (ambiguous referent, missing base quantity, distractor quantity). The
gold is the authority — each fixture is reviewable without joining files.
- run_r1() scores the CURRENT reader against this gold.
Investigative result (the deliverable): **10/10 setup_refused, setup_wrong=0, setup_correct=0.**
The reader refuses every R1 shape with a TYPED reason (non_digit_quantity /
no_quantity_template / unreadable_quantity_clause / admissibility_refused) — it NEVER
misreads an R1 case as a simpler form. So the wrong=0 posture holds all the way down, and
PR-5c (the R1 frame) starts from a clean foundation: every R1 case is currently a safe
refusal, and the frame's job is refusal -> correct, with the oracle ensuring no
setup_wrong is ever introduced.
`python -m evals.setup_oracle r1` prints the auditable R1 report (exit 0 iff
setup_wrong==0). The 15-case setup gate + relational_metric answer lane are untouched.
Strengthen the ruler before it judges real GSM8K frames — NOT a capability change, no
new parser behavior, no serving path. The setup-oracle now grades UNITS, read from the
binding-graph itself, against an independent expected-unit gold.
- New per-symbol unit axis (symbol_unit_signature) — read from the GRAPH's symbols
(reader_symbol_units), not the answer projection; covers fact + relation-operand units
(operands are symbols). A None unit canonicalizes to "unset" so an unmodelled symbol
never silently matches a declared gold unit.
- The question-target signature gains the target's expected_unit (reader from
BoundUnknown.expected_unit; gold from the fixture).
- expected_units.json — independent, hand-authored MODELING unit per entity for the 15
cases (a discrete sortal count -> the generic count unit "item"; money -> "dollars").
Authored from the PROBLEM, not copied from the reader.
- The reader's per-symbol units match the independent gold on all 15 (setup_wrong=0) —
agreement, not circularity (both apply the same correct count->item / money modelling).
- Meaningful-fail: a reading whose STRUCTURE matches but whose UNIT diverges now FAILS
(test_unit_mismatch_is_caught, test_target_unit_mismatch_is_caught). The ruler is not
decoration on the unit axis either.
Gate held: setup-oracle 15/15 setup_wrong=0 (now structure + units + target). The
relational_metric answer lane is untouched (oracle-only change). No generate/ change.
This is the unit-aware ruler PR-5b (independent R1 gold) and PR-5c (the first R1
comparative/derived-symbol frame) must pass before serving.
Internal hygiene + future-proofing. No serving path, no new capability — the 15-case
reader is structurally cleaner and the projection no longer recovers meaning by
re-parsing a string.
- New generate/quantitative_expr.py: a typed Expr IR (Literal/Symbol/Add/Sub/SumOf) with
to_canonical_string (BYTE-IDENTICAL to the legacy "ref + delta" / "ref - delta" / "a + b"
format), dependencies, operation_kind, and to_relation (the structured projection).
- The reader builds the Expr per equation as the SOURCE OF MEANING; rhs_canonical,
dependencies, and operation_kind are all DERIVED from it (BoundEquation stays a string —
the binding-graph's deliberate decoupling layer is untouched). QuantComprehension carries
the IR as equation_exprs.
- to_relational_metric reads the IR via to_relation — the rhs_canonical string-reparse is
GONE. An unhandled equation shape refuses (None).
- The dead _rhs string builder is removed.
Gates held: relational_metric answer lane 15/15 wrong=0; setup-oracle 15/15 setup_wrong=0;
malformed-target refusals intact; realize-binding-graph + architectural invariants green
(95). rhs_canonical is byte-identical, so the binding-graph + downstream hashes are
unchanged. No model change.
This is the foundation PR-5's R1 frames build on — structured equations, not string parsing
— and only after independent R1 gold is hand-authored.
Completes the PR-1 migration. The question target now has a single source of truth:
the binding-graph's sole BoundUnknown. The sidecar QuantQuery dataclass + the
QuantComprehension.query field are DELETED.
- New helper single_unknown(graph) -> BoundUnknown | None: returns the sole target, or
None on a graph that does not carry exactly one. Zero unknowns (no question) and
multiple unknowns (ambiguous) both REFUSE — the consumer must never pick one.
- to_relational_metric reads the query from single_unknown(graph) (refuses on None).
- realize_quantitative reads the asked symbol from single_unknown(bg) (NotRealized on None).
- Tests: the .query assertions move to single_unknown; new malformed-graph tests prove
0 and >1 unknowns REFUSE rather than pick one (the wrong=0 boundary).
Byte-identical where it matters: relational_metric answer lane 15/15 wrong=0, setup-oracle
15/15 setup_wrong=0, realize-binding-graph + architectural invariants green. No serving
path touched. No dangling QuantQuery reference remains.
Two coupled, additive, off-serving changes toward the typed math-comprehension organ.
No serving path touched; the relational_metric answer lane stays 15/15 wrong=0.
PR-1 — QuantQuery → BoundUnknown. comprehend_quantitative now emits the question
target as a BoundUnknown INSIDE the binding-graph (symbol_id, state_index="terminal",
question_form "count"|"total", expected_unit), so the graph is a real question-bearing
mathematical object and its canonical serialization carries the target. The external
QuantQuery is RETAINED, consistent-by-construction, so the two consumers
(to_relational_metric, realize/quantitative) are byte-identical; a follow-up rewires
them onto graph.unknowns and drops the duplicate field.
Setup-oracle lane (evals/setup_oracle) — grade the READING, not the answer. The
relational_metric lane scores answers, which can bless a semantically-wrong derivation
that coincidentally lands on the right number (the exact hazard the held-out
measurements + the 2/87 resolve_pooled probe exposed). The setup-oracle compares the
reader's comprehended STRUCTURE — a span-free signature of facts + typed equations +
the BoundUnknown target — against the INDEPENDENT gold structure (the relational_metric
cases' own relations/query, authored separately from the binding-graph reader). A
structural mismatch is setup_wrong, the wrong=0-critical count, even when the answer
would be right. v1 grades structure (units deferred — covered by admissibility). The
reader reads all 15 cases with the gold structure (setup_wrong=0); a meaningful-fail
test proves the oracle catches a right-answer/wrong-structure reading (it is not
decoration). `python -m evals.setup_oracle` exits nonzero iff setup_wrong > 0.
This is the measurement rig BEFORE investing in frame families: setup_wrong=0 is the
gate; serving must not move while setup_wrong > 0. It is the first milestone of the
math-comprehension organ, not a path to "solve GSM8K".
Verified: setup-oracle 15/15 setup_correct wrong=0; quantitative + setup-oracle unit
tests (17); realize-binding-graph + binding-graph + architectural invariants (183).
Completes the ADR-0206 §5 math-serving deferral in its own careful PR. The GSM8K
wrong=0 serving gate, select_self_verified, now takes a ReachPolicy. This changes
NO serving behavior today — it is the safe, byte-identical, sanctioned first edit to
the most wrong=0-critical line, with a live-wiring test, so a future VERIFIED
widening has a precise, proven integration point.
Why it can't actually widen yet (the tension scoping surfaced): GSM8K wrong=0 is
ABSOLUTE (zero wrong, ever); a reliability license is STATISTICAL (a 0.99 Wilson
floor); math answers aren't disclosed like the cognition path (E). So widening the
math serve on a statistical license would eventually serve a SILENT wrong. ADR-0206
§4 foresaw this: VERIFIED is "the only state that will license widening past gold,"
and it is reserved pending a canonical-comparison pass (the soundness≠correctness
gap) that is unbuilt.
Design (safe by construction):
- select_self_verified(..., policy=STRICT_POLICY). STRICT (the default every one of
the 5 callers passes) is the prior logic verbatim — unique answer → Resolution;
zero-verify or disagreement → refuse. Byte-identical: the pinned serving-lane SHAs
(demo_composition 3a3d09f3, fabrication 01e1b6b7, math_teaching_corpus eaf160d1)
are unchanged; Resolution is unchanged; no import cycle.
- A wider reach resolves a disagreement ONLY via _canonically_verified — the VERIFIED
gate — whose body returns None (capability unbuilt). So the widening is
STRUCTURALLY inert: a disagreement refuses regardless of policy. wrong=0 holds by
construction, not caller discipline. A statistical reliability license is NOT
consulted by the math path (the cognition/math asymmetry is deliberate: cognition
discloses, math is absolute).
- test_seam_is_live_wiring injects the gate to prove the consumer fires under a wider
reach — and STRICT still refuses even then.
Verified: smoke (90), invariants (56), governance + selfverify + ms2 (354 incl. the
new seam tests), GSM8K serving regression byte-identical (the one red,
test_serving_unchanged_by_search expecting stale 6/44, fails identically on clean
main — a pre-existing stale artifact, not this change). Three-lens adversarial review
(byte-identity/seal, structural wrong=0 inertness, no-overclaim): all held.
The real unlock — a VERIFIED canonical-comparison producer — is scoped in
docs/analysis/VERIFIED-canonical-comparison-scoping-2026-06-06.md (recommended:
back-substitution / constraint-satisfaction on a checkable problem class). Still
deferred: SITUATE (stakes), the live FEED-BACK loop, reach_level JSONL emission.
The final AGI-spine step (A INSTRUMENT → B WIRE → C DEEPEN → D CLOSE → E ESTIMATION).
The engine may now SERVE a DISCLOSED estimate for a query it would otherwise refuse —
but only for a predicate-class that has measured itself reliable, and never as fact.
This executes the ADR-0206 §5 cognition-path widening: the bridge's LICENSE node
(reliability_gate.license_for), previously "built — not yet called from serving", is now
called. govern_response returns APPROXIMATE iff a genuine licensed Action.SERVE
LicenseDecision is passed (STRICT for every other input — so every existing serving call
site is byte-identical); shape_surface DISCLOSES the estimate as "[approximate] …".
Mechanism:
- generate/determine/estimate.py — a BLIND converse-guesser: told p(a,b), asked p(b,a),
it commits the converse. It never reads the pack's symmetry metadata; whether the guess
is right is MEASURED, not assumed.
- evals/determination_estimation/ — the gold lane: run_practice (sealed, ADR-0199) folds
the converse-guesser over symmetric (sibling_of) vs directed (parent_of) cases, scored
against the pack's graph.edge.symmetric truth (gold independent of the solver). The gate
DISCRIMINATES: sibling_of earns SERVE (660 correct → Wilson floor 0.990046 ≥ θ_SERVE),
parent_of does not (660 wrong → 0.0). The license is earned by VOLUME — 657 perfect
commits is the exact θ_SERVE=0.99 threshold (656 is below).
- generate/determine/data/estimation_ledger.json — the ratified committed ledger,
hash-verified on load (a hand-edited ledger raises RatifiedLedgerError); it IS the
deterministic sealed-practice output (a GSM8K-style --check test pins this).
- chat/runtime.py — when a converse query is refused and the class holds a SERVE license,
the disclosed estimate is surfaced through the bridge (gated by config.estimation_enabled,
default OFF; only meaningful with accrue_realized_knowledge).
Invariants:
- wrong=0 by construction — an estimate is ALWAYS disclosed ([approximate]), never a silent
commit (UNVERIFIED_POSSIBLE is never in APPROXIMATE's admissible set), and only a genuine
ratified license widens (a forged {"licensed":True} dict / a PROPOSE license / an
unlicensed SERVE all stay STRICT). Defense-in-depth: type-gate ∧ admissible-set ∧
hardcoded disclosed state.
- never self-authored — ceilings stay at safe defaults (θ_SERVE=0.99); the engine cannot
raise its own bar. The ledger is sealed practice, hash-verified.
- session/serving only — no corpus/pack/identity/proposal/vault mutation; the HITL teaching
path is untouched. Deterministic; no clock/random.
- byte-identical for every non-E turn (the 2643 govern_response call passes no license).
Out of scope (separate ADR-0206 §5 PRs): the math-serving seam (select_self_verified,
touches the sealed metric), SITUATE (stakes), and the live FEED-BACK loop.
Verified green: smoke (90), architectural invariants (56), response_governance (321,
incl. the new license-gated widening test), the determination-estimation lane (12), and
the B/D/determine regression net. Four-lens adversarial review (disclosure/wrong=0,
calibration integrity, byte-identity, boundary/determinism): all held. Design:
docs/analysis/E-estimation-design-2026-06-06.md.
Review/ratification pass over the 6-phase afferent environment-falsification
roadmap (rebased onto D-inclusive main). The roadmap validated green: smoke (90),
architectural invariants (56), the full sensorium suite, all phase tests, and the
four eval lanes (environment-falsification / event-vision / vision / sensorimotor)
report failed:0. A five-lens adversarial review found the efferent gate
fail-closed (default-deny, conjunctive safety∧ethics∧tool_scope, refuses before
the decoder, hash-only traces, no real decoder, no overclaim), the afferent
Phases 1-5 read-only (runtime import-guards + frozen dataclasses), and the
expected-artifact obligations deterministic + meaningfully-failing. Two
reconciliations from that review:
1. Witness-log importer trust boundary (sensorium/logs/importer.py). The offline
JSONL importer now rejects malformed input with a deterministic ValueError
instead of a raw JSONDecodeError/KeyError, names the 1-based line on a bad
JSON line, rejects non-object records and non-integer ticks, and size-caps the
file (8 MiB) so an oversized log is refused rather than read whole. The trust
boundary is now stated in the module + function docstrings. Behavior for VALID
logs is byte-identical (the environment-falsification lane hashes are
unchanged). New rejection-path tests added.
2. ADR-0198 reconciliation. The §3 verdict-enforcing GATE mechanism
(VerdictEnforcingEfferentGate + MotorActionIntent) has now landed, so the
Implementation Status is amended to say so — while being explicit that the §3
lowering itself (deriving verdicts from the ADR-0029/0033/0036/0037 governance
packs), the motor decoder (every adapter is decoder=None), and ratification by
the dedicated motor governance ADR (now drafted as ADR-0216, Proposed) all
remain deferred. No physical motor decode is authorized.
Disjoint from the GSM8K serving path and from Step D (generate/determine|realize,
chat/runtime, core/config) — clean rebase, no overlap.
CLOSE the autonomous-loop spine: when idle, the engine consolidates each
soundly-derived determination back into the held self, so the next determine()
reaches it directly and can chain one hop further. The directly-answerable set
climbs monotonically across idle ticks to the deductive-closure fixed point.
Mechanism — generate/determine/consolidate.py::consolidate_once runs ONE
semi-naive layer of the member/subset deductive closure (member∘subset → member,
subset∘subset → subset; NEVER member∘member — instance-of is not transitive).
Each one-hop conclusion not yet realized is VERIFIED by the sound+complete
proof_chain ROBDD (reusing C's single verifier _verify_subsumption) and written
back via generate/realize::realize_derived as a SPECULATIVE realized record
carrying derived-provenance (premise structure_keys + rule + the ENTAILED
verdict). idle_tick gains a consolidation pass gated by the new
config.consolidate_determinations (default OFF); IdleTickResult.facts_consolidated
reports the layer.
Invariants held:
- wrong=0 — every consolidated fact is a sound-rule conclusion confirmed by the
sound+complete decider; member∘member is structurally unreachable (a member fact
is only ever extended by a subset edge). _verify_subsumption now refuses a
mislabeled/wrong-arity path (belt-and-suspenders now that consolidation is a
second caller), so the fallacy cannot be laundered through a corrupted chain.
- honesty — a fact derived from SPECULATIVE premises stays SPECULATIVE / as-told;
the soundness of the inference never upgrades the standing of the premises.
COHERENT is never minted.
- teaching-safety — SESSION memory (immediate), an extension of the realize path;
NOT corpus mutation and NOT coupled to proposals. The HITL path is untouched.
- determinism/replay — pure function of the realized set; sorted write order;
derived structure_key identical to a told fact's; provenance round-trips through
the Shape B+ snapshot (consolidated facts resume the SAME life across reboot).
- no new normalization — writes reuse the INV-21-allowed vault writer;
algebra/versor.py keeps closure.
Falsification — evals/determination_closure: a frozen replay seeds a deep is-a
chain and runs idle ticks; asserts the closure climbs monotonically to a complete
fixed point (no-op final tick), wrong=0 (member∘member canary never derived, no
fabricated membership), all derived facts SPECULATIVE, and every derived record
re-verifies ENTAILED from its recorded premises.
Verified green: smoke, runtime, cognition, architectural invariants, plus the new
D unit + lane tests and the determine/realize/persistence regression net. Five-lens
adversarial review: 4 lenses held; the 5th (normalization) was a misattribution
(pre-existing vault reproject, triggered identically by the merged realize path,
on sanctioned null-vector storage). Design + findings: docs/analysis/
D-close-consolidation-design-2026-06-06.md.
DETERMINE was one-hop. C answers a member/subset query that holds by SOUND transitive
subsumption when direct entailment misses: member∘subset* and subset∘subset (the
Description-Logic is-a rules). 'Socrates is a man. All men are mortal. Is Socrates
mortal?' -> yes, as told.
wrong=0 hazard surfaced and closed: member∘member is NOT transitive (instance-of vs
subclass-of) — 'Socrates is a man. Man is a species.' does NOT entail 'Socrates is a
species'. That rule is NEVER an edge, so the fallacy is unreachable (bite-tested). The
reader's member/subset split (X is a Y vs all Xs are Ys) is exactly the instance-of/
subclass-of distinction that makes the included rules sound.
Mechanism: SEARCH-then-VERIFY. Reachability over the sound edges finds the chain, then
the proof_chain ROBDD verifies that chain's propositional entailment (O(path) premises).
Full O(n³) grounding into proof_chain was tried and rejected: it overruns the
canonicalizer's per-conjunct recursion, and transitive closure is reachability, not
general SAT. Search-then-verify scales (11-hop chain determines in ~2ms) and keeps the
sound+complete decider as the wrong=0 verifier + proof artifact.
subset is now a supported direct-entailment predicate (a told subset(a,b) answers it);
the other categoricals (disjoint/intersects/some_not) stay excluded. Two existing tests
updated accordingly (subset->ungrounded, disjoint stays unsupported_query).
When accrue_realized_knowledge is on and a question turn is Determined over realized
knowledge, the user-facing surface IS that answer (generate.determine.render_determination):
'Yes — as I was told, truth is a concept.' The realizer's articulation_surface is
RETAINED as evidence — the determination is a user-facing SELECTION (like the
unknown-domain gate), not a rewrite. An Undetermined turn keeps the default
articulation surface (the honest 'I don't know'); flag-off never takes this path.
Basis is rendered honestly: SPECULATIVE grounds read 'as I was told', never 'verified'
(D0 only asserts answer=True, so the surface is an affirmation — no fabricated or
asserted-False string). Selection only: no field op, no normalization, proposes no
learning (HITL untouched).
Updates docs/runtime_contracts.md selection policy + adds the contract test in the
same PR (per the surface-contract discipline).