Compare commits

...

536 commits

Author SHA1 Message Date
Shay
4308519ca9
feat(ask): add served ASK artifact adapter
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
2026-06-09 12:49:52 -07:00
Shay
426adbd197 docs(stage2): map disclosure-bus implementation state and next slices 2026-06-09 11:32:00 -07:00
Shay
9cff9755cd
feat(ask): split question artifact path from proposal path
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.
2026-06-09 11:28:52 -07:00
Shay
b687f62cf0
feat(ask): integrate off-serving ASK delivery in pass manager
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.
2026-06-09 10:50:36 -07:00
Shay
3cfaaf9153
feat(config): add explicit default-dark serving gate fields
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.
2026-06-09 10:24:28 -07:00
Shay
45835987b9
feat(verified): add default-dark VERIFIED serving gate helper
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.
2026-06-09 10:06:36 -07:00
Shay
1fff7dad59
Merge pull request #672 from AssetOverflow/docs/verified-serving-wiring-scoping
docs(verified): scope serving-time VERIFIED wiring and gold-free independence gate
2026-06-09 09:51:52 -07:00
Shay
ec9102a8d6 docs(verified): scope serving-time verification gate 2026-06-09 09:41:34 -07:00
Shay
ffbaf0f27c
Merge pull request #671 from AssetOverflow/docs/ask-serving-integration-scoping
docs(epistemic): scope ASK serving integration and carve-out retirement
2026-06-09 09:38:04 -07:00
Shay
4a6153d53e docs(epistemic): tighten pass_manager scoping wording and use relative links 2026-06-09 09:26:49 -07:00
Shay
17e36acdc0 docs(epistemic): scope ASK serving integration gate 2026-06-09 09:22:35 -07:00
Shay
297349fd8d
feat(ask): add default-dark ASK serving gate helper
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.
2026-06-09 09:17:14 -07:00
Shay
9a54374048
docs(epistemic): record accepted Q1-D delivery decisions
Rewrites the Q1-D ASK bus delivery scoping document into the durable decision record for decisions implemented in #668. No code or runtime behavior changes.
2026-06-09 09:16:24 -07:00
Shay
28cb42d0dd
docs(epistemic): scope ASK and VERIFIED serving gates
Accepts the ASK + VERIFIED serving-integration scoping briefs.

No served-surface code is introduced. ASK is identified as the next gated serving slice; VERIFIED remains blocked on a gold-free second reader and explicit eval-gold serving ban.
2026-06-08 22:22:35 -07:00
Shay
07149c208a
Merge pull request #668 from AssetOverflow/feat/q1-d-ask-delivery
feat(epistemic): Q1-D — off-serving ASK delivery (QUESTION_NEEDED tenant)
2026-06-08 19:27:18 -07:00
Shay
65413c415f docs(epistemic): Q1-D — disambiguate question_only from proposal_only lane
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.
2026-06-08 19:18:47 -07:00
Shay
f88e03e53a feat(epistemic): Q1-D — off-serving ASK delivery (QUESTION_NEEDED tenant)
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.
2026-06-08 19:10:54 -07:00
Shay
0401ec4b39
Merge pull request #666 from AssetOverflow/feat/q1-c-question-renderer
feat(epistemic): Q1-C — grounded-only question renderer (off-serving)
2026-06-08 18:52:06 -07:00
Shay
5f74351729 feat(epistemic): Q1-C strictly single-slot — refuse multi-slot ASK
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.
2026-06-08 18:42:40 -07:00
Shay
b186f07382 feat(epistemic): Q1-C grounded-only question renderer (off-serving)
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.
2026-06-08 17:20:49 -07:00
Shay
710743c38f
feat(verified): P1-C — split bound_slots_digest into separate obligation
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.
2026-06-08 17:17:31 -07:00
Shay
05b83a57cf
Merge pull request #664 from AssetOverflow/feat/q1-b-ask-residue
feat(epistemic-disclosure): Q1-B — typed ASK residue + missing_* reclassification (carve-out)
2026-06-08 16:57:30 -07:00
Shay
dca58c6c35
Merge pull request #663 from AssetOverflow/feat/p1-b-verified-r2-producer
feat(verified): P1-B — gold-setup-backed off-serving R2 verification producer
2026-06-08 16:57:13 -07:00
Shay
f90d84b4f7 feat(epistemic-disclosure): Q1-B — typed ASK residue + missing_* reclassification (carve-out)
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
2026-06-08 16:51:53 -07:00
Shay
8c0311b847 feat(verified): P1-B — gold-setup-backed off-serving R2 verification producer
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.
2026-06-08 16:50:16 -07:00
Shay
05a044d9a5
Merge pull request #662 from AssetOverflow/feat/p1-a-verified-contract
feat(epistemic-disclosure): P1-A — the VERIFIED contract + meaningful-fail tests
2026-06-08 16:29:26 -07:00
Shay
ec7c680cfc feat(epistemic-disclosure): P1-A — the VERIFIED contract + meaningful-fail tests
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.
2026-06-08 16:20:19 -07:00
Shay
4fc251da2d
Merge pull request #661 from AssetOverflow/feat/p0-3-served-disposition
feat(epistemic-disclosure): P0-3 — ServedDisposition mapping scaffold
2026-06-08 16:09:56 -07:00
Shay
178e3967ed feat(epistemic-disclosure): P0-3 — ServedDisposition mapping scaffold
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.
2026-06-08 16:00:15 -07:00
Shay
89b4b97fc9
Merge pull request #660 from AssetOverflow/feat/p0-2-disclosure-claim
feat(epistemic-disclosure): P0-2 — the DisclosureClaim axis
2026-06-08 15:56:52 -07:00
Shay
8811917280 feat(epistemic-disclosure): P0-2 — the DisclosureClaim axis
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.
2026-06-08 15:48:20 -07:00
Shay
14d81887df
Merge pull request #659 from AssetOverflow/feat/p0-1-limitation-assessment
feat(epistemic-disclosure): P0-1 — limitation pass as a consolidating view
2026-06-08 15:39:13 -07:00
Shay
159b6830e4 feat(epistemic-disclosure): P0-1 — limitation pass as a consolidating view
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.
2026-06-08 15:29:46 -07:00
Shay
a024ac4d9e
Merge pull request #658 from AssetOverflow/docs/stage2-bus-and-q1-scoping
docs(scoping): epistemic disclosure bus (Stage 2 / VERIFIED v1) + Q1 question-articulation briefs
2026-06-08 15:10:34 -07:00
Shay
c2c189a1d5 docs(scoping): lock VERIFIED as a distinct disclosure mode, not APPROXIMATE (review fix)
Resolve the one open S2-1 design decision in the doc before it leaves draft:
VERIFIED must NOT reuse ReachLevel.APPROXIMATE or the [approximate] prefix.

- §0: add the claim-vs-reach caution — VERIFIED is a disclosed-surface
  *license / verification claim*, not a point further out on the speculation
  scale; it is more LICENSED than strict/gold, not more speculative. Record the
  principled three-axis decomposition (Disposition × DisclosureClaim × ReachLevel)
  and allow v1 to collapse to a distinct ReachLevel.VERIFIED iff it carries its
  own admissible-set, rationale, and [verified] prefix.
- §4: replace the open question with the settled S2-1 decision (Shay's wording):
  VERIFIED gets a distinct disclosed-surface mode with its own deterministic
  [verified] prefix; APPROXIMATE is for bounded estimates, VERIFIED for
  independently-confirmed answers under the canonical-comparison contract.
- §1 SQ-5, §9 S2-1, §11 non-claims: propagate — only the enum shape
  (distinct ReachLevel.VERIFIED vs a separate DisclosureClaim axis) is left to S2-A.

Still scoping only — NO CODE. Stays DRAFT pending Shay's design-lock.
2026-06-08 15:00:38 -07:00
Shay
8352f154b6 docs(scoping): epistemic disclosure bus (Stage 2 / VERIFIED v1) + Q1 question-articulation briefs
Scope the frontier after the frozen R1–R4 comprehension ladder: the
served-surface disclosure machine and its first two tenants. Scoping
only — NO CODE.

- session doc §1.5 — the pre-question limitation pass / intake gate:
  classify the limitation KIND before choosing a disposition (asking is
  one of several). Key phrase: question-asking is the intake mechanism
  for resolvable missing state; limitation assessment is the gate that
  decides whether intake is appropriate.

- Doc 1 (primary) — the Epistemic Disclosure Bus:
  EpistemicState + LimitationAssessment → ServedDisposition, two axes
  (DISPOSITION × the existing ReachLevel). VERIFIED is the ONLY v1
  tenant; ASK/PROPOSE/REPORT/SCOPE reserved. Grounded in the built-but-
  inert ADR-0206 seam (govern_response/shape_surface; _canonically_verified
  returns None at verify.py:188). Honours the 2026-06-06 dead-path
  verdict: v1's VERIFIED producer is a comprehension-organ answer (R2
  first, R4 second) with independence in the READING not the solving,
  validate-first holdout (INV-25) BEFORE any widening, behind a new
  verified_serving_enabled=False, GSM8K seal untouched.

- Doc 2 (companion) — Q1 question articulation: maps the four ASK
  dispositions onto the shipped failure_family registry; the
  missing_total_count / missing_weighted_total reclassification (decide
  with tests, never silently re-key); the grounded-rendering wrong=0
  invariant; the structured-residue-on-refusal gap; Q2 re-enters the
  gate (augment + rerun, never mutate). Second bus tenant.

HOLD for review.
2026-06-08 14:53:47 -07:00
Shay
5881420653 docs(session): epistemic question articulation — first skill of contemplation
Records the design reached in discussion: the first real skill of contemplation
is typed, deterministic, failure-family-driven question generation. A question is
a typed request for missing state, not a conversational habit. Proposes
core/epistemic_questions/ organ (EpistemicQuestion, MissingSlot, AnswerBinding),
the QUESTION_NEEDED terminal distinct from PROPOSAL_EMITTED, and the Q1/Q2 PR
ladder. Traces the connection to the ServabilityBlade clarify mode and the
minimal-sufficient-question discipline.
2026-06-08 14:22:46 -07:00
Shay
8e28671e45
Merge pull request #657 from AssetOverflow/docs/cmb-e-capability-ledger
docs(comprehension): freeze combined-rate (R4) v1 — capability ledger + non-claims (CMB-e)
2026-06-08 14:22:31 -07:00
Shay
f2f7d6962d docs(comprehension): freeze combined-rate (R4) v1 — capability ledger + non-claims (CMB-e)
Closes the CMB ladder with a docs-only lockfile. No behavior change.

- New docs/analysis/combined-rate-capability-ledger-2026-06-08.md: the frozen
  R4 v1 claim, supported solved grid, solver/reader boundaries, the three
  proposal-allowed deferrals, router/contemplation integration, the
  domain-precedence adjudication (named, with proof cases cmb-11/15/12·13·14),
  a prominent non-claims list, and the verification lanes. Every cited number
  re-confirmed against the lanes: gold 19/19 (6/5/8), solver 6/0/5, reader
  11/0/8 -> answers 6/0/13; the 7 R4 test files 93 passed.
- Updates comprehension-organ-capability-ledger-2026-06-08.md to fold R4 in as a
  fourth organ: organ block, 4-organ router + cmb_* namespacing, a named
  domain-precedence section, R4 lane rows, and a reframed deferred/next that
  points the next frontier at served-surface epistemology (not more comprehension).

Honest accounting: 1 pre-existing red (test_findings_reset_between_turns) is red
on clean origin/main and imports nothing from R4 — unrelated.

Off-serving unchanged (sealed); no generate.derivation / core.reliability_gate.
2026-06-08 14:13:39 -07:00
Shay
3381e0319d
Merge pull request #656 from AssetOverflow/feat/cmb-d-router-wiring
feat(contemplation): wire CMB into multi-organ router and failure registry
2026-06-08 14:07:43 -07:00
Shay
6a75f5203a feat(contemplation): wire CMB into multi-organ router and failure registry (CMB-d)
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.
2026-06-08 13:58:48 -07:00
Shay
c441411c85
Merge pull request #655 from AssetOverflow/docs/cmb-lookback-and-h3-fix
docs(cmb): lookback review + fix single-agent-attribution hygiene hazard (H3)
2026-06-08 13:36:44 -07:00
Shay
a4d0c4ebf3 docs(cmb): lookback review + fix single-agent-attribution hygiene hazard (H3)
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.
2026-06-08 13:27:17 -07:00
Shay
84d3456e61
Merge pull request #654 from AssetOverflow/feat/cmb-c-reader
feat(combined-rate): CMB-c — combined-rate prose reader
2026-06-08 13:09:37 -07:00
Shay
47aaa09019 feat(combined-rate): CMB-c — combined-rate prose reader (prose -> CombinedRateProblem | Refusal)
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.
2026-06-08 12:59:32 -07:00
Shay
93fb55b813
Merge pull request #653 from AssetOverflow/feat/cmb-b-solver
feat(combined-rate): CMB-b — exact combined-rate solver (int-or-refuse)
2026-06-08 08:10:02 -07:00
Shay
6b13c6af23
Merge pull request #652 from AssetOverflow/feat/r3-vac-oracle
test(rate-oracle): port non-vacuous _canonical_outcome validation to R3 (R3-vac)
2026-06-08 08:09:58 -07:00
Shay
6f586f83d4
Merge pull request #651 from AssetOverflow/docs/servability-doc-adr0206-reconcile
docs: amend servability-blade session note — reconcile with ADR-0206 (no parallel substrate)
2026-06-08 08:09:54 -07:00
Shay
2a7e94c011 feat(combined-rate): CMB-b — exact combined-rate solver (int-or-refuse)
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.
2026-06-08 07:59:45 -07:00
Shay
a7eb123579 docs(session): amend servability-blade note — reconcile with ADR-0206, no parallel substrate
The 2026-06-08 practice-attempts/servability-blade session note under-referenced the substrate
that already ships the serving half of its design. Doctrine ('wrong=0 = no false presentation of
epistemic status') stands; the build plan must reuse existing organs, not fork parallel ones.

Adds a binding Amendment + reframes sequencing:
- ServabilityDecision/ServabilityBlade => advance ADR-0206 core/response_governance/ (ReachPolicy /
  govern_response / shape_surface, ReachLevel STRICT<APPROXIMATE<EXTRAPOLATE<CREATIVE, gated STRICT,
  already disclosing [approximate]) — NOT a new object.
- the epistemic_state Literal => reuse core.epistemic_state.EpistemicState (ratified taxonomy), not
  a smaller renamed enum.
- ProblemAttemptSession => extend core/comprehension_attempt/, don't fork.
- first activation = produce reserved VERIFIED via canonical comparison (scoping doc already exists),
  consume through govern_response/shape_surface, prove on the yardstick, one state at a time.

Preserves the research trail (amendment, not rewrite). Docs only.
2026-06-08 07:50:27 -07:00
Shay
e77f93c485 test(rate-oracle): port _canonical_outcome non-vacuous validation to R3 (R3-vac)
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.
2026-06-08 07:38:20 -07:00
Shay
f6292f1ecf docs(session): practice-time attempts vs served answers — the servability blade
Records the wrong=0 refinement reached in discussion: wrong=0 is not a binary
answer/refuse gate but 'no false presentation of epistemic status'. Practice/
contemplation may explore typed, isolated candidate guesses; served output needs
a servability blade with more modes than verified|refuse. Proposes
ProblemAttemptSession (PRA) and ServabilityDecision (SRV), sequenced PRA -> REL ->
SRV, and traces the idea to the R1/R2/R3/CMB reader trajectory. Session doc, not
an ADR yet.
2026-06-08 07:17:14 -07:00
Shay
52731c02aa
Merge pull request #650 from AssetOverflow/feat/cmb-a-combined-rate-oracle
feat(combined-rate): CMB-a — combined-rate setup ruler (model + gold + oracle)
2026-06-08 07:07:58 -07:00
Shay
36bf2a6456 feat(combined-rate): CMB-a — combined-rate setup ruler (model + gold + oracle)
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
2026-06-08 06:58:17 -07:00
Shay
67f470ede8
Merge pull request #649 from AssetOverflow/docs/organ-capability-ledger
docs: whole-system organ capability ledger + fix ADR-0211 collision
2026-06-08 06:27:45 -07:00
Shay
df1d14dce4 docs(comprehension): whole-system organ capability ledger + fix ADR-0211 number collision
Consolidation/true-up after #646-#648. Records the off-serving comprehension
system as one ledger: R1 relational arithmetic, R2 finite-integer constraints,
R3 explicit single-rate + exact minute/hour conversion, the router/contemplation/
proposal loop, the proposal-review reporter, idle_tick read-only visibility, and
the standing router-organ-hygiene invariant. Pins the whole-system lane state
(answer_wrong==0 everywhere) and the off-serving import-disjointness guarantee.

Also fixes a documentation-integrity defect the R2 batch introduced: the R2 ADR
took number 0211, which collided with ADR-0211 (Conformal Falsification Bench,
2026-06-06 — earlier, test-pinned, depended on by ADR-0216). The conformal ADR
keeps 0211; the R2 ADR is renumbered to the next free number 0217. Ratified
decision content unchanged; only the identifier and references move.
2026-06-08 06:15:51 -07:00
Shay
801f0e23dd
Merge pull request #648 from AssetOverflow/feat/r3-2-be
feat(rate): R3.2b–e — time-unit conversion integration (model/solver/reader/gold) [merge LAST]
2026-06-08 06:03:27 -07:00
Shay
7e39357e11
Merge pull request #647 from AssetOverflow/feat/r3-2-a
feat(rate): R3.2a — exact-rational time-unit conversion primitive [merge after #646]
2026-06-08 06:03:23 -07:00
Shay
e98289ee89
Merge pull request #646 from AssetOverflow/feat/router-organ-hygiene-pr
test(contemplation): standing router-organ-hygiene invariant
2026-06-08 06:03:19 -07:00
Shay
1c59f331f4 feat(rate): exact time-unit conversion for single-rate (R3.2b-e)
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.
2026-06-08 05:43:51 -07:00
Shay
89321e617d feat(rate): R3.2 exact-rational time-unit conversion primitive (R3.2a)
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.
2026-06-08 05:30:57 -07:00
Shay
0dfc001c1c test(contemplation): standing router-organ-hygiene invariant
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.
2026-06-08 05:28:06 -07:00
Shay
f1e7925444
Merge pull request #645 from AssetOverflow/feat/r3-1-router-pr
feat(contemplation): R3.1 — wire R3 into the multi-organ router + pass manager
2026-06-08 05:23:49 -07:00
Shay
73345e641e feat(contemplation): wire R3 into the multi-organ router + pass manager (R3.1)
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.
2026-06-08 02:05:34 -07:00
Shay
a320c69a3a
Merge pull request #644 from AssetOverflow/feat/r3-e
feat(rate): R3e — R3 ledger + failure-family wiring [merge LAST]
2026-06-08 01:56:37 -07:00
Shay
f95657bb8f
Merge pull request #643 from AssetOverflow/feat/r3-d
feat(rate): R3d — single-rate reader + run_reader lane [merge after c]
2026-06-08 01:56:33 -07:00
Shay
54c9f60442
Merge pull request #642 from AssetOverflow/feat/r3-c
feat(rate): R3c — exact integer single-rate solver [merge after b]
2026-06-08 01:56:30 -07:00
Shay
09e147cac5
Merge pull request #641 from AssetOverflow/feat/r3-b
feat(rate): R3b — RateProblem model + gold + setup oracle [merge after a]
2026-06-08 01:56:26 -07:00
Shay
24f7b4bb03
Merge pull request #640 from AssetOverflow/feat/r3-a
feat(rate): R3a — compound-unit primitive [merge 1st]
2026-06-08 01:56:23 -07:00
Shay
780f993c73 feat(rate): R3 inventory ledger + failure-family wiring (R3e)
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.
2026-06-07 23:12:23 -07:00
Shay
6b1fdbff45 feat(rate): single-rate reader + run_reader lane + answer-choice reuse (R3d)
generate/rate_comprehension/reader.py: read_rate_problem comprehends explicit single-rate prose into a RateProblem via structural recognizers — rate value (<N> <plural> per <singular>), duration (for/in <N> <unit>), standalone quantity (outside those spans), query (how many <unit> by unit-match, or speed in <X> per <Y> -> rate). The compound-unit-consistency check is the wrong=0 gate: a duration whose unit isn't the rate denominator (miles/hour for 30 minutes) REFUSES rate_unit_mismatch, never converts.

Refuses combined_rates (>=2 rate clauses), temporal_state (clock markers), missing_rate/time/quantity (underdetermined). run_reader lane + 'reader' CLI. R3 reader: setup_correct 8 / setup_wrong 0 / refused_correct 4 -> answers 6 correct / 0 wrong / 6 refused (the 6/0/6 target). Reuses R2 verify_answer_choice end-to-end. Off-serving. 6 reader tests + constructed unit-mismatch guard.
2026-06-07 23:07:51 -07:00
Shay
6560300101 feat(rate): exact integer single-rate solver (R3c)
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.
2026-06-07 23:03:30 -07:00
Shay
4c271f5e9f feat(rate): R3 RateProblem model + rate gold + setup oracle (R3b)
generate/rate_comprehension/model.py: RateProblem (quantity = rate × time, exactly one unknown == query; rate_unit fixes all three units by construction; structural guard refuses zero/two unknowns). evals/rate_oracle/: span-free signature, a 12-fixture gold (6 solved / 2 solver_refuses / 2... reader_refuses), closed expect+reason taxonomy, gold-validation runner + CLI. No reader yet — proves the ruler (12/12 valid).

Gold shape gives the 6/0/6 target: 6 single-rate solved (forward + both inverses, non-temporal 'per box' included), 2 non-integer-inverse solver refusals, 4 reader refusals (unit mismatch minutes-vs-hour, missing time, combined rates, clock-time temporal). Off-serving. 9 oracle + 10 unit tests; per-branch meaningful-fail.
2026-06-07 23:01:25 -07:00
Shay
d9ebec7b70 feat(rate): R3 compound-unit primitive — the dimensional substrate (R3a)
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.
2026-06-07 22:57:13 -07:00
Shay
a00e87b297
Merge pull request #639 from AssetOverflow/feat/it-c
docs(proposal-review): IT-c — idle-integration boundary ledger [merge LAST]
2026-06-07 22:49:50 -07:00
Shay
5345c8a8a0
Merge pull request #638 from AssetOverflow/feat/it-b
feat(runtime): IT-b — read-only proposal-review sub-pass in idle_tick [merge after A]
2026-06-07 22:49:47 -07:00
Shay
16cd3955f9
Merge pull request #637 from AssetOverflow/feat/it-a
feat(proposal-review): IT-a — pure idle_summary function [merge 1st]
2026-06-07 22:49:45 -07:00
Shay
9b83cf7972 docs(proposal-review): idle-integration boundary ledger (IT-c)
Pins the boundary: existing idle_tick remains the only idle_tick; proposal review is read-only; NOT L10 always-on heartbeat; does not advance learning; only surfaces review obligations. Documents how IT-b works (gate, sub-pass, failure-isolation, no-did_work) and that R3 (rates/time/state) is next.
2026-06-07 22:33:17 -07:00
Shay
32385d954c feat(runtime): read-only proposal-review sub-pass in idle_tick (IT-b)
core/config.py: review_pending_proposals: bool = False (opt-in, mirrors consolidate_determinations). chat/runtime.py: IdleTickResult gains an additive optional proposal_review field; idle_tick runs a READ-ONLY sub-pass (after consolidation, gated) that surfaces core.proposal_review.idle_summary(). It does NOT set did_work / checkpoint / mutate / ratify / mount / modify readers; a reporter exception is CAPTURED (safe=False, errors=('proposal_review_failed:<type>',)), never propagated.

docs/runtime_contracts.md documents the sub-pass + field (required for runtime-surface changes). Contract tests: default-off -> proposal_review None (existing shape preserved); enabled -> summary surfaced; exception captured not propagated; read-only review never checkpoints; other passes unperturbed. 91-test smoke green incl. architectural invariants + both existing idle contract suites. Existing idle_tick remains the only idle_tick; NOT L10 always-on.
2026-06-07 22:32:20 -07:00
Shay
65c8587207 feat(proposal-review): pure idle_summary function (IT-a)
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.
2026-06-07 22:25:00 -07:00
Shay
3ec5e91433
Merge pull request #636 from AssetOverflow/feat/proposal-review-d
feat(proposal-review): RPT-d — boundary ledger [merge LAST]
2026-06-07 22:22:25 -07:00
Shay
b53f728ee9
Merge pull request #635 from AssetOverflow/feat/proposal-review-c
feat(proposal-review): RPT-c — safety dry-check + read-only CLI [merge after B]
2026-06-07 22:22:22 -07:00
Shay
4049681b85
Merge pull request #634 from AssetOverflow/feat/proposal-review-b
feat(proposal-review): RPT-b — deterministic review report [merge after A]
2026-06-07 22:22:19 -07:00
Shay
eb675064ce
Merge pull request #633 from AssetOverflow/feat/proposal-review-a
feat(proposal-review): RPT-a — read-only scanner over the proposal sink [merge 1st]
2026-06-07 22:22:16 -07:00
Shay
48386f3237 docs(proposal-review): boundary ledger — read-only reporter, not idle_tick, not L10 (RPT-d)
Documents the boundary: observes + reports + verifies; never advances teaching / ratifies / mounts / modifies readers / affects serving. ChatRuntime.idle_tick remains the only idle_tick (different stream, different verb). A future PR may call this reporter from idle_tick as a read-only sub-pass. L10-adjacent, NOT L10; the always-on heartbeat remains separate/unbuilt and is not claimed here.
2026-06-07 21:44:07 -07:00
Shay
cc3003f425 feat(proposal-review): safety dry-check + read-only CLI (RPT-c)
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.
2026-06-07 21:43:18 -07:00
Shay
3fc73eb14b feat(proposal-review): deterministic review report (RPT-b)
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.
2026-06-07 21:40:35 -07:00
Shay
3c6f820b54 feat(proposal-review): read-only scanner over the comprehension-failure sink (RPT-a)
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.
2026-06-07 21:39:03 -07:00
Shay
31a984e69b
Merge pull request #632 from AssetOverflow/feat/contemplation-pack-e 2026-06-07 12:57:58 -07:00
Shay
1bc7f1ae26
Merge pull request #631 from AssetOverflow/feat/contemplation-pack-d 2026-06-07 12:56:19 -07:00
Shay
cfa8bdd5fa
Merge pull request #630 from AssetOverflow/feat/contemplation-pack-c 2026-06-07 12:55:17 -07:00
Shay
787aa833fd
Merge pull request #629 from AssetOverflow/feat/contemplation-pack-b 2026-06-07 12:53:40 -07:00
Shay
b37794814d
Merge pull request #628 from AssetOverflow/feat/contemplation-pack-a 2026-06-07 12:53:19 -07:00
Shay
1adf4f5de5 feat(contemplation): contemplation v0 pass manager (N6) + boundary-first growth fix
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.
2026-06-07 08:59:47 -07:00
Shay
5903cba08a feat(teaching): proposal-only failure-artifact emitter (N5)
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.
2026-06-07 08:48:45 -07:00
Shay
c1ad5b8142 feat(comprehension): failure-family registry (N4)
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.
2026-06-07 08:46:07 -07:00
Shay
5ae516f4a2 feat(comprehension): deterministic multi-organ setup router (N3)
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.
2026-06-07 08:42:35 -07:00
Shay
67c54d6813 feat(comprehension): shared ComprehensionAttempt model + classify (N2)
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.
2026-06-07 08:40:53 -07:00
Shay
0b191055a8 docs(analysis): comprehension capability ledger — R1/R2 baseline + refusal taxonomy (N1)
Post-merge baseline for the contemplation batch: R1 7/0/3, R2 reader 10/0/0, gold 13/13, serving unchanged. Enumerates the full typed refusal surface across both organs (R1 reader/admissibility, R2 reader/solver/answer-choice) and a first-pass cross-organ failure-family taxonomy with must_remain_refused / proposal_allowed / owner — the table N4 will make executable. Repeats the load-bearing distinction: correct refusal != missing capability.
2026-06-07 08:36:22 -07:00
Shay
51b4a6e37d
Merge pull request #627 from AssetOverflow/feat/r2-reader
feat(constraint): R2 Pack C — two-category reader (the capability) + ledger
2026-06-07 08:32:11 -07:00
Shay
9337223733
Merge pull request #626 from AssetOverflow/feat/r2-solver-verifier
feat(constraint): R2 Pack B — exact integer solver + answer-choice verifier
2026-06-07 08:28:12 -07:00
Shay
ae16c0cae7
Merge pull request #625 from AssetOverflow/feat/r2-ruler
feat(constraint): R2 Pack A — constraint IR + setup oracle + gold + ADR-0211 (the ruler)
2026-06-07 08:19:31 -07:00
Shay
8d2c270144
Merge pull request #624 from AssetOverflow/feat/r1-inverse-frame
feat(comprehension): R1 inverse reader frame — close R1 at 7/0/3 (PR-7b)
2026-06-07 08:04:23 -07:00
Shay
a3a26b8fc5 test(constraint): pin the three Pack-C review hazards as regression 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.
2026-06-07 07:49:10 -07:00
Shay
faecb21c37 docs(analysis): R2 inventory ledger — reader 10/0/0, 6 families read+solved+verified (R2 C9)
Records the final R2 v1 state: reader setup_correct 10 / setup_wrong 0 + 3 correct reader-refusals; 7 solved answers + 3 solver-refused + 3 reader-refused; gold 13/13. Per-fixture table, the four wrong=0 gates, the reader-vs-solver division of labor (equal coefficients are a solver refusal, not a reader one), covered families, and R3 deferrals. R1 7/0/3 and 15-case 15/0/0 unchanged.
2026-06-07 07:36:17 -07:00
Shay
4ddaef4d00 feat(constraint): two-category reader — prose to ConstraintProblem (R2 C5-C9)
generate/constraint_comprehension/reader.py: read_constraint_problem comprehends two-category constraint prose into a typed ConstraintProblem via five recognizers — C5 category-pair (from coefficient clauses; >2 -> too_many_categories), C6 coefficient (Each <cat> holds/has/costs/is-worth <N> <unit>; shared measured unit), C7 total-count (collective-unit sentence -> x+y=N; absent -> missing_total_count), C8 weighted-total (measured-unit sentence -> ax+by=T; absent -> missing_weighted_total), C9 query-target (How many <cat> -> the asked unknown). Refuses, never mis-assembles.

Reconciles the sketch's 'no equal coefficients' note: equal coefficients are a SOLVER refusal (indistinguishable_weights), not a reader one — the reader reads r2-010 setup_correct and the solver refuses it. run_reader lane + 'reader' CLI subcommand. R2 reader: setup_correct 10 / setup_wrong 0 / refused_correct 3; R1 7/0/3 and 15-case 15/0/0 unchanged. Off-serving. 8 reader tests incl. full read->solve->verify chain + meaningful-fail guards.
2026-06-07 07:34:55 -07:00
Shay
121362c52a feat(answer-choices): multiple-choice verifier with contradiction flag (R2 C4)
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.
2026-06-07 07:26:00 -07:00
Shay
babcf2fdb2 feat(constraint): exact integer 2-var solver — Cramer's rule, refusal-first (R2 C3)
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.
2026-06-07 07:23:23 -07:00
Shay
a098519bc1 feat(constraint): R2 setup oracle + gold + ADR-0211 — the ruler (R2 C2)
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.
2026-06-07 07:19:29 -07:00
Shay
e71531c0c9 feat(constraint): R2 linear-constraint IR — typed problem model (R2 C1)
generate/constraint_comprehension/{expr,model}.py: frozen, slots'd dataclasses, no behavior. expr = LinearExpr (sum(coeff*symbol)+constant) + LinearConstraint (lhs eq rhs, optional source_span). model = Unknown (symbol/entity/unit/finite-integer domain), AttributeFact (per-category coefficient provenance), ConstraintQuery (symbol+unit), ConstraintProblem (unknowns/facts/constraints/query).

Terms pinned as (symbol, coefficient) to match the gold serialization. Query is a minimal dedicated type, not R1's BoundUnknown (no degenerate fit). Off-serving package; no generate.derivation / reliability_gate import. 9 IR tests (shape + frozen + defaults).
2026-06-07 07:10:20 -07:00
Shay
60b40d3e3a feat(comprehension): inverse reader frame — base of a more/fewer-than (PR-7b / R2 C0)
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).
2026-06-07 07:06:26 -07:00
Shay
0e6a7f9ac0
Merge pull request #619 from AssetOverflow/feat/inverse-solve-contract
feat: narrow reverse-solve oracle contract (PR-7a, contract only)
2026-06-07 06:44:07 -07:00
Shay
036ed8c659 feat(oracle): narrow reverse-solve contract — base of one more/fewer_than (PR-7a)
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).
2026-06-07 06:22:36 -07:00
Shay
a8707d3bac
Merge pull request #618 from AssetOverflow/feat/aggregate-query-variants
feat: additive aggregate query variants — 'altogether' / 'in total'
2026-06-07 05:59:16 -07:00
Shay
ef06923866 feat(comprehension): additive aggregate query variants — 'altogether' / 'in total'
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.
2026-06-07 05:49:28 -07:00
Shay
82b14065f2
Merge pull request #617 from AssetOverflow/docs/r1-inventory-ledger
docs: R1 comprehension inventory ledger (4/0/6 baseline)
2026-06-07 05:49:12 -07:00
Shay
261c0e4c2d docs(analysis): R1 comprehension inventory ledger
Baseline decision artifact at main@5ada1392 (post-#616). Records the R1
4/0/6 state per-fixture: the 4 admitted semantic families and the invariant
protecting each, and a classification of the 6 refusals — 2 pure phrasing
gaps (r1-03/r1-04 aggregate-query), 1 real reverse-solve capability (r1-07),
and 3 correct wrong=0 boundaries that must stay refused (r1-08/09/10).

Source-verified against the merged reader (refusal codes + line refs) and
the live lanes (setup 4/0/6, answers 4/0/6 setup_wrong/gold_error 0,
15-case 15/0/0). Docs-only; no code, no serving lane, no SHA-lane content.
2026-06-07 05:30:14 -07:00
Shay
5ada1392a5
Merge pull request #616 from AssetOverflow/feat/partition-aggregate-divide
feat(comprehension): aggregate-then-divide partition frame — "split equally into N boxes" (PR-6d)
2026-06-07 05:24:50 -07:00
Shay
f9ef9e56a4 feat(comprehension): aggregate-then-divide partition frame — "split equally into N boxes" (PR-6d)
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).
2026-06-06 21:07:19 -07:00
Shay
409615fcef
Merge pull request #615 from AssetOverflow/feat/half-as-many-divide
feat(comprehension): the divisive comparative frame — "half as many" as exact integer division (PR-6c)
2026-06-06 20:44:52 -07:00
Shay
0951d80e04 feat(comprehension): the divisive comparative frame — "half as many" as exact integer division (PR-6c)
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).
2026-06-06 20:18:39 -07:00
Shay
5ac1526536
Merge pull request #614 from AssetOverflow/pr-6b-r1-answer-oracle-followup
test(comprehension): prove R1 times-as-many answer oracle (PR-6b follow-up)
2026-06-06 19:35:57 -07:00
Shay
f4cf490e8d test(comprehension): prove R1 answer-oracle lane 2026-06-06 18:32:21 -07:00
Shay
15b0b583aa test(comprehension): expose R1 answer lane CLI 2026-06-06 18:31:41 -07:00
Shay
56260629b8 test(comprehension): add answer gold for setup-correct R1 cases 2026-06-06 18:31:12 -07:00
Shay
88d8950d3e test(comprehension): expose R1 answer lane 2026-06-06 18:28:49 -07:00
Shay
c364d434da feat(comprehension): add R1 answer-oracle lane 2026-06-06 18:26:30 -07:00
Shay
ce8897e84f feat(comprehension): add off-serving times_as_many answer oracle 2026-06-06 18:20:23 -07:00
Shay
2786b32d62
Merge pull request #613 from AssetOverflow/feat/scalar-multiply-invariant 2026-06-06 18:08:41 -07:00
Shay
c2b97f40bf test(comprehension): prove the scalar-multiply contract (PR-6a)
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.
2026-06-06 17:42:22 -07:00
Shay
9a285dfbdb
Merge pull request #612 from AssetOverflow/feat/r1-setup-gold
feat(comprehension): R1 gold (PR-5b) + the multiplicative comparative frame (PR-5c)
2026-06-06 17:38:35 -07:00
Shay
e9cbe65d77 feat(comprehension): the multiplicative comparative frame — first R1 capability (PR-5c)
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).
2026-06-06 17:29:23 -07:00
Shay
5b193280a5 feat(setup-oracle): independent R1 gold + the reader refuses, never misreads (PR-5b)
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.
2026-06-06 17:18:45 -07:00
Shay
6c8f2b8332
Merge pull request #611 from AssetOverflow/feat/setup-oracle-units
feat(setup-oracle): make the ruler UNIT-AWARE (setup-oracle v2, PR-5a)
2026-06-06 17:13:20 -07:00
Shay
4056069152 feat(setup-oracle): make the ruler UNIT-AWARE (setup-oracle v2, PR-5a)
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.
2026-06-06 17:09:45 -07:00
Shay
3d006d2e19
Merge pull request #610 from AssetOverflow/feat/quant-expression-ir
refactor(comprehension): typed expression IR as the source of meaning (PR-4)
2026-06-06 17:03:21 -07:00
Shay
06450928c9 refactor(comprehension): typed expression IR as the source of meaning (PR-4)
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.
2026-06-06 16:57:53 -07:00
Shay
0882a4f8ef
Merge pull request #609 from AssetOverflow/feat/drop-quantquery-read-unknowns
refactor(comprehension): drop QuantQuery — consumers read graph.unknowns (PR-3)
2026-06-06 16:52:10 -07:00
Shay
0d32a655f1 refactor(comprehension): drop QuantQuery — consumers read the target from graph.unknowns (PR-3)
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.
2026-06-06 16:49:09 -07:00
Shay
a700b4503c
Merge pull request #608 from AssetOverflow/feat/quant-bound-unknown-and-setup-oracle
feat(comprehension): question target in the graph (PR-1) + setup-oracle lane
2026-06-06 16:42:52 -07:00
Shay
59974865ef feat(comprehension): question target in the graph (PR-1) + setup-oracle lane (grade the reading)
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).
2026-06-06 16:40:15 -07:00
Shay
61ac070c91
Merge pull request #607 from AssetOverflow/feat/verified-canonical-comparison
docs(analysis): VERIFIED producer scoping — validate-first verdict is DO NOT BUILD
2026-06-06 16:18:14 -07:00
Shay
6f2053bcb7 docs(analysis): VERIFIED producer — validate-first verdict is DO NOT BUILD (comprehension-bound)
Scoping + holdout validation of the VERIFIED canonical-comparison producer (the
ADR-0206 §5 math-serving widening's gate). Records the empirical verdict so it is
not re-chased: on holdout_dev the fold-derivation reader is 2 correct / 87 wrong on
candidate-graph-refused cases, and a pure-chain certifier would admit 37 wrong (a
wrong=0 breach) — the train_sample 3/7 was overfit. math_verifier.verify is
solver-replay soundness, not correctness; the R1 graph reader is nested (0 flips).
Math serving is comprehension-bound; the math seam correctly stays inert; the real
lever is the general COMPREHEND organ, not a narrow GSM8K certifier. No code change.
2026-06-06 16:09:41 -07:00
Shay
cbaa3e72d7
Merge pull request #606 from AssetOverflow/feat/math-serving-reach-seam
feat(derivation): math-serving reach seam — select_self_verified is policy-aware (ADR-0206 §5)
2026-06-06 15:56:21 -07:00
Shay
c5f0c90738 feat(derivation): math-serving reach seam — select_self_verified is policy-aware (ADR-0206 §5)
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.
2026-06-06 15:46:14 -07:00
Shay
546045eb4d
Merge pull request #605 from AssetOverflow/feat/learned-estimation
feat(determine): calibrated disclosed estimation — the engine earns the right to guess (Step E)
2026-06-06 13:57:56 -07:00
Shay
7cb826a548 feat(determine): calibrated disclosed estimation — the engine earns the right to guess (Step E)
The final AGI-spine step (A INSTRUMENT → B WIRE → C DEEPEN → D CLOSE → E ESTIMATION).
The engine may now SERVE a DISCLOSED estimate for a query it would otherwise refuse —
but only for a predicate-class that has measured itself reliable, and never as fact.

This executes the ADR-0206 §5 cognition-path widening: the bridge's LICENSE node
(reliability_gate.license_for), previously "built — not yet called from serving", is now
called. govern_response returns APPROXIMATE iff a genuine licensed Action.SERVE
LicenseDecision is passed (STRICT for every other input — so every existing serving call
site is byte-identical); shape_surface DISCLOSES the estimate as "[approximate] …".

Mechanism:
- generate/determine/estimate.py — a BLIND converse-guesser: told p(a,b), asked p(b,a),
  it commits the converse. It never reads the pack's symmetry metadata; whether the guess
  is right is MEASURED, not assumed.
- evals/determination_estimation/ — the gold lane: run_practice (sealed, ADR-0199) folds
  the converse-guesser over symmetric (sibling_of) vs directed (parent_of) cases, scored
  against the pack's graph.edge.symmetric truth (gold independent of the solver). The gate
  DISCRIMINATES: sibling_of earns SERVE (660 correct → Wilson floor 0.990046 ≥ θ_SERVE),
  parent_of does not (660 wrong → 0.0). The license is earned by VOLUME — 657 perfect
  commits is the exact θ_SERVE=0.99 threshold (656 is below).
- generate/determine/data/estimation_ledger.json — the ratified committed ledger,
  hash-verified on load (a hand-edited ledger raises RatifiedLedgerError); it IS the
  deterministic sealed-practice output (a GSM8K-style --check test pins this).
- chat/runtime.py — when a converse query is refused and the class holds a SERVE license,
  the disclosed estimate is surfaced through the bridge (gated by config.estimation_enabled,
  default OFF; only meaningful with accrue_realized_knowledge).

Invariants:
- wrong=0 by construction — an estimate is ALWAYS disclosed ([approximate]), never a silent
  commit (UNVERIFIED_POSSIBLE is never in APPROXIMATE's admissible set), and only a genuine
  ratified license widens (a forged {"licensed":True} dict / a PROPOSE license / an
  unlicensed SERVE all stay STRICT). Defense-in-depth: type-gate ∧ admissible-set ∧
  hardcoded disclosed state.
- never self-authored — ceilings stay at safe defaults (θ_SERVE=0.99); the engine cannot
  raise its own bar. The ledger is sealed practice, hash-verified.
- session/serving only — no corpus/pack/identity/proposal/vault mutation; the HITL teaching
  path is untouched. Deterministic; no clock/random.
- byte-identical for every non-E turn (the 2643 govern_response call passes no license).

Out of scope (separate ADR-0206 §5 PRs): the math-serving seam (select_self_verified,
touches the sealed metric), SITUATE (stakes), and the live FEED-BACK loop.

Verified green: smoke (90), architectural invariants (56), response_governance (321,
incl. the new license-gated widening test), the determination-estimation lane (12), and
the B/D/determine regression net. Four-lens adversarial review (disclosure/wrong=0,
calibration integrity, byte-identity, boundary/determinism): all held. Design:
docs/analysis/E-estimation-design-2026-06-06.md.
2026-06-06 13:49:07 -07:00
Shay
7373183dc0
Merge pull request #604 from AssetOverflow/review/codex-env-falsification
feat(sensorium): environment-falsification roadmap (6 phases, afferent) — ratified + hardened
2026-06-06 13:15:04 -07:00
Shay
7f5958dbd5 chore(sensorium): ratify environment-falsification roadmap — witness-importer hardening + ADR-0198 reconciliation
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.
2026-06-06 12:54:31 -07:00
Shay
d67c83b794 Add motor verdict enforcing gate 2026-06-06 12:37:57 -07:00
Shay
c5da00107e Add passive tabletop lab protocol 2026-06-06 12:37:57 -07:00
Shay
b5f892bd95 Add offline witness log importer 2026-06-06 12:37:57 -07:00
Shay
c5eefd1650 Add event vision sensorium lane 2026-06-06 12:37:57 -07:00
Shay
598a3e1a3d Add falsification scenario layer 2026-06-06 12:37:57 -07:00
Shay
3e061ea0f9 Add conformal falsification bench contract 2026-06-06 12:37:57 -07:00
Shay
8320c2cbb4
Merge pull request #603 from AssetOverflow/feat/loop-learns-determined
feat(determine): idle deductive consolidation — the loop learns from determined facts (Step D)
2026-06-06 12:36:45 -07:00
Shay
8edafd04ac feat(determine): idle deductive consolidation — the loop learns from determined facts (Step D)
CLOSE the autonomous-loop spine: when idle, the engine consolidates each
soundly-derived determination back into the held self, so the next determine()
reaches it directly and can chain one hop further. The directly-answerable set
climbs monotonically across idle ticks to the deductive-closure fixed point.

Mechanism — generate/determine/consolidate.py::consolidate_once runs ONE
semi-naive layer of the member/subset deductive closure (member∘subset → member,
subset∘subset → subset; NEVER member∘member — instance-of is not transitive).
Each one-hop conclusion not yet realized is VERIFIED by the sound+complete
proof_chain ROBDD (reusing C's single verifier _verify_subsumption) and written
back via generate/realize::realize_derived as a SPECULATIVE realized record
carrying derived-provenance (premise structure_keys + rule + the ENTAILED
verdict). idle_tick gains a consolidation pass gated by the new
config.consolidate_determinations (default OFF); IdleTickResult.facts_consolidated
reports the layer.

Invariants held:
- wrong=0 — every consolidated fact is a sound-rule conclusion confirmed by the
  sound+complete decider; member∘member is structurally unreachable (a member fact
  is only ever extended by a subset edge). _verify_subsumption now refuses a
  mislabeled/wrong-arity path (belt-and-suspenders now that consolidation is a
  second caller), so the fallacy cannot be laundered through a corrupted chain.
- honesty — a fact derived from SPECULATIVE premises stays SPECULATIVE / as-told;
  the soundness of the inference never upgrades the standing of the premises.
  COHERENT is never minted.
- teaching-safety — SESSION memory (immediate), an extension of the realize path;
  NOT corpus mutation and NOT coupled to proposals. The HITL path is untouched.
- determinism/replay — pure function of the realized set; sorted write order;
  derived structure_key identical to a told fact's; provenance round-trips through
  the Shape B+ snapshot (consolidated facts resume the SAME life across reboot).
- no new normalization — writes reuse the INV-21-allowed vault writer;
  algebra/versor.py keeps closure.

Falsification — evals/determination_closure: a frozen replay seeds a deep is-a
chain and runs idle ticks; asserts the closure climbs monotonically to a complete
fixed point (no-op final tick), wrong=0 (member∘member canary never derived, no
fabricated membership), all derived facts SPECULATIVE, and every derived record
re-verifies ENTAILED from its recorded premises.

Verified green: smoke, runtime, cognition, architectural invariants, plus the new
D unit + lane tests and the determine/realize/persistence regression net. Five-lens
adversarial review: 4 lenses held; the 5th (normalization) was a misattribution
(pre-existing vault reproject, triggered identically by the merged realize path,
on sanctioned null-vector storage). Design + findings: docs/analysis/
D-close-consolidation-design-2026-06-06.md.
2026-06-06 12:28:09 -07:00
Shay
6abc2819dd
Merge pull request #602 from AssetOverflow/feat/transitive-determination
feat(determine): transitive subsumption — sound is-a reasoning over accrued knowledge (Step C)
2026-06-06 11:20:45 -07:00
Shay
cd1a9cbe80 feat(determine): transitive subsumption — sound is-a reasoning over accrued knowledge (Step C)
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).
2026-06-06 11:09:41 -07:00
Shay
ea4ed4abb3
Merge pull request #601 from AssetOverflow/feat/surface-determination
feat(runtime): surface the determination — the engine answers from accrued knowledge (Step B-2)
2026-06-06 11:01:48 -07:00
Shay
dc06bd5a3c feat(runtime): surface the determination — the engine answers from accrued knowledge (Step B-2)
When accrue_realized_knowledge is on and a question turn is Determined over realized
knowledge, the user-facing surface IS that answer (generate.determine.render_determination):
'Yes — as I was told, truth is a concept.' The realizer's articulation_surface is
RETAINED as evidence — the determination is a user-facing SELECTION (like the
unknown-domain gate), not a rewrite. An Undetermined turn keeps the default
articulation surface (the honest 'I don't know'); flag-off never takes this path.

Basis is rendered honestly: SPECULATIVE grounds read 'as I was told', never 'verified'
(D0 only asserts answer=True, so the surface is an affirmation — no fabricated or
asserted-False string). Selection only: no field op, no normalization, proposes no
learning (HITL untouched).

Updates docs/runtime_contracts.md selection policy + adds the contract test in the
same PR (per the surface-contract discipline).
2026-06-06 10:52:54 -07:00
Shay
70902393a7
Merge pull request #600 from AssetOverflow/feat/wire-realize-determine
feat(runtime): inline realization — the conversation accrues knowledge (Step B-1)
2026-06-06 10:49:58 -07:00
Shay
3cdc4faa5f feat(runtime): inline realization — the conversation accrues knowledge (Step B-1)
Wires comprehend->realize/determine into the live turn loop: a declarative turn
REALIZES a fact into the held self (session vault, SPECULATIVE/as-told); a question
turn is DETERMINED over realized knowledge (answered as-told, or refused open-world).
The first time a CORE conversation actually accumulates knowledge it can recall and
reason over — the 'one continuous life' telos made real, including across reboot
(the accrued fact is in the Shape B+ snapshot, so it survives the process ending).

Seam: all chat() returns funnel through _checkpointed_response; accrual runs there
BEFORE the checkpoint (so the fact persists this turn), gated by a new
accrue_realized_knowledge flag (default OFF — one-shot/eval runtimes don't accrue;
the production L10 process enables it with persist_session_state).

Discipline: SESSION memory, not ratified learning — proposes nothing, HITL untouched.
Realization writes go through generate.realize (INV-21 allowed writer). Additive: a
failure is a clean no-op, never crashes a turn. Slice B-1 RECORDS the outcome
(last_turn_accrual(), introspectable) and leaves the ChatResponse/surface contract
UNCHANGED; surfacing the determination is the B-2 follow-up (needs runtime_contracts
+ contract-test updates).

Tests: declarative accrues; question determines as_told; untold refuses open-world;
idempotent retell not recreated; flag-off no-ops with identical surface; and the
lived-spine proof — accrued knowledge survives reboot (determined as_told after a
fresh runtime over the same checkpoint).
2026-06-06 10:40:20 -07:00
Shay
a9e75ada23
Merge pull request #599 from AssetOverflow/feat/edge-budget-gate
feat(edge): edge-deployment budget gate — deterministic per-turn persistence cost (A2)
2026-06-06 10:35:32 -07:00
Shay
66b8c7c431 feat(edge): edge-deployment budget gate — deterministic per-turn persistence cost
A2 of the refined sequencing. Proves (deterministically, not by assertion) what a
long-running CORE life costs to persist per turn on a constrained offline device.
Measures the Shape B+ checkpoint BYTES per turn (session_state.json) over the real
turn loop — bytes, not wall-clock latency (machine-dependent → flaky). Reuses the L10
continuity corpus.

Measured cliff: save_session_state re-serializes the FULL snapshot every turn, so
per-turn bytes are O(n) in the accumulated life — 3,811 → 88,189 bytes (23x) over 24
turns, ~1.3KB/vault-entry re-written every turn. That blocks continuous-life at the edge.

The gate encodes the edge REQUIREMENT (≤16 KiB/turn regardless of session length) as
xfail(strict): it fails today (documenting the cliff), runs green in CI, and flips to
a hard failure the moment incremental/append-only persistence (O(Δ)/turn) lands —
forcing us to retire it. Plus a regression ceiling (passes today) and a determinism
check (the byte metric is reproducible → a valid gate).

The fix is algorithmic (incremental persistence, Python/Ring-2), NOT a language
rewrite. Tagged core/array_codec.py as the locked reference contract for a future
gated Ring-1 Zig byte-exact codec (ADR-0196 G0-G8) — step 3, only after the O(Δ) fix
and only if this gate proves the codec is still the bottleneck. See contract.md.
2026-06-06 10:27:10 -07:00
Shay
4692d3fae7
Merge pull request #598 from AssetOverflow/feat/capability-index-relational
feat(measure): put the relational reader (#596) on the capability index (breadth 8→9)
2026-06-06 10:17:38 -07:00
Shay
2a3f422783 feat(measure): put the relational reader (#596) on the capability index
A1 of the refined sequencing — the binary-relation reader was inert w.r.t. the
yardstick (contributing 0). This adds a comprehension_relational_predicate domain:
binary-relation prose scored against hand-authored independent gold (predicate,
subject, object) triples — INV-25 independent / INV-27 reader-disjoint (the reader
never produced the gold). Index breadth 8->9, capability_score 0.937258->0.944030,
wrong_total still 0; baseline.json re-frozen to digest 1ea91c1e.

Rigor split: the index lane is POSITIVE-ONLY (clean coverage, consistent with the
other 8 lanes — mixing adversarial refuse-cases into the coverage denominator would
make 'added capability' read as a score drop). The #596 fabrication-catch lives in a
dedicated falsification test (evals/relational/v1/refusals.jsonl): the trailing-
qualifier / dangling-copula / negation / verb-form cases MUST refuse — bites if the
reader ever fabricates. Honest coverage gap recorded: overlaps_event has no copular
surface form (verb-form 'A overlaps B' refuses), so 17 positives cover 15/16 predicates.
2026-06-06 10:09:15 -07:00
Shay
a03927dd13
Merge pull request #597 from AssetOverflow/feat/relational-reader
fix(comprehend): relational reader fail-closed on leftover relational structure (wrong=0)
2026-06-06 10:00:03 -07:00
Shay
47a31d3ed0 fix(comprehend): relational reader fail-closed on leftover relational structure
Lookback (adversarial verify) found a comprehension-layer wrong=0 hole: an argument
slot only refused on reader._RESERVED tokens, so connective heads leaked through —
'Carol is the sibling of Dan during school.' fabricated sibling_of(carol,
dan_during_school) and realized it, while the asymmetric twin '… during the trip.'
refused (article leaked). The fabricated compound entity entered realized memory,
breaching the never-fabricate floor.

Fix: (1) argument slots must be FREE of connective tokens (a leaked connective head =
unparsed relational structure -> refuse 'extra_relational_structure'); (2) the copula
must sit STRUCTURALLY adjacent to the connective, so a dangling copula ('Monday before
Friday is.') and a period-question-as-statement ('Is Monday before Friday.') refuse
instead of fabricating a fact. determine stays sound by construction; this closes the
reader-layer hole. Bite tests added for both classes + a 'no fabricated entity enters
realized memory' assertion.
2026-06-06 09:45:10 -07:00
Shay
b134c133b0
Merge pull request #596 from AssetOverflow/feat/relational-reader
feat(comprehend): relational reader — binary-relation NL→structure
2026-06-06 09:42:07 -07:00
Shay
aa66047b35 feat(comprehend): relational reader — binary-relation NL→structure
First consumer of en_core_relational_predicates_v1: a fail-closed sibling reader
that maps '<A> is [the] <connective> <B>' onto the pack's closed predicate
vocabulary (parent_of, less_than, left_of, before_event, ...), producing a standard
Comprehension. realize_comprehension consumes it unchanged; determine's guard widens
from the single 'member' predicate to a CLOSED direct-entailment set (member + the
ground binary relational predicates), keeping categorical/propositional predicates
soundly excluded.

Direct entailment only — no transitive/symmetric/rule inference (symmetric-converse
questions are a sound-but-incomplete refusal, never a faked assertion). Only the
predicate is closed-vocabulary; arguments may be OOV. The pack is loaded explicitly,
not default-mounted.

wrong=0 bites: reader refuses non-template/negated/pack-absent surfaces; determine
asserts only on direct entailment and excludes categorical predicates.
2026-06-06 09:32:08 -07:00
Shay
79b89a5e2d
Merge pull request #595 from AssetOverflow/proposal/brief-c-relational-predicates-pack
Brief C: propose curated relational predicate pack
2026-06-06 09:19:30 -07:00
Shay
ac3b139a8c docs(language-packs): document relational predicate proposal 2026-06-06 08:03:34 -07:00
Shay
4d7ce2e247 docs(language-packs): add relational predicate pack manifest 2026-06-06 08:02:33 -07:00
Shay
fce483623c docs(language-packs): propose relational predicate pack lexicon 2026-06-06 07:58:34 -07:00
Shay
71baf63eb8
Merge pull request #594 from AssetOverflow/feat/determine-d0
feat(determine): D0 — reason over realized structure → assert (as-told) / refuse (Step 4)
2026-06-06 06:43:20 -07:00
Shay
7f52be967f
Merge pull request #593 from AssetOverflow/feat/realize-r1c-oov-generalization
feat(realize): R1c + OOV — binding_graph substrate and OOV subjects
2026-06-06 06:42:35 -07:00
Shay
872fecf478 test(determine): lookback coverage — multi-query, malformed-query, non-comprehension
From the mandated lookback audit: three D0 refusal branches were asserted-but-unproven.
Now they bite:
- not_single_query: reachable from REAL input — a two-question prompt yields two queries.
- malformed_query: a unary `member` query (hand-built; the reader only emits arity-2).
- not_a_comprehension: a non-Comprehension/Refusal input.

No code change — determine.py already refused all three; this closes the
schema-proof-obligation gap (each would now fail if its guard were removed).
2026-06-06 06:33:13 -07:00
Shay
6816e6220c fix(realize): lookback hardening — defensive admissibility re-assertion + coverage
From the mandated lookback audit of the composed R0→R1→R1c→OOV surface:

- wrong=0 HAZARD (medium): realize_quantitative trusted equation admissibility it
  never checked. `comprehend_quantitative` runs real check_admissibility, but the
  type does not enforce it, so a future non-reader constructor could slip a
  dimensionally-incoherent 'pending'/'refused' equation into the held self (then
  surfaced as-told by DETERMINE). Now RE-ASSERTS admitted-status defensively ->
  NotRealized("unadmitted_equation"); docstring corrected to match (no longer claims
  the type guarantees it). Bite test via a hand-built pending-equation graph.
- Defensive: wrap binding_graph hashing -> NotRealized("unhashable_structure") so a
  future numeric field is a clean refusal, not an uncaught TypeError mid-write.
- Coverage (the obligations now bite, not decoration): unadmitted_equation,
  not_a_quant_comprehension, no_bound_fact, grounding_failed (monkeypatched probe),
  cross-substrate coexistence (meaning_graph + binding_graph in one vault — recall
  isolates, structure_keys differ), and the negated_relation refusal (hand-built,
  since the reader encodes declarative negation in the PREDICATE not rel.negated).
- Drift: R0 idempotency test now names the actual dedup key (structure_key, not
  content_hash); scope doc notes #591 made OOV placement injective (the §0/§1
  non-injectivity finding describes the pre-#591 substrate).

Note: the lookback flagged negated_relation as a high reachable-untested hazard, but
verification showed the reader never sets rel.negated=True for declaratives (it
refuses "X is not a Y" or uses some_not/disjoint predicates) — so it is a defensive
branch, tested via a hand-built graph. Green: 35 realize + ruff clean.
2026-06-06 06:31:54 -07:00
Shay
bf9ad77fda feat(determine): D0 — reason over realized structure → assert (as-told) / refuse
The honest gear (roadmap Step 4). `determine(question: Comprehension | Refusal, ctx)`
answers a membership question ("Is X a Y?") ONLY from what the held self has already
REALIZED (R1a structural recall) — never from the field, an LLM, or absence.

Honesty (from the design review): every realizable record is SPECULATIVE, and
ADMISSIBLE_AS_EVIDENCE = {COHERENT}, so a determination grounded in SPECULATIVE records
carries `basis="as_told"` — "based on what I was told (unverified)", NEVER "verified".
Until COHERENT promotion exists (out of scope), D0 produces only as-told assertions or
typed `Undetermined` refusals. No estimation; no corpus mutation (teaching stays HITL
proposal-only).

Soundness (open-world, wrong=0): D0 asserts an answer ONLY on DIRECT structural
entailment by a realized fact; absence never refutes (so it never asserts a positive
answer from missing knowledge — it refuses). It asserts only `answer=True` on a direct
hit; it never asserts False. Negated questions and non-`member` queries are an explicit
`Undetermined` — D0 ships no entailment path the reader cannot exercise.

Tests (test_determine_d0.py, 8) BITE: present-but-non-entailing (a record about the
subject exists but not the asked relation) MUST refuse; the same question against a
fresh context with no realized fact flips Determined→Undetermined (the verdict is
entailment, not mere presence); the negated + unsupported-query refusals are exercised
via hand-built queries (the reader refuses them upstream, so a comprehend-based test
would pass vacuously). Independent of #593: reasons over R0/R1 `member` facts only.
2026-06-06 06:18:55 -07:00
Shay
be2d4b487e feat(realize): R1c + OOV — binding_graph substrate and OOV subjects
Generalizes what REALIZE accepts, on the structural-key foundation from #592.

R1c — binding_graph / arithmetic realize:
- `realize_quantitative(QuantComprehension | Refusal, ctx)` realizes a comprehended
  arithmetic structure (its admissibility-checked binding_graph) as a SPECULATIVE
  record with `structure_kind="binding_graph"` and a span-free `structure_key` over
  symbols/facts/equations. The only wrong=0 guard needed is the `Refusal` check —
  factless input is already a Refusal upstream (the fact-count gate would be vacuous).
- Extracts the shared `_realize_structured` write path so meaning_graph and
  binding_graph share ONE wrong=0 dedup/store point (the `.store` call stays in the
  INV-21-allowlisted realize.py).

OOV — lift the in-vocab subject gate:
- R0 declined OOV subjects on the mistaken belief that OOV grounding is
  non-deterministic. It is deterministic and reboot-stable, and #591 makes it
  injective. Correctness rests on the structural key, not the versor, so OOV subjects
  now realize normally. The "side-effect-free" comment is corrected: probe_ingest of
  an OOV token mutates the shared vocab via a session-scoped `insert_transient` that
  is NOT serialized into the snapshot, so reboot-stability rests on the vault record.

Honesty: the binding_graph entities (alice, the synthesized `total`) are symbolic/OOV
so the placement versor is deterministic-GIVEN-session-state, NOT subject-determined;
reboot-stability is carried by the Shape B+ snapshot of the exact bytes, and
distinctness by structural recall — never the (possibly colliding) metric. SPECULATIVE
always; versor_condition<1e-6 + exact CGA recall preserved; no parallel learning path.

Tests: test_realize_r1c_binding_graph.py (8) + test_realize_oov.py (6, incl. a
fresh-vocab reboot proving stability rests on the vault record not the transient) +
test_realize_r0.py oov test flipped to realize. Green: 35 realize + smoke locally.
2026-06-06 06:16:21 -07:00
Shay
ba50f3933b
Merge pull request #592 from AssetOverflow/feat/realize-r1a-structural-recall
feat(realize): R1 — structural identity & recall (relation-space recall + span-free idempotency)
2026-06-06 06:13:12 -07:00
Shay
6f716d4970
Merge pull request #591 from AssetOverflow/codex/oov-grounding-determinism
[codex] derive OOV grounding from token content
2026-06-06 06:01:09 -07:00
Shay
ef3181aa01 feat(realize): R1 — structural identity & recall (relation-space + span-free idempotency)
R0 keyed a realized fact by its subject's field versor, which is NOT injective:
two facts about one subject embed to byte-identical versors and collide at inf on
metric recall (proven). R1 adds the missing structural key.

- RealizedRecord/metadata carry ordered `relation_arguments` (the relation-space
  key R0's sorted `entity_names` discards) and a span-free `structure_key`.
- `recall_realized(ctx, subject=/predicate=/content_hash=/structure_key=/
  structure_kind=/entity=)` retrieves realized facts by EXACT structural metadata
  (no metric / ANN), via a new read-only `VaultStore.iter_metadata()` accessor.
- Idempotency now dedups on the span-free `structure_key`, so the same proposition
  told from a different source/offset collapses (R0's span-inclusive content_hash
  could not). Guarded by an ambiguous-entity-name refusal — a wrong=0 defense,
  since `Entity.name` is non-unique in the model (only `entity_id` is enforced).
- `content_hash` retained for provenance + replay_hash; `vault_index` pinned to the
  live deque position.

Design adversarially verified (docs/analysis/REALIZE-R1-DETERMINE-scope-2026-06-06.md);
the false "established pattern" private-access comment is removed in favor of the
public accessor. wrong=0 + versor_condition<1e-6 + exact CGA recall preserved;
vault/store.py adds only a read-only accessor (no normalization). Green: 23 realize
+ 110 invariant/vault + 90 smoke; ruff check clean.
2026-06-06 05:52:49 -07:00
Shay
2a9285817d fix: derive oov grounding from token content 2026-06-06 05:46:01 -07:00
Shay
16ba2771bb
Merge pull request #590 from AssetOverflow/feat/realize-r0
feat(realize): R0 — one told fact becomes a SPECULATIVE, reboot-stable vault entry
2026-06-06 02:39:35 -07:00
Shay
43fcd08ce8 feat(realize): R0 — one told fact becomes a SPECULATIVE, reboot-stable vault entry
REALIZE roadmap Step 3, slice R0: the boundary that turns comprehension from an
EVAL ARTIFACT into accumulating living knowledge. A comprehended declarative fact is
integrated into the held self as a structured vault entry (versor, metadata) — NOT a
new store — so it inherits exact cga_inner recall, EpistemicStatus stamping, and
bit-exact Shape B+ persistence for free. Scope: docs/analysis/REALIZE-scope-2026-06-06.md.

generate/realize/realize.py — realize_comprehension(Comprehension|Refusal, ctx) ->
Realized(record, created) | NotRealized(reason):
  - eligibility: a Comprehension (not Refusal) with NO queries and EXACTLY ONE
    non-negated relation whose subject grounds IN-VOCABULARY. Everything else is a
    typed NotRealized with zero vault writes (wrong=0).
  - in-vocab-subject only: OOV grounding is non-deterministic across reboots (an
    empirically-confirmed substrate gap), so OOV subjects are declined in R0.
  - SPECULATIVE always (COHERENT is never a default — ADR-0021); provenance via
    MeaningSpan source_span + structure_canonical; content_hash + replay_hash via
    canonical-JSON SHA-256 (floats forbidden).
  - idempotency: dedup by content_hash within the session (exact-canonical,
    span-inclusive — safe direction only, never drops a distinct fact).
  - durable schema admits a 2nd substrate later via structure_kind/structure_canonical.
  - stores via the existing VaultStore.store path: no new embedder, no normalization
    (closure stays algebra/versor.py), no parallel learning path.

Explicitly OUT of R0: COHERENT promotion, teaching-loop proposals, trace-folding,
relation-space recall, the arithmetic/binding_graph path.

tests/test_realize_r0.py (10) — eligibility (refusal/query/multi-relation/OOV all
write nothing); record fields; idempotency (re-told fact doesn't grow the vault);
SPECULATIVE status firewall (recall(min_status=COHERENT) excludes it); the
falsifiable exit gate (told -> realize -> snapshot -> reboot -> recall: byte-exact
content_hash + score + versor bytes, speculative, versor_condition<1e-6); and
replay_hash re-derivable after reboot. Gate verified to BITE (COHERENT default,
refusal-writes, reprojection-on-load all fail it).

INV-21: generate/realize/realize.py added to ALLOWED_VAULT_WRITERS — a sanctioned
writer (same VaultStore.store path, SPECULATIVE default, nothing on Refusal).

Adversarially reviewed (3 lenses: invariants/wrong0/honesty, determinism/boundary,
scope/adjustments) — no critical/high/medium findings; low/nit honesty wrinkles
folded (state-dependent-placement wording, span-inclusive-dedup note, direct
versor-bytes gate assert, replay_hash re-derivation test). 10 R0 + 90 smoke green;
lane SHAs 8/9 (sole miss = public_demo env flake; deductive_logic + math_teaching
unchanged -> no GSM8K coupling).
2026-06-06 02:33:48 -07:00
Shay
f88b9971d2
Merge pull request #589 from AssetOverflow/docs/realize-scope
docs(analysis): REALIZE phase scope (Step 3) — verified against the real substrate
2026-06-06 02:29:46 -07:00
Shay
255d3bd055 docs(analysis): REALIZE phase scope (Step 3) — verified against the real substrate
Scopes REALIZE (comprehended/told structure -> the held self with an
EpistemicStatus, persisted via Shape B+, recalled exactly) — roadmap Step 3, the
boundary that turns comprehension from an eval artifact into accumulating living
knowledge; intake ("being told") lands here.

Produced by a reconnaissance -> design -> adversarial-verification pass over the
real substrate (vault, EpistemicStatus, Shape B+ persistence, teaching/intake,
comprehension output, determinism/replay). Central finding: a realized record is a
structured vault entry (versor, metadata), NOT a new store — so it inherits exact
cga_inner recall, EpistemicStatus stamping (teaching/epistemic.py already exists),
and bit-exact snapshot/restore for free.

Defines the realized-knowledge record, the told->realize->recall->survives-reboot
contract, the composition seams (verified APIs), the obligations (wrong=0,
provenance, EpistemicStatus honesty, no forbidden normalization, no parallel
learning path, determinism), slice R0 + a falsifiable exit gate, and the design
forks/risks/open-questions.

Adversarially corrected: R0 must catch embedding KeyError/empty-decomposition ->
no-op (+ OOV grounding determinism); determinism is state-restore-based not
input-pure (versor/score are session-state-dependent); MeaningSpan (not
SourceSpanLink) is the provenance API; sharpened gate arms (history-dependence +
idempotency/placement re-run).
2026-06-06 01:48:19 -07:00
Shay
7feb940b9a
Merge pull request #588 from AssetOverflow/feat/comprehension-arithmetic
feat(comprehend): arithmetic word-problems via binding_graph (5th domain, real admissibility)
2026-06-06 01:32:02 -07:00
Shay
a005a92fed feat(comprehend): arithmetic word-problems via binding_graph (5th domain, real admissibility)
The binding-graph's FIRST comprehension consumer (doctrine-aligned: quantities live
in binding_graph, NOT the MeaningGraph). generate/quantitative_comprehension.py
reads arithmetic prose into SymbolBinding/BoundFact/BoundEquation and runs the REAL
check_admissibility (shell -> verify -> rebuild with the actual UnitProof) — there
is NO stamped "admitted": an equation is admitted only if its operand units verify.
Then to_relational_metric projects the binding-graph to the independent
relational_metric oracle for the verdict.

Templates (digits only; non-digit quantity REFUSES):
  "<X> has <N> <unit>"                 -> BoundFact(X = N)
  "<Y> has <N> more <unit> than <X>"   -> BoundEquation(Y = X + N)  op=add
  "<Y> has <N> fewer <unit> than <X>"  -> BoundEquation(Y = X - N)  op=subtract
  "How many <unit> does <Y> have"      -> ask Y
  "How many <unit> do <X> and <Y> have"-> total = X + Y; ask total

Unit modelling (honest, not faked): a noun the closed en_units_v1 pack knows is
used verbatim (dollars -> dollar/money); an UNKNOWN sortal noun (stickers, coins)
is a count of discrete objects -> the existing 'item' lemma (dimension count). So
admissibility stays a REAL check: count+count admits, count+money (a mixed-unit
sum) REFUSES with unit_mismatch — verified to bite.

comprehension_relational_metric: 15/15 wrong=0 (full coverage). Located OUTSIDE
generate/meaning_graph (it targets binding_graph, not the MeaningGraph) so INV-28
neutrality stays intact; oracle imports none of the SUT (new INV-25 lane).
Capability index breadth 7->8, score 0.928622 -> 0.937258, wrong_total 0, digest
50e0675b…

Tests: reader templates + count/known-unit modelling + admissibility-bite (mixed
unit refuses) + non-digit refusal; end-to-end full-coverage wrong=0; arithmetic
added to the structure-preservation generative panel (projected relations+query ==
ground truth); capability breadth 7->8; INV-25 arithmetic lane. 93 targeted + 90
smoke green; lane SHAs 8/9 (sole miss = public_demo env flake; deductive_logic +
math_teaching unchanged -> no GSM8K coupling).
2026-06-06 00:43:16 -07:00
Shay
ed4c8bec2b
Merge pull request #587 from AssetOverflow/feat/comprehension-inv-firewall
test(inv): firewall comprehension lanes (INV-25) + MeaningGraph neutrality (INV-28)
2026-06-06 00:11:20 -07:00
Shay
dd233844fc
Merge pull request #586 from AssetOverflow/feat/comprehension-structural-gold
test(comprehend): structure-preservation + perturbation invariance
2026-06-06 00:08:33 -07:00
Shay
8bc227cd88 test(inv): firewall the comprehension lanes (INV-25) + MeaningGraph neutrality (INV-28)
Addresses the review's hygiene point: the comprehension lanes deserve the same
repo-wide independent-gold + neutrality discipline as the older reasoning lanes.

INV-25 (independent gold): register the four comprehension lanes
(set_membership / syllogism / total_ordering / propositional) so their domain
oracles are statically proven to import NONE of the comprehension organ
(generate.meaning_graph reader + projectors). The gold the reader is scored
against is thereby independent of the reader — the anti-overfit firewall now covers
comprehension, not just the structured-input lanes. All four oracles are clean
(stdlib-only), so the firewall passes.

INV-28 (MeaningGraph neutrality): a new repo-wide invariant mirroring INV-26's
binding-graph neutrality — no generate/meaning_graph module imports
field/algebra/evals/vault/chat/core/sensorium or numpy. The MeaningGraph is the new
neutral meeting point where prose-read structure projects into independent oracles;
it earns the same firewall. numpy is forbidden too: Path β reads structure
SYMBOLICALLY and quantities are the binding-graph's domain, not the MeaningGraph's.
Non-vacuity test included (the predicate flags a module known to import the field
engine).

Tests-only. 56 architectural-invariant tests pass (was 53 + 3 new INV-28 +
4 INV-25 lanes wired into the existing oracle-firewall test).
2026-06-06 00:07:35 -07:00
Shay
f63e790ab0 test(comprehend): structure-preservation + perturbation invariance (close coincidental-correctness)
Addresses the central review finding: the generative wrong=0 tests compared oracle
VERDICTS, so a misread graph that coincidentally yields the same verdict passed
silently (coincidental correctness in cleaner clothes). This adds the conjugate
check — the reader must recover the EXACT structure the prose encodes, not merely a
verdict-equivalent one.

tests/test_comprehension_structure_preserving.py:
  - Structure preservation (all 4 domains): over randomly generated structures
    rendered to prose that FULLY determines them, assert projected structure AND
    query == ground truth exactly (order-insensitive canonicalization), or refuse.
    Empirically this is strictly stronger: under a subject<->predicate premise swap,
    361/400 reads are structurally wrong and 307 of those (85%) coincide in verdict
    — the answer test misses all 307; the structure test catches all 361.
  - Perturbation invariance: meaning-preserving surface changes (premise/clause
    reordering, capitalization, extra whitespace) yield the SAME structure.

The existing answer-preservation property tests stay (verdict agreement is still a
valid, separate check — exactly the "assert structure, then separately assert
oracle agreement" the review recommends). Tests-only; no source change; capability
index unchanged. 7 new + 86 comprehension/capability targeted green.
2026-06-05 23:59:20 -07:00
Shay
5cc7a78e1e
Merge pull request #585 from AssetOverflow/docs/arithmetic-comprehension-brief
docs(handoff): brief — arithmetic comprehension via binding_graph (5th domain)
2026-06-05 23:56:02 -07:00
Shay
7663073e5e docs(handoff): brief — arithmetic comprehension via binding_graph (5th domain)
Execution-ready brief for the next comprehension increment (user-chosen,
doctrine-aligned): read arithmetic word-problem prose into the binding_graph
quantity substrate -> project to the independent relational_metric oracle ->
wrong=0. Reuses the existing 15-case relational_metric gold lane (no gold
authoring).

Captures the full scope from this session: the oracle grammar (fact/more_than/
fewer_than/sum_of), the new number-parsing reader capability, binding_graph
construction (SymbolBinding/BoundFact/BoundEquation with REAL admissibility — a
proof obligation, not a stamp), the direct-construction recommendation (keep the
comprehension organ disjoint from the GSM8K MathProblemGraph / serving path), the
projector, wiring, the generative round-trip anti-overfit test, and the
validation gates.

Briefed rather than rushed: this is binding_graph's first comprehension consumer,
a load-bearing integration whose correctness hinges on real equation
admissibility — it deserves fresh context, per "no shortcuts on architectural
gaps".
2026-06-05 23:46:57 -07:00
Shay
6b12cdd386
Merge pull request #584 from AssetOverflow/feat/comprehension-propositional
feat(comprehend): propositional-logic comprehension (4th domain, flagship oracle)
2026-06-05 23:43:15 -07:00
Shay
f66f2ee47f feat(comprehend): propositional-logic comprehension (4th domain, flagship oracle)
Adds comprehension_propositional — the comprehension organ now reads the classic
propositional ARGUMENT FORMS end-to-end into the flagship deductive_logic ROBDD
oracle (the most robustly independent gold in the repo). The neutral MeaningGraph
now feeds FOUR independent oracles (set-membership, syllogism-validity,
total-ordering, propositional-entailment) from one interlingua — the Option-B
interlingua thesis validated.

reader.py: propositional templates (atoms are chunked NP ids; fits the existing
entities + n-ary relations + negation model — NO interlingua change, propositional
is not arithmetic-quantities):
  - "if <P> then <Q>"        -> implies(P, Q)
  - "not <P>"                -> asserted(P, negated=True)
  - "<P> or <Q>"             -> or(P, Q)
  - "<P>" (single token)     -> asserted(P)   (bare-atom, single-token only to
                                                keep the parse-or-refuse floor)
  - "therefore <prop>"       -> query of the same predicate
Relations now carry a negated flag end-to-end (asserted negation).

projectors.py: to_deductive_logic serializes propositional relations/query into
formula strings (keyword operators the oracle tokenizer accepts); returns None
(refusal) unless the comprehension is purely propositional, so categorical/ordering
comprehensions never leak into the entailment oracle.

evals: new evals/propositional_logic/v1 (12 cases — modus ponens/tollens,
hypothetical & disjunctive syllogism, the affirming-consequent / denying-antecedent
fallacies which the oracle marks "unknown"; gold = oracle verdict) + gold-only
runner + evals/comprehension/propositional_runner.py. Oracle "refused" (formula
unevaluable) is treated as a decline, never a wrong.

Scores: comprehension_propositional 12/12 wrong=0 (full coverage); no regression on
the 3 existing lanes (8/8, 7/8, 7/8). Capability index breadth 6->7, score
0.917231 -> 0.928622, wrong_total 0, digest 51df7bba…

Tests: reader propositional templates; to_deductive_logic projector tests;
end-to-end full-coverage wrong=0; propositional generative round-trip added to the
wrong=0 property suite (verified to BITE under a reversed-implies mutation);
capability breadth 6->7. 115 targeted + 87 smoke green. Lane SHAs 8/9 (sole miss =
public_demo env wall-clock flake; deductive_logic_v1 unchanged).
2026-06-05 23:24:54 -07:00
Shay
07f1c235a9
Merge pull request #583 from AssetOverflow/feat/comprehension-multiword-chunking
feat(comprehend): multi-word NP chunking under a canonicalization contract
2026-06-05 22:54:39 -07:00
Shay
a733fc5737 feat(comprehend): multi-word NP chunking under a canonicalization contract
Recovers the multi-word-NP cases the reader previously refused, by adopting ONE
principled canonicalization contract (evals/comprehension/CANONICALIZATION.md) that
the reader AND the gold lanes both follow — so a committed answer can only match
gold or refuse, never silently mean something else.

Contract: a noun-phrase slot -> tokens lowercased, joined with "_"; a plural class
slot singularizes its head first ("metal objects"->"metal_object",
"North station"->"north_station", "Level one"->"level_one"). JOIN is chosen over
head-word-only ("metal objects"->"metal") because head-word-only is
information-destroying — it collapses "metal objects" and "metal tools" into one
false identity, itself a wrong=0 hazard.

reader.py: slot-based templates chunk multi-token NPs (_chunk / _chunk_class
replace the single-token _one / _one_class). Reserved-function-word guard fires only
INSIDE a multi-token slot (a lone "A" item is content, not the article). Still
parse-or-refuse: reserved-word leaks ("Compare beta with beta in the same order"),
non-pluralizable class heads (adjectival "trained"), and the ambiguous adjacent
two-NP subset query ("Are all <Xs> <Ys>?") all REFUSE.

gold (the contract update, logic-preserving — only term NAMES change):
  - sy-v1-0008: metal/soft -> metal_object/soft_object (was head-word-only)
  - to-v1-0005: red -> red_rank (was head-word-only)
  - to-v1-0004: prose made internally consistent ("is after", "north station") +
    north -> north_station (original prose used "North station" in the fact but
    "north" in the query — a latent inconsistency)
  - to-v1-0007: already conformed (level_one…), no change
Gold-only integrity runners stay 8/8 both lanes (structure+query+gold consistent).

Scores: set_membership 8/8, syllogism 6/8->7/8, total_ordering 4/8->7/8, all
wrong=0. Capability index re-frozen: score 0.814356 -> 0.917231, breadth 6,
wrong_total 0, digest 13d7db6c…

Tests: reader chunking + refusal tests; multi-word generative round-trip added to
the wrong=0 property suite (verified to BITE under a head-word-only mutation —
collapsed ids produce a wrong verdict the test catches); pinned counts updated.
100 comprehension/capability targeted + 87 smoke green.
2026-06-05 22:47:34 -07:00
Shay
14fa922116
Merge pull request #582 from AssetOverflow/feat/comprehension-organ-three-domains
feat(comprehend): complete 3-domain comprehension organ (syllogism + total_ordering)
2026-06-05 22:46:49 -07:00
Shay
d1908ec4c8 test(comprehend): generative wrong=0 hardening — reader is faithful or refuses
Proves the wrong=0 invariant GENERALIZES beyond the 8-case gold lanes. Over 1000
randomly generated single-word problems across all three domains, the reader
either refuses or reproduces the EXACT verdict the independent oracle gives on the
ground-truth structure the prose encodes — it never changes the answer.

Non-circular: the generated structure S is ground truth; we render prose P(S),
then compare oracle(project(comprehend(P(S)))) to oracle(S) computed directly.
The oracle is independent of the reader, so agreement = lossless carry and refusal
= honest decline; both are wrong=0. Verified to BITE: flipping a comparator
direction makes the total_ordering case produce a reversed sort the test catches.

Single-word vocab on purpose — multi-word NPs are the known gold-canonicalization
wall the reader refuses; the generator stays in the readable regime where
faithfulness is the whole claim. Anti-overfit: random vocab proves the templates
key on function words + order, not on memorized gold content.
2026-06-05 21:22:13 -07:00
Shay
e831ed2615 feat(comprehend): complete 3-domain comprehension organ (syllogism + total_ordering)
Phase 2a r2/r3/r4 of the redefined plan: the general comprehension reader now
reads THREE independent-gold reasoning domains end-to-end (prose -> MeaningGraph
-> projection -> independent oracle -> answer vs gold), all wrong=0, and all
three are wired into the capability index.

reader.py — new domain-agnostic templates (function words + order; parse-or-refuse):
  - categorical E/I/O: "no Xs are Ys"->disjoint, "some Xs are Ys"->intersects,
    "some Xs are not Ys"->some_not  (A "all Xs are Ys"->subset already existed)
  - "therefore <categorical>" -> conclusion QUERY (same neutral predicate vocab)
  - comparative facts: "<X> [is] <comp> [than] <Y>" -> less(...), closed
    less/greater comparator lexicon, elided-copula support
  - sort query ("sort ascending|descending", "... order from <low> to <high>")
    and compare query ("compare <X> with <Y>")
  - clause-splitting on commas / leading and|or for multi-clause sentences

projectors.py — to_syllogism (premises + validity conclusion, finite-model size 3)
  and to_total_ordering (less-facts + sort/compare). Both return None when nothing
  is honestly askable of their oracle (caller treats as refusal).

capability_index — wire 3 comprehension lanes into ADAPTERS; re-freeze baseline
  breadth 3->6, capability_score 0.919641->0.814356 (geomean falls BY DESIGN as
  honest partial-coverage domains join; wrong_total stays 0). digest 0a98b9b4...

Scores: set_membership 8/8, syllogism 6/8, total_ordering 4/8 — all wrong=0.

Multi-word NP handling is DEFERRED on purpose, not missed: the gold lanes
canonicalize multi-word NPs three contradictory ways ("North station"->"north",
"Level one"->"level_one", "metal objects"->"metal"), so no single general rule is
wrong=0-safe. The reader refuses multi-word NPs until the gold lanes carry a
canonicalization contract. Every refusal is a genuine harder phenomenon
(multi-word NP, adjectival predicate, trailing tokens) — never a readable case
silently dropped.

Tests: reader templates, projector unit tests, syllogism/total_ordering
end-to-end wrong=0 with pinned counts, capability breadth 3->6. 138 targeted +
87 smoke green. Lane SHAs 8/9 (sole miss = public_demo env wall-clock flake).
2026-06-05 21:02:43 -07:00
Shay
32f7441d28
Merge pull request #581 from AssetOverflow/feat/phase2a-comprehension-reader
feat(comprehend): general comprehension reader + set_membership end-to-end (Phase 2a)
2026-06-05 16:48:31 -07:00
Shay
96b9b942e6 feat(comprehend): general comprehension reader + set_membership end-to-end (Phase 2a-r1/r2)
Disciplined Path β (field decode α was empirically falsified). Reads S-P-O
structure SYMBOLICALLY from the token sequence via domain-agnostic templates
keyed on FUNCTION WORDS + ORDER, mints content as MeaningGraph entities/relations,
parse-or-refuse (wrong=0 at the comprehension layer).

Templates (set_membership): 'X is a Y' -> member; 'all Xs are Ys' -> subset;
'is X a Y?' / 'are all Xs Ys?' -> queries; definite-NP ('the X is a Y');
conservative singularization incl. irregulars (people->person), refusing
unknown morphology rather than guessing.

End-to-end: prose -> comprehend -> project -> INDEPENDENT set_membership oracle
-> answer vs gold. Result on the real v1 lane: 8 correct / 0 wrong / 0 refused
(full coverage, wrong=0). Cross-content generality (animals/professions/geography)
asserted. 14 reader unit tests + 3 end-to-end tests, each bites under its
violation. Additive only; invariants + capability index green.
2026-06-05 16:32:34 -07:00
Shay
50ba1183b2
Merge pull request #580 from AssetOverflow/evals/capability-baseline-freeze
evals: freeze capability-index baseline + digest regression guard
2026-06-05 16:23:52 -07:00
Shay
d47745dfe7
Merge pull request #579 from AssetOverflow/evals/staged-gold-lanes
evals: staged independent-gold lanes (set-membership/ordering/syllogism) for Phase 2 cross-domain proof
2026-06-05 16:16:04 -07:00
Shay
2fc1d73a8f evals: freeze capability index baseline 2026-06-05 16:14:30 -07:00
Shay
4c4e15bb9b
Merge pull request #578 from AssetOverflow/audit/l10-l11-lookback
audit(l10-l11): lookback review of the lived spine #567–#573
2026-06-05 16:08:18 -07:00
Shay
96cb5b34bc evals: add staged independent gold lanes 2026-06-05 16:03:26 -07:00
Shay
b39a6077ec
Merge pull request #577 from AssetOverflow/feat/phase2a-meaning-graph
feat(comprehend): MeaningGraph — neutral general-meaning interlingua (Phase 2a)
2026-06-05 16:00:32 -07:00
Shay
31cb741005 audit(l10-l11): lookback review lived spine 2026-06-05 15:52:40 -07:00
Shay
4c9f5a44e0
Merge pull request #576 from AssetOverflow/docs/phase2-comprehension-scope
docs(analysis): Phase 2 general-comprehension organ scope
2026-06-05 15:50:39 -07:00
Shay
733d4df366 feat(comprehend): MeaningGraph — neutral general-meaning interlingua (Phase 2a)
The refusal-first, provenance-carrying structure the field-decode produces and
the domain reasoners project from. Sibling of the binding-graph (ADR-0132) but
carries GENERAL meaning (entities + n-ary named relations), neutral to the
engine substrate (no algebra/field/numpy import), and imposes NO acyclicity
(relation cycles are well-formed, unlike the equation DAG).

Refusal-first construction: non-identifier ids, empty predicates, zero-arity
relations, duplicate entity ids, and relations referencing unknown entities all
refuse at construction. Deterministic to_canonical_string for replay/hashing.
Polarity (negated) is first-class. kind/predicate carry no closed vocab yet
(defer-substrate-vocab).

Phase 2a foundation under Path alpha (field standing-hand + refusal floor); the
field-decode -> refusal-floor -> MeaningGraph reader is the next increment.

18 new tests (each bites under its named violation); architectural invariants +
capability index green.
2026-06-05 15:45:59 -07:00
Shay
7bccca9012 docs(analysis): Phase 2 spike — syntax pack is not a parser; structural-decode fork
Spike falsified §5's assumption: en_core_syntax_v1 is a 24-entry lexicon of
grammatical terminology, not a parser. No general structural parser exists.
The field's Proposition already decodes S-P-O but FrameRegistry.select never
refuses (confabulation hazard). New load-bearing fork: Path alpha (field
standing-hand + refusal floor) vs beta (build a minimal parser). Recommend
alpha. Updated section 5 and section 9.
2026-06-05 15:41:36 -07:00
Shay
d97be49ffa docs(analysis): Phase 2 general-comprehension organ scope
Scope-before-build for the AGI-roadmap make-or-break phase (COMPREHEND).
Honest substrate map: the reasoners (binding-graph/proof_chain/grammars) and
the articulation path (PropositionGraph) are built, but no organ turns general
NL into reasoning-ready structure. Recommends Option B (a general MeaningGraph
the existing structures project into), syntax-keyed parse-or-refuse reader,
cross-domain proof on the Phase-1 yardstick, and the overfit-trap guardrails.
No code.
2026-06-05 15:36:51 -07:00
Shay
ea503f6d51
Merge pull request #575 from AssetOverflow/feat/capability-index
feat(evals): AGI-roadmap Phase 1 — cross-domain capability index (MEASURE yardstick)
2026-06-05 15:29:44 -07:00
Shay
a4209c24a3
Merge pull request #574 from AssetOverflow/docs/agi-roadmap-and-l10-l11-design
docs(design): L10/L11 lived-spine design records + AGI-candidacy roadmap
2026-06-05 15:20:29 -07:00
Shay
514c6c52ca feat(evals): AGI-roadmap Phase 1 — cross-domain capability index (the MEASURE yardstick)
The instrument that gates every later "more capable" claim and makes "general,
not narrow" a number. evals/capability_index/ composes the self-loading
independent-gold reasoning lanes (deductive_logic, dimensional, relational_metric)
into one report with honest, un-gameable axes:

- accuracy (of committed answers; wrong stays 0 in assert mode),
- coverage (attempted-not-refused),
- coverage_geomean — the headline: geometric mean of per-domain coverage, which is
  0 if ANY domain has zero coverage, so a narrow per-domain win cannot move it; it
  rises only when breadth rises,
- capability_score = coverage_geomean × accuracy, HARD-GATED to 0 if any domain
  committed a wrong answer (assert-mode invariant),
- a deterministic digest (the replayable baseline the autonomous loop must climb).

Baseline (today): score 0.9196, accuracy 1.0, breadth 3, wrong_total 0 — high
because all three composed lanes are formal/structured; when comprehension-gated
NL domains join, the geomean will honestly drop to expose the breadth gap (the
instrument working). Adapters surface any lane that fails to run as not_covered —
no silent drop (proven: it caught a deductive-report shape mismatch mid-build).

Pure aggregation + the geomean anti-gaming property + the wrong=0 hard gate are
unit-tested; a real-composition integration test asserts wrong=0 + breadth=3.
10 tests + 52 architectural invariants pass. Additive (new evals/ package).
Part of docs/analysis/AGI-candidacy-autonomous-improvement-roadmap-2026-06-05.md (Phase 1).
2026-06-05 15:17:46 -07:00
Shay
afbcc8e41d docs(design): L10/L11 lived-spine design records + AGI-candidacy roadmap
Commits the design/analysis artifacts produced while building the lived spine and
planning the next arc (the work itself already merged in #563-#573):

- AGI-candidacy-autonomous-improvement-roadmap-2026-06-05.md — the path from the
  lived spine to AGI-candidacy: the comprehend→realize→determine→learn loop, the
  cross-domain capability+calibration yardstick, the logical-necessity × technical-
  priority execution order, and the corrected epistemic foundation (grounded
  honesty designed-in; estimation learned/ratified; confidence always evidence-
  grounded; intake first-class — NOT "no ingestion"; calibration+grounding the
  measured invariant).
- L10-runtime-scoping + L10-continuity-spike-design — the L10 decision surface and
  the falsifiable spike spec (P1-P5) that became evals/l10_continuity/.
- L10-shapeBplus-persistence-scope — the A->E scope that became Shape B+ resume.

.gitignore: ignore the local .system-map/ navigation index (per-developer, never
tracked; regenerated on demand).
2026-06-05 15:11:08 -07:00
Shay
81f791e2ab
Merge pull request #573 from AssetOverflow/feat/l11-continuous-learning
feat(learning): continuous learning in idle — idle_tick advances the reviewed-learning flywheel
2026-06-05 14:18:22 -07:00
Shay
1c5bd19a11 feat(learning): continuous learning in idle — idle_tick advances the reviewed-learning flywheel
The second lived-spine half: the engine learns WHILE IT LIVES, not only when
prompted. ChatRuntime.idle_tick() advances the contemplation/proposal flywheel
between turns (no user input):

- contemplates the pending discovery backlog (enrichment), then runs the
  replay-gated propose_from_candidate into a persistent, file-backed ProposalLog
  (engine_state/proposals.jsonl) held on the runtime.
- PROPOSAL-ONLY: it never ratifies. Raw cold-start candidates are 'undetermined'
  and the eligibility gate refuses them outright (the engine won't propose what
  it hasn't determined). A determined candidate only reaches 'pending' — moving
  to 'accepted'/corpus-append stays HITL via teaching/review. No idle tick emits
  an accepted / accepted_corpus_append event.
- The proposal log and the candidate backlog both live in the engine-state dir,
  so idle learning persists across reboot and accumulates (CL-2) — building on
  Shape B+ resume + L11 identity continuity.

idle_tick returns IdleTickResult(candidates_contemplated, proposals_created,
pending_proposals). None proposal log under no_load_state (ephemeral runtimes
keep no learning lineage).

5 dedicated tests: no-op on empty backlog, contemplates the backlog, refuses
undetermined (safety), proposes-a-determined-candidate-but-never-ratifies,
idle-learning-persists-across-reboot.
2026-06-05 14:05:52 -07:00
Shay
0986e9461b
Merge pull request #572 from AssetOverflow/feat/l11-identity-continuity
feat(identity): L11 identity continuity — same identity across reboot, not just same bytes
2026-06-05 14:05:48 -07:00
Shay
f2dac1dc5c feat(identity): L11 identity continuity — same identity across reboot, not just same bytes
Builds the L11 lived-spine half on top of Shape B+ T-resume: prove the
continuous/resumed life is the SAME identity, with a content-derived, hash-chained
lineage and a falsifiable behavioral proof.

- core/engine_identity.py (L11-1): EngineIdentity = sha256 of the ratified
  PERSONALITY substrate (identity/safety/ethics/register/anchor-lens pack files)
  + code revision. Content-derived, NOT entropy — same substrate => same identity
  (cross-engine portable). The "who am I" hash; bumped by a ratified identity
  change, NOT by lived learning (that is experience, carried by Shape B+).
- engine_state + chat/runtime (L11-2): every checkpoint manifest stamps
  engine_identity + parent_engine_identity (git-like lineage). Stable substrate
  => identity == parent (one continuous life); a ratified change => the bump.
- chat/runtime + config (L11-3): on reboot, recompute identity and compare to the
  stamped one. Mismatch (substrate changed while down) surfaces a warning +
  identity_continuity_break flag; strict_identity_continuity (opt-in) refuses
  (IdentityContinuityError). Default warns — reboot is recovery, not control flow
  (ADR-0157); the operator must not be bricked by a benign ratified pack swap.
- tests (L11-4): the proof. Continuity is SUFFICIENT (byte-identical resume +
  no break under a fixed identity), identity is LOAD-BEARING (distinct packs =>
  distinct hashes), and the CONTRAPOSITIVE holds (resuming under a different
  identity raises the break). Same identity <=> continuous; different => break.

Test hygiene (required by L11's always-on identity stamping): conftest isolates
the default engine_state dir per test; the refusal-calibration cold-start probe
uses no_load_state=True. Both prevent cross-test identity-lineage pollution.

19 dedicated tests; curated smoke green (no spurious break warnings).
2026-06-05 13:52:57 -07:00
Shay
49457a7bcc
Merge pull request #571 from AssetOverflow/feat/l10-shapeBplus-phaseD
feat(persistence): Shape B+ Phase D+E — reboot is transparent (resume-as-same-life)
2026-06-05 13:25:48 -07:00
Shay
5ed9fbb8e7 feat(persistence): Shape B+ Phase D+E — opt-in lived-state persistence; reboot transparent
The load-bearing L10 milestone: with resume mode enabled, a reboot resumes the
SAME life. Wires SessionContext.snapshot/restore (Phases A-C) into the
engine-state checkpoint and flips the L10 spike's P2b oracle to transparent.

Persistence is OPT-IN (RuntimeConfig.persist_session_state, default False): it is
a deliberate always-on-runtime mode, and per-turn snapshotting has an O(turns)
cost, so demos / evals / one-shot runtimes do NOT pay for resume they don't use.
This keeps every existing ChatRuntime byte-for-byte unchanged (no perf tax, no
pinned-lane SHA drift, no test breakage); only the L10 continuity lane and the
production L10 process enable it.

Phase D (wiring):
- core/config.py: persist_session_state flag (default False).
- engine_state/__init__.py: bump _SCHEMA_VERSION 1->2; add save_session_state /
  load_session_state (atomic, ADR-0156). v1 checkpoints still load (1 <= 2) with
  no session_state -> fresh session.
- chat/runtime.py: when persist_session_state, checkpoint_engine_state saves the
  session snapshot BEFORE the manifest (manifest = the commit marker / WAL force
  boundary); _load_engine_state restores it into self._context.

Phase E (flip the oracle):
- evals/l10_continuity/runner.py: the continuity lane forces persist on (it IS
  the resume-mode lane).
- tests/test_l10_continuity.py: test_p2b_documents_current_resume_gap ->
  test_p2b_reboot_is_transparent (asserts post_reboot_transparent, divergence
  None). predicates.py / runner.py / contract.md: P2b is now the
  resume-as-same-life guard.
- tests/test_adr_0146_engine_state.py: manifest schema_version 1 -> 2.

Validation: full spike (P1 closure, P2a determinism, P2b NOW TRANSPARENT, P3
bounded, P4 crash-recovery determinism + commit point, P5b/P5c) +
reboot-restores-lived-state + v1 back-compat + ADR-0146, all green;
[run K -> reboot -> run M] byte-identical to [run K+M]. With persistence off
(default), the curated smoke + showcase budget + pinned lanes are unchanged.

Closes the A->E Shape B+ scope (docs/analysis/L10-shapeBplus-persistence-scope-2026-06-05.md).
2026-06-05 13:17:30 -07:00
Shay
285c97336d
Merge pull request #570 from AssetOverflow/feat/l10-shapeBplus-phaseC
feat(persistence): Shape B+ Phase C — SessionContext.snapshot/restore (full lived state)
2026-06-05 12:25:40 -07:00
Shay
5feedcebd9 feat(persistence): Shape B+ Phase C — SessionContext.snapshot/restore (full lived state)
Composes the FieldState (A) and VaultStore (B) codecs with new codecs for
SessionGraph/TurnNode, ReferentRegistry/ReferentEntry, Proposition, and
DialogueTurn into SessionContext.snapshot()/restore() — the complete lived
session state that must survive reboot for resume-as-same-life.

- session/graph.py: TurnNode + SessionGraph to_dict/from_dict (versors bit-exact).
- session/referents.py: ReferentEntry + ReferentRegistry, preserving the
  _slots<->_history object aliasing via slot->history-index (update_turn_versor
  relies on `is` identity).
- generate/proposition.py + generate/dialogue.py: Proposition + DialogueTurn
  codecs (relation_norm is derived in __post_init__, not persisted).
- vault/store.py: complete the metadata codec — vault metadata can hold a
  Proposition ({"kind":"proposition",...} from generate/proposition.py), tagged
  on encode and reconstructed on decode (lazy import, cycle-free). This closes a
  gap Phase B assumed away ("metadata is primitives only"); surfaced by the
  Phase C JSON-safe integration test.
- session/context.py: snapshot()/restore(). vocab/persona are NOT serialized
  (shared, supplied at restore); restore() mutates self by design (a load).

Exit gate: a real 4-turn session, snapshotted and restored into a fresh context,
is field-equal — field bit-exact, vault recall identical, graph/referents/
dialogue preserved (incl. the referent aliasing). 9 new tests; INV-02 +
session-coherence regression green (68 passed).

Part of the A->E Shape B+ scope (Phase C).
2026-06-05 12:13:46 -07:00
Shay
9d7c2420c3
Merge pull request #569 from AssetOverflow/feat/l10-shapeBplus-phaseB
feat(persistence): Shape B+ Phase B — VaultStore (de)serialize, exact recall preserved
2026-06-05 12:01:20 -07:00
Shay
c0aebf142c feat(persistence): Shape B+ Phase B — VaultStore (de)serialize, exact recall preserved
Adds VaultStore.to_dict/from_dict on top of Phase A's array codec. Persists the
versors (bit-exact via the codec), metadata, store_count, reproject_interval,
and max_entries; rebuilds the derived _exact_index on load and leaves the lazy
_matrix_cache None.

Bright line (vault/store.py is a CLAUDE.md forbidden normalization site): the
load path performs NO reprojection / normalization / repair — it restores the
exact persisted bytes (already null-projected at their last live reproject
boundary) and rebuilds only the pure index. Proven by a test asserting the
restored versors are BIT-IDENTICAL to the originals (a reproject would change
them via null_project) and that exact CGA recall — including the score==inf
exact-match short-circuit — is identical after a save/load cycle.

5 new tests + INV-02 (normalize-not-called-outside-gate) + all vault tests pass
(116 passed). Part of the A->E Shape B+ scope (Phase B).
2026-06-05 11:49:58 -07:00
Shay
09a766f9f1
Merge pull request #568 from AssetOverflow/feat/l10-shapeBplus-phaseA
feat(persistence): Shape B+ Phase A — bit-exact array codec + FieldState (de)serialize
2026-06-05 11:49:40 -07:00
Shay
21e2ccb508 feat(persistence): Shape B+ Phase A — bit-exact array codec + FieldState (de)serialize
Foundation for L10 resume-as-same-life persistence. Adds:
- core/array_codec.py: a leaf (numpy+base64) codec encoding arrays as
  {dtype, shape, b64(raw bytes)} — BIT-EXACT, never decimal. Float round-trips
  lose zero precision, so a restored versor keeps versor_condition < 1e-6 and a
  replayed turn keeps its trace_hash. dtype carries byte order; float32 is never
  conflated with float64.
- field/state.py: FieldState.to_dict/from_dict. Multivector arrays (F, holonomy)
  go through the byte codec; energy/valence round-trip exactly via JSON-safe
  helpers (lazy physics imports keep field/ cycle-free).

Exit gate (the scope's #1 risk, de-risked first): bit-exact round-trip AND
closure preserved — versor_condition(restored.F) == versor_condition(fs.F)
exactly. 10 codec/FieldState tests + 55 architectural-invariant/runtime tests
pass. Purely additive; no existing behavior changed.

Part of docs/analysis/L10-shapeBplus-persistence-scope-2026-06-05.md (Phase A).
2026-06-05 11:38:58 -07:00
Shay
457d1ae94e
Merge pull request #567 from AssetOverflow/feat/l10-continuity-spike
feat(evals): L10 continuity spike — falsifiable long-horizon soak lane
2026-06-05 11:26:30 -07:00
Shay
23bc28caf9 feat(evals): L10 continuity spike — falsifiable long-horizon soak lane
Build evals/l10_continuity/, the empirical gate between the two L10 targets
(T-resume: provable same-life resume; T-experience: continuous experiencing
field-life). Drives the REAL turn loop (ChatRuntime + CognitiveTurnPipeline)
over a deterministic in-vocab corpus, with reboot and orphan-crash legs, and
evaluates falsifiable predicates over recorded evidence. Additive only; no
existing file touched; read-only over the runtime; no serving-path import.

Predicates (each with a *_holds real-soak test AND a *_bites mutation test, per
the CLAUDE.md schema-as-proof discipline):
- P1 closure: versor_condition < 1e-6 every turn (green guard).
- P2a determinism: two independent runtimes -> byte-identical trace_hash.
- P2b reboot transparency (the diagnostic): a reboot never alters pre-reboot
  turns (hard guard); post-reboot transparency is MEASURED and today FALSE --
  the mechanical proof that Shape B (ADR-0146) discards the lived field/vault,
  i.e. "many lives sharing a checkpoint". A pinned test flips if persistence is
  ever added, forcing a doc update so the gap can't close silently.
- P3 bounded resources: vault grows linear-bounded/monotonic (RSS recorded).
- P4 crash recovery: two recoveries from one checkpoint converge (determinism)
  + commit-point/ARIES force boundary (recovered turn_count == committed) +
  atomic-write survives mid-os.replace kill (ADR-0156).
- P5b anchor stability (T-experience crux): field anchors without COLLAPSE
  (dist_to_anchor not -> 0) or FREEZE (turn_movement not -> 0); the long-horizon
  test of the sanctioned _session_anchor_pull (alpha=0.05). Thresholds measured.
- P5c coherence: surfaces stay non-empty and not collapsed to one output, over
  more than one corpus cycle.
- P5a recall precision@k: recorded as not_covered (needs a held-out probe set).

report.py assembles the panel into a structured report with a hardware-stable
deterministic_digest (trace_hash sequence + verdicts; excludes RSS/wall-clock)
as the freeze handle. Run: python -m evals.l10_continuity [n_turns] [reboot_turn].

24 tests pass; adversarially reviewed across 4 lenses (bite-discipline,
invariant/trust-boundary, honesty/determinism, correctness) before landing.
2026-06-05 11:14:17 -07:00
Shay
2d6f9d7ac3
Merge pull request #566 from AssetOverflow/feat/l10-grounding-pack-evals-adr
feat(l10): add finite grounding pack and adversarial wrong=0 fixtures
2026-06-05 10:09:52 -07:00
Shay
bbf75ff195 docs(l10): reconcile grounding ADR to registry (ADR-0210) + flag gold consistency
Rename adr-012-l10-grounding.md -> ADR-0210-l10-grounding-pack.md: the
requested number collided with ADR-0012-core-ingest-governance-layer.md and
did not follow the ADR-NNNN convention. 0210 is the next free number.

Record the one latent soundness item: l10-adv-003 (false) vs l10-adv-008
(refuse) take different stances on an unsatisfied guard; the discriminating
principle (same-sort negative -> false; cross-sort mismatch -> refuse) must
be encoded by the future symbolic runner before the fixtures become a live
wrong=0 oracle. Pack + fixtures remain inert and byte-identical.
2026-06-05 10:01:17 -07:00
Shay
b1f35b1d6c
Merge pull request #565 from AssetOverflow/feat/l10-stream-forbidden-site-cleanup 2026-06-05 09:14:41 -07:00
Shay
6eccdb41ea docs(l10): propose finite grounding ADR 2026-06-05 09:08:42 -07:00
Shay
51c6852b0c test(l10): add independent-gold adversarial logic fixtures 2026-06-05 09:08:23 -07:00
Shay
fb3ca87fd0 docs(l10): describe finite grounding pack 2026-06-05 09:07:55 -07:00
Shay
543e6c04f5 feat(l10): add finite grounding pack manifest 2026-06-05 09:07:42 -07:00
Shay
21aa8b765f feat(l10): add finite grounding lexicon 2026-06-05 09:07:13 -07:00
Shay
8c394b8818 refactor(generate): remove redundant forbidden-site _close_final_state; rename "Drift fix 2"
generate/stream.py is a CLAUDE.md-forbidden normalization site, yet _close_final_state
re-closed the walk's final state with unitize_versor. The walk is built entirely from
versor_apply / Spin-manifold rotors (persona voicing, recall transitions, propagate_step),
so versor_condition < 1e-6 holds on the output BY CONSTRUCTION — the final unitize was a
true no-op (measured: final_state versor_condition = 2.98e-17 WITH and WITHOUT it).

- Remove _close_final_state + its unitize_versor import; GenerationResult.final_state=current.
- Reframe the "Drift fix 2" comment -> "recall-confidence weighting" (a selection policy,
  not normalization; mislabeled per the L10 Decision 0 bright line).
- Test-first: add test_generated_final_state_satisfies_versor_condition_by_construction
  (exercises voicing + seeded-vault recall); green before AND after removal.

Brings stream.py into forbidden-sites compliance.
2026-06-05 08:17:17 -07:00
Shay
54707f53a0
Merge pull request #564 from AssetOverflow/feat/l10-anchoring-doctrine
L10 Decision 0: sanction session semantic anchoring (CLAUDE.md) + rename anchor pull
2026-06-05 08:08:39 -07:00
Shay
6f19c831a5
Merge pull request #563 from AssetOverflow/feat/l10-engine-state-hardening
L10 engine-state hardening: byte-stability tests + schema-version migration (steps 1+2)
2026-06-05 08:08:16 -07:00
Shay
cd45103da8
Merge pull request #559 from AssetOverflow/docs/independent-comprehension-gate
docs(research): define independent comprehension gate + field wedge
2026-06-05 08:07:59 -07:00
Shay
81ea72482f refactor(field): rule the session anchoring family as sanctioned semantic anchoring (L10 Decision 0)
L10 scoping Decision 0 ruled the session/context.py "drift fix" family as
sanctioned SEMANTIC anchoring, not forbidden drift-repair:
- closure (versor_condition<1e-6) is owned by the sanctioned algebra/versor.py
  sandwich closure and holds BY CONSTRUCTION (measured: 100k-step field walk,
  max versor_condition ~6e-13, flat, with AND without the anchor pull);
- the family preserves the invariant by construction (rotor_power /
  word_transition_rotor / versor_apply on the Spin manifold, no post-hoc
  unitize) and expresses the session concept-attractor model.

CLAUDE.md: add session/context.py semantic anchoring to sanctioned normalization
sites behind a two-clause guard, plus a bright-line paragraph (semantic anchoring
vs drift repair; closure owned solely by algebra/versor.py; no "drift fix" naming).

Rename _anchor_pull -> _session_anchor_pull; reframe the "Drift fix 1/3" /
"conjugate correction against slow angular drift" docs as semantic anchoring
(no behavior change). Update test_session_coherence.py to the new name.

Out of scope (flagged for separate review): generate/stream.py "Drift fix 2"
sits in a forbidden normalization site.

Verified: 15 targeted tests green; INV-02 normalization invariant unaffected.
2026-06-05 07:48:46 -07:00
Shay
0b19e08306 feat(engine-state): schema-version migration discipline (L10 step-2)
Versioned additive-optional migration (L10 scoping step-2 ruling): a checkpoint
schema bump is a recorded lineage transition, not death-and-rebirth.

- engine_state.load_manifest() now REFUSES (IncompatibleEngineStateError) a
  checkpoint whose schema_version > this build's _SCHEMA_VERSION, and tolerates
  <= current (older/equal read any missing newer fields via additive-optional
  defaults). Never silently mis-loads newer state.
- chat.runtime._load_engine_state() loads the manifest FIRST so the version
  refusal gates before any recognizers/candidates are read.
- DerivedRecognizer.from_json documents the additive-optional convention
  (new fields .get-defaulted + omitted-when-default), mirroring DiscoveryCandidate.

Tests (TDD): refuses newer schema_version; tolerates older. Prerequisite for the
L10 continuity spike's P2 byte-identity gate (it may now assume a fixed schema
within a run, with version bumps handled explicitly).
2026-06-05 07:48:46 -07:00
Shay
3911db66a3 test: lock ADR-0146 engine-state byte-stability + discovery store path
The ADR-0146 round-trip tests proved object-equality but not byte-stability,
and the only non-empty discovery test bypassed EngineStateStore. Mutation
testing confirmed object-equality has teeth (a dropped field is caught) while
the store's non-empty discovery round-trip, save->load->save idempotence, and
cross-instance byte-determinism were untested.

Adds 4 locking tests (mutation-verified to fail under a lossy from_dict):
- recognizers_save_load_save_is_idempotent
- recognizers_save_is_deterministic_across_instances
- discovery_store_round_trips_nonempty_candidate
- discovery_store_save_load_save_is_idempotent

Deliberately not golden-format pins: a deterministic format change is harmless
for a content hash, and pinning would make every legitimate schema bump a
death-and-rebirth event. Prerequisite for any cross-reboot EngineIdentity
content-hash (ADR-0146 / L10).
2026-06-05 07:19:20 -07:00
Shay
d47741f5df
Merge pull request #562 from AssetOverflow/feat/cga-incidence-primitives
algebra: incidence algebra — graded_wedge, dual, meet + honest outer_product
2026-06-04 21:52:20 -07:00
Shay
ed42a83e7a
Merge pull request #561 from AssetOverflow/feat/relational-grounding
Deductive grounding: binary relations + multi-variable rules (finite propositional)
2026-06-04 21:47:10 -07:00
Shay
de645055ea feat(algebra): incidence algebra — graded_wedge, dual, meet + honest outer_product
Adds the correct grade-raising "wire" the field substrate was missing — so cga_inner
can operate on RELATIONS among entities (lines/planes/incidence), not just pairwise
point distance. Built only from existing Cl(4,1) primitives (geometric_product,
grade_project) + the pseudoscalar; no normalization, no approximation, versor_condition
path untouched (flats are null-cone wedges, not unit versors).

- outer_product: DOCSTRING-ONLY honesty fix (behavior byte-identical, every caller
  unchanged). It is the commutator 0.5*(XY-YX) = the wedge ONLY for grade-1 vectors;
  for higher grades it is the Lie bracket, NOT the wedge, and does NOT build a k-blade
  by repetition. Existing callers consume it as an opaque cga_inner-reduced feature
  (none read it by grade), so the relabel is safe. Points to graded_wedge for the real
  exterior product.
- graded_wedge(X,Y) = <XY>_{grade(X)+grade(Y)} — the true wedge; agrees with
  outer_product on grade-1, differs above (pinned by test). Builds lines/planes.
- is_incident(point, flat): EXACT zero-test (point^flat == 0, no float tolerance to
  admit — near-incident is refused, per wrong=0). Exact at scale in f64.
- dual(X) = X*I5^{-1} (I5^2=-1 confirmed); involutive up to sign.
- meet(A,B) = dual(dual(A)^dual(B)): correct for spanning operands (two planes -> their
  line, incidence verified). HONEST ENVELOPE: degenerates for non-spanning operands
  (coplanar lines) — returns the ZERO multivector (detectable, documented, tested),
  never a silent wrong value. The general coplanar intersection needs the join-relative
  meet, deliberately NOT faked here.

Green: smoke 87, algebra 82, incidence 8, outer_product consumers + invariants 109;
zero regressions (outer_product behavior unchanged).
2026-06-04 21:43:35 -07:00
Shay
5f274b75b7 feat(deductive): binary relations + multi-variable grounding (finite propositional)
Extends evals/deductive_logic/grounding.py from unary predicates / single-var rules to
binary relations + multi-variable universal rules, still by FINITE PROPOSITIONAL
grounding into the regime the ROBDD engine + the independent truth-table oracle both
decide. wrong==0 stays structural. This is the real capability step a RuleTaker/
ProofWriter-style mirror needs (the unary fragment alone is trivial).

- atom_n lowers pred(a,b) -> pred__a__b; arity-1 is byte-identical to the old atom, so
  the live unary panel lowers unchanged (proven by an exact-string back-compat test).
- multi-variable universal rules ground over n^k assignments — transitivity now decides.
- range-restriction: a rule with a head variable unbound in the body refuses (unsafe_rule)
  — it grounds soundly but is outside the clean regime real benchmarks use.
- typed refusals: arity>=3/functions, explicit quantifiers, variable-free rules, bounds.

Honest ceilings (documented in docs/analysis/relational-grounding-extension-2026-06-04.md):
- THE binding constraint is the GOLD, not the grammar: the truth-table oracle is
  O(2^atoms), so grounding refuses above MAX_GROUND_ATOMS=20 => binary problems cap at
  ~4 entities/predicate. A real lift needs a 2nd genuinely-independent sub-enumeration
  oracle (not built).
- OPEN-WORLD only: RuleTaker/ProofWriter's main splits are closed-world + NAF; a future
  adapter MUST refuse CWA/NAF (mapping CWA "False"->"refuted" is a wrong=0 breach).
- arity <= 2, function-free.

Validated: held-out differential fuzz (400 random binary problems, oracle-golded) = 0
engine/oracle mismatches; unary back-compat byte-identical; INV-25b reproducibility green;
deductive lane wrong=0 16/16; smoke 87.
2026-06-04 20:17:33 -07:00
Shay
5c2f005e96
Merge pull request #560 from AssetOverflow/feat/field-reasoner-wedge
Field-reasoner wedge: f64 foundation, lineage firewall (INV-27), and the honest C3 ablation verdict
2026-06-04 20:00:29 -07:00
Shay
5c77c9eece feat(field-wedge): ablation verdict — field is decoration on additive (C3) (Phase W.2)
The falsifiable experiment's measurements #2 (ablation) and #3 (diversity). Builds the
competent, code-disjoint SYMBOLIC reader (the control arm AND the C3 capability path)
and the ablation instrument that runs both readers through the real
verify_tier2_agreement gate.

VERDICT: C3 — the field is decoration on this domain (a sanctioned, honest negative):
- field_wrong_commits = []  (wrong=0 holds; the per-step drift guard refuses bad ints)
- field_caught_symbolic_errors = []  (the field caught ZERO symbolic errors)
- per-class diversity = 0 everywhere (both readers agree and are both correct)
- the only admitted-set change is the field LOSING coverage at the precision ceiling.

Insight: on forward-substitutable relations, geometric translation IS arithmetic
addition, so there is no metric over-determination for the field to exploit — field and
symbol are common-mode (Knight-Leveson), not a genuine second derivation. This is the
deductive finding's twin: logic was combinatorial (field can't earn it), additive is
arithmetically trivial (field adds nothing). The field needs metric-nontrivial AND
arithmetically-hard structure to earn a reasoning role — dedicated research, not
near-term. Field-as-reasoner is NOT earned; no field vote enters any serving path; the
field stays a servant. Capability path = symbolic (C3), not shipped here.

- generate/relational_symbolic_reader.py: competent independent reader (pure int).
- evals/relational_metric/ablation.py: the reusable decoration instrument.
- docs/analysis/field-wedge-ablation-result-2026-06-04.md: the recorded verdict.

All prior artifacts STAY (field reader = real wrong=0 read demo + 3rd panel domain).
Green: full wedge suite 104; 53 architectural invariants.
2026-06-04 19:44:22 -07:00
Shay
145d797196 feat(field-wedge): geometric field reader — relational-metric lane wrong=0 (Phase W.1)
Measurement #1 of the field-reasoner falsifiable experiment: does the CL(4,1) field,
given an honest metric encoding, read forward-substitutable quantitative-relational
problems from TEXT with wrong==0? It does — 14/15 correct, 0 wrong, 1 refused
(precision ceiling), scored against an independent arithmetic oracle.

- generate/relational_field_reader.py: reads problem text into conformal points on
  the e1 number line; additive/part-whole relations are conformal TRANSLATOR versors
  (versor_apply(T_delta, embed[x]) == embed[x+delta], exact); the answer reads back
  by projective dehomogenization. Refusal-first: fences multiplicative/ratio (the
  sign/orientation-blind cases), the precision ceiling, non-forward-substitutable
  references, negatives. A per-step exactness self-check turns any f64 translator
  drift into a refusal (precision_drift) — it NEVER commits a wrong integer. Its
  parser is an independent reimplementation importing no generate.derivation/math_*.
- evals/relational_metric/: independent arithmetic oracle (computes gold from the
  STRUCTURE, shares no code with the reader), 15-case fixture, and a runner that
  enforces gold integrity + wrong==0.
- INV-25: relational_metric registered in INDEPENDENT_GOLD_LANES (oracle proven
  code-disjoint from the field reader and the algebra engine). The independently
  golded panel is now three domains: deductive, dimensional, relational-metric.

Green: smoke 87, 53 architectural invariants, 16 new tests; deductive + dimensional
lanes unperturbed (wrong=0).
2026-06-04 19:34:43 -07:00
Shay
568face63e feat(reasoning): field-wedge foundation — f64 embedding, lineage firewall, INV-27
Phase 0 of the field-reasoner wedge — net hardening regardless of the
experiment's outcome.

- algebra/cga.py: embed_point gains a dtype kwarg (f32 default byte-unchanged;
  cl41.geometric_product already preserves f64) + read_scalar_e1 projective
  dehomogenization read-back (weight-invariant; correct for dilations, where a
  raw distance-from-origin is wrong) + EMBED_EXACT_MAX pinned magnitude ceiling.
  f32 silently collapsed integer coordinates past ~1e4.
- core/reasoning/evidence.py: verify_tier2_agreement now keys independence on a
  reader_lineage pathway token (refuses SAME_READER_LINEAGE), replacing the
  label-only len(set(signatures))<2 check a single reader could satisfy by
  relabeling. reader_lineage is excluded from canonical serialization, so the
  entailment trace_hash is unchanged.
- tests INV-27: transitive reader-disjointness over TIER2_READER_PATHWAYS makes
  the lineage check load-bearing (distinct lineage => proven import-disjoint
  pathway). The two seeded readers share zero transitive first-party modules.

Green: smoke 87, algebra 82, cognition 121, 53 architectural invariants,
reasoning/deductive/r1 50; 16 new f64-exactness tests; zero regressions.
2026-06-04 19:22:16 -07:00
Shay
991be784fa docs: field-reasoner wedge design + falsification dossier
Source-grounded design of the field<->symbol coherence-gate wedge, corrected by
an 11-agent adversarial review into a falsifiable experiment with a sanctioned
negative outcome (field stays a servant -> C3 two code-disjoint symbolic
readings). Records the substrate ledger (the CL(4,1) field has one exact strength
relevant to reasoning -- the conformal distance metric -- and no solver/incidence
machinery), the corrected design, the three adversarial verdicts, the falsifiable
experiment (ablation + per-class diversity), and the phased path.
2026-06-04 19:22:16 -07:00
Shay
b53f6e2465 docs(research): select quantitative field reasoner wedge 2026-06-04 17:23:57 -07:00
Shay
83429f5135 docs(research): define independent comprehension agreement gate 2026-06-04 17:23:20 -07:00
Shay
388a0a6d80
Merge pull request #558 from AssetOverflow/docs/session-2026-06-04-universal-structure
docs: session record (2026-06-04) + runway PR-1/PR-2 shipped
2026-06-04 16:57:50 -07:00
Shay
f97fcaf453 docs: session record (2026-06-04) + mark runway PR-1/PR-2 shipped
Session journey: the deductive pivot (verified, not asserted), INV-25 independent-
gold ratification, the universal-structure + field<->symbol coherence-gate synthesis,
and the autonomous build of the foundation + 3-domain anti-overfit panel (#554-557).
Records the two field-as-reasoner findings (logic is combinatorial; independence must
live in the reading) that defer the geometric wedge to dedicated research.

Marks the deductive-logic runway's PR-1 (finite-entity grounding) and PR-2 (oracle
parity) as shipped via #556.
2026-06-04 16:54:03 -07:00
Shay
4c6290f773
Merge pull request #557 from AssetOverflow/feat/dimensional-reasoning-domain
Dimensional-reasoning lane — 3rd diversity-panel domain
2026-06-04 16:48:55 -07:00
Shay
c0d7ec48db
Merge pull request #556 from AssetOverflow/feat/phase2-finite-entity-grounding
Phase 2 — finite-entity grounding compiler (2nd diversity-panel domain)
2026-06-04 16:48:40 -07:00
Shay
96c1d4bcee feat: dimensional-reasoning lane — 3rd diversity-panel domain
The first non-GSM8K consumer of the binding-graph interlingua's unit algebra as a
load-bearing reasoner: given two units and an operation, decide the result's
dimension. SUT = generate.binding_graph.units; gold = evals/dimensional/oracle.py,
a genuinely INDEPENDENT dimensional reasoner (own unit->base-exponent table, own
exponent arithmetic, own canonical-string renderer; shares no code with the SUT).

12 cases (area / speed / wage / mass-density / dimensionless / 2 refused) gated by
SUT == oracle == gold (wrong=0). Registered in INV-25's INDEPENDENT_GOLD_LANES,
proving the independent-gold discipline generalizes to a SECOND oracle.

This is the 3rd structurally-distinct golded domain (logic / grounding / dimensional)
— the anti-overfit >=2-domain panel is now real, and the interlingua is load-bearing
beyond GSM8K.
2026-06-04 16:38:56 -07:00
Shay
3e2a52870d feat: Phase 2 — finite-entity grounding compiler + Phase 1.5 finding
The first comprehension->structure compiler: evals/deductive_logic/grounding.py
lowers a typed finite-entity problem (finite entities + unary predicates +
single-variable universal rules) into the propositional regime the ADR-0206
entailment operator decides, refusal-first with a closed typed reason vocabulary
(unsafe_symbol / unknown_entity / unsupported_predicate_arity / unsupported_
quantifier / malformed_case / empty_case) and collision-safe atom slugging.

finite_entity/v1/cases.jsonl: 8 cases with INDEPENDENT (oracle-derived) gold
(4 entailed, 2 unknown, 1 refuted, 1 refused) — chained rules, conjunctive
bodies, negative heads, inconsistent premises. 20 tests gate engine==oracle==gold.

This is the second diversity-panel domain (distinct comprehension, same checkable
substrate) — the universal-structure thesis validated on a different problem shape,
with the anti-overfit >=2-domain discipline now live.

Phase 1.5 finding recorded: a clean geometric/algebraic propositional decoder
agrees 716/716 with the oracle but is O(2^n) (enumeration-class), so logic is the
wrong first domain for field-as-reasoner; the wedge redirects to quantitative-
relational structure where the field is the natural non-redundant decoder.
2026-06-04 16:32:03 -07:00
Shay
4d964020a9
Merge pull request #555 from AssetOverflow/feat/phase1-universal-structure
Phase 1 — canonize binding-graph interlingua + INV-26 neutrality
2026-06-04 16:29:56 -07:00
Shay
8d620d6257 feat: Phase 1 — canonize binding-graph interlingua + INV-26 neutrality
Declares SemanticSymbolicBindingGraph the universal problem-structure interlingua
(the corpus callosum where the geometric field and symbolic ROBDD decodings meet
and must agree). INV-26 enforces that neutrality structurally: 26a no binding-graph
module imports field/algebra/eval/vault/chat/core/sensorium; 26b the core imports
no domain reader (only allowlisted bridges adapter/question_target may); 26c proven
non-vacuous (flags pipeline.py's field import + the adapter's domain import).

Amends the plan doc with the structurally-diverse checkable panel (logic/grounding/
dimensional/execution/constraint, each with independent gold) and the 'a capability
change must move >=2 structurally-distinct domains or it is suspected overfitting'
rule, woven in from Phase 2 — the anti-overfit instrument the train_sample breach
proved we need.

Validated: architectural invariants 49 + binding_graph model = 118 passed.
2026-06-04 16:20:12 -07:00
Shay
0932c0e92e
Merge pull request #554 from AssetOverflow/claude/modest-franklin-85067f
Ratify independent-gold invariant (INV-25) + SHA-pin deductive lane
2026-06-04 16:05:50 -07:00
Shay
0ac725ccfd docs: plan — universal structure + field<->symbol coherence gate
Planning-only design doc for the comprehension->structure->solve->verify spine:
converge reading onto the binding-graph interlingua; field comprehends, structure
bridges, symbol verifies; their AGREEMENT is the wrong=0 gate and the genuine
second derivation that unblocks t2_precision. Field-as-reasoner must EARN each
domain against independent gold (Phase 1.5 wedge: field-decides-entailment vs
ROBDD oracle); never asserted unfalsifiably. INV-25 is Phase 0.
2026-06-04 15:56:28 -07:00
Shay
a447dce5d1 feat: ratify independent-gold invariant (INV-25) + SHA-pin deductive lane
INV-25 makes the GSM8K lesson structural: no capability claim is valid unless
its gold is computed by a procedure sharing no code with the system under test.
Three meaningfully-failing checks (proven able to fail): 25a oracle imports no
SUT module (AST); 25b every committed deductive gold is reproduced by the
independent oracle AND matched by the engine; 25c an unsound engine disagrees
on committed cases.

SHA-pin the deductive lane (deductive_logic_v1, dev+holdout+external 716/716,
wrong=0, refused=0) via a deterministic --report writer + run_as_module (the
lane dir's local generate.py shadows the package in script mode) + CLAIMS regen.

Fix drift the review surfaced: contract.md + pivot doc claimed an 8,000/7,340
fuzz and an 'external mirror' -> corrected to the real gated 3,000-case fuzz
(2,796 definite) and 'hand-authored, NOT a published-benchmark mirror'; pivot
doc GSM8K holdout 0->5 (all 5 are R1 reconstruction; composer scored 0).

Validated: smoke 78, deductive 23, proof 29, invariants 44, lane-sha+claims 9.
2026-06-04 15:56:28 -07:00
Shay
4215f1b4cf
Merge pull request #553 from AssetOverflow/codex/tier2-evidence-spine
feat: add tier2 reasoning evidence spine
2026-06-04 14:21:05 -07:00
Shay
134a91c48a feat: add tier2 reasoning evidence spine 2026-06-04 14:12:18 -07:00
Shay
efbd646ba1
Merge pull request #552 from AssetOverflow/codex/gsm8k-r1-reconstruction
feat: add gsm8k r1 reconstruction
2026-06-04 13:59:07 -07:00
Shay
7155f5ab34 feat: add gsm8k r1 reconstruction 2026-06-04 13:25:11 -07:00
Shay
0cc2618f5c
Merge pull request #551 from AssetOverflow/codex/deductive-phase2-benchmark-runway
docs: define deductive logic phase 2 benchmark runway
2026-06-04 13:24:55 -07:00
Shay
d4a07f23b1 docs: define deductive logic phase 2 benchmark runway 2026-06-04 09:33:23 -07:00
Shay
fc20d01832
Merge pull request #550 from AssetOverflow/codex/deductive-capability-foundation
feat: add deductive proof evidence gates
2026-06-04 08:47:24 -07:00
Shay
3389cc68c3 feat: add deductive proof evidence gates 2026-06-04 08:37:51 -07:00
Shay
cc885157a5
Merge pull request #549 from AssetOverflow/feat/deductive-logic-entailment
feat: sound+complete propositional entailment operator + deductive-logic lane (the pivot)
2026-06-04 08:11:36 -07:00
Shay
602a76221a docs: record the GSM8K->deductive-logic pivot, post-mortem, and plan
Load-bearing strategic record: what the GSM8K chase got wrong (overfit
50-case ruler hiding a sealed wrong=0 breach), the held-out measurement
that exposed real capability=0% / composer 17% wrong, why GSM8K is the
wrong terrain for a deterministic verifiable engine, and the deductive-
logic path now (Phase 1 done: 500/500 held-out wrong=0, independent-
oracle verified) with phases 2-4 and the anti-recurrence disciplines.
2026-06-04 07:51:36 -07:00
Shay
48827f281a feat: sound+complete propositional entailment operator + deductive-logic lane
The first SIZEABLE, honestly-verified reasoning capability — built on CORE's
own terrain (exact, verifiable, deterministic), not GSM8K's stochastic terrain.

THE OPERATOR (generate/proof_chain/entail.py, ADR-0206):
- evaluate_entailment(premises, query) -> entailed | refuted | unknown | refused.
- The multi-hop inference operator evals/symbolic_logic/gaps.md said did not exist
  ("no operator that takes A->B, B->C and returns A->C") and ADR-0205 deferred.
- Built on the ADR-0201 ROBDD canonicalizer: premises |= Q iff (AND P) -> Q is a
  tautology. SOUND AND COMPLETE for propositional logic, not single-step.
- wrong=0 is structural: an exact tautology check refuses (LogicError) on
  malformed / out-of-decidable-regime (quantified/predicate) input, never guesses.

THE HONEST METRIC (evals/deductive_logic/):
- holdout v1 (500 cases): 500 correct / 0 WRONG, incl. 227 non-trivial deductions
  (117 entailed + 110 refuted). dev (200): 200/0.
- Gold from an INDEPENDENT truth-table oracle (oracle.py) sharing zero code with
  the engine. 8,000-case fuzz across two independent decision procedures:
  0 disagreements. This is the soundness evidence the GSM8K composer could never
  produce (it could not separate its 2 right from its 87 wrong answers).
- contract.md states the load-bearing honesty boundary: PROPOSITIONAL ONLY, and
  given-formulas (NL->logic grounding is a separate later layer, kept out of scope
  so we do not re-step on the GSM8K natural-language rake).

TESTS (17, all green): classic inference shapes (MP, multi-hop chain, modus
tollens, disjunctive/hypothetical syllogism, conjunctive rules, genuine unknown),
refusal boundary (inconsistent / quantified / predicate / malformed), and a
deterministic engine-vs-oracle fuzz cross-check.

Pure new module — does NOT touch serving. Smoke 73 passed; invariants 40 passed.
2026-06-04 07:47:01 -07:00
Shay
f301f3a928
Merge pull request #548 from AssetOverflow/feat/holdout-dev-set
feat(eval): held-out dev lane — honest iteration metric (real GSM8K capability = 0/500)
2026-06-04 07:14:58 -07:00
Shay
f56a0cfdba
Merge pull request #546 from AssetOverflow/docs/reconcile-current-state-2026-06-03
docs: reconcile current-state claims after GSM8K + sensorium progress
2026-06-04 07:14:39 -07:00
Shay
d4a002e626 docs: fourth lift attempt falsified (strict sum-reader 0/2) — exhaustive
Four genuine build-and-measure attempts on held-out, all falsified:
- composer 87 wrong, narrow reader 61 wrong, strict sum-reader 2 wrong (100%),
  candidate-graph 0 admissible. Even 'exactly 2 numbers + total cue' cases are
  multi-step in disguise. No sound committing strategy exists in this substrate.
2026-06-04 02:52:24 -07:00
Shay
c31bc82621 docs: third leg — candidate-graph builds 0 admissible candidates on held-out
All three GSM8K paths exhausted on the held-out 500:
- candidate-graph (sound filter): 0 admissible candidates built (498/500 build nothing)
- composer (open): 87 wrong
- narrow reader (open): 61 wrong
The sound path is wrong=0 only because it constructs no reading to commit. No sound
lift exists in the current substrate; confirmed empirically, three ways.
2026-06-04 02:49:00 -07:00
Shay
92e9631467 docs: two lift attempts falsified + the ADR-0207 architectural impasse
Built and measured two committing readers on held-out (not just diagnosed):
- resolve_pooled composer: 2 correct / 87 wrong (17%)
- maximally-narrow forced reader: 0 correct / 61 wrong (100% when it fires)

Shallow committing CANNOT be sound on real GSM8K (measured). The impasse:
- candidate-graph path is SOUND (wrong=0) but FROZEN by ADR-0207 §4, covers 0 real
- composer is OPEN (ADR-0207 §5) but UNSOUND (17% wrong, no separating gate)
ADR-0207's premise (composer is the wrong=0-safe path) is FALSIFIED by held-out data;
'feed the composer' is a path to more confabulation. Needs a follow-up ADR.
2026-06-04 02:47:03 -07:00
Shay
c2e74fe2fd docs: the honest real-GSM8K capability measurement (0% sound; composer 17% wrong on held-out)
The result that should have been visible for weeks. Measured on the new holdout_dev
lane (500 real held-out cases) + sealed test:
- real capability 0% (train_sample's 4 correct generalize to 0/500 held-out)
- resolve_pooled (the composition substrate) commits 17.4% WRONG on held-out
- NO structural gate (step/pool/op) separates its 2 correct from 87 wrong -> the
  composer cannot tell right readings from wrong; gating it is overfitting
- the only wrong=0 policy on real GSM8K is refusal (current serving 0/0/500)

Real lift needs a verifiable reading mechanism, not composer tuning. Non-serving (docs).
2026-06-04 02:42:14 -07:00
Shay
d81084ffe3 feat(eval): held-out dev lane — the honest iteration metric (real capability = 0)
The 2026-06-04 sealed-breach post-mortem proved the 50-case train_sample has ZERO
predictive validity (its 4 "correct" are overfit; they hid a 5-wrong sealed breach).
This adds the instrument we never had: 500 real GSM8K cases CORE was NOT built on —
the train split minus the 50 train_sample, deterministic sha256(question) sort.

Same scorer as train_sample + the sealed lane, so the three are directly comparable:
  train_sample(50): 4/0/46   holdout_dev(500): 0/0/500   sealed test(1319): 0/0/1319

Real GSM8K capability is 0%. The 4 train "correct" generalize to NOT ONE of 500
held-out cases. wrong=0 holds (refuses, never confabulates).

- evals/gsm8k_math/holdout_dev/v1/: cases.jsonl (500), runner, report (0/0/500), README.
- tests/test_holdout_dev_lane.py: floor (wrong==0, forever) + baseline snapshot (0/500).

Discipline: iterate here (open, large enough to resist trivial overfit); the sealed
test stays the final arbiter. wrong=0 is the floor; correct rising is the goal;
"refuse everything" is the FAILING baseline to beat, not a pass. Non-serving (eval only).
2026-06-04 02:30:42 -07:00
Shay
431699791c
Merge pull request #547 from AssetOverflow/fix/disable-unsound-serving-bridges
fix(gsm8k): disable unsound serving bridges — restore sealed wrong=0 (0/5 → 0/0)
2026-06-04 02:25:49 -07:00
Shay
763c46d2f4 fix(gsm8k): disable unsound serving bridges — restore sealed wrong=0 (0/5 -> 0/0)
The FIRST real sealed measurement (operator-decrypted 1,319 held-out GSM8K)
found `0 correct / 5 WRONG` — a wrong=0 breach hidden for weeks because the
working metric was the 50-case train sample the bridges were tuned to. Bisection
isolated it to the product_bridge serving promotion (ADR-0195).

- generate/math_candidate_graph.py: REMOVE both serving promotion bridges
  (product_bridge + goal_residual/ADR-0207 §5 step 2). Serving = main-graph-only.
  Restores sealed 0/0/1319 (verified by bisect: disabling product_bridge -> 0 wrong).
  Production modules remain in generate/derivation/; only serving promotion is
  unwired, until a gate is proven wrong=0 on the SEALED set (never the train sample).
- Honest numbers everywhere: train_sample 7/43/0 -> 4/46/0 (the bridges' "correct"
  was train-overfit). report.json + coverage probe regenerated. 7 ADR test lanes
  de-pinned from the inflated count. corpus: cv-0005 (R4) reverts to refuse; cv-0020
  (a "baseline control" that solved ONLY via product_bridge) reclassified.
- docs/claims_ledger.md: dated wrong=0-breach-and-remediation note + the rule:
  the train_sample number had ZERO predictive validity for the exam; never the score.
- docs/analysis/gsm8k-lift-program-strategy: the program to actually move the 1,319.

NOTE the exit gate stays `correct>=10 AND wrong==0` — refusing-everything is an
explicit FAIL, not a wrong=0 pass; serving still commits (main graph). Verified:
broad regression 848 passed, smoke 73 passed.
2026-06-04 01:55:05 -07:00
Shay
399720817f docs: reconcile ADR-0207 execution status 2026-06-03 22:45:07 -07:00
Shay
f387dcfe5a docs: reconcile operator instructions with current state 2026-06-03 22:43:50 -07:00
Shay
7dc9dbaa6a docs: reconcile train sample README metric 2026-06-03 22:42:35 -07:00
Shay
e7392f118e docs: reconcile claims ledger with current state 2026-06-03 22:41:19 -07:00
Shay
3a72d69678
Merge pull request #545 from AssetOverflow/docs/ratify-sensorium-afferent
docs: ratify sensorium afferent ADRs + work-sequencing acknowledgment
2026-06-03 22:29:50 -07:00
Shay
7160e5e77f
Merge pull request #544 from AssetOverflow/feat/r4-goal-residual
feat(r4): cv-0005 goal-residual to serving — train_sample 6/44/0 → 7/43/0 (ADR-0207 §5 step 2)
2026-06-03 22:29:21 -07:00
Shay
bd6e0377e1 docs: ratify sensorium afferent ADRs + acknowledge arc in work-sequencing
Authority-ratified per the ADR-0207 convention (Accepted (ratified DATE)):
- ADR-0181 (audio), ADR-0197 (vision), ADR-0208 (environmental loop),
  ADR-0209 (sensorimotor afferent) -> Accepted (ratified 2026-06-03).
  All four are implemented + test-backed (0208/0209 falsifiability deeply
  verified in the lookback; 0181/0197 backed by CRDT-merge/compiler/mount/
  eval-gate suites, all green in the 182-test sweep).
- ADR-0198 deliberately NOT ratified: partially-implemented spike (Gap A +
  fail-closed efferent gate landed; \xc2\xa73 verdict-lowering + motor decoder
  deferred behind a dedicated motor governance ADR).

CLAUDE.md Work Sequencing now records the sensorium/modalities arc as a
sanctioned parallel track that does not displace the near-term sequence and
is disjoint from the GSM8K serving path. Markdown only; no test asserts ADR
status or the work-sequencing text.
2026-06-03 22:21:50 -07:00
Shay
ad9cf57069 feat(r4): flip cv-0005 to serving — train_sample 6/44/0 -> 7/43/0 (ADR-0207 §5 step 2)
Wires the R4 goal-residual production to serving via
resolve_promotable_goal_residual (math_candidate_graph.py, mirroring
product_bridge). cv-0005 / train_sample 0037 now solves on serving as
goal - Σprogress = 10 - 3 - 4 = 3. First Phase-5b composition lift on serving.

wrong=0 preserved on every runnable surface:
- train_sample 6/44/0 -> 7/43/0 (0037 added; 6 prior correct intact; wrong=0).
- Fires on 2/455 visible GSM8K cases, both correct, ZERO wrong.
- Gain-goal divergence firewall proves it reads the GOAL, not a possession.
- smoke 73, math+invariants 53, derivation/pool/practice 341, corpus, all green.

Lockstep updates (the ratified metric move, 6/44/0 -> 7/43/0):
- report.json; 7 ADR test lanes that pinned 6/44/0; corpus cv-0005 baseline
  fields + snapshot (4/18 -> 5/17) + contract; plan-doc cv-0018 control fix.

⚠ SEALED MEASUREMENT REQUIRED — NOT DONE. The sealed 1,319 (encrypted, not
CI-reproducible) is the real bar (ADR-0207 §6) and was NOT re-measured. The
operator/CI must decrypt+run it and confirm sealed wrong==0; if wrong>0, revert
the resolve_promotable_goal_residual block (isolated). See
docs/handoff/sealed-measurement-obligation-2026-06-04.md.
2026-06-03 22:20:12 -07:00
Shay
4006a8eb1a
Merge pull request #543 from AssetOverflow/docs/reconcile-sensorium-adr-status
Reconcile sensorium ADR statuses + fail-closed efferent gate (ADR-0198 §3)
2026-06-03 22:13:45 -07:00
Shay
b4fb9df8e7 feat(sensorium): fail-closed efferent gate for actuating decode (ADR-0198 §3)
DefaultEfferentGate is a capability/shape pre-filter only; it does not
lower decoded actions into safety/ethics pack verdicts (ADR-0198 §3 /
§1.2 Gap B). ModalityRegistry.decode/decode_batch now refuse fail-closed
any emission through a gate whose enforces_action_verdicts is False,
unless an explicit allow_unverified_efferent sandbox opt-in is set.

A real motor decoder thus cannot emit through the capability-only gate;
the §3 verdict-lowering gate must be built and installed first. No
production caller of the decode path exists today, so this closes the
latent hazard before a motor decoder makes it load-bearing. Adds two
falsifiable tests (fail-closed refusal; verdict-enforcing gate allowed).
Disjoint from the GSM8K serving path.
2026-06-03 22:03:35 -07:00
Shay
e5bc73ebb9 docs: reconcile sensorium ADR statuses with shipped implementation
ADR-0181/0197/0208/0209 were marked Proposed despite test-backed
afferent implementations merged to main; flip to Accepted with
verifiable Implementation lines (paths + PRs + falsifiable tests).
ADR-0198's 'no implementation' status was stale (Gap A protocol
change + baseline efferent gate landed in #541); correct it and
document the deferred §3 safety/ethics verdict-lowering as an
explicit blocking obligation before any motor decoder mounts.
2026-06-03 21:51:59 -07:00
Shay
94d8137ad1 feat(r4): goal-residual production — ADR-0207 §5 step 2 (sealed lane)
First composition lift-target built end-to-end: cv-0005 (train_sample 0037)
now resolves in the sealed pool as goal - Σprogress = 10 - 3 - 4 = 3.

- generate/derivation/goal_residual.py: new R4 production. Reads a GOAL anchor
  (goal-intent lexeme) + a residual question, subtracts each same-referent
  progress quantity (progress reduces the residual regardless of world-polarity).
  Gated by the unchanged self-verification gate.
- wrong=0 firewall (test_reads_goal_not_possession): on a gain goal the
  goal-residual (20-5-6=9) DIVERGES from possession-accumulation (20+5+6=31);
  the production gives 9 and is all-subtract -> it reads the goal, not the
  possession. This is the coincidental-correctness trap cv-0005 alone hides
  (10-3-4 == 10-(3+4)).
- pool.py: goal_residual added to pooled_candidates (sealed). Verified: fires on
  exactly one train_sample case (0037, correct), zero new pool wrong-commits
  (the 8 are pre-existing, gated off serving by product_bridge).

Does NOT move the serving metric: train_sample stays 6/44/0 byte-identical
(serving = candidate-graph; product_bridge promotes only pure products, never a
subtract chain). The serving promotion gate for goal-residual is the next,
separately-gated step (needs the sealed 1,319 verdict). Smoke 73, math 4,
derivation/pool/practice 196, corpus, completeness-guard all green.
2026-06-03 21:50:59 -07:00
Shay
f7e95be211
Merge pull request #542 from AssetOverflow/docs/exec-scope-composition-wall
docs: composition-wall execution scope (ADR-0207 §5 step 2)
2026-06-03 21:24:54 -07:00
Shay
5ae377065e docs: composition-wall execution scope (ADR-0207 §5 step 2)
Three reviewed, non-serving analysis/handoff docs for the COMPOSITION lever.
Read-only; no serving code; train_sample 6/44/0 untouched.

- analysis/wiring-promotion-gate-brief: product_bridge is the only serving
  derivation tendril (two-token target whitelist); resolve_pooled refuses all
  ten R1/R4/R5/R6 positives and wrong-commits 0016 -> 510. Wiring is trivial;
  the gate is the work.
- analysis/composition-wall-execution-plan: per-case stage taxonomy (A/B/C/D)
  for all 15 corpus positives, reproduced live; R4-first scope (new
  goal-residual production, verified by code-read of accumulate.py, NOT a
  target tweak); promotion gate generalizes product_bridge, never disagreement
  alone; gates inherit ADR-0207 §6 (corpus + sealed 1,319).
- handoff/stage-c-composition-investigation: dispatch task for the execution
  lane to pin the per-case bail for the production-wrong shapes (the real
  grouping/op-order wall), read-only, in-tree.
2026-06-03 21:14:02 -07:00
Shay
b84ca33548
Merge pull request #541 from AssetOverflow/codex/sensorium-runtime-eval-governance
Add sensorium eval and governance runway
2026-06-03 21:03:50 -07:00
Shay
9cabeeb40d Add sensorium eval and governance runway 2026-06-03 20:53:05 -07:00
Shay
d9fc7f9e56
Merge pull request #540 from AssetOverflow/codex/vision-eval-environment-sensorimotor
[codex] Add vision evidence and sensorimotor contracts
2026-06-03 20:41:10 -07:00
Shay
2d2b096784 Add vision evidence and sensorimotor contracts 2026-06-03 20:27:46 -07:00
Shay
2a3b95489d
Merge pull request #539 from AssetOverflow/docs/amend-adr-0207-s5-wiring
docs: amend ADR-0207 §5 step 1 — WIRING not the lowest-risk crux (tree-verified)
2026-06-03 20:26:52 -07:00
Shay
d0d305f86a
Merge pull request #538 from AssetOverflow/master-workbench-state-implementation
feat: CORE Workbench W3 Ratification Corridor
2026-06-03 20:26:26 -07:00
Shay
6d1f6bba0c feat: implement CORE Workbench W3 Ratification Corridor 2026-06-03 20:13:36 -07:00
Shay
a6c36cc26f docs: amend ADR-0207 §5 step 1 — WIRING not the lowest-risk crux (tree-verified)
Reading the actual gate logic (product_bridge/pool/verify) corrected §5's
sequencing optimism, before merge:

- The safe wire is already done (product_bridge at :530); its safety is a
  two-token target whitelist (money+make/earn, weight+total/move) hardcoded to
  0003/0021 — it generalizes nothing.
- Verified live (2026-06-03): the general composer resolve_pooled refuses ALL
  ten R1/R4/R5/R6 corpus positives (R1/R4 build zero candidates; R5 only wrong
  ones) -> wiring yields +0 correct; and it wrong-commits 0016 -> 510, so wiring
  it wholesale is a live wrong=0 regression the disagreement rule does not catch.

So WIRING and COMPOSITION are the same task: composer production per shape, each
behind a structural promotion gate (extract_target + target_units, not
disagreement alone); the wire is the trivial last step. Core ratification
(freeze, design-of-record, gates, sealed-set measurement) unchanged.
2026-06-03 20:13:03 -07:00
Shay
fc01be715c
Merge pull request #537 from AssetOverflow/codex/sensorium-compiler-vision
[codex] Add sensorium compiler law and tile vision
2026-06-03 20:09:31 -07:00
Shay
3da0846c98
Merge pull request #536 from AssetOverflow/docs/ratify-adr-0207-gsm8k-substrate
docs: ratify ADR-0207 — GSM8K comprehension/composition substrate
2026-06-03 19:58:59 -07:00
Shay
282679bd85 Add sensorium compiler law and tile vision 2026-06-03 19:58:36 -07:00
Shay
94bf1be1bc docs: ratify ADR-0207 — GSM8K comprehension/composition substrate
Consolidating ratification of the GSM8K design of record. Ratify the built
comprehension/derivation substrate, freeze the serving regex recognizer/
injector path to lexemes + refusal-only, pin Phase 5b execution to
WIRING -> COMPOSITION -> LEXICON.

- ADR-0207: new consolidating decision (Accepted, ratified 2026-06-03).
  Supersedes ADR-0163 §Phase B-E + ADR-0136 regex sentence-template
  prescriptions. Freeze + wrong=0 gates (22-case corpus + sealed 1,319).
- ADR-0164/0165/0174/0178/0179: -> Accepted (ratified by ADR-0207,
  2026-06-03). 0164 keeps its implementation clause (Phase 1+2 shipped;
  remainder per §5) so Accepted != fully built.
- composition_validation/v1: 20 -> 22 cases (2nd R4/R5 positives,
  dataset-sourced golds), +contract invariants 6-7, +dataset-gold test.
  Baseline 4/18/0; 47 passed.
- docs/analysis: extraction-richness audit (read-only) reconciling
  ADR-0179 to the tree (EX-1/2/4/5/6 landed; EX-3 deferred).

Non-serving (evals/docs/tests only). train_sample 6/44/0 unchanged;
no-ref <N> times hazard stays refused. GB3b/0136 untouched.
2026-06-03 19:42:47 -07:00
Shay
2cb09223e6
Merge pull request #535 from AssetOverflow/codex/core-next-runway-milestone
[codex] Add Phase 5b composition validation runway
2026-06-03 18:48:39 -07:00
Shay
c89adc6547 test(gsm8k): executable invariants for composition validation corpus
The composition_validation/v1 corpus shipped as data + a prose contract
with no executing test -- decoration until a test can fail (CLAUDE.md,
Schema-Defined Proof Obligations). Add the load-bearing gate:

- forever-invariants: wrong=0 firewall (admit => ==gold; null-gold =>
  refuse), baseline-control regression net, permanent-refusal permanence,
  and frozen baseline-field/tree match for the non-positive rows.
- current snapshot: aggregate 4 solve / 16 refuse / 0 wrong -- the single
  assertion a Phase 5b slice updates when it flips a positive.

Future positives (5b-* gates) are checked by the firewall only, so a
refuse->solve flip at 5b stays green without rewriting frozen rows.

Verified: 44 passed; falsified (corrupting a control's gold fails the
firewall + control + snapshot tests); completeness guard still 21 passed.
2026-06-03 18:40:37 -07:00
Shay
a8027ca34d test(gsm8k): add composition validation corpus 2026-06-03 16:30:29 -07:00
Shay
b2b3678deb docs: mark ADR-0174 Phase 5a shipped in composition-capability-scope
Phase 5a (retire the inert GSM8K scoring-path comprehension-reader
dispatch, -1,038 LOC) landed in 3fd3172, an ancestor of HEAD 8327c6b,
but composition-capability-scope.md still listed it as remaining work --
drift that mis-scoped a downstream brief. Re-anchor the canonical doc to
the tree:

- §1: the lifecycle.py GSM8K-scoring dispatch row now reads "retired in
  Phase 5a (3fd3172)" instead of "inert ... retirable in 5a".
- §2: Phase 5a marked SHIPPED with a runway-status banner; the §9
  completeness-gate precondition marked LANDED (PR #534, e1bcdf6).
- §4: sequencing restructured -- items 0 (§9 precondition), 1 (gating
  analysis), 3 (5a) are DONE; the live runway starts at item 2 (the
  <=20-case validation sub-corpus), then Phase 5b.
- §9: status banner noting the recommended guard landed (#534); the
  "RED on main" statements are the pre-landing measurement, kept for
  provenance.

Docs-only. No code/eval/serving change. Baseline re-verified at HEAD:
train_sample 6/44/0 (all 44 refusals at branches_enumerated=0),
completeness-guard suite 21 passed.
2026-06-03 16:23:53 -07:00
Shay
8327c6bfa1
Merge pull request #534 from AssetOverflow/codex/ntimes-completeness-guard
Harden no-reference n-times comparative guard
2026-06-03 15:38:21 -07:00
Shay
018181c95b Add canonical composition analysis docs 2026-06-03 15:35:58 -07:00
Shay
e1bcdf6286 Harden no-reference n-times comparative guard 2026-06-03 15:22:42 -07:00
Shay
3e29559532
Merge pull request #533 from AssetOverflow/docs/correct-gsm8k-serving-metric 2026-06-03 11:50:58 -07:00
Shay
6a7faa3fc2 docs: correct stale GSM8K serving metric in CLAUDE.md (3/47/0 → 6/44/0)
CLAUDE.md's GSM8K substrate header asserted "serving stays `3/47/0` until
ratified". That figure is two ratified metric-moves stale: the official
candidate-graph serving metric (evals/gsm8k_math/train_sample/v1/report.json,
scored by `_score_one_candidate_graph` in train_sample/v1/runner.py:88) moved
3/47/0 → 4/46/0 (#488, ADR-0189/0189a) → 6/44/0 (#500, ADR-0195), each PR
re-baselining report.json.

Verified on main 7e98ad3: committed report.json == live `build_report()` ==
{correct: 6, refused: 44, wrong: 0}. wrong==0 preserved.

The stale header was the source of recurring 3/47/0 confusion across sessions.
Historical ADRs (0164/0166/0174/0175/0176/0177/0178/0179) that say "serving
stays 3/47/0" are point-in-time decision records and are intentionally left
unchanged.
2026-06-03 11:27:10 -07:00
Shay
7e98ad3326
Merge pull request #532 from AssetOverflow/feat/adr-0206-response-governance
feat(adr-0206): response-governance bridge scaffold (STRICT-only, inert)
2026-06-03 10:47:35 -07:00
Shay
ec6c591b7b
Merge pull request #531 from AssetOverflow/test/promote-falsifiability-to-smoke-gate
test: promote identity-falsifiability eval into the pre-merge smoke gate
2026-06-03 10:41:02 -07:00
Shay
d5872a2e96 feat(adr-0206): response-governance bridge scaffold (STRICT-only, inert)
Adds the first consumer of CORE's two built epistemic substrates — the
decode-state taxonomy (core/epistemic_state.py) and the risk-reward
reliability gate (core/reliability_gate/, ADR-0175) — beginning to close
the LABEL-ONLY consumption gap they sat behind.

Ships the scaffold only:
- core/response_governance/policy.py — ReachLevel (STRICT < APPROXIMATE <
  EXTRAPOLATE < CREATIVE), ReachPolicy, govern_response (STRICT-only stub),
  shape_surface (STRICT = identity transform; higher levels real but
  unreachable in production), and the 9/5/1 EpistemicState partition.
- chat/runtime.py — cognition-path seam: the response surface now flows
  through shape_surface(govern_response(...)). STRICT = identity, so the
  path is byte-identical to pre-bridge. reach_level carried on ChatResponse.
- core/physics/identity.py — reach_level on TurnEvent (default "strict").

wrong==0 untouched: select_self_verified is NOT touched (ADR-0206 §5);
reach_level is NOT added to the telemetry JSONL dict, so pinned lane SHAs
stay byte-identical. Widening, SITUATE/FEED-BACK, the math-serving seam,
and the EVIDENCED reconcile are deferred to their own PRs.

Tests (tests/test_response_governance.py): STRICT-only contract over all
states x license x stakes, STRICT identity, live-wiring proof (forces
APPROXIMATE -> policy-sensitive surface), total+disjoint taxonomy
partition. Each fails loudly under the violation it guards.
2026-06-03 10:39:12 -07:00
Shay
0c467ba3b6 test: promote identity-falsifiability eval into the pre-merge smoke gate
test_pack_measurements_phase2.py (ADR-0043) was reachable only under
`--suite full` and post-merge full-pytest.yml — outside the blocking PR
smoke gate. A regression flipping identity falsifiability (ratified packs
must diverge directionally), the pack-invariant grounding/refusal floor,
or the zero-fabrication invariant therefore cleared the PR gate and
surfaced only post-merge on main (the smoke blind spot for dedicated
test files).

Add the file to both the `smoke` suite tuple (core/cli.py) and the
smoke.yml CI gate so the falsifiability claim blocks-on-regression
rather than detect-after-merge. Adds ~4 min to the PR gate.

Verified: modified smoke gate green on the main base via the
CI-equivalent invocation (140 passed).
2026-06-03 10:31:56 -07:00
Shay
4c095c3621
Merge pull request #529 from AssetOverflow/codex/mp-corpus-fixture
[codex] Add modus ponens oracle corpus
2026-06-03 09:32:57 -07:00
Shay
7e5c260bbb
Merge pull request #530 from AssetOverflow/claude/tender-raman-028946
Green the full-surface gate: reconcile 31 failures + 6 errors (clean main, pre-#529)
2026-06-03 09:28:04 -07:00
Shay
010e713d1b test(adr-0167): decouple self-teaching e2e from the evolving train_sample reader
The thesis demo ratified the unknown word 'sees' in train_sample case 0040 and
asserted the refusal moved. The reader has since advanced past that barrier —
case 0040 now first-refuses at quantity_extraction@s0 and never reaches 'sees',
and no train_sample case cleanly first-refuses at an unknown word anymore. So
the fixture was stale, not the machinery (which is unit-tested in
test_math_lexical_ratification.py + 9 others).

Repoint to a reader-stable synthetic statement ('Sam zorps 5 apples and
quibbles 3 oranges...'): 'zorps' first-refuses at lexicon_entry, gets ratified
as a drain_token and resolves, and the second unknown verb 'quibbles' keeps the
statement refused (wrong=0). Removed the now-dead RATIFICATION_TARGET_* consts.
2026-06-03 02:11:41 -07:00
Shay
1fe57643e1 test: update to evolved contracts (propose-from-exemplars corpora; pronoun guard)
- propose-from-exemplars: --all now proposes 6 exemplar corpora (the ME-1..ME-5
  matcher waves added currency_amount, discrete_count_statement,
  multiplicative_aggregation). All pending. Updated the pinned set + renamed
  the test off 'three_corpora'.

- adr_0163_d2 pronoun: the design moved from hard-refusing a bare pronoun
  subject at extraction to admitting it tagged requires_pronoun_resolution=True,
  with the candidate-graph (math_candidate_graph.py:704/723) refusing to commit
  unless it resolves. Verified end-to-end: 'He has 5 apples. How many...' still
  returns answer=None. Pin the defensive flag (its absence re-opens the ADR-0174
  wrong=0 hazard) instead of the obsolete None return.
2026-06-03 02:02:58 -07:00
Shay
6ea52f8af0 test(adr-0157): patch get_git_revision (the seam load_manifest actually calls)
A refactor split the revision helper into public get_git_revision() with
_git_revision() as a backward-compat alias, and pointed load_manifest() at
get_git_revision(). The revision-mismatch tests still patched _git_revision,
so the patch was a no-op and load_manifest read the real HEAD SHA — emitting
(or suppressing) warnings against the wrong 'current' revision. Patch the
function load_manifest calls. Feature unchanged; only the test seam was stale.
(The reboot path in chat/runtime.py still uses _git_revision, tested by
test_adr_0158 — left as-is.)
2026-06-03 01:59:05 -07:00
Shay
6d62ac2f24 test(telemetry): isolate engine_state so emission-count tests are order-independent
The telemetry tests built ChatRuntime(config=RuntimeConfig()) against the
default (shared) engine_state/ store. In a full-suite run, earlier runtime
tests persist a checkpoint there, so these tests restored ambient state and
emitted an extra 'reboot' telemetry event (restored_turn_count=NNNN), breaking
the 'one line per turn' counts (assert 3 == 2). They passed alone, failed
after siblings — a classic order-dependent isolation bug, not a telemetry
regression.

Build with no_load_state=True so each test exercises clean per-turn emission.
No telemetry test asserts the reboot/restore path, so none lose coverage.
(engine_state/ data files are already gitignored per ADR-0146.)
2026-06-03 01:56:05 -07:00
Shay
0d05d3a1b3 fix(refusal-taxonomy): parse new refusal-reason format; reconcile to reader gains
build_refusal_taxonomy_cases._STATEMENT_RE only matched the old 'no admissible
candidate for ...' shape, so post-#359 'recognizer matched but produced no
injection ... (category=X)' refusals were silently dropped (44 refusals -> 12
extracted). Extend it to both shapes (same gap fixed in rescan_v4 before that
layer was retired).

The lane mirrored 50 cases from the all-refused era; the reader now admits 6,
so it covers the 44 refused. Regenerated the cases fixture + committed
report.json and updated the count pins (50 -> 44).

Removed the perverse categorized_rate >= 0.95 floor: the exact histogram is
already pinned by test_committed_report_matches_categorizer, and the rate
drifts DOWN as the reader graduates categorized refusals — it fought reader
progress. Replaced with a sanity floor.

adr_0126: the unparseable 'contemplates' input still refuses (wrong=0); only
the reason wording changed (#359). Accept either non-admission phrasing.
2026-06-03 01:49:41 -07:00
Shay
78001d6f78 chore: deprecate orphaned rescan_v4 archaeology layer
The rescan_v2/v3/v4 chain has zero runtime consumers (nothing in core/,
generate/, teaching/, chat/ imports it). v2/v3 are frozen disk-snapshot
tests; v4 alone re-ran the LIVE reader and asserted a barrier-shift count +
admission set, so it broke on every legitimate reader improvement (it was 8
of the red tests). Its only real invariant (wrong=0) is guarded directly by
the serving runner and test_runtime_wrong_zero_preserved.

No ADR doc references the v4 layer (only S2/S3-post-rescan exist). Remove the
brittle live test plus the now-orphaned generator and its two artifacts.
Keep the frozen, tested v2/v3 layer as the historical record.

Net: -8 brittle tests, removes a maintenance-only archaeology surface rather
than greening it.
2026-06-03 01:38:49 -07:00
Shay
eed293c31c test: update enumeration pins to current system state (no behavior change)
- recognizer_registry: ratified registry grew past round-1 (later ratification
  rounds); assert the Phase C three remain present (subset) rather than pinning
  exactly 3.
- brief_11b: 0021 joined the pre_frame_filler set as the verb-class auditor
  evolved; it refuses, rows[0]=pre_frame_filler, and post-skip still refuses
  (zero-lift verified). Canary 0050 untouched; wrong=0 unaffected (audit-only).
- cli_test_suites: assert the packs invocation against the live _TEST_SUITES
  definition instead of a hardcoded file list (suite grew).
2026-06-03 01:27:40 -07:00
Shay
c655f2fcda fix(rescan-v4): recognize new refusal-reason format + record reader advances
build_rescan's _FIRST_REFUSAL_RE only matched 'no admissible candidate for
...' and required the quote at end-of-string. PR #359 added the
'recognizer matched but produced no injection ... (category=X)' shape, so
extraction returned None and manufactured spurious shifts. Extend the regex
to both shapes and tolerate the trailing (category=...).

With extraction fixed, the live reader legitimately diverges from the v3
baseline on 10 cases (was 2 at the S.4 cut): 5 more first-refusals shifted
one sentence deeper (0019/0023/0025/0027/0047 — overrides added) and 3
refusals became correct admissions (0003/0021/0024 — the +3 that moved
serving 3->6). wrong stays 0; no admission lost. Tests retargeted to the
live counts and artifacts regenerated.

Follow-up: the v4 tests re-run the live reader, so they will drift again on
the next advance; a frozen v5 snapshot (or artifact-derived expectations)
would decouple them.
2026-06-03 01:13:10 -07:00
Shay
b1f12e1dce chore(evals): refresh stale committed reports
G3_numerics report.json: refusal reason-string drift (PR #359 made
recognized-but-uninjectable refusals more specific); verdicts and wrong=0
identical. train_sample coverage probe: admitted_solved 4->6 reflecting
reader coverage gains earlier in the window; wrong=0, safety_rail_intact.
Regenerated via their runners.
2026-06-03 01:01:50 -07:00
Shay
4c0fa30785 fix(ADR-0136.S.3): expose both consumed tokens to completeness guard
_init_mutation_candidates collapses the initial (n_raw) and mutation
(m_raw) source tokens into one derived initial value but only surfaced
n_raw via matched_value_token. The ADR-0191 completeness guard
(uncovered_quantities) then saw m_raw as unconsumed and over-refused
sound compound initial-mutation readings ('had 20 ... lost 8' -> 12).

Expose both via consumed_value_tokens, matching the documented
aggregating-initial contract (_candidate_consumed_tokens) and sibling
composers. Restores S3 lane to 24/24 wrong=0; serving metric unchanged
(6/0/44 before and after — verified by stash compare).
2026-06-03 01:01:50 -07:00
Shay
943368e0ce Add modus ponens oracle corpus 2026-06-02 23:39:20 -07:00
Shay
b304270998
Merge pull request #528 from AssetOverflow/feat/adr-0205-modus-ponens
proof_chain 2.3: modus_ponens + disagreement rule (ADR-0205)
2026-06-02 22:47:06 -07:00
Shay
64f3da18a7 feat: modus_ponens + disagreement rule — proof_chain wrong=0 mechanism (ADR-0205)
Phase 2.3: the first inference rule + the wrong=0 mechanism for proofs.

- generate/proof_chain/rules.py: evaluate_modus_ponens / evaluate_proof_conclusion.
  Proof-layer dispatch (Option B) over proposition FORMULAS via the canonicalizer;
  never touches check_admissibility/_resolve_dep_units (proofs have no units).
  Disagreement rule = the select_self_verified twin: pool ALL admissible single-step
  MP derivations, require a unique canonical key == declared conclusion. Pooling (not
  filter-to-declared-first) is the soundness mechanism.
- generate/logic_canonical.py: parse_top_implication (+ _unparse) — recovers an
  implication's syntactic antecedent/consequent (the ROBDD form doesn't preserve it).
- Closed typed-reason set; the corpus's finer labels consolidate (6 disagreement
  refuse-labels -> conclusion_disagreement; 4 antecedent-flavor labels ->
  unestablished_antecedent — same redundancy, same mechanism-makes-one-distinction
  principle).
- Honesty boundary (exact scope): guarantees a unique conclusion among SINGLE-STEP MP
  over the premises, NOT "uniquely entailed" by all strategies.

Cross-check: all 24 GPT-5.5 adversarial corpus cases agree on OUTCOME against the real
rule (no rule bug / no corpus outcome-misread); reasons consolidate as above.
Mutation: filter-to-declared-first makes DISAGREE-007/010 wrongly admit -> pooling
tests fail (pooling load-bearing).

Drive-by fix (cleanup-as-you-find): merged ADR-0204 ProofNode.__post_init__ was
dedented to module level -> all ProofNode validation was silently DEAD (smoke skips
the dedicated test file; the smoke != full-suite hazard). Re-indented; validation
restored.

Additive (math lane untouched). Full binding-graph surface green; smoke 67.
2026-06-02 20:56:57 -07:00
Shay
0850e6fde7
Merge pull request #527 from AssetOverflow/feat/adr-0204-proof-builder
proof_chain 2.2: proof-graph builder (ADR-0204)
2026-06-02 20:22:34 -07:00
Shay
934b4e557f
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-02 20:07:48 -07:00
Shay
4cae904563 feat: proof-graph builder — proof_chain's first binding-graph consumer (ADR-0204)
Phase 2.2, structure only (no inference rule — modus_ponens is 2.3). Translates a
Proof into a SemanticSymbolicBindingGraph; the ADR-0203 acyclicity guard + ADR-0132
referential integrity fire at construction.

- generate/proof_chain/model.py: the one canonical proof input shape (ProofNode/Proof
  + proof_from_premises desugaring of the corpus premises/conclusion shape).
- generate/proof_chain/builder.py: build_proof_graph — node→symbol+equation;
  canonical_key→rhs_canonical, depends_on→dependencies, rule→operation_kind;
  premises = empty-deps/op="premise"; unit_proof=PROOF_NO_UNIT, admissibility="pending";
  conclusion tracked as conclusion_symbol_id.
- tests: 9 — valid DAG (PC-MP-001 desugared) + multistep construct; PC-CYCLE-001
  refuses THROUGH the real builder (circular_dependency); canonical_key round-trips
  byte-identical + equivalent formulas share rhs_canonical; admissibility-dispatch
  confirmation; self/dangling refusals; out-of-regime node refuses. Mutation-verified
  (drop dep-wiring -> cycle admitted -> test fails).

Admissibility dispatch confirmed graceful on proof operation_kinds: unknown kinds
-> AdmissibilityError(unknown_operation), unitless deps -> unit_unbound; NEVER
misroutes into _check_additive. Named 2.3 constraint: check_admissibility runs
_resolve_dep_units before dispatch, so modus_ponens must bypass unit-resolution.

Open items named for 2.3 (ADR-0204): conclusion typing (BoundUnknown revisit),
semantic_role="unknown" (closed vocab has no "proposition"), unit_proof sentinel.

Additive (first consumer; math lane untouched). Full binding-graph surface green;
smoke 67. Honesty boundary: through 2.3, sound over declared atoms, not grounded in input.
2026-06-02 19:53:41 -07:00
Shay
69c7ba92fa
Merge pull request #526 from AssetOverflow/codex/proof-chain-robdd-corpus
[codex] Add proof-chain ROBDD corpus fixtures
2026-06-02 19:35:44 -07:00
Shay
15307ca1f3 Add proof-chain corpus fixtures 2026-06-02 19:26:46 -07:00
Shay
6ee6bb7915
Merge pull request #525 from AssetOverflow/feat/adr-0201-1-out-of-regime
Canonicalizer hardening: principled out_of_decidable_regime detector (ADR-0201.1)
2026-06-02 19:23:32 -07:00
Shay
69a7d8bae8 feat: principled out-of-regime detector — typed out_of_decidable_regime (ADR-0201.1)
GPT-5.5's independent corpus caught that the canonicalizer refused quantified/
predicate input only by accident (tokenizer chokes on '.'), not by design — a
by-luck-not-by-design refusal the wrong=0 discipline rejects. ADR-0202 §3 names a
typed `out_of_decidable_regime` refusal; the keystone emitted a generic grammar error.

- logic_canonical.py: LogicRegimeError(LogicError) + OUT_OF_DECIDABLE_REGIME;
  _reject_out_of_regime_text (quantifier words forall/exists + symbols ∀/∃, pre-scan)
  and _reject_out_of_regime_tokens (predicate-application ATOM-then-LPAREN), run BEFORE
  the generic grammar error. Refusal only — no predicate/FOL capability added.
- logic_equivalence.py: typed regime branch (before the generic LogicError branch).
- tests: 43 total (10 new) — OOR refuses with typed reason; equivalence path too;
  genuine grammar errors stay plain LogicError (no over-fire); `not (P)` not mistaken
  for predicate application. Mutation-verified by-design (neuter -> falls through to
  generic grammar error).
- ADR-0201.1: additive sub-ADR of 0201 (not an amendment; sub-number preserves the
  landed ADR-0203 forward refs + phase-2 plan numbering). Honesty boundary load-bearing.

Corpus now 22/22 (PC-OOR-001/002 agree on the principled reason). Full canonicalizer
suite green; smoke 67 passed. modus_ponens rule-reasons remain deferred to ADR-0205 (2.3).
2026-06-02 19:12:41 -07:00
Shay
f715c54e3c
Merge pull request #524 from AssetOverflow/feat/adr-0203-acyclicity-guard
proof_chain 2.1: binding-graph acyclicity invariant (ADR-0203)
2026-06-02 18:52:53 -07:00
Shay
451c4b1e17 feat: binding-graph acyclicity invariant — circular_dependency guard (ADR-0203)
proof_chain phase 2.1: the acyclicity guard at the shared binding-graph
construction boundary, before phase 2.2 wiring can build a cyclic-capable structure.

- generate/binding_graph/acyclicity.py: pure find_cycle(adjacency) detector
  (deterministic three-colour DFS; isolated, no model import).
- model.py __post_init__: builds {lhs: deps} adjacency over equations and raises
  BindingGraphError(circular_dependency ...) on a cycle. Runs on every binding
  graph (math + future proof) — illegal states unrepresentable for all consumers.
- tests/test_binding_graph_acyclicity.py: 17 tests (pure checker + construction
  enforcement); mutation-verified non-vacuous.
- ADR-0203: new ADDITIVE invariant referencing ADR-0132 (not an amendment —
  preserves the why-added-later history).

Math-lane regression proof: the only producer (math adapter) is acyclic by
construction (fresh result symbol per op, deps point backward); full
binding-graph + admissibility surface 392 green; guard refuses no existing graph.

Honesty boundary (load-bearing): through phase 2.3, proof_chain is SOUND OVER
DECLARED ATOMS, not grounded in recognized input (grounding is phase 2.4).

full binding-graph/admissibility surface: 392 passed. smoke: 67 passed.
2026-06-02 18:42:40 -07:00
Shay
2631b36148
Merge pull request #523 from AssetOverflow/feat/logic-canonicalizer
proof_chain keystone: propositional canonicalizer + representation contract (ADR-0201/0202)
2026-06-02 16:26:30 -07:00
Shay
e8e0fbb014 feat: propositional canonicalizer keystone + representation contract (ADR-0201/0202)
The proof_chain keystone: a hand-rolled ROBDD canonicalizer mirroring
math_symbolic_equivalence one domain over (normalize -> canonical key ->
byte-equality -> three-valued verdict; REFUSED preserves wrong=0).

- generate/logic_canonical.py: formula -> ROBDD identity under sorted-atom
  ordering; LogicError/LogicBudgetError refusals; inspectable canonical key.
- generate/logic_equivalence.py: EQUIVALENT/NOT_EQUIVALENT/REFUSED wrapper.
- tests/test_logic_canonical.py: 33 standalone tests (canonicity laws,
  discrimination, terminals, determinism, refusals); mutation-verified non-vacuous.
- ADR-0201: canonicalizer decision (ROBDD not CNF/DNF; hand-rolled;
  propositional-only honesty boundary).
- ADR-0202: proposition representation contract — single source the canonicalizer
  and the proof corpus conform to (formula grammar + atom layer binding to ADR-0144
  EpistemicNode + honesty boundary).

Additive: no existing file touched, zero consumers. Standalone keystone only;
binding-graph wiring, acyclicity refusal, and inference rules deferred. smoke: 67 passed.
2026-06-02 16:24:32 -07:00
Shay
1417ffad12
Merge pull request #522 from AssetOverflow/docs/brain-corp-brief
Add Brain Corp technical brief
2026-06-02 12:15:03 -07:00
Shay
b2d1125743 Add Brain Corp technical brief
1-page exec + backing brief for a first technical conversation, written to
survive a skeptical CTO. Every number/status sourced to docs/claims_ledger.md;
substrate-beneath-BrainOS framing held (not perception/motor); verify block is
fresh-clone-green. Prepared for internal review — not for distribution.
2026-06-02 12:07:32 -07:00
Shay
191bfa8fab
Merge pull request #520 from AssetOverflow/codex/amr-decision-substrate
[codex] add AMR decision substrate demo
2026-06-02 11:12:31 -07:00
Shay
f0c270fded harden amr demo replay fixture 2026-06-02 10:38:00 -07:00
Shay
f3680aa302 reconcile amr demo wording with claims ledger 2026-06-02 10:38:00 -07:00
Shay
99c94f71f0 add amr decision substrate demo 2026-06-02 10:38:00 -07:00
Shay
dcb5172f00
Merge pull request #521 from AssetOverflow/docs/claims-ledger-and-reconciliation
docs(claims): claims ledger + ADR-0200 expert-claim reconciliation
2026-06-02 10:37:37 -07:00
Shay
2d18976fa4 docs(claims): ADR-0200 reconciliation — expert claim to audit-passed truth
Reconcile every artifact that asserted the (since auto-reverted) mathematics_logic
expert promotion to the live machine state. Determinism proven intact (Week-1a):
the digest divergence is genuine single-source evidence-drift (GSM8K coverage probe
3/47 -> 4/46 via #310/#488), not a non-determinism defect. ADR-0120's fail-closed
property fired as designed; CORE revoked its own expert claim.

History keeps receipts; current-state reconciles to truth:
- Regenerate expert_claims_math_v1_signed.json -> promote_admitted:false,
  reviewer_signature_matches:false, digest 02f6d3c8.
- reviewers.yaml math_expert_claims: quarantine note; entry kept (mismatch-refusal
  keeps firing); intentionally NOT re-signed.
- ADR-0120-math-expert-ledger-flip: dated valid-at/auto-reverted header note.
- README: "next gate" narrative -> built-attempted-reverted; refresh stale count.
- docs/decisions/README: revert note + ADR-0200 index row.
- 3 fail-closed tests (2 files): "is-expert" -> fail-closed-revert assertions.
  Were RED on main; now green (30 passed).

No eval gate, threshold, or safety boundary changed.
2026-06-02 10:06:16 -07:00
Shay
96b6c5e4b2 docs(claims): ADR-0200 + claims ledger — single source of truth (additive)
Add docs/claims_ledger.md (every public claim -> in-repo evidence, with the
four GSM8K numbers separated and labeled) and ADR-0200 recording the
mathematics_logic expert fail-closed revert as designed behavior (proven
single-source evidence-drift; determinism intact).

Additive only: no existing claim, eval gate, or test is touched. The
review-gated reconciliation diffs (reviewers.yaml quarantine note, ADR-0120
header note, signed-JSON regen, README narrative, 3 test reconciliations) are
listed in ADR-0200 section 4 and await operator ratification.
2026-06-02 09:35:44 -07:00
Shay
c058d966f9
Merge pull request #518 from AssetOverflow/docs/adr-index-0197-0199
docs(adr): backfill ADR index — 12 missing rows (0184–0195, 0197–0199)
2026-06-02 06:34:59 -07:00
Shay
ab9e1afe04 docs(adr): backfill ADR index — add 0184-0195 + 0197-0199 (12 missing rows)
The decisions README index table stopped at ADR-0183 then jumped to
ADR-0196, omitting every top-level ADR that landed in between plus the
three newest. PRs #513/#514/#515 added ADR-0197/0198/0199 files without
touching the index; the 0184-0195 gap predates them.

Adds 12 rows in numeric order with title + status pulled from each ADR
header. No content/runtime change; pure index backfill.

- files-vs-index drift check: now empty (was 12)
- all 12 new link targets verified to resolve
2026-06-01 12:14:44 -07:00
Shay
f8b6f91627
feat(learning-arena): ADR-0199 PR-2 — extract domain-agnostic run_practice (#516) 2026-05-31 21:07:23 -07:00
Shay
1d12c3b01e
docs(adr): ADR-0199 cross-domain learning arena contract (#515) 2026-05-31 20:36:56 -07:00
Shay
918eb5552d
docs(adr): ADR-0198 motor efferent decoder spike (#514) 2026-05-31 20:32:26 -07:00
Shay
81062bb0fb
ADR-0197: vision compiler over Delta-CRDT (docs only) (#513) 2026-05-31 20:32:08 -07:00
Shay
1c56ac710e
Merge pull request #512 from AssetOverflow/fix/lilibeth-canary 2026-05-31 20:31:03 -07:00
Shay
77feadaab1
Merge pull request #511 from AssetOverflow/feat/adr-0180-crdt-contract-lock 2026-05-31 20:30:26 -07:00
Shay
497d1a9a22
Merge pull request #510 from AssetOverflow/feat/lang-foundation-reland 2026-05-31 20:30:00 -07:00
Shay
cd97d59f13 fix(math): restore WAVE-A multiplicative aggregate completeness provenance
The ADR-0191 completeness guard (added after WAVE-A) requires aggregating
initials to expose every consumed source token via
`consumed_value_tokens`, so it can confirm no source quantity was
silently dropped.  The WAVE-A "each weighing" injector predates the guard
and left that field empty, so for every "<Subject> <verb> M <outer>, each
... N <unit>" reading the guard computed required={M, N} vs
consumed={M*N} and refused as "incomplete reading: source quantities
[M, N] not consumed".  This silently regressed the entire WAVE-A
capability — the canary `test_lilibeth_canary_solves_end_to_end` has been
red on main (it failed byte-identically on f79b647; the smoke gate does
not collect this dedicated test file, so it merged green originally).

Fix: populate `consumed_value_tokens=(count_a_token, count_b_token)` on
the composed initial — exactly the contract the day-enumeration and
embedded-quantifier aggregators already satisfy.

wrong==0 preserved: the two tokens genuinely ARE the multiplicands of the
emitted value; the guard remains refusal-only.  Serving frozen: this
shape does not occur in train_sample, so the fix is serving-neutral.

Evidence:
- tests/test_wave_a_multiplicative_aggregation_injector.py: 11 passed
  (was 1 failed) incl. test_wrong_zero_preserved (full train_sample eval,
  wrong==0).
- core test --suite packs: 141 passed (was 1 failed, 140 passed).
- core test --suite smoke: 67 passed.
- scripts/verify_lane_shas.py: lanes 8/8 match pinned SHAs — the
  train_sample_v1 serving SHA is byte-identical, proving zero serving
  count change.

PR checklist:
- Capability: restores WAVE-A multiplicative-aggregate reading regressed
  by ADR-0191.
- Invariant: wrong==0 (completeness guard stays refusal-only; tokens are
  true multiplicands).
- Lane: core test --suite packs / smoke + lane-SHA gate (8/8).
- No hidden normalization, stochastic fallback, approximate recall, or
  unreviewed mutation.
- Trust boundary: none widened — internal candidate provenance only.
2026-05-31 16:36:44 -07:00
Shay
7628395b29
Merge pull request #509 from AssetOverflow/docs/adr-0196-zig-doctrine-ratify
docs: ratify Zig native-substrate doctrine (ADR-0196)
2026-05-31 16:32:11 -07:00
Shay
3fba044e51 docs(zig): add STATUS.md handoff page (current state, next action, gotchas)
Single portable 'where we are / what's next' page for the Zig native-substrate
work: gate status per component, the locked G1 CRDT contract, the immediate
next action (a new ADR to clear G2 before any Zig code), env gotchas
(PYO3_PYTHON=3.12; the public_demo wall-clock lane flake), and verification
commands. Lets any operator/agent resume cold.

Sibling links resolve once #509 (docs/zig/**) lands on main; merge #509 first.
2026-05-31 16:30:44 -07:00
Shay
92ea9ee6f5 feat(vault): lock Delta-CRDT reference contract (ADR-0180 -> Accepted, gate G1)
Establishes the canonical Delta-CRDT reference contract so a future native
(Rust/Zig) backend is gate-G1-eligible under ADR-0196 — the ZC-0 'contract
pinning' slice. No Zig code; ZC-1+ remains gated at G2.

- vault/crdt.py: canonical Python reference (ArenaEntry, Delta, LocalArena,
  merge_kernel, canonical_bytes, delta_hash). Pure content law — content-
  addressed by IEEE-754 bits then provenance; no normalization, no versor
  closure, no global Vault writes.
- ZC-0 contract tests (semilattice C-1..C-5; content ordering / signed-zero /
  NaN bit-addressing; C-7 no-global-write) — all failable (mutation-checked:
  no-dedup breaks C3/C5, arrival-order breaks C1).
- Golden fixture corpus (tests/fixtures/crdt/) regenerated deterministically
  from the reference; single source of truth also emits the Rust expected hex.
- core-rs: Delta::canonical_bytes + test_crdt_hash_parity.rs proving Rust
  produces byte-identical canonical_bytes to the Python reference.
- ADR-0180 -> Accepted: locked contract, byte layout, obligation map, and the
  explicit boundary that no Zig is authorized.

Verification: ZC-0 21 passed, Rust arena+parity 16 passed, architectural
invariants 40 passed, smoke 67 passed. Serving frozen: 7/8 lane SHAs match;
the public_demo miss is a pre-existing wall-clock budget overrun (ADR-0099,
~46-48s > 30s) reproduced identically on clean main — environmental.
2026-05-31 16:25:21 -07:00
Shay
18a679c29d feat(packs): reland en_core_syntax_v1 foundation vocabulary (collision-audited)
First foundation-curriculum substrate pack: 24 reviewed noun lemmas for
syntax/claim/provenance vocabulary (subject, predicate, agent_role,
clause, antecedent, consequent_role, polarity, negation, evidence_span,
…).  Substrate only — no parser, no runtime generation change.

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

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

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

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

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

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

PR checklist:
- Capability: foundation-curriculum language substrate vocabulary.
- Field invariant: pack loads through checksum-sealed compiler; no
  algebra/versor path touched.
- Lane: core test --suite packs / smoke; dedicated pack test.
- No hidden normalization, stochastic fallback, approximate recall, or
  unreviewed mutation.
- Trust boundary: language-pack loading only; manifest checksums hash the
  exact bytes on disk (recomputed after the two renames).
2026-05-31 16:18:26 -07:00
Shay
9ead270fda docs: ratify Zig native-substrate doctrine (ADR-0196)
Bring the docs/zig/** decision package and README 'Native Substrate
Direction' section into main, and record ADR-0196 (Accepted) as the
binding ratification.

The doctrine is explicitly NOT a wholesale Zig rewrite. It establishes a
ring architecture (Python = semantic source of truth; Rust = incumbent
algebra backend; Zig = Ring 1 native-substrate candidate only) and the
G0-G8 adoption gate ladder. Zig may enter only by clearing gates against
a locked reference contract; default-by-availability is forbidden.

ADR-0196 forward-references ADR-0180 as the first G1 instantiation (the
CRDT contract lock / ZC-0 slice). No Zig code is authorized.
2026-05-31 15:13:19 -07:00
Shay
f79b647671
Merge pull request #508 from AssetOverflow/chore/revert-503-broken-syntax-pack
Revert #503: en_core_syntax_v1 pack shipped with failing tests
2026-05-31 09:02:32 -07:00
Shay
5c75853a0b Revert "Merge pull request #503 from AssetOverflow/feat/en-core-syntax-relations-v1"
This reverts commit 2405e9b06e, reversing
changes made to 95bf0dfa60.
2026-05-31 09:00:15 -07:00
Shay
69b89df606
Merge pull request #495 from AssetOverflow/feat/adr-0175-propose-step
feat(adr-0175): wire the PROPOSE step — autonomous attempt-and-eliminate loop closes
2026-05-31 08:37:23 -07:00
Shay
2b299c1090
Merge pull request #501 from AssetOverflow/docs/foundation-curriculum-roadmap
docs(curriculum): foundation curriculum roadmap
2026-05-31 08:37:20 -07:00
Shay
2405e9b06e
Merge pull request #503 from AssetOverflow/feat/en-core-syntax-relations-v1
feat(language): add en_core_syntax_v1 foundation pack
2026-05-31 08:35:30 -07:00
Shay
95bf0dfa60 docs(handoff): next-subjects readiness brief for ChatGPT (GitHub-connector lane)
Execution-free brief for the read-only GitHub-connector lane: comprehension-
primitive inventory, question-layer gap survey, subject-readiness recommendation,
then gated capability-axis/ADR/corpus drafts. Proposal-only; serving stays frozen
(wrong=0, pinned lanes). Companion to the Claude execute/validate/commit lane.
2026-05-31 08:19:12 -07:00
Shay
f0c51b002c docs(curriculum): note syntax consequent role collision boundary 2026-05-31 03:58:41 -07:00
Shay
3a5a08b6e8 test(language): pin consequent ownership during syntax pack tests 2026-05-31 03:57:06 -07:00
Shay
6b58839beb fix(language): refresh syntax pack checksums 2026-05-31 03:55:52 -07:00
Shay
71c4e83f2c fix(language): update syntax gloss collision rename 2026-05-31 03:55:26 -07:00
Shay
4dfc7fd764 fix(language): avoid syntax consequent collision 2026-05-31 03:54:15 -07:00
Shay
74570757df docs(curriculum): document en core syntax pack 2026-05-31 03:52:24 -07:00
Shay
9866ee8d05 test(language): loosen syntax gloss wording invariant 2026-05-31 03:51:44 -07:00
Shay
5321130882 test(language): pin en core syntax pack 2026-05-30 19:16:12 -07:00
Shay
65eab68080 feat(language): register syntax pack in resolver 2026-05-30 19:15:37 -07:00
Shay
27f677971d feat(language): add en core syntax glosses 2026-05-30 19:14:11 -07:00
Shay
212b078f72 feat(language): add en core syntax lexicon 2026-05-30 19:13:40 -07:00
Shay
ae84ce8641 feat(language): add en core syntax pack manifest 2026-05-30 19:12:45 -07:00
Shay
f8d9a36eeb docs(curriculum): add foundation curriculum tracker 2026-05-30 18:35:47 -07:00
Shay
e74a51f5db docs(curriculum): add foundation curriculum roadmap 2026-05-30 18:35:13 -07:00
Shay
9df1e6522b
feat(adr-0195): GSM8K product promotion bridge — serving 4/46/0 → 6/44/0, wrong=0 (#500)
Narrow product promotion boundary (`generate/derivation/product_bridge.py`)
wired into `generate/math_candidate_graph.py`: only complete pure-product
derivations with a product-target question and no known hazard surface lift
from the sealed pooled derivation reader into serving.

- Serving train_sample: 4/46/0 → 6/44/0, wrong=0; case 0050 still refused.
- Renumbered from the collided ADR-0194 (labeled-container, #499) to ADR-0195
  and rebased onto current main.

CI: smoke + verify-pinned-lane-SHAs green on the merge commit.
2026-05-30 17:33:56 -07:00
Shay
84db129629
feat(adr-0194): labeled-container subject entity shape — 'Jar A has N' parses, wrong=0-proven (substrate) (#499)
GSM8K labels containers/regions with a trailing single-letter or short-numeric
label ('Jar A has 28 marbles', 'Section G has 10 cars', 'District 2 has 19
voters'); the initial-possession entity slot captured only 'Jar' and the label
broke the match. Adds a separate sibling pattern _INITIAL_HAS_LABELED_RE
(mirroring ADR-0136.S.4 localisation) that REQUIRES the label, so the global
_ENTITY is unchanged and bare subjects yield no duplicate candidate.

- Composes with ADR-0193 aggregate question: 'Jar A has 28 marbles. Jar B has
  12 marbles. How many marbles are there in total?' -> 40.0.
- 0 real-corpus metric flip (honest substrate): the one real multi-container
  aggregate additionally needs comparative + multiplicative + lowercase-ref.
- wrong=0 HOLDS full corpus (7,473 q); train_sample byte-identical 4/46/0;
  synthetic-registry capability-axis gate + G5 lane green; smoke 67 passed.
- Label bounded by the possession verb: multi-word nouns ('Jar Apple') do NOT
  match. wrong=0 held downstream by completeness + round-trip + disagreement.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:56:09 -07:00
Shay
39bf549e94
feat(adr-0193): aggregate existential question frame — 'are there <cue>' composes, wrong=0-proven (#498)
Extends ADR-0131.G.5's total-across aggregate branch with the existential
verb frame 'How many <unit> are there <cue>?' over the SAME closed cue
vocabulary (no cue-set widening). The solver already sums entity=None
total-across; the wall was purely the question parser's verb-frame coverage.

- Composes end-to-end: 'Jamie has 28 marbles. Kyle has 12 marbles. How many
  marbles are there in total?' -> 40.0 (load-bearing, not inert).
- 23 real GSM8K problems now parse the question (advance past the Q-wall to
  the statement-parse wall).
- wrong=0 HOLDS on the full 7,473-q corpus; train_sample byte-identical
  4/46/0; no metric delta (composition-wall lesson, third instance).
- Cross-ADR discipline: 'What is the total number of <unit>?' is DEFERRED,
  not contradicted — ADR-0131.G.5 pins it as a refusal probe; promoting it
  must amend that ADR's closed-cue contract. test_total_number_of_still_deferred
  locks the boundary.

Firewall: completeness guard (ADR-0191) + question round-trip refuse conjoined
('dogs and cats') and derived ('animal legs') units. G5 lane + synthetic-registry
wrong=0 capability-axis gate green; smoke 67 passed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:35:22 -07:00
Shay
a7024bb1f8
feat(adr-0192): open discrete_count noun class — 8x statements parse, wrong=0-proven (substrate) (#497)
The discrete_count matcher gated the counted noun on a CLOSED ratified set
(observed_counted_nouns): 'Betty has 24 marbles' matched, 'Randy has 60 mango
trees' / 'Sam has 12 red apples' did not — purely because the noun was unseen.

Open the single-anchor possession/acquisition path to an open noun phrase
(adjective* + 1-3 word head, bounded by a stop-word lookahead so it never
swallows a trailing PP), keeping every other narrowness layer (proper-noun
subject, verb whitelist, single numeric token, no clause-split). Closed
observed nouns still match (capitalized compounds preserved); compound
enumeration stays closed.

Safe because ADR-0191 moved the wrong=0 guarantee downstream: an open-vocab
mis-parse hits the completeness guard + round-trip + branch-disagreement.
Proof: full real corpus 61->494 discrete_count anchors (8x), wrong=0 HOLDS,
zero confabulations.

Substrate PR — 0 metric delta by design (train_sample byte-identical 4/46/0;
the problems still need composition downstream). Value: the foundation every
discrete_count flip consumes, and empirical proof open-vocab is firewall-safe.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:06:25 -07:00
Shay
25580e18b0
fix(adr-0191): candidate-graph completeness guard — real-corpus wrong 5→0 (#496)
* fix(adr-0191): candidate-graph completeness guard — real-corpus wrong 5->0

The candidate-graph reader (serving) checked grounding + round-trip but had
no completeness obligation, so problems whose later clauses failed to parse
emitted a partial reading. Over the full 7,473-question real GSM8K train
split this confabulated 5 answers (wrong!=0) the 47-case train_sample hid;
2 were regressions from #488.

Add the missing admissibility leg (mirrors the derivation reader's verify.py):
every source quantity (all statements + question) must be consumed by the
chosen reading, else refuse. Refusal-only -> cannot create a wrong answer.
Number-sense is pack-authoritative (en_numerics_v1 parse_compound_cardinal +
lookup_multiplier + all 6 currency symbols) so it never disagrees with the
engine; aggregating initials expose consumed_value_tokens provenance.

Evidence: real-corpus wrong 5->0, correct held at 4; train_sample byte-
identical 4/46/0; G1-G5+S1+G3.1 green; smoke 67 passed; math_teaching_corpus
lane byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(adr-0191): committed full-corpus GSM8K microscope (standing wrong=0 + coverage instrument)

Promotes the throwaway tmp/ microscope that found the 5 confabulations into a
committed tool. Runs the canonical serving reader over any GSM8K corpus and
reports failures-first: correct/wrong/refused, every wrong answer by name,
refusal families, and the no-injection per-category coverage map that ranks
which injector to build next by real frequency.

Default corpus is the committed 47-case train_sample (always available);
--corpus path runs the full real split. This is the ADR-0191 follow-up: re-run
after every capability PR, not just train_sample — a flip is only real if it
does not widen the confabulation surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:45:07 -07:00
Shay
b82897a0dd feat(adr-0175): wire the PROPOSE step — autonomous loop closes (attempt->tether->ledger->propose)
The attempt/score/ledger half existed (run_practice -> ClassTally scored vs
gold); nothing consulted the gate to turn earned reliability into a ratifiable
proposal. Adds core/reliability_gate/propose.py (propose_from_ledger +
RatifiableProposal): for each class, license_for(PROPOSE) emits a proposal iff
its conservative Wilson floor (0 below N_MIN=10) clears theta=0.85. Refusals
never penalize; deterministic; PROPOSAL-ONLY (never a serving mutation).

propose_runner.py closes the loop end-to-end with an aggressive sealed scorer
(resolve_pooled): practice 95c/5w/50r -> ONE proposal (additive, reliability
0.8608>=0.85, 95/100); 5 wrongs tolerated but floor held; rest stayed sealed.
The gold-tethered autonomous contemplation: the engine earns the right to ASK,
not to SERVE. 11 failing-under-violation tests.
2026-05-30 13:50:24 -07:00
Shay
0770648257
feat(GSM8K): comprehension reading → first metric move 3/47/0 → 4/46/0 (#488)
* feat(adr-0189): comparative reading — anchor-verb widening + multi-word units

The candidate-graph comparative extractor (ADR-0131.G.2) read only has/have +
single-word units, so real-GSM8K comparatives ('Brooke does three times as many
jumping jacks as Sidney') didn't parse — a dark statement in 17 places blocking
15 of the 47 refused train_sample cases, despite the ADR-0123 solver already
supporting compare_additive/compare_multiplicative.

Widens the anchor-verb set (reusing legacy vetted lemmas + does/collected/
gained/studied…), EXCLUDING polarity-inverting verbs (lose/spend/give/sell/win)
to preserve wrong=0; admits 1-2 word units via the existing multi-word
_unit_grounds branch. Feeds the existing solver unchanged.

wrong=0 proven: G2_comparatives 29/29, G3 20/0, G4 32/32, train_sample 3/47/0
byte-identical; polarity-inverting verbs proven refused (failing-under-violation).
Chain composes correctly in isolation (146 -> 438). Flips 0 cases ALONE — every
comparative case needs a composing partner (aggregation / multi-word-noun
injection); this ships the component, not yet a flip.

- generate/math_candidate_parser.py: _comparison_anchor_verb widening + 1-2 word
  unit slots in the two multiplicative comparative regexes.
- tests/test_adr_0131_G2a_*: 5 tests incl. polarity-inversion wrong=0 guards.
- docs/decisions/ADR-0189: gap, change, wrong=0 evidence, honest scope.

* feat(adr-0189a): first metric move 3/47/0 -> 4/46/0 (case 0024, comprehension-composed)

Case 0024 now SOLVES (answer 438) by composing three general comprehension
capabilities feeding the unchanged ADR-0123 solver:
  1. day-of-week count enumeration: Sidney = 20+36+40+50 = 146
     (_day_enumeration_candidates; derived sum grounds via first count token,
      mirroring _embedded_quantifier; closed to the 7 day names)
  2. comparative reading (ADR-0189): Brooke = 3 x Sidney
  3. activity question 'How many <unit> did <Entity> <verb>?' (_Q_DID_RE)
Plus do/does/did added to the CandidateInitial anchor whitelist (production-
possession), admitted only via the closed day-enumeration shape.

wrong=0 PROVEN across every lane: all 8 capability axes wrong=0 (G2_comparatives
29/29, G3 20/0, G4 32/32, G5/S1/S3/S4 all pass), train_sample 4/46/0 wrong=0,
verify_lane_shas exit 0 (no pinned lane changed), generate_claims --check OK.
872 tests pass; new tests are failing-under-violation incl. wrong=0 guards
(non-day comma list not summed; polarity-inverting comparative verbs refused).

Re-baselined report.json + train_sample_coverage_report.json (latter also clears
pre-existing reason drift) + CLAIMS.md to the new 4/46/0 metric. Decode-not-guess:
0024 solved by READING its structure, not storing an answer. Remaining pre-existing
failures (G3 committed-report, telemetry) unrelated, fail on pristine main.

- generate/math_candidate_parser.py: day-enum extractor + _Q_DID_RE + does-anchor.
- tests/test_adr_0189a_day_enum_activity.py: 5 tests (incl. end-to-end 0024=438).
- docs/decisions/ADR-0189a + report.json/coverage/CLAIMS re-baseline.
2026-05-30 09:21:48 -07:00
Shay
a719f1b58c
feat(adr-0184): extract semantic-state helper seam S1 (#490)
* feat(adr-0184): add semantic-state helper package

* feat(adr-0184): add referent binding helpers

* feat(adr-0184): add change cue helpers

* refactor(adr-0184): use semantic-state helpers in accumulation

* test(adr-0184): cover semantic-state helper guards
2026-05-30 08:36:02 -07:00
Shay
390b6f376d
docs: semantic-state transition blueprint and ADR-0184 scope (#489)
* docs: add semantic state transition blueprint

* docs: add ADR-0184 semantic state transitions

* docs: add RB-GSM solver design input notes
2026-05-30 08:35:50 -07:00
Shay
171b1cd7a5
feat(adr-0186): sealed candidate-graph injector lane + topology correction (#487)
* docs(adr-0186): sealed candidate-graph injector lane (resume ADR-0170 W2-W5 under ADR-0175 seal)

Topology audit found two disjoint GSM8K readers: the candidate-graph reader
owns the official 3/47/0 metric and already has divide/multiply/compare; its
wall is the recognizer->injection coverage gap (ADR-0170 W2-W5 backlog), not
arithmetic. The derivation reader (resolve_pooled) is a separate sealed organ
that cannot reach the goal without an unbuilt Phase-5 bridge.

ADR-0186 reconciles ADR-0170's injector roadmap with ADR-0175's serving seal:
develop W2-W5 injectors behind a default-off 'sealed' flag on inject_from_match,
measured on a new report_sealed.json, with frozen 3/47/0 byte-identical until a
reviewed Phase-5 promotion. wrong=0 gated on both paths.

Live-loop instrumentation found a schema-vs-extraction split: rate_with_currency
and temporal_aggregation matchers already extract full anchors (blocked on the
CandidateRate union / apply_rate primitive), while discrete_count/currency/
multiplicative are blocked on matcher extraction. And no refused case is one
injector away - every case is multi-statement, so the unit of measurable
progress is a target case's complete injector+composition set. Sequencing:
seal mechanism first, CandidateRate schema next, then a first complete
target-case unlock.

* feat(adr-0186): sealed candidate-graph injector lane mechanism (default-off)

Adds the seal mechanism for developing ADR-0170 W2-W5 injectors without
mutating the frozen serving metric:

- inject_from_match(match, sentence, *, sealed=False): when sealed=True,
  consults the new _SEALED_INJECTORS table first; default-off never touches it.
- _SEALED_INJECTORS: empty at land (this PR ships the mechanism; the first
  sealed capability is the CandidateRate schema, ADR-0186 §5.3).
- parse_and_solve(text, *, sealed=False): threads the flag to the per-statement
  injection site. The seal is injector eligibility, not a forked reader.

wrong=0 guarantees (tests/test_adr_0186_sealed_injector_lane.py, failing-
under-violation): empty seal is a strict no-op; a registered sealed injector
admits ONLY under sealed=True and is invisible to the frozen path; train_sample
report stays 3/47/0. Verified: coverage probe 3/47/0 byte-identical, smoke +
architectural invariants green, 288 candidate-graph/recognizer tests pass (2
pre-existing failures unrelated, confirmed on unmodified main).

* docs(adr-0185): mark superseded by ADR-0186 (premise refuted by topology audit)

The topology audit proved ADR-0185's premise ('the engine cannot divide') is
true only of the derivation reader, which is disjoint from the candidate-graph
reader that owns the official 3/47/0 metric. The goal organ already divides;
its wall is injection coverage. ADR-0185 is retained as a record, not
implemented; cross-referenced from ADR-0186 §header.
2026-05-30 08:35:43 -07:00
Shay
e195a229c9
feat(adr-0184): distinct-unit product rule — sealed reader wrong 13→8 (#486)
* docs(adr-0184): scope the distinct-unit product rule — cut the product-of-all over-commit

The 47-refusal coverage diagnostic surfaced that the headline 3/47/0 (serving
recognizer) hides the sealed comprehension reader's real state: resolve_pooled over
the 50 real train_sample cases is 2 correct / 13 WRONG / 35 refused. The confuser
probe's wrong=0 was a misleading proxy — all 13 real wrongs are the whole-text
product-of-all, the unique complete candidate, committed unopposed.

Scopes the first lever, decided by MEASURING candidate refusal rules against the real
metric (correct up, wrong down on train_sample):

  baseline                         2 / 13 / 35
  distinct-unit product (chosen)   2 /  8 / 40   <- cuts 5, zero coverage loss
  product spans >1 clause          1 /  4 / 45   <- destroys correct 0003
  drop all products                0 /  2 / 48

The distinct-unit rule: multiply/divide may compose DISTINCT units but a multiply
step whose operand repeats a non-empty unit already in the product (apples x apples,
cards x cards) is unit-incoherence -> refuse (unit^2 is never the answer). Empty-unit
operands exempt (0003 multiplies a blank-unit 0.75). Dimensional, not lexical
(ADR-0165-safe); refines verify.py clause 3 shared by self_verifies + classify.

Honest scope: 13->8, NOT 0. The remaining 8 are distinct-unit products in the wrong
shape (rate problems) = cue precision (ADR-0177 CP-2b), the next lever, NOT to be
faked with a per-case rule. Establishes the real scoreboard (resolve_pooled over
train_sample) and notes the ratification bridge (ADR-0175 Phase 5) as the separate
dependency for any of this to reach the serving headline.

Spec only; serving 3/47/0 untouched (verify is not on the serving path).

* feat(adr-0184): distinct-unit product rule — sealed reader real-GSM8K wrong 13->8

Cuts the over-eager product-of-all on real GSM8K. The sealed comprehension reader
(resolve_pooled over train_sample) was 2 correct / 13 WRONG / 35 refused; all 13 are
the whole-text product-of-all committed unopposed (0042->2.4M, 0048->19200,
0001->14400). This is the first lever measured against the REAL metric (resolve_pooled
over train_sample), not the curated confuser count.

Mechanism (verify._is_repeated_unit_product + classify_derivation downgrade):
a pure multiplicative chain whose operands repeat a non-empty unit forms unit^2
(apples x apples, cards x cards) -- never the answer; it is the product-of-all
multiplying independent groups. Such a product is classified `exempt`
(commit-INELIGIBLE), NOT removed. Empty units exempt (0003 multiplies blank-unit
0.75); divide exempt (feet/feet = legitimate count). Dimensional, not lexical
(ADR-0165-safe).

Implementation finding (folded into ADR §3.1): the naive version put the predicate
in the shared _base_reasons gate, which DROPPED the product and regressed the
confuser probe 1->3 -- the disguised-polarity 0001/0003 refuse only because the
coins x coins product DISAGREES with the coins + coins accumulation reading; dropping
it unmasked the additive reading as a unique wrong commit (80/30). The fix is the
downgrade: keep it as a commit-ineligible `exempt` candidate so it still forces the
disagreement. Pinned by test_downgrade_not_removal_preserves_disagreement_refusal.

Evidence (sealed lane; chat/ does not import verify -> serving frozen):
- resolve_pooled over train_sample: 2 correct / 8 wrong / 40 refused (was 2/13/35);
  the 5 repeated-unit products (0001/0017/0042/0045/0048) now refuse, 0003/0021 kept.
- confuser probe: wrong unchanged (no 0001/0003 regression), positives still solve.
- serving train_sample 3/47/0 and practice (accumulation + search) 3/47/0
  byte-identical; self_verifies/_base_reasons unchanged so search lanes are untouched.
- 171 derivation/pool/verify tests + 40 architectural invariants green.

Honest scope: 13->8, NOT 0. The remaining 8 (0011/0016/0018/0019/0025/0028/0032/0047)
are distinct-unit products in the wrong shape (rate problems) = cue precision
(ADR-0177 CP-2b), the next lever -- not to be faked with a per-case rule. Carries the
corrected ADR-0184 (supersedes the spec-only #484).
2026-05-30 08:35:35 -07:00
Shay
4e36abff6e docs: add edge sync completion status 2026-05-29 19:50:44 -07:00
Shay
cde30f5b05 test(sync): prove optional S3 adapter behavior 2026-05-29 19:47:42 -07:00
Shay
dc9e4b5b11 feat(sync): export optional S3 adapter 2026-05-29 19:47:24 -07:00
Shay
b415486c84 feat(sync): add optional S3 object-store adapter 2026-05-29 19:47:08 -07:00
Shay
6a5c8d1591 test(sync): guard hot path from object-store imports 2026-05-29 19:46:49 -07:00
Shay
aa542cad13 feat(sync): export object-store seam 2026-05-29 19:46:31 -07:00
Shay
bea79c12e2 feat(sync): add object-store adapter seam 2026-05-29 19:46:15 -07:00
Shay
7e091525bf test(sync): prove local sync journal semantics 2026-05-29 19:45:57 -07:00
Shay
b9cc7d8988 feat(sync): export local sync journal 2026-05-29 19:45:38 -07:00
Shay
d11c792ef2 feat(sync): add local sync journal 2026-05-29 19:45:20 -07:00
Shay
3741c1321a test(sync): prove edge artifact activation ledger 2026-05-29 19:19:07 -07:00
Shay
817cbb920a feat(sync): export activation ledger 2026-05-29 19:18:35 -07:00
Shay
f645dc2bdb feat(sync): add edge artifact activation ledger 2026-05-29 19:17:52 -07:00
Shay
da5c2a9e76 test(sync): prove edge manifest validation rules 2026-05-29 19:01:00 -07:00
Shay
63462ffbf7 feat(sync): export manifest validator 2026-05-29 19:00:39 -07:00
Shay
e81c4385a1 feat(sync): add edge artifact manifest validator 2026-05-29 19:00:15 -07:00
Shay
646229f803 test(sync): prove edge artifact authority contract 2026-05-29 18:58:49 -07:00
Shay
9b333a40a1 feat(sync): export edge artifact authority model 2026-05-29 18:58:23 -07:00
Shay
aab1e70cb9 feat(sync): add edge artifact authority model 2026-05-29 18:58:10 -07:00
Shay
c20842c3f3 docs: add edge sync implementation brief 2026-05-29 18:56:50 -07:00
Shay
76fe5be379 docs: add edge sync artifact contract 2026-05-29 18:52:54 -07:00
Shay
c654b3f22d docs: add edge S3 persistence architecture 2026-05-29 18:50:11 -07:00
Shay
f008550d79
feat(adr-0182): anchor-skip + intra-clause accumulation — distractor 0016 refuses, twin 0017 solves (confuser wrong 1→0) (#481)
* feat(adr-0182): anchor-skip + intra-clause accumulation — distractor 0016 refuses, twin 0017 solves (confuser wrong 1->0)

The last confuser wrong. 0016 ("A train travels 60 miles per hour for 2 hours. Tom
has 8 tickets and buys 4 more tickets. How many tickets?") committed the blunt
product 60x2x8x4=3840 because it was the unique complete reading: the train sentence
is an all-foreign anchor-position block (2 quantities, can't seed an anchor) and the
Tom sentence packs state+change in ONE sentence ("has 8 ... and buys 4 more"), which
the sentence-level reader couldn't decompose -> accumulation_candidates was empty ->
no rival reading -> product committed.

The microscope confirmed intra-clause state+change is a REAL GSM8K pattern (train-0010
"Yun had 20 paperclips initially, but then lost 12"; practice-0121 "Sam has 30 apples
and gives 10 to Anna") -- so this is genuine comprehension, not a 0016-only patch.

Mechanism (added to accumulation_candidates ONLY -> feeds only the pool; train_sample
serving + practice use compose_accumulation, unchanged -> 3/47/0 byte-identical by
construction):
- _sub_clauses: sentence clauses further split on coordinating conjunctions
  (and / then / and then). LOCAL to the ungated candidate generator -- the global
  segmenter (GB-1/GB-2/serving) is untouched.
- _build_accumulation_anchor_skip: anchor = first single-quantity sub-clause (leading
  non-anchorable all-foreign blocks are skipped); chain the conjunction-mate change
  ("buys 4 more" -> +4). Referent guard + polarity-cue requirement still gate it
  (a no-cue sub-clause -> refuse), so GB-2 same-unit lists ("6 apples and 4 apples")
  produce no spurious candidate.

Result (sealed lane):
- confuser wrong 1 -> 0. distractor-quantity 0 wrong / 2 refused: 0016 refuses
  (product 3840 [complete] vs additive 12 [exempt] disagree). BONUS: the clean twin
  0017 ("Tom has 8 tickets and buys 4 more tickets", no distractor) now *solves* 12
  -- genuine comprehension of a real positive, the comprehension-vs-surface-match
  discrimination the corpus exists to measure. genuine positives 7 -> 8 solved.
- the only non-clean verdict left is 1 spurious (0010 multi-referent "altogether" --
  a separate H1 graduation question, not this lever).
- train_sample 3/47/0 and practice 3/47/0 byte-identical; 243 derivation/pool tests
  + 40 architectural invariants green.

Tests:
- test_adr_0182_pool.py TestAnchorSkipIntraClause: intra-clause twin resolves to 12;
  the 0016 anchor-skip candidate classifies `exempt`; 0016 refuses via disagreement;
  no spurious extra candidate without a conjunction.
- test_adr_0163_f2_confusers.py: baseline wrong 1->0, positives_solved 7->8; renamed
  test_distractor_quantity_refuses asserts BOTH 0014 and 0016; new
  test_intra_clause_twin_0017_solves.

Stacked on #480 (prior-state guard); merge #480 first. Confuser arc wrong 7->5->2->1->0.

* test(adr-0182): close anchor-skip refuse-branch coverage gap (lookback finding)

The four-PR lookback review (EX-6/pooling/prior-state/anchor-skip) found the
anchor-skip refuse branches in _build_accumulation_anchor_skip untested: the
referent guard (new named actor -> refuse) and the polarity-cue requirement were
asserted but not proven (no test would fail if removed). Per the schema-obligation
discipline, add NON-VACUOUS failing-under-violation tests:

- test_anchor_skip_referent_guard_discriminates_actor: identical structure, change
  sub-clause subject is a pronoun ('he', same referent -> 12 IS produced) vs a new
  name ('Sara' -> guard suppresses it). Removing _same_referent makes the new-actor
  case also produce 12 -> the second assertion fails. (Non-vacuous: the positive
  control proves the path is reachable, so the negative isn't trivially-empty.)
- test_anchor_skip_requires_a_polarity_cue: 'gets 4 more' (cue -> 12) vs 'owns 4'
  (no cue -> not guessed). Removing the polarity gate makes the no-cue case produce
  a reading -> fails.
- test_anchor_skip_refuses_without_single_quantity_anchor: no single-quantity
  sub-clause -> () (does not force a multi-quantity clause to anchor).

No code change; behavior unchanged. Confuser wrong stays 0; 281 derivation tests +
40 invariants green.
2026-05-29 15:05:31 -07:00
Shay
d8b37069c0
test(adr-0182): real guard for exempt commit-ineligibility (was vacuous) (#485)
The wrong=0-critical clause in pool.resolve_pooled (an exempt-only answer
never commits; a complete reading is required to resolve) had no test that
failed when it was removed: the _EXEMPT_ONLY fixture's pool also contained a
complete product (20*3*5=300), so refusal came from the disagreement rule,
not commit-ineligibility. Mutation-disabling the clause left all tests green.

Inject a single-exempt pool directly (the aggressive composers manufacture a
competing complete product for any natural text, so a corpus fixture cannot
isolate the branch). Removing the clause now commits 25 and fails loudly.
Rename the old fixture/test to state honestly that it refuses via disagreement.
2026-05-29 14:53:52 -07:00
Shay
6ed8fc21fc
docs(adr-0182): realized results + status Accepted/Implemented (lookback amendment) (#483)
The lookback review of the four-PR stack (EX-6/pooling/prior-state/anchor-skip)
found the ADR-0182 spec under-predicted and under-scoped 0016. Records actuals
without editing the original spec (retained for provenance):

- Status -> Accepted/Implemented (PRs #476, #480, #481).
- §8 Realized results: confuser wrong spec-predicted 5->4->3, ACTUAL 5->2->1->0.
  Pooling also caught disguised-polarity (bonus); 0016 needed anchor-skip PLUS
  intra-clause accumulation (a conjunction split the spec framed as anchor-skip
  alone) and yielded the bonus 0017 *solve*; the prior-state guard (#480) is a
  distinct question-time lever sharing resolve_pooled.
- Folds in the lookback findings: solid items, the closed test-coverage gap
  (anchor-skip refuse branches, #481 4205605), no live wrong=0 hazard (multi-actor
  product-commit predates anchor-skip), and the open 0010 multi-referent spurious
  (a graduation question, not a wrong).

Serving 3/47/0 byte-identical throughout. Doc only.
2026-05-29 14:50:22 -07:00
Shay
4dd824f4f6 docs(adr-0055): status reflects Phase C shipped (0056/0057), D–E landed; add cross-refs 2026-05-29 14:32:53 -07:00
Shay
84570a9c7d
docs: refresh ADR README narrative (current frontier, chain notes, session logs) (#482)
* docs: refresh ADR README narrative (current frontier, chain notes, session logs)

* docs: drop stray historical marker on refreshed Proposed list
2026-05-29 14:27:17 -07:00
Shay
a862d73084
feat(adr-0182): prior-state question guard — temporal-scope confuser refuses (confuser wrong 2->1, pair-tells ->0) (#480)
The "before/left" reading-rule lever. The microscope showed only the question-time
reading is cleanly achievable: "for N money = spend" is cue-precision-blocked (the
`for` cue is overloaded across train_sample -- `for $2`, `for 14 days`, `for 10 reps`
-- so a spend rule risks regressing train-0021/0003 and is the overfitting trap),
and the disguised-polarity cases already refuse via pooling. "left" is already handled
by loss verbs.

What ships: a question-scope guard. A question asking for a state *before* a stated
change ("How much did Lisa have before lunch?", gold 50) asks for a temporal point
the forward composers do not compute -- they derive the final/net state (50-20=30).
Until a question-time reader exists that is a refusal, never a guess at the wrong
point. target.asks_prior_state detects before/initially/originally/at first/to begin
with/to start with/at the start in the QUESTION CLAUSE only (the last `?`-sentence),
so body narrative does not trip it -- verified safe against train-0003 ("sells before
school starts"), 0010 ("had 20 initially, then lost 12"), 0028. `used to` is excluded
(the purpose infinitive "beads used to make a bracelet" is a false positive).
resolve_pooled refuses when asks_prior_state holds.

Result (sealed lane; chat/ does not import these -> serving 3/47/0 frozen):
- confuser wrong 2 -> 1 (only 0016 distractor-anchor-skip remains). temporal-scope
  category cleared (0 wrong / 2 refused). pair-tells 1 -> 0: 0020 ("before lunch")
  refuses while its minimal-pair twin 0021 ("left", gold 30) still solves the net --
  the discrimination the corpus is built to measure.
- genuine positives still 7 solved, 0 wrong.
- train_sample 3/47/0 and practice 3/47/0 byte-identical.
- 205 derivation/target/pool tests + 40 architectural invariants green.

Tests:
- test_adr_0182_pool.py TestPriorStateQuestionGuard: detector true/false edges
  (before-in-question vs before-in-body vs `used to make` false positive vs the
  `left` twin) + resolve_pooled refuses the before-question while the left-twin
  resolves forward to 30.
- test_adr_0163_f2_confusers.py: baseline wrong 2->1, pair_tells 1->0; new
  test_temporal_scope_does_not_misfire + test_before_left_minimal_pair_discriminated.

Stacked on #476 (pooling); merge #476 first. 0016 anchor-skip is the next lever.
Pairs with ADR-0163-F2 graduation amendment (#478).
2026-05-29 14:22:28 -07:00
Shay
ac7bda019b docs: refresh ADR index — all 229 ADRs (was 51)
Regenerated the docs/decisions/README.md ## Index table from each ADR's own
title + Status line, naturally sorted by ADR id (handles 0114a, 0119.1,
0131.G.3.1, 0136.S2, etc.). The index had drifted to 51 of 229 ADRs; it now
lists every ADR through 0183. Only the Index table was replaced; the Current
frontier / chain notes / reasoning-capable-domains / session-log narrative is
untouched (it remains a curated, historically-scoped account, not the index).
2026-05-29 14:08:34 -07:00
Shay
83de943cdd docs(adr-0183): stub ADR for the lawful audio→lexeme path
Records the fork for getting words from audio WITHOUT a serving-time learned
model: (A) words-as-text, or (B) deterministic formant/phonetic decode + taught
vocabulary. Status: Proposed (stub) — deferred, not a committed design. Captures
the problem, the 0-param/decode-not-borrow/refuse-over-fabricate/reviewed-growth
constraints, and the open questions a full ADR must answer. Exists so the
serving path doesn't silently reach for Whisper. Cross-linked from
docs/audio_pipeline_overview.md §9.
2026-05-29 14:02:40 -07:00
Shay
a58571d410 docs(audio): record teacher=scaffolding / serving-path-stays-Whisper-free boundary
Adds §9 subsection: a teacher is bootstrap scaffolding for the teaching phase,
not a production component (not ML distillation — CORE has no weights). Serving
path must never call a teacher. Production is Whisper-free conditioned on a
lawful runtime path: (A) words-as-text, or (B) matured deterministic
audio→lexeme decode. Flags the trap: teaching with a model does not auto-transfer
word-recognition into a 0-param engine. 'The teacher teaches; the lawful path
serves.'
2026-05-29 13:59:43 -07:00
Shay
c7290f5ab9 docs: start running model & dependency size tally
Living ledger of architecture/model/dependency footprint. Headline: CORE is a
0-parameter, 0-weight architecture (substrate is algebra, not weights), enforced
by the teacher versor-invariance test. Tracks: learned params (0), optional
teacher lanes (4 declared, all inert, no model size chosen), 10 Python runtime
deps (datasets dominates), 8 Rust crates, 0 weight files. Includes a Whisper
size reference for when/if a lane is wired, and an update-on-PR + changelog
discipline. Lives at docs/model_dependency_size_tally.md.
2026-05-29 13:51:30 -07:00
Shay
ae48a3cbf9 docs: audio section pipeline overview & primer
A from-the-ground-up reference for the audio modality: waveform → canonicalize
→ frames → acoustic lexer → typed AudioIR → operators/rotors → (32,) versor,
with every spec/constant grounded in the code (PR-2..PR-6 stack).

Includes the three-way clarification of learned models: embeddings (never),
audio specs (CORE's own DSP, not any model), text transcript (Whisper/NeMo as
typed labels only). Documents the substrate-vs-evidence split, the verbatim
teacher policy, current inert/gated status, and the 'scrutinise the consumer'
flag. Cross-links ADR-0180/0181 + spec + eval-plan. Lives at
docs/audio_pipeline_overview.md.
2026-05-29 13:47:31 -07:00
Shay
f017785a6d
feat(adr-0181-p6): audio teacher/shadow lanes — typed hints, never substrate (#479)
Implements ADR-0181 PR-6 (eval-plan §4): teachers label or align; they never
define the substrate and never fold embeddings into the versor path.

- sensorium/audio/teachers.py:
  - TeacherHint: typed, versioned, checksummed annotation (no raw embeddings).
  - AudioTeacher protocol (pure on the signal).
  - attach_teacher_hints: the ONLY admission path — appends content.* anchors to
    the IR's content_anchors (immutable, recomputes ir_sha256). content.* is not
    an operator key, so compile_events skips it: versor + projection_sha256 stay
    byte-identical; only the ir leg of the merge_key moves (evidence recorded).
  - KNOWN_TEACHER_LANES (whisper/nemo/clap/encodec): declared + gated behind
    optional extras; load_teacher import-guards and fails loudly (never a silent
    fallback). StubTranscriptTeacher is the deterministic reference instance.
- parser.py: extract _ir_payload + ir_sha256_of (DRY single source of truth for
  ir_sha256; byte-identical to parse() output — regression-guarded).
- pyproject.toml: audio-whisper/nemo/clap/encodec optional extras (never
  runtime-required).

16 failable proof tests in tests/test_audio_teachers.py. Load-bearing:
test_teacher_hint_does_not_change_versor. Mutation-verified — giving a teacher
anchor an operator event_type (folding it into the versor) fails the
versor-invariance proof; reverted, all pass.

Additive only (ADR-0013): no core layer touched. Audio suite 57/57; eval-gate
ir_sha256 pins unchanged by the parser refactor; architectural invariants 40/40.
Real model adapters are deferred until extras+weights are present; this PR ships
the policy, the typed-hint contract, and the shadow-only guarantee.
2026-05-29 13:36:33 -07:00
Shay
938d2bfeb3 ci(smoke): add audio sensorium tests to the PR smoke gate
tests/test_audio_*.py (compiler, eval-gates, pack-manifest, sensorium-mount,
CRDT-merge, and teachers once #479 lands) ran only post-merge via full-pytest;
they now gate PRs too. Glob auto-includes future audio test files. 54 tests,
~3s — well within the smoke budget.
2026-05-29 13:31:58 -07:00
Shay
47a406b590
docs(adr-0163-f2): clarify refuse is maturity-relative — confusers are solvable coverage targets that graduate (#478)
A confuser's `expected: refuse` does not mean unanswerable. Every confuser carries
its true gold (`answer_numeric`) and is a solvable coverage target; `refuse` means
"the reader can't yet comprehend this category, so refusing is the honest outcome —
committing a WRONG value is the defect." Example: 0001 "buys a toy for 30 coins …
how many left?" is plain 50-30=20; it is refuse only because today's reader takes
`buys` as a gain.

Adds §2.1 (graduation protocol): when a general mechanism reads a category correctly
(validated on train_sample + the category, wrong=0 preserved), those cases graduate
refuse -> solve and committing their gold becomes a win. Reframes the `spurious`
verdict as "solved-before-graduation" (a graduation signal, with pair-consistency as
the genuine-reading-vs-surface-match discriminator), not automatically a defect.
Notes that no v1 category is degenerate, and how a truly-unanswerable case would be
labelled (answer_numeric: null + degenerate: true) so the two senses of "refuse"
stay distinguishable.

Spec only; no corpus/runner change (today's commits are wrong-reading, so spurious
stays flagged until a category genuinely graduates).
2026-05-29 13:23:02 -07:00
Shay
dd529ad408
feat(adr-0181-p5): audio Delta-CRDT arena + merge kernel (sequential==concurrent proof) (#477)
Wires AudioCompilationUnits into the Delta-CRDT substrate (ADR-0180 §2.1/§2.2)
at the Python layer — audio is the first concrete exerciser of ADR-0180.

- sensorium/audio/arena.py:
  - AudioArena (§2.1): thread-local, share-nothing, lock-free accumulation;
    non-destructive snapshot(); thread_local_audio_arena() accessor.
  - AudioDelta (§2.2): canonical, content-addressed merge_key order + dedup;
    join is commutative/associative/idempotent.
  - merge_audio_deltas (§2.2): the Merge Kernel — folds arena deltas into one
    content-addressed, deduped, totally ordered set.
  - audio_merge_trace_hash: PCM-free, order-invariant trace hash — the
    sequential==concurrent proof anchor (ADR-0181 §4.2 A-3/A-6).

Mirrors the Rust LocalArena/SemilatticeDelta/merge_kernel (core-rs/src/vault.rs,
ADR-0180 §4.1) so the layers stay in parity when the Rust↔Python binding lands
(§1.5.5, deferred — pure-CPU/Python path first).

10 failable proof tests in tests/test_audio_crdt_merge.py covering the
load-bearing hash(Sequential)==hash(Concurrent) gate (real threads), the three
semilattice legs, content-addressed ordering, dedup idempotence, and trace
hygiene. Mutation-verified: disabling the content sort fails 6 of 10 loudly.

Additive only: no core layer touched (ADR-0013). Audio tests 35/35; architectural
invariants 40/40. Zero impact on existing eval lanes (no serving path changed).
2026-05-29 13:22:44 -07:00
Shay
0fbcce429b
feat(adr-0182): cross-composer disagreement pooling — distractor 0014 + disguised-polarity refuse (confuser wrong 5->2) (#476)
Implements ADR-0182's first win on top of EX-6 (#473). The distractor-quantity
confuser 0014 misfired (20x3x5=300) because a blunt product-of-all was the *unique*
self-verifying reading: the completeness clause forces the distractor into it, and
no rival reading existed to trigger the wrong=0 disagreement rule. No tight cue rule
separates it from the legitimate cross-unit products (`for` licenses both the 0014
distractor AND the correct train-0021 product) -- that is the deferred cue-precision
problem. So instead of a reactive patch, let the disagreement rule do the refusing.

Mechanism (sealed lane; chat/ does not import these -> serving 3/47/0 frozen):
- verify.classify_derivation: a derivation is `complete` (commit-eligible),
  `exempt` (verified but for an isolated-foreign unused quantity -> commit-
  INELIGIBLE), or None. Refactored self_verifies into _base_reasons +
  _unused_quantities so the two share logic and cannot drift (behavior identical;
  385 derivation tests + smoke 67 green).
- accumulate.accumulation_candidates: exposes the ungated readings, incl. a
  distractor-skip reading that drops an isolated-foreign quantity from a multi-
  quantity change clause (20+5=25). compose_accumulation is byte-identical
  (drop_isolated_foreign=False + the same gate).
- search.multiplicative_candidates / multistep.candidate_chains: ungated candidates.
- pool.resolve_pooled: pools every composer's readings; disagreement -> refuse; a
  single answer commits only if a `complete` candidate produced it (exempt-only ->
  refuse, so the commit-path completeness guarantee from ADR-0175 is untouched).
- confuser runner: _engine_answer now delegates to resolve_pooled (the prior
  first-composer-wins order could not notice it held two incompatible readings).

Result (the microscope):
- confuser wrong 5 -> 2. distractor 0014 refuses (product 300 vs additive 25);
  BONUS: both disguised-polarity cases (0001/0003) refuse -- the spurious
  "buys X for N coins" product disagrees with the accumulation reading. Remaining
  wrong: 0016 (distractor in the anchor clause -> needs anchor-skip, separate step)
  and 0020 (temporal-scope). pair-tells 4 -> 1.
- genuine positives still 7 solved, 0 wrong.
- train_sample 3/47/0 and practice 3/47/0 byte-identical (they call
  compose_accumulation/search directly -- unchanged -- not the pool).
- smoke 67, architectural invariants 40, lane-SHA freeze 8/8.

Tests:
- test_adr_0182_pool.py: classify (complete/exempt/None incl. narrow-exemption
  edges) + resolve_pooled, with the wrong=0 obligation test_exempt_only_never_commits
  (a distractor with no multiplicative cue must refuse, not commit 25 -- fails loudly
  if the exemption is made commit-eligible).
- test_adr_0163_f2_confusers.py: baseline tightened wrong 5->2, pair_tells 4->1;
  new test_distractor_0014_refuses_via_pooling + test_disguised_polarity_does_not_misfire.

Stacked on #473 (EX-6); merge #473 first. 0016 + the remaining wrongs are follow-ups.
2026-05-29 13:22:19 -07:00
Shay
3f9edd06da
feat(adr-0180): LocalArena + SemilatticeDelta CRDT substrate (pure-CPU Rust) (#475)
Implements ADR-0180 §4.1 item 1: the Delta-CRDT write-accumulation substrate
in core-rs/src/vault.rs.

- ArenaEntry: (versor, provenance) — provenance is part of the content key.
- LocalArena (§2.1): thread-local, share-nothing, lock-free write cache;
  snapshot() emits a canonical Delta; non-destructive (flush/GC is the kernel's
  concern, safe across the §3.2 eventual-consistency window).
- SemilatticeDelta trait + Delta (§2.2): join is commutative, associative,
  idempotent under content-addressed equality (IEEE-754 versor bits +
  provenance bytes), never arrival order — per the §2.2 amendment landed today.
- merge_kernel (§2.2): folds deltas into one content-addressed, deduped,
  totally ordered set; permutation- and duplicate-invariant — the property
  §4.3's hash(Sequential)==hash(Concurrent) rides on.

Pure-CPU only (§1.5.5): no MLX/UMA handshake, no Python binding — those are
downstream (§4.1 item 2; ADR-0181 PR-5). Existing vault recall/reproject paths
untouched; zero eval impact.

10 failable property tests in tests/test_arena.rs (mutation-verified: disabling
the content sort fails 6 of them loudly, per CLAUDE.md §Schema-Defined Proof
Obligations). Also fixes a pre-existing broken doctest in the vault.rs header
(indented math block was parsed as a Rust doctest).
2026-05-29 13:21:56 -07:00
Shay
a8f9e3685d
docs(adr-0182): scope cross-composer disagreement pooling for distractor refusal (#474)
The confuser probe's two distractor-quantity wrongs (0014 ->300, 0016 ->3840)
have no clean tight fix: the `for` cue licenses both the 0014 distractor product
and the correct train-0021 product, and a target+foreign-unit refusal rule breaks
the canonical train-0003 boxes x erasers product. Telling a real multiplier from a
distractor by cue is the cue-precision problem ADR-0177 measured as not-yet-solvable
-- a tight rule here would be a reactive surface patch (the overfitting trap).

This ADR scopes the general, non-reactive alternative: let the wrong=0 DISAGREEMENT
rule do the refusing. Pool self-verifying candidates across composers so a blunt
product reading and a competing additive reading of the same problem disagree ->
refuse; legitimate products (0021/0003) have no competing reading, so they still
commit -- the structural property that distinguishes them, expressed without a cue.

Scoping surfaced two things a one-line plan would have missed:
- Completeness FORCES the distractor into every reading, so naive pooling changes
  nothing. The mechanism needs a narrow, commit-INELIGIBLE completeness exemption
  for isolated-foreign quantities -- it can only buy a refusal, never an answer, so
  the commit-path completeness guarantee (ADR-0175's 9->2 multi-step fix) is intact.
- MS-1 Target exposes the union of body unit-shapes, NOT the question's asked-for
  unit (verified). The exemption is therefore keyed to a reading's used-operand
  units, not an asked unit that does not exist.
- 0016's distractor sits in the anchor-position clause, so it needs distractor-aware
  anchor selection too: 0014 is the guaranteed win (wrong 5->4); 0016 lands (->3)
  only when anchor-skip is built. Doc declines to claim both before that is proven.

Spec only, no code. Sealed lane; serving 3/47/0 untouched.
2026-05-29 13:21:36 -07:00
Shay
475d44d245
feat(adr-0163-f2): EX-6 hyphen-bonded unit extraction — pseudo-accumulation confusers refuse (wrong 7->5) (#473)
The confuser probe's two pseudo-accumulation misfires (0005 ->796, 0007 ->996)
both traced to the same extraction blind spot: a number bonded to its unit by a
hyphen (`25-foot sections`, `20-inch pieces`) was invisible to the base
`number + space + word` pattern, so the self-verification completeness clause
never saw the divisor and the bare `buys ... gives` accumulation read as
"complete". This is the highest-leverage lever the 2026-05-29 session named
("the gate must see the fractions/25-foot it currently misses").

EX-6 adds a tight, ADR-0165-safe lexeme pass: a digit run, a single hyphen, an
alphabetic unit word. The alphabetic-only unit group keeps numeric ranges (`3-5`)
out; taking only the first hyphen segment keeps the postmodifier tail
(`25-year-old`) from inflating the unit — so it stays clear of the deferred EX-3
multi-word-unit traps. Over-extraction here is strictly refuse-preferring: making
the divisor visible drives 0005/0007 to refuse via the polarity-None
`cuts`/`splits` clause, never to a wrong answer.

Evidence (deterministic, the microscope):
- confuser probe: wrong 7 -> 5; pseudo-accumulation 0 wrong / 4 refused;
  genuine positives still 7 solved; pair-tells unchanged (4).
- train_sample (capability): 3/47/0 byte-identical.
- practice accumulation: 3/47/0 (wrong=0) byte-identical.
- smoke 67 passed; lane-SHA freeze 8/8 (serving frozen).

Tests:
- TestEX6HyphenatedUnitNumbers pins the new lexeme (value+unit, decimal, no
  double-count, word-compound unaffected, numeric range not read as a unit).
- TestProbeBaseline tightened wrong 7->5; new test_pseudo_accumulation_does_not_misfire
  is the failing-under-violation obligation (fails loudly if the pass regresses).
- TestSlashFractionLeakHazard pins the deferred `1/4`->`4` denominator leak: not
  fixed here because suppressing the leaked operand *removes* a quantity and can
  unblock the completeness clause (not unambiguously refuse-preferring), so it
  needs its own train_sample + probe validation.
2026-05-29 12:23:26 -07:00
Shay
62d9db7a7c docs(adr-0180): amend §1.5.3/§2.2 per T-1..T-4 findings
§1.5.3: content-addressed re-sort obligation is vacuous at compute_trace_hash
(folds vault_hits count, not contents); amend to apply at recall() result set
+ any future contents-bearing hash.

§2.2: Merge Kernel must content-address equal-score recall ties (multivector +
provenance bytes), not arrival/deque-index order — the general-path analog of
ADR-0181 §2.2's audio merge key.

Both sharpen the substrate ahead of the vault.rs LocalArena/SemilatticeDelta
refactor; neither blocks it. Sourced: docs/audit/ADR-0180-t1-t4-findings.md.
2026-05-29 12:20:54 -07:00
Shay
53573263cb
feat(adr-0181-p4): audio compiler eval gate lane (sensorium/audio) (#470)
PR-4 of ADR-0181. The acceptance-gate lane that decides whether audio_core_v1's
gate may open. Deterministic synthesis-spec fixtures (no .wav blobs) with
predicted parses, so the gates grade parser semantics as well as determinism.

evals/audio_sensorium/:
- synth.py            deterministic fixture synthesis (PCG64 + float32-at-boundary)
- fixtures.json       5 specs: silence, rising-pitch question, falling statement,
                      noise burst, speech-then-pause
- generate_expected.py reproducible pin generator (uv run -m ...)
- expected_ir.jsonl   frozen canonical_sha256 + ir_sha256 + event_type_counts
- expected_projection.json frozen projection_sha256 + reference versor

tests/test_audio_eval_gates.py (12): the gate table per fixture —
shape/dtype, versor_condition<1e-6, within-run replay, canonical-checksum
stability (hard int/cast-stable pin), IR-replay + frozen ir_sha256, semantic
event_type_counts (parser-accuracy gate), and cross-platform versor stability
within atol=1e-6 of the reference (float-safe per eval plan); plus trace
hygiene and gate-closure refusal.

Verified semantics: rise→prosody.rise, fall→prosody.fall, silence→pause.long+
turn.boundary, noise→nonspeech.noise, speech_then_pause→all three.

Cross-platform note: int/quantized-derived hashes are pinned hard; the float
versor is compared within tolerance rather than hash-pinned, since cos/sin/
geometric_product can differ by a ULP across arches. This is the eval-plan's
"equal within declared numeric tolerance" reading — keeps CI stable.

All audio 44 + arch-invariants 40 + smoke 67 green. No core mutation.
2026-05-29 11:20:31 -07:00
Shay
d95f8a99e0
docs: session wrap-up (comprehension chaining + overfitting discipline) + CLAUDE.md substrate (#472)
- New session record SESSION-2026-05-29-comprehension-and-overfitting-discipline.md:
  the reconciliation wave, CP-1/CP-2a (no cue reliable yet -> structure-first),
  GB-3a referent guard, GB-3b.1 accumulation (practice 0->55), the GB-3b.2
  overfitting course-correction (96/150 synthetic vs 1/50 real -> torn down), and
  the confuser corpus (baseline: 7 defects surfaced). Serving 3/47/0 throughout.
- Lands the prior dangling SESSION-2026-05-29-multistep-build-arc.md (was untracked).
- CLAUDE.md "Current Key Modules": adds the sealed GSM8K comprehension substrate
  (core/reliability_gate, generate/derivation, generate/cue_precision,
  evals/gsm8k_math {train_sample,practice,confusers}, the lane-SHA gate).
2026-05-29 11:18:27 -07:00
Shay
a67b475e59
docs(adr-0173): accept workbench ratification trust boundary (W0); queue reconciled W3 corridor brief (#469)
Flip ADR-0173 Proposed -> Accepted. This is the W0 gate for the
workbench-UI ratification wave: it amends ADR-0160's read-only stance
narrowly to admit operator-driven invocation of the three existing,
replay-gated Tier 1.5 handlers (apply_lexical_claim, apply_frame_claim,
apply_composition_claim) as a local keyboard accelerator over ADR-0161
Surface C. No UI code; implementation remains gated to the W1..W4
acceptance gates in the ADR.

Also lands the reconciled W3 ratification-corridor brief. It supersedes
the stale W3 section of WORKBENCH-UI-WAVE-BRIEF-PACK.md, which assumed a
'frontend zero' state (W1/W2 already merged via #295/#299/#329/#327/
#328/#303) and prescribed a Zustand store this repo never adopted. The
reconciled brief names the load-bearing facts the old one glossed:
- /ratify is advisory-only today (routes, never applies); W3's core
  backend task is the advisory->in-process flip.
- live proposals carry shape_category='uncategorized', so the safe
  category/polarity must be operator-supplied and allowlist-gated, not
  hard-coded -- the exact silent-wrong-category path case 0050 guards.

Verification this session: read-only workbench v1 confirmed working
(API serves real deterministic data, honest 404s, traversal rejected,
ratify advisory/no-mutation; 31 python + 104 UI tests green; pnpm build
green).

No runtime/algebra/eval paths touched. Docs only.
2026-05-29 11:14:25 -07:00
Shay
31d9cb429d
docs(adr-0163-f2): confuser corpus spec — a discrimination probe, not a coverage target (#468)
Follow-on to ADR-0163 §F that corrects the metric exposed by the overfitting
finding (96/150 synthetic flips vs 1/50 real; the 0002 cable/fraction problem read
as accumulation -> 996). Specs a small, hand-curated, real-sourced set of hard
negatives + near-miss confusers (disguised-polarity verbs, pseudo-accumulation
fractions, multi-referent/multi-actor, distractor quantities, temporal/question-
scope, comparative-referent, unit confusers) with minimal-pair construction.

Scored the opposite way from a coverage lane: success = wrong=0 on confusers +
pair-consistency + honest refusal frontier, NEVER solved-count. Held-out by
contract (not a training-to-fit target); the CP ledger consumes the wrong attempts
as the hard negatives the templated corpus lacked. Guardrails forbid reactive vocab
growth and template expansion.
2026-05-29 11:13:53 -07:00
Shay
6a4d356ce9
feat(adr-0163-f2): confuser corpus v1 + discrimination-probe runner (#471)
Builds the corpus from the ADR-0163-F2 spec: 30 hand-curated, real-sourced cases
across the proven misfire categories (disguised-polarity, pseudo-accumulation/
fractions, multi-referent H1, multi-actor-pronoun ADR-0174, distractor-quantity,
temporal-scope H3, comparative-referent H2, unit-confuser) + genuine-positive
minimal-pair twins. Schema carries category/surface_trap/expected/pair_id/source.

The runner scores OPPOSITE to a coverage lane: the bar is `wrong` -> 0 (a confuser
*answered* is a defect regardless of value) plus pair-consistency (solving a twin
but answering its confuser = a surface-matching tell). It runs the realistic sealed
attempt (accumulation -> multiplicative -> chain, first to resolve).

Honest measured baseline (the probe's whole point — these are the defects the
templated corpus hid): 30 cases -> 7 solved / 15 refused / 7 WRONG / 1 spurious;
4 pair-tells (0001/0003/0014/0020). Wrong by category: disguised-polarity 2
(buys-a-toy-for-30 -> +30), pseudo-accumulation 2 (the 0002 cable/fraction),
distractor-quantity 2, temporal-scope 1 (before-giving -> gave the now-value).

Per the overfitting lesson, the composers are NOT reactively patched to pass the
probe (that is the trap). The baseline is pinned as a no-regression gate (wrong
<= 7, pair-tells <= 4, positives keep solving); future fixes must be GENERAL
mechanisms validated on train_sample, driving wrong down. Sealed: serving 3/47/0
byte-identical (lane-SHA 8/8, claims OK); architectural invariants green.
2026-05-29 11:13:49 -07:00
Shay
7f619d44e5
feat(adr-0181-p3): audio_core_v1 pack + AudioProjectionHead (gate-closed) (#467)
PR-3 of ADR-0181. Externalises the PR-2 in-code operator registry to a
versioned, checksum-locked pack and wires the sensorium adapter. Mounts
gate-closed until the PR-4 eval gates pass. No core mutation (ADR-0013).

Pack (packs/audio/audio_core_v1/):
- manifest.json   canonical params, resampling config, gating defaults, ordering
- operators.jsonl the 8 elliptic operators (byte-equivalent to DEFAULT_OPERATOR_REGISTRY)
- basis_map.json  alias→index over the six elliptic planes {6,7,8,10,11,13}
- resample_fir_v1.npy  pinned odd-length symmetric windowed-sinc low-pass (unit DC)
- checksums.json  sha256 of every artifact

Loader (packs/audio/loader.py) — trust boundary (ADR-0051 / CLAUDE.md §Security):
- _validate_pack_id rejects traversal/separators/dotfiles before any path join
- fail-closed checksum verification: every file re-hashed vs checksums.json
- resolved path must stay under the packs root (defense in depth)
- JSON format (matches every other CORE pack loader; spec §6.1 TOML was
  illustrative; no tomllib dependency added)

Adapter (sensorium/adapters/audio.py):
- AudioProjectionHead wraps AudioCompiler; project(AudioSignal)->(32,) f32
- make_audio_pack loads+verifies the pack, builds a gate-closed ModalityPack

Tests (19): PR-2↔PR-3 registry byte-parity via manifest_sha256, elliptic-only
operators, FIR integrity, fail-closed checksum mismatch, path-traversal guard,
gate-closed refusal, mount-time unitarity (+ bad-unitarity blocks mount),
gate-engage requires checksum_verified, engaged pack projects (32,) f32.

All audio 32 + arch-invariants 40 + smoke 67 green.
2026-05-29 11:05:57 -07:00
Shay
4d59e66043
feat(adr-0181-p2): deterministic audio compiler substrate (sensorium/audio) (#466)
PR-2 of ADR-0181. Lands the deterministic substrate only — no pack artifacts
(PR-3), no evals (PR-4), no Delta-CRDT wiring (PR-5), no teachers (PR-6).

Pipeline (spec §1): canonical signal → frame grid → acoustic lexer →
typed AudioIR → canonical event ordering → elliptic rotor lowering →
versor composition → AudioCompilationUnit (the future CRDT delta).

Modules (sensorium/audio/):
- types.py      frozen AudioSignal/Token/Event/IR + AudioCompilationUnit.merge_key
- checksum.py   layered sha256 chain (source→canonical→token→ir→manifest→projection)
- resample.py   pure-numpy polyphase FIR (scipy absent; FIR taps are a PR-3 artifact)
- canonical.py  mono/24kHz canonicalisation + provenance hashes
- frames.py     20ms/10ms deterministic frame grid (zero-padded tail)
- lexer.py      quantized per-frame descriptors (energy/voicing/zcr/centroid/pYIN-style F0)
- parser.py     runs → typed spans/events (pause/speech/prosody/turn/non-speech)
- operators.py  elliptic-ONLY rotor registry; planes {6,7,8,10,11,13} square to -1
- compiler.py   compile_events serialization barrier (ADR-0181 §2.1) + checksum chain
- trace.py      trace-safe audio evidence (no PCM)

Correctness: v1 restricts to the six elliptic grade-2 planes (e_i e_j, i,j∈{1..4})
so every rotor and every composition is a unit versor — versor_condition < 1e-6
holds without weakening the threshold (CLAUDE.md §Non-Negotiable Field Invariant).
Non-elliptic blades (those touching e5) are rejected at OperatorSpec construction.

Tests (tests/test_audio_compiler.py, 13 passed): A-1 determinism, A-4 serialization
barrier order-sensitivity, A-5 versor condition, A-6 trace hygiene, IR-replay,
shape/dtype, elliptic-plane lawfulness. Smoke 67 + arch-invariants 40 green.

No core mutation: ingest/field/generate/vault/vocab untouched (ADR-0013).
unitize_versor is the only normalization, algebra-owned (CLAUDE.md §Normalization).
2026-05-29 10:50:28 -07:00
Shay
274c81ea37
docs(adr-0178-gb3b): scope referent-aware accumulation chaining (#464)
Grounded scope for GB-3b, the next Gap B move. Measured opportunity: 46/150
additive practice cases are sum-of-all==gold, all currently refusing, all the
single-referent accumulation shape ('X has N. He buys M more.' -> N+M). They
refuse correctly (GB-3a's multi-clause guard / the Alice+Bob hazard); the safe
ones differ by referent identity + a change-verb cue, which GB-3b reads.

Scope covers: the reading (anchor + change steps), the wrong=0-critical referent
model (minimal no-new-actor guard; cross-references ADR-0164.2/.3 + ADR-0174's
multi-actor hazard; deliberately does NOT resurrect the retired gender-blind
resolver), the proof-carrying wrong=0 obligations, the cue-precision coupling
(change cues should finally be reliable, closing CP-2a), the increments, and
honest flip expectations (big chunk in practice-additive; modest train_sample;
serving stays 3/47/0 until ratification).
2026-05-29 10:41:54 -07:00
Shay
6611a7017d
feat(adr-0178-gb3b1): single-referent accumulation chaining (practice 0 -> 55) (#465)
The first cross-clause comprehension reading: one actor's quantity changes over
successive clauses ("Sam has 14 apples. He buys 9 more." -> 14 + 9). It is the
safe specialisation of the cross-clause sum that GB-3a refuses wholesale (the
Alice/Tom hazard) — we chain only when (same referent) AND (a licensed change
cue of unambiguous polarity), else refuse.

generate/derivation/accumulate.py — compose_accumulation:
- anchor on clause 1's single quantity; apply +M (gain) / -M (loss) per later
  change clause, operand taken in the anchor's unit (accumulation is same-dimension);
  routed through the unchanged self-verification gate.
- polarity (ordered, so ambiguous "gives" is resolved not guessed): "more" -> gain;
  else unambiguous loss verb -> loss; else gives/gave + to/away -> loss; else
  unambiguous gain verb -> gain; else REFUSE.
- referent guard (the ADR-0174 multi-actor hazard's defensive fix, built minimally
  in the clean lane — NOT the retired gender-blind resolver): a later clause's
  subject token must be a pronoun or the anchor's name; a NEW named subject (Tom)
  -> refuse. Pronoun gender/number is not matched; a new name is the only signal.

evals/.../accumulation_runner.py — practice scorer: on a base refusal, attempt
compose_accumulation and gold-check (mirrors search_runner). Sealed: fires only
on already-refused cases, never alters serving.

Measured (sealed practice additive lane): 0 -> 55 correct, wrong unchanged at 1
(the base scorer's pre-existing one; accumulation added 55 correct, 0 wrong). The
36 still-refused are multi-change (GB-3b.2) or unrecognised verbs (vocab growth) —
conservative, never wrong.

Proof obligations (tests fail under the violation): new-named-actor refuses (H1),
no/ambiguous change cue refuses, list anchor refuses, multi-change refuses,
determinism. 136 targeted tests + architectural invariants green; serving 3/47/0
byte-identical (lane-SHA 8/8, claims --check OK).
2026-05-29 10:41:51 -07:00
Shay
cecf7b82cc
test(adr-0180): T-1..T-4 Delta-CRDT pre-refactor obligations (Python, green) (#463)
ADR-0180 §1.5.4 + CLAUDE.md work-sequencing item 5 require these four
properties green on main before any core-rs/src/vault.rs change. They are
also the foundation ADR-0181 PR-5 (audio Delta-CRDT wiring) rides on.

  T-1  set-equality of vault writes under shuffled ingest (+ idempotent
       re-ingest at the content-addressed layer)
  T-2  trace-hash invariance to vault order, + recall result-set invariance
       to insertion order (the genuinely-failable half)
  T-3  versor_apply non-commutativity (negative guard)
  T-4  ProjectionHead.project purity across calls and threads

Findings (docs/audit/ADR-0180-t1-t4-findings.md):
- compute_trace_hash folds only vault_hits (a count), NOT vault contents, so
  ADR-0180 §1.5.3's "re-sort vault state in content-addressed order" is
  currently vacuous at the trace-hash layer; the live order-invariance
  obligation is at recall() (result-set + count). Recommend amending §1.5.3.
- equal-score recall ties are index-sensitive; the Merge Kernel needs a
  content-addressed tiebreak (mirrors ADR-0181 §2.2 merge key). Recommend
  amending §2.2.
- append is genuinely semilattice-eligible; versor_apply is non-commutative.

7 passed; smoke suite green. No runtime/core mutation — tests + audit only.
2026-05-29 10:40:52 -07:00
Shay
7451e7cd74
feat(adr-0177-cp2a): cue-precision ledger training + measurement (+ unit hygiene) (#461)
CP-2a populates the CP-1 ledger from gold-labelled candidate readings and reports
per-pattern reliability — the measurement the cue-precision thesis rests on. Plus
the function-word unit filter, whose value this measurement makes concrete (clean
unit_shape labelling).

What landed (all sealed; serving 3/47/0 byte-identical):
- generate/cue_precision/trainer.py — train_from_cases(cases, enumerators): folds
  gold-labelled candidate chains into the ledger via record_case. Decoupled (the
  candidate enumerators are injected, so the package still imports nothing from
  search). candidates_for dedupes a reading shared by two enumerators.
- generate/derivation/multistep.py — extracted the enumeration half of search_chain
  into public candidate_chains(problem_text); search_chain now delegates (verified
  byte-identical: ms3 tests + practice counts unchanged). CP-2 needs the readings
  the search weighs, not just the one it resolves.
- generate/derivation/extract.py — function-word unit filter (_NON_UNIT_WORDS):
  blanks spurious function-word units ($0.75 each -> "", 3/4 of -> "") that
  corrupt same-unit detection and unit_shape. Closed lexeme set, ADR-0165-safe.
- evals/gsm8k_math/practice/v1/cue_precision_report.py — trains over 200 sealed
  cases (50 train_sample + 150 ADR-0163-F additive) with the real enumerators and
  prints the per-pattern reliability table.
- tests/test_adr_0177_cp2a_training.py — trainer obligations (credit/dedupe/
  determinism/empty) via synthetic enumerators; real-measurement well-formedness;
  search_chain parity.

Load-bearing finding (recorded in ADR-0177): over 200 cases EVERY (cue,op,unit_shape)
pattern floors at ~0.0 reliability (best: for-multiply-cross_unit 0.0116 at 2/34).
The blunt product/sum-of-all readings are almost always wrong vs gold, so the
conservative floor correctly trusts nothing. => CP-2b (trust reliable cues) is
blocked on candidate GENERATION, not the ledger: candidate readings must get less
crude (clause/referent structure, ADR-0178 GB-3b) before any cue earns reliability.
Cue-precision and compositional structure are coupled; structure comes first.

Verification: 107 targeted tests green (CP-2a/CP-1/extract/ms3/GB-1/2/3/MS-1/2) +
architectural invariants; serving CLAIMS.md sha unchanged; practice 4/1/45 and
0/1/149 unchanged. Inert: trains/reports only, consulted by no search/gate.
2026-05-29 10:21:58 -07:00
Shay
fd96074230
docs(adr-0181): propose CORE-native audio compiler over Delta-CRDT substrate (#462)
Maps the AssetOverflow audio-compiler proposal onto the existing
sensorium ProjectionHead boundary (ADR-0013) and the Delta-CRDT
sharded substrate (ADR-0180).

Core mapping decision: the audio chunk boundary IS the CRDT delta
boundary. In-chunk rotor composition (compile_events) is the explicit
serialization barrier ADR-0180 §1.5.2 requires for non-commutative
versor_apply; the resulting AudioCompilationUnit is the order-invariant
delta written at the only semilattice-eligible layer (vault/store).
The compiler's checksum chain supplies the content-addressed merge key
(canonical, ir, projection sha256) that ADR-0180 §1.5.3 demands, making
idempotence structural and the sequential==concurrent trace-hash proof
checkable.

Adds:
- docs/decisions/ADR-0181-audio-compiler-delta-crdt.md (decision + mapping)
- docs/plans/audio-compiler-spec.md (typed AudioIR, operator table,
  manifest, numeric determinism, delta interface)
- docs/plans/audio-compiler-eval-plan.md (corpus, gates, A-1..A-6 CRDT
  proof obligations, teacher-migration policy)

Docs only; no runtime/core mutation. PR-2..PR-6 substrate work sequenced
in the ADR, with PR-5 gated on ADR-0180 §1.5.4 (T-1..T-4) green on main.
2026-05-29 10:19:54 -07:00
Shay
de6df1edc9
feat(adr-0163-f): scale sealed practice case set to 150 additive cases (#459)
Adds `evals/gsm8k_math/practice/v1/cases.jsonl` — 150 GSM8K-style word
problems covering only additive/subtractive operations.  All cases carry
`<<a+b=c>>` / `<<a-b=c>>` annotations; none contain `*` or `/`, so every
case classifies as `"additive"` under `classify_operation`.

Four difficulty bands:
  0001–0030  single add (14 distinct units, 15 entity names)
  0031–0060  single subtract
  0061–0090  two same-direction operations
  0091–0150  mixed add+subtract and multi-step (2–4 steps)

IDs are `gsm8k-practice-v1-NNNN`, deterministically ordered.
`train_sample/v1/cases.jsonl` and its pinned SHA are untouched.
`build_search_report` continues to run unchanged.

Adds `_PRACTICE_CASES_PATH` constant and `_load_practice_cases()` /
`build_practice_report()` to `practice/v1/runner.py` as additive
symbols; `build_report()` and all existing imports are preserved.

New practice case count: 150.
2026-05-29 10:02:00 -07:00
Shay
d5c79e87f4
docs(adr-0179-ex3): record second deferral trap — postmodifier adjectives (#460)
Track C of docs/handoff/PARALLEL-WORK-PLAN-2026-05-29.md asked for a tight
EX-3 multi-word-unit redo satisfying (a) "12 jumping jacks." -> "jumping
jacks", (b) "6 apples and 4 apples." -> two apples, (c) all GB-1/2/3 tests
green. The cleanest tight rule that satisfies all three —

    (?<![\w.])(\d+(?:\.\d+)?)\s+([a-z]+\s+[a-z]+)(?=\s*[.?!,]|\s*$)

— was implemented and passed the four pinned test files. Full-suite
verification then surfaced a second trap the audit at
docs/handoff/AUDIT-ADR-0179-EX-RECONCILE.md did not anticipate:

  postmodifier-adjective tails. "25 years old?" fires the tight rule and
  produces unit "years old" rather than "years", regressing
  test_adr_0176_ms1_question_target.py::TestQuestionQuantities::
  test_extracts_quantity_stated_in_question and the "X years old" pattern
  in tests/test_adr_0176_ms2_chain.py. The pattern is endemic in GSM8K
  (cases 0006 and 0033 both use "X years old"); closing it would need a
  second closed lexeme set ({old, tall, long, wide, deep, away, ago, ...})
  which the brief judged too open-ended to enumerate responsibly.

Per the brief's escape hatch ("If no rule satisfies all of (a)-(c) without
a grammar template, write a note and ship no code — a refusal is fine")
this commit:

* updates extract.py's module docstring to name BOTH known traps
  (connective-crossing AND postmodifier-adjective tails);
* adds tests/test_adr_0179_extract.py::TestEX3StillDeferred with two pins
  asserting the postmodifier-adjective shape stays at unit "years" alone,
  so no future redo silently re-introduces the regression;
* ships NO extractor code change — the regex remains exactly as on main.

Scope/safety:
* Files touched are within Track C's allowed set (extract.py + its test).
* Zero functional change: extract_quantities byte-identical to main.
* Serving lane untouched (chat/ does not import this module).
* Safe alongside GB-3b on compose.py / clauses.py.
2026-05-29 10:01:00 -07:00
Shay
b1416814ea
docs(adr-0180): propose CRDT-sharded vault concurrency substrate (#457)
* docs(handoff): parallel-work plan post-GB-3a (CP-1 / scale / EX-3 tracks)

Three disjoint-file tracks dispatchable in parallel with Claude's serial Gap-B
line; records the hard constraint that GB-3b/4/5 are serial on compose.py.

* docs(adr-0180): propose CRDT-sharded vault concurrency substrate

Drafts ADR-0180 (Proposed) for a Delta-CRDT sharded substrate to support
forthcoming multimodal ingestion (vision, kinematics) without serializing
on a global Vault lock.

§1.5 grounds the proof obligation against the existing single-threaded
Python ingest path (sensorium → ingest/gate → field → vault/store →
compute_trace_hash) and enumerates four pre-refactor test obligations
(T-1..T-4) that must be green on main before any change to
core-rs/src/vault.rs lands, per CLAUDE.md work-sequencing item 5.

§1.5.5 explicitly fences out: approximate recall (exact CGA recall is
non-negotiable), hidden background execution (Merge Kernel must be
explicitly mounted with telemetry), and MLX/UMA hardware optimization
(deferred to a follow-up ADR; CPU-only Rust path lands first).

Proposed only — no code changes to core-rs, sensorium, or field.
2026-05-29 09:42:01 -07:00
Shay
1f559344ca
feat(adr-0177-cp1): cue-precision reliability ledger substrate (inert) (#458)
CP-1 of ADR-0177: the per-(cue, op, unit_shape) reliability ledger + credit
assignment mechanism. Mirrors the ADR-0175 per-class ledger discipline
(core/reliability_gate/ledger.py): counts-only integers, reliability via the
pinned conservative_floor, refusals never counted as commitments.

- generate/cue_precision/ledger.py
  - CuePattern: (cue, op, unit_shape) key; op in VALID_OPS, unit_shape closed-set.
  - pattern_for_step / patterns_in_chain: per-step extraction. unit_shape compares
    the operand unit to the model's running (primary/start) unit; a dimensionless
    comparative scalar scales within the dimension -> same_unit.
  - PatternTally: counts-only (correct/wrong, no refused axis); reliability =
    conservative_floor(correct, committed); 0.0 while cold/below N_MIN.
  - CuePrecisionLedger: immutable pattern->tally map (canonical sorted tuple);
    record_chain / record_case credit candidate chains by gold label, independent
    of whether the search resolved or refused.

Inert substrate: not wired into the gate, any scorer, or the search (CP-2/CP-3).
Imported by nothing outside its own tests (asserted by a source-tree scan).

Tests (tests/test_adr_0177_cp1_ledger.py, 27 passing): pattern validation;
unit_shape classification; cold ledger -> 0 reliability; credit assignment;
refusals-not-counted; reliability earned by volume; determinism/replay;
immutability; inertness scan. Smoke suite green (67 passed).
2026-05-29 09:38:51 -07:00
Shay
5c1e9e7fe4
feat(adr-0178-gb3a): clause-scoped referent guard — refuse cross-clause aggregation (#456)
The mandated lookback review before GB-3 (CLAUDE.md §Lookback Review Discipline)
confirmed the audit's hazards H1/H2/H3 were LIVE: compose_sequential summed
same-unit quantities from the whole problem, merging unrelated referents/scopes
and admitting wrong structures whose value happened to ground:
  H1 (second actor's apples)      -> 6+4+2  = 12
  H2 (comparative on other actor) -> (6+4)*2 = 20
  H3 (later depletion event)      -> 6+4+3  = 13

Root cause is the audit's G1/D2 drift: GB-2a re-extracts from the whole text and
ignores GB-1's clause structure. The fix is the GB-3 increment — make the composer
clause-scoped (consume segment_clauses), refusing when the licensed structure spans
clauses, because this slice cannot model referents:
  - quantities must live in exactly ONE clause (0 or >1 -> refuse);
  - a comparative outside that clause -> refuse (unmodelled referent binding).

All three hazards now refuse; all 7 GB-2 single-clause structures preserved
(list-sum, three-item, sum-then-scale, and the mixed-unit/disagreement/too-few
refusals). tests/test_adr_0178_gb3_referent_guard.py would fail against the
pre-guard code (12/20/13), so the obligation is proven, not decorative.

Scope/safety:
- compose_sequential is sealed substrate, not wired to a scorer -> serving
  byte-identical 3/47/0 (lane-SHA 8/8, generate_claims --check OK); practice
  unchanged 4/1/45. No new test failures (2 pre-existing on main).
- ADR-0178 amended: GB-2 relabelled GB-2a (list slice, drift G1 recorded);
  GB-3 split into GB-3a (this referent guard, landed) and GB-3b (constructive
  cross-clause chaining, next).
2026-05-29 09:15:52 -07:00
Shay
38ef66c2a9 docs(backlog): capture everyday-atoms grounding pack idea (closed #449)
#449 shipped an inert, duplicated, unvalidated pack off-brief; closed it but kept
the on-thesis idea (ground the nouns GSM8K problems use) with the real scope it
would need to be beneficial.
2026-05-29 08:55:08 -07:00
Shay
65d857e72a
feat(adr-0179): integrate EX-1/EX-4/EX-5 extraction richness (sealed lane) (#455)
Reconciles ChatGPT's four independently-branched extraction PRs (#451/#452/
#453/#454) into one coherent generate/derivation/extract.py. They each rewrote
the same file + same new test off main, so they conflicted pairwise and needed
integration, not a merge.

Integrated (span-tracked, most-specific-pass-first so numbers are never double
counted):
- EX-1 word-numbers (#452): reuses WORD_NUMBERS; tens-one hyphen compounds;
  factor-bearing half/third/quarter excluded.
- EX-4 list-unit inheritance (#451): bare numeric list with one trailing unit.
- EX-5 sentence-final numbers (#454): bare final number with empty unit.

Deferred: EX-3 multi-word units (#453). Its greedy lowercase span reads
"6 apples and 4 apples" as unit "apples and", regressing GB-2's
test_same_unit_list_sums, and still can't recover real multi-word units from
0024-class text ("jumping jacks on"). Needs a tighter rule; see
docs/handoff/AUDIT-ADR-0179-EX-RECONCILE.md.

Verification (sealed lane only; chat/ does not import this module):
- Serving frozen: lane-SHA 8/8 match, generate_claims --check OK -> 3/47/0
  byte-identical, wrong=0 held.
- Sealed practice improved 4/2/44 -> 4/1/45: case 0025 flips wrong->refused.
  EX-1 reads "three", so completeness sees a quantity the 6x50 chain omits and
  refuses the spurious 300 (gold 1200) instead of committing it.
- No new test failures (3 pre-existing on main).

Also fixes stale test drift from EX-2 (#447): TestDecimalGroundingGapIsDeferred
asserted decimals still refuse, but #447 made $0.75-class resolve to 864.
Renamed to TestDecimalGroundingResolves and updated to assert the flip.

Honest scope note: EX-4 does NOT unblock real case 0024 (its PR test used a
fabricated bare-list paraphrase). TestRealCase0024StillBlocked pins the true
boundary.
2026-05-29 08:43:03 -07:00
Shay
e7032f9e0a
audit: ADR-0178 GB-1/GB-2 lookback (findings only — no code change) (#450)
* add audit report

* docs(audit): expand ADR-0178 GB-1/GB-2 lookback report
2026-05-29 08:42:58 -07:00
Shay
fbcefba00c
docs(adr-0179): scope extraction richness — the prerequisite that unblocks coverage (#446)
The recurring wall every recent measurement hit: the structure machinery (MS-3,
GB-1, GB-2) is built but STARVED by thin extraction. extract.py's digit+single-word
regex loses word-numbers ('three'), multi-word units ('jumping jacks'), currency/
decimals, sentence-final numbers, and mis-attributes units ('36 on Tuesday'->'on',
which blocks GB-2 same-unit list-sum on 0024).

Two layers, very different risk:
(A) sealed derivation extractor (extract.py) — safe to enrich (over-extraction is
caught by the gate's completeness+grounding+uniqueness; refuse-preferring). Bulk
lives here.
(B) shared grounding primitive (_value_grounds/_tokens) — wrong=0-sensitive (serving
round-trip uses it). EXACTLY ONE change: bare-decimal grounding ('N.M' grounds when
digit-runs N,M appear — symmetric with existing .NN/N/M logic), wrong=0-gated by
serving 3/47/0 byte-identical (lane-SHA gate). _value_grounds already handles /bin/zsh.75
but the extractor strips the $ -> bare 0.75 fails; that's the 0003 gap.

Reuses en_numerics_v1 + WORD_NUMBERS (word-numbers) + existing currency/fraction/
compound grounding (ADR-0128/0131.G.3) — extends, never reinvents. Lexeme-level
(ADR-0165). Sub-phases EX-1 word-numbers, EX-2 currency/decimal + grounding fix,
EX-3 multi-word units, EX-4 list-unit inheritance (unblocks 0024), EX-5 sentence-
final, EX-6 measure.

Honest payoff: unblocks BUILT capability — uniform units -> GB-2 list-sum fires
(0024); decimals -> MS-3 product flips (0003); word-numbers -> comparatives resolve;
new gold-matching chains -> feed cue-precision (0177's bottleneck). Flip-curve
should finally move on extraction-blocked cases; op-ambiguity + scale still gate the
rest. wrong=0 obligations: serving byte-identical after the grounding fix; over-
extraction refuse-preferring; determinism.
2026-05-29 08:42:54 -07:00
Shay
4c890f24f9
Merge pull request #447 from AssetOverflow/feat/adr-0179-ex2-decimal-grounding
ADR-0179 EX-2: bare-decimal grounding (shared round-trip primitive)
2026-05-28 17:45:08 -07:00
Shay
8807d29f50 docs(handoff): remote-work brief for ChatGPT connector (audit GB-1/GB-2 + ADR-0179 sealed extraction) 2026-05-28 17:44:48 -07:00
Shay
939fa56671 feat(adr-0179-ex2): bare-decimal grounding in the shared round-trip primitive
EX-2 — the ONE shared-primitive (serving-path) touch of extraction richness.
_value_grounds already grounds the symbol form $N.NN (currency) and N/M (fraction);
a decimal written WITHOUT a symbol ('0.75') is never a single token (the tokenizer
splits on '.') so it failed to ground — refusing correct products like 0003
(48*24*0.75=864). Now a bare decimal 'N.M' grounds when both digit-runs appear,
symmetric with the $N.NN and N/M branches. Only returns True on a match; non-matching
decimals fall through (refuse).

wrong=0 (the load-bearing obligation, this being a shared/serving-path change):
- serving 3/47/0 BYTE-IDENTICAL (verified).
- round-trip + candidate-graph tests 51/51.
- lane-SHA gate (pinned eval lanes unchanged) — verified before merge.

Payoff: 0003 unblocked in sealed practice (search_chain -> 864 = gold; +1 flip).
Decimal cases that now ground but mis-compose become sealed eliminations (learning
signal); serving untouched. 6 EX-2 tests (decimal grounds/refuses, integer
unchanged, serving byte-identical, 0003 resolves).
2026-05-28 17:43:12 -07:00
Shay
16fbc600e6
Merge pull request #445 from AssetOverflow/feat/adr-0178-gb2-compose
ADR-0178 GB-2: sequential composition — same-unit list-sum-then-scale
2026-05-28 17:39:02 -07:00
Shay
b0cee4e3f8 feat(adr-0178-gb2): sequential composition — same-unit list-sum-then-scale
GB-2 first increment (ADR-0178). compose_sequential() adds the structure the blunt
MS-3 shapes couldn't reach: a same-unit quantity LIST sums (additive cue), and any
stated comparative scales the sum (sum-then-scale, 0024-family). Op-per-step from
text structure (list => add; comparative => scale); operands are text quantities
(grounded) + comparative steps (cue-grounded) on the flat left-fold — no derived-
intermediate model needed (running value is the intermediate).

Deliberately narrow: same-unit lists only. A stated comparative is ALWAYS applied
(no bare-vs-scaled self-disagreement). A product base over the same list is added
WITHOUT a comparative tail purely as a disagreement-safety candidate -> a same-unit
list that also carries a mult cue (ambiguous) REFUSES. Product-of-all/cross-unit
products stay MS-3's job (avoids the product x comparative blowups a blunt all-bases
composer produced: 0024 -> 4.3M).

Clean-case capability proven: 8 tests (list-sum, sum-then-double/triple, mixed-units
refuse, ambiguous-disagreement refuse, determinism). Honest practice result: 3/2/45
— NO new flips (extraction wall: real cases like 0024 extract non-uniform units
'36 on' so they aren't seen as same-unit lists), 2 sealed eliminations (0037/0039:
list-sum was the wrong structure -> learning signal). Coverage gated by extraction
richness + cue precision, as predicted.

Sealed; serving untouched. Full derivation surface 53/53; ruff clean; smoke 67.
Continuation: richer relational ops (per/each->multiply, more/older->add), branch/
DAG (0033), and the extraction richness (uniform-unit extraction) that unblocks this
on real cases.
2026-05-28 17:29:53 -07:00
Shay
de3fc38fc2
Merge pull request #444 from AssetOverflow/feat/adr-0178-gb1-clauses
ADR-0178 GB-1: clause segmentation + clause-local sub-derivation (stacked on #441)
2026-05-28 17:20:25 -07:00
Shay
c41fac2f78 feat(adr-0178-gb1): clause segmentation + clause-local sub-derivation
GB-1 — first slice of the comprehension-guided composer (ADR-0178). Reads the
problem one clause at a time and derives each clause's LOCAL contribution; GB-2
combines them across clauses.

generate/derivation/clauses.py:
- segment_clauses(text): sentence-level orthographic split (ADR-0165; not grammar).
- clause_local_results(text) -> tuple[ClauseResult]: per clause, 0 quantities =
  context (hold), 1 = leaf (its value), >=2 = bounded local search (reuses MS-3
  search_chain). Refuse-preferring: ambiguous multi-quantity clause -> unresolved
  hold, not guessed.

Locality is the guidance that bounds the search + steers grouping. 9 GB-1 tests
(segmentation, leaf/context/local-product, ambiguous-holds, determinism,
per-clause structure of a multi-sentence problem). Full derivation surface 86/86;
ruff clean; smoke 67. Sealed; not wired into serving (ClauseResults ready for
GB-2 sequential combination).
2026-05-28 17:19:50 -07:00
Shay
5dacc6625e
Merge pull request #443 from AssetOverflow/docs/adr-0178-compositional-structure
ADR-0178 (Proposed): Compositional Structure — comprehension-guided multi-step (Gap B)
2026-05-28 17:15:52 -07:00
Shay
70cf9752c3
Merge pull request #442 from AssetOverflow/docs/adr-0177-cue-precision-learning
ADR-0177 (Proposed): Cue-Precision Learning — the self-supervised half
2026-05-28 17:10:09 -07:00
Shay
9a7f9f6a66 docs(adr-0178): scope Gap B — comprehension-guided compositional structure
The actual coverage lever (ADR-0177 established cue-precision is its gate+prune,
not the unlock). Gap B = which quantities group via which ops in what order.

The reframe that resolves the rich-search-vs-uniqueness tension: structure-from-
READING, not enumeration. The text encodes its own structure — every gold case
fits a sequential clause-by-clause read (0021 one clause 15x10x3; 0003 48 ->x24
->x0.75; 0024 sum ->x3; 0033 seq chain + branch). So Gap B is comprehension: read
the problem, build the derivation structure as you read, hold alternatives on
ambiguity, reevaluate on lookback. This IS the project's original 'word-to-word
with lookback + problem-solving throughout' articulation, and the synthesis of
reader (0174) + solver/gate (0176) + learning (0177) + packs.

Decision: a comprehension-guided sequential composer — clause-local bounded
sub-derivations combined across clauses via relational cues (per/each/of->multiply,
and-list->sum, comparative->scale, more/older->add, fewer/less->subtract), held-
hypothesis on ambiguity + eliminate/reevaluate (REPOINT ADR-0174's inert reader
substrate from parse to STRUCTURE — where it finally becomes load-bearing), scored
by the 0176 self-verification gate + uniqueness, guided by 0177 cue-precision.
Locality bounds the search; relational cues constrain the cross-clause op so
uniqueness can RESOLVE (not just refuse). Coverage rises only as far as the reading
constrains structure; irreducible ambiguity refuses (wrong=0).

wrong=0 obligations: per-step self-verification, irreducible-ambiguity-refuses,
completeness+uniqueness over the whole structure, no-spurious-structure, determinism,
sealed. Honest hard parts: relational-cue precision (co-dependent with 0177 +
data-starved), clause segmentation, referent binding, branch/DAG (0033 quantity
reuse, GB-5), scale. This is the comprehension core — largest remaining capability;
serving lift materialises here, incrementally. Sub-phases GB-1..GB-6.
2026-05-28 17:06:35 -07:00
Shay
b57e5dca9a docs(adr-0177): scope cue-precision learning (the self-supervised half)
The lever MS-1->MS-3 proved: learn which (cue->op) readings are reliable from
practice eliminations, closing ADR-0175's self-verification 'necessary-not-
sufficient' gap before Phase 5.

Mechanism: per-(cue, op, unit_shape) reliability ledger (reuses ADR-0175 ClassTally
+ conservative_floor), fed by gold-labelling search candidate chains. Three uses:
U1 self-verification TRUST (near-term value: makes the Phase 5 proposal gate honest;
cold ledger => refuse, no junk proposals), U2 search guidance, U3 disagreement
resolution (coverage lever, hard-gated: margin+theta, ties refuse).

Load-bearing honesty (the bottleneck): a pattern earns POSITIVE signal only from a
gold-MATCHING chain; current blunt shapes match gold for ~4/50 cases, so the ledger
is starved (all-blame) AND structure failures (Gap B) pollute cue->op credit (a
correct op penalised for a product-of-ALL structure error). Plus data starvation
(50 cases << N_min). So cue-precision is COUPLED to richer guided shapes (Gap B) +
scale; it co-evolves (Gap B supplies gold-matching candidates -> cue-precision earns
signal -> prunes Gap B's search). It is the TRUST substrate + pruning engine, NOT
the coverage unlock by itself.

Recommended sequencing: CP-1/CP-2 (mechanism + self-verification trust, near-term
correctness value) now; richer guided shapes (Gap B) next as the flip lever; scale
makes it compound. wrong=0 obligations: cold=>no regression, ties refuse, theta-gated
serving, credit-noise can't flip serving (floor+N_min+margin+ratification+gold-tether),
determinism. Sub-phases CP-1..CP-4.
2026-05-28 17:00:33 -07:00
Shay
ebaa603b68
Merge pull request #441 from AssetOverflow/feat/adr-0176-ms3-search
ADR-0176 MS-3: target-guided bounded multi-step search (stacked on #440)
2026-05-28 16:55:37 -07:00
Shay
309f3fc10c feat(adr-0176-ms3): target-guided bounded multi-step search
MS-3 composes MS-1 (Target) + MS-2 (comparative chains + completeness) + the gate.
generate/derivation/multistep.py: search_chain(problem_text, target=None).

Shape-based, NOT blind enumeration: enumerates a small principled candidate set
(product-of-all if a multiplicative cue is present; sum-of-all if an aggregation
hint is present; each optionally + comparative scalars), each using all
quantities, routed through select_self_verified (grounding ∧ cue ∧ unit ∧
completeness ∧ uniqueness). Bounded (MAX_QUANTITIES, refuse-on-overflow) +
deterministic. Target supplies the aggregation cue + question quantities; target-
UNIT matching is deferred (answer_unit=start.unit is wrong for cross-unit products
-> a unit gate would over-refuse; documented).

Honest practice measurement (sealed lane): 4 correct / 9 wrong / 37 refused
(baseline 3/0/47). +1 flip is the unambiguous whole-problem product (0021); the 9
wrongs are product-of-all eliminations on multi-step problems (caught by gold,
the learning signal). Whole-problem shapes add no coverage beyond the unambiguous
product WITHOUT cue precision: when product and sum both self-verify they disagree
-> uniqueness refuses (safe-but-low-coverage by design). The lever remains cue
precision (the ADR-0175 learning loop).

Microscope finding: 0003-class flips (48*24*0.75=864=gold) are blocked by a
DECIMAL/currency grounding gap -- '$0.75' tokenizes to 0/75 so '0.75' is not
grounded by the shared round-trip primitive. Not a search bug; deferred
extraction-richness work (won't casually change the serving round-trip primitive).
A test documents the current refusal so the fix is detectable.

wrong=0: serving untouched (sealed); ambiguity + no-licensed-cue refuse; routes
through the proven gate. 8 MS-3 tests; full derivation surface 77/77; ruff clean;
smoke 67.
2026-05-28 16:51:43 -07:00
Shay
1e726dc777
Merge pull request #440 from AssetOverflow/feat/adr-0176-ms2-chain
ADR-0176 MS-2: multi-step chain model (text + comparative operands)
2026-05-28 16:44:34 -07:00
Shay
5a9454af20 feat(adr-0176-ms2): multi-step chain model — text + comparative operands
MS-2 of multi-step composition. Extends the derivation model so a chain mixes
text-quantity operands and COMPARATIVE-scalar operands (twice->x2, 'N times'->xN,
half->x0.5), self-verifying the whole chain with completeness over body+question
and question-target matching.

- model.py: Step gains comparative flag.
- comparatives.py: ComparativeScalar gains number_token (the '<N> times' number,
  so completeness counts the consumed body quantity); comparative_step(cs) bridges
  a scalar into a Step (operand grounded by cue, not a text value token).
- verify.py: self_verifies exempts comparative operands from value-grounding
  (clause 1) — they are cue-grounded (clause 2); completeness (Counter) counts a
  digit comparative's number_token as consuming the body quantity. Adds target_units
  to select_self_verified: a chain whose answer_unit isn't the asked unit is dropped
  (question-target match; empty target_units imposes no constraint).

Proves the multi-step shapes from the gold structures: 0024 (text sum then 'three
times' scale -> 438), 0033 father-chain (digit-comparative '7 times' + fixed 'half'
+ text add -> 47). Full 0033 DAG (quantity reuse + the question's 25) deferred.

25 MS-2 tests; full derivation surface 69/69 (3a/3b/comparatives/ms1/ms2); ruff
clean; smoke 67. Not wired into serving (model ready for MS-3 target-guided search).
2026-05-28 16:35:41 -07:00
Shay
2400bc58e2
Merge pull request #439 from AssetOverflow/feat/adr-0176-ms1-question-target
ADR-0176 MS-1: question-targeting (stacked on #438)
2026-05-28 16:32:08 -07:00
Shay
4ecc17c5ec feat(adr-0176-ms1): question-targeting
MS-1 of multi-step composition. Turns the question into a Target = what the
problem asks for, the search's pruning signal + stopping criterion (MS-3).

Lexeme-level only (ADR-0165): the existing question parser returns nothing on
these GSM8K questions, and 0165 forbids new question-shape grammar regex. Three
robust signals:
- quantities: numbers stated IN the question (0033's 'when she is 25') via the
  body's lexeme extractor — they participate in the derivation.
- aggregation: presence of an aggregation lexeme (total/altogether/combined/sum/
  'in all'/'in total') — soft hint the final step is a sum.
- units: asked units resolved by INTERSECTION with the body's known units
  (precise lexeme match, e.g. 'jumping'). Superordinates (weight<->pounds) are
  NOT faked — deferred to a curated superordinate-units pack; until then the unit
  signal is precise-but-incomplete and the search leans on completeness.

Refuse-preferring: empty target field is not an error, just a weaker prune.
generate/derivation/target.py: Target + extract_target(question, known_units=()).

12 MS-1 tests (question-quantity, aggregation, body-unit intersection,
superordinate-not-faked, determinism, frozen). Verified: derivation suite 57/57;
ruff clean; smoke 67. Not wired into serving (Target ready for MS-2/MS-3).
2026-05-28 16:21:40 -07:00
Shay
0aaec09059
Merge pull request #438 from AssetOverflow/feat/comparatives-pack
ADR-0176: en_core_comparatives_v1 pack + comparative-scalar extraction
2026-05-28 16:15:44 -07:00
Shay
752e1e13bd
Merge pull request #437 from AssetOverflow/docs/adr-0176-multistep-composition
ADR-0176 (Proposed): Multi-step grounded composition with question-targeting
2026-05-28 16:13:01 -07:00
Shay
37cbdeee3f
Merge pull request #436 from AssetOverflow/feat/adr-0175-selfverify-completeness
ADR-0175: strengthen self-verification with a completeness clause (practice wrongs 9→2)
2026-05-28 16:12:37 -07:00
Shay
63f2544862 feat(adr-0176): en_core_comparatives_v1 pack + comparative-scalar extraction
The curated, irreducible world-fact primitives multi-step composition needs
(ADR-0175 section 10: the engine can't derive 'twice = 2' from arithmetic). The
microscope flagged these via the 0015/0025/0024/0033 wrongs.

language_packs/data/en_core_comparatives_v1/: 9 closed-set multiplicative
comparatives (twice/double/triple/quadruple/half/quarter + inflections) -> scalar
ops. manifest.json with sha256 of the bytes on disk (CLAUDE.md pack rule).
Refusal-preferring: non-terminating/ambiguous comparatives (a third, several)
deliberately excluded; expansion via HITL corridor.

generate/derivation/comparatives.py: extract_comparative_scalars() ->
ComparativeScalar(op, scalar, span, cue). Fixed lexemes + the '<number> times'
pattern (digit or word-number via WORD_NUMBERS). Lexeme-level (ADR-0165);
deterministic (text-order); supplies only the SCALAR primitive — referent
binding is the multi-step search's job (ADR-0176).

14 tests incl. refusal-preferring discipline + pack integrity (manifest checksum
matches bytes on disk). Verified: derivation suite 45/45; ruff clean; smoke 67;
packs 141. Not wired into serving (data + extractor ready for ADR-0176 MS phases).
2026-05-28 16:07:35 -07:00
Shay
68e6cbd4ef docs(adr-0176): scope multi-step grounded composition with question-targeting
The dominant remaining lever for serving lift (79% need mul, median 3 steps;
single-step search + completeness flips only 0021). Grounded in gold step
structures: derivations are CHAINS with intermediate results as operands;
quantities come from body AND question (0033's '25'); several need comparatives
(half/N-times).

Decision: bounded, deterministic, TARGET-GUIDED multi-step grounded derivation
search, gated by ADR-0175's strengthened self-verification (grounding ∧ cue ∧
unit ∧ completeness) + uniqueness + a new question-target match. Sealed practice.

Two new ideas beyond 0175: question-targeting (turn the question into a target =
search-pruning + stopping criterion) and multi-step chaining (intermediates as
derived operands). Sub-phases MS-1..MS-5, wrong=0-first (gate/target before broad
search). Invariant #2 extended to chains. Honest hard part: search explosion +
uniqueness refuses most -> target-pruning + cue-guidance + depth-bound are the
tractability levers; low coverage initially; comparatives pack is a prerequisite;
serving lift still waits on 0175 Phase 5 ratification. Reuses solver +
question extraction + round-trip primitives.
2026-05-28 16:00:00 -07:00
Shay
74fbc6090e
Update generate/derivation/verify.py
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-05-28 15:57:04 -07:00
Shay
04e70dabfa
Update generate/derivation/verify.py
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-05-28 15:56:54 -07:00
Shay
6212943c5a feat(adr-0175): strengthen self-verification with a completeness clause
Self-verification strengthening (microscope-driven). The Phase 3b measurement
showed self-verification was necessary-but-not-sufficient: 9/13 self-verified
attempts were wrong. Inspecting them deterministically revealed most were
correct FIRST STEPS of multi-step problems that ignored numbers stated elsewhere.

Adds clause 5 to self_verifies: a derivation must account for every quantity the
problem states (problem quantities subset of used). Refuse-preferring: unused
quantities -> not self-verified. This catches the multi-step-incomplete attempts
the grounding/cue/unit clauses cannot (their operands ARE grounded).

Practice measurement: wrongs 9 -> 2 (4 correct / 2 wrong / 44 refused). The 2
survivors (0015, 0025) are COMPLETE but wrong due to missed WORD-quantities
('twice', 'her friends') -> the microscope points the next change at extraction.

Updated the disagreement test to use two complete derivations; added an
incomplete-refusal test. 32 tests pass; smoke green; serving untouched (sealed).
2026-05-28 15:53:11 -07:00
Shay
dd6064065f
Merge pull request #431 from AssetOverflow/feat/adr-0175-calibrated-learning
ADR-0175 (Proposed): Calibrated Attempt-and-Eliminate Learning — two regimes under wrong=0
2026-05-28 15:43:15 -07:00
Shay
dfb370a47e
Merge pull request #435 from AssetOverflow/feat/adr-0175-phase3b-mult-search
ADR-0175 Phase 3b: bounded multiplicative search in the sealed practice lane
2026-05-28 15:43:11 -07:00
Shay
52227920f3
Merge pull request #430 from AssetOverflow/feat/adr-0174-phase5a-retire-inert-reader
ADR-0174 Phase 5a: retire inert GSM8K scoring-path reader (net -1,038 LOC)
2026-05-28 15:37:23 -07:00
Shay
1a73d4bab9
Merge pull request #432 from AssetOverflow/feat/adr-0175-phase1-reliability-gate
ADR-0175 Phase 1: reliability ledger + attempt/refuse gate substrate
2026-05-28 15:35:40 -07:00
Shay
5ecfda3fab docs(adr-0175): record Phases 1-3b lookback review
Solid: 4 invariants proven by failing-under-violation tests; 84 green; seal
verified. No live hazards (9 search wrongs are sealed eliminations).
Drift: (1) 3a gate is PARTIAL vs spec — round-trip + no-contradiction not wired;
3b's necessary-not-sufficient finding follows -> a self-verification-strengthening
phase must precede Phase 5. (2) class taxonomy: Phase 2 uses gold operation-class
not capability-axis. (3) minor defensive-branch test gaps (no risk).
2026-05-28 15:34:21 -07:00
Shay
872ed3b52d feat(adr-0175-phase3b): bounded multiplicative search in the sealed practice lane
ADR-0175 Phase 3b — the first live attempt generator. Runs only in the sealed
practice lane, only on cases the engine refused; every proposal is gated by the
Phase 3a self-verification gate.

generate/derivation/:
- extract.py: extract_quantities() — lexeme-level (number + unit word; ADR-0165).
- search.py: search_multiplicative() — one in-clause product candidate per
  sentence with >=2 quantities + a present multiplicative cue; gated by
  select_self_verified. Per-sentence scope + multi-candidate disagreement give
  the uniqueness gate real teeth (two qualifying sentences -> refuse). The cue
  set {each,every,for,per,times} is an explicit PROVISIONAL hypothesis the
  practice loop refines, not a claimed-correct grammar.
evals/gsm8k_math/practice/v1/search_runner.py: search_augmented_scorer +
  build_search_report — base scorer, then a practice-only attempt on refusals.

MEASUREMENT (the deliverable, per the breadth-of-impact test):
  practice with search:  correct=4  wrong=9  refused=37   (baseline 3/0/47)
- Flips +1 (0021, the clean in-clause aggregate) and its renumbered/reworded
  variants (ADR-0114a perturbation guard) -> a real capability, not memorisation.
- 9 wrong attempts -> elimination records (§9), the learning signal. The naive
  full-product cue model over-attempts; the eliminations are exactly the signal
  that refines it.

HONEST FINDING: self-verification (grounding ∧ cue ∧ unit ∧ uniqueness) is
NECESSARY but NOT SUFFICIENT — 9/13 self-verified attempts were wrong vs gold.
The gap is cue PRECISION / which-quantities-compose (the knowledge axis), not
'can we multiply' (skill). This is why the search runs sealed: gold catches the
9, and case 0050 (canary) attempted-and-failed IN PRACTICE without touching
serving -> validates the seal.

Invariants: #1 seal (serving still 3/47/0; 0050 refuses in serving; no
generate/chat import of the lane), #3 determinism. Serving wrong=0 untouched.

Verified: 3a+3b 31/31; ruff clean; serving lane 4/4; smoke 67/67.
2026-05-28 15:29:08 -07:00
Shay
0bdb3a441c feat(adr-0175-phase3a): self-verification gate (built before the search)
ADR-0175 Phase 3 splits wrong=0-first: build the gate (3a) and PROVE invariant #2
before the bounded search (3b) that could exploit gaps.

generate/derivation/:
- model.py: Quantity / Step / GroundedDerivation. A derivation is a left-fold over
  text-sourced quantities; each Step carries its licensing cue (the lexeme the
  search claims licenses the op).
- verify.py: self_verifies() — grounded operands ∧ grounded operation cues ∧ unit
  consistency ∧ no divide-by-zero. Grounding REUSES the canonical primitives from
  math_roundtrip (_tokens/_token_in/_value_grounds) so the gate cannot drift from
  the round-trip contract. select_self_verified() adds the uniqueness rule:
  unique self-verifying answer resolves; zero or disagreeing refuse (wrong=0).

INVARIANT #2 proven (TestInvariant2_NoSpuriousSelfVerification): the gate refuses
to self-verify a derivation that is not grounded+unit-consistent+unique even when
its value coincides with gold — the 20/5==4 class:
- invented operand not in text -> refused
- operation cue not in text -> refused (division not licensed by any present cue)
- value coincidence (20/5=4) with ungrounded op -> still refused
- add across units (pounds + reps) -> refused
- divide-by-zero -> refused
Plus uniqueness: disagreeing grounded derivations -> refuse; agreeing -> resolve.

Phase 3a is inert (nothing wires generate.derivation into serving). 3b is the
bounded search that produces derivations for this gate + measures the flip-curve
in the practice lane under perturbation.

Verified: 16/16; ruff clean; smoke 67/67; no serving import.
2026-05-28 15:19:02 -07:00
Shay
d90887b80f feat(adr-0175-phase2): sealed practice lane over GSM8K train
ADR-0175 Phase 2 — a NEW lane (evals/gsm8k_math/practice/v1/), separate from the
wrong=0-pinned serving runner which is NOT modified. Runs the 50 cases in
practice mode: scores correct/wrong/refused as practice metrics, feeds per-class
counts into the Phase 1 ledger, diagnoses every refusal (§8), emits an
elimination record per wrong.

- classify_operation: gold-derived primary op class {multiplicative,divisive,
  additive} from <<a*b=c>> calc annotations (Tier-1 checkable in practice).
- diagnose_refusal (§8): skill_gap / knowledge_gap / genuine_ambiguity router.
- EliminationRecord (§9): wrong attempt gold caught -> pruning signal.
- PracticeReport: counts + per-class ledger + diagnoses + eliminations; as_dict.
- run_practice(cases, scorer=...): injectable scorer for tests; defaults to the
  candidate-graph scorer (read-only — never alters serving).

Live result mirrors serving (3 correct / 0 wrong / 47 refused of 50) because the
engine still refuses rather than guesses — attempts/eliminations go live in
Phase 3. But the diagnosis is already actionable: 35 skill_gap / 12 knowledge_gap
/ 0 genuine_ambiguity — 74% of refusals are skill gaps (Phase 3's search target),
quantifying the skill-vs-knowledge split.

Invariants: #1 seal (serving still 3/47/0; no generate/chat import of the lane),
#3 determinism (report byte-identical across runs). Elimination + wrong-tolerance
paths unit-tested via injected scorer (no live wrongs yet).

Verified: Phase 1+2 53/53, serving train_sample tests 4/4 (seal), smoke 67/67,
ruff clean.
2026-05-28 15:12:33 -07:00
Shay
8775765881 feat(adr-0175-phase1): reliability ledger + attempt/refuse gate substrate
ADR-0175 Phase 1 — standalone, deterministic, zero serving change. Nothing in
the serving/eval path imports it.

core/reliability_gate/:
- floor.py: conservative_floor(s,k) — pinned one-sided Wilson lower bound over
  COMMITTED trials. z=2.576, N_MIN=10; range [0,1) (never exactly 1.0); float64
  rounded half-to-even to 1e-9 for cross-backend replay. Perfect record reduces
  to k/(k+z²) (earned by volume).
- ledger.py: ClassTally — immutable per-class counts; reliability = commitment
  precision (refusals excluded so coverage never penalizes reliability);
  t2_precision over the anchor set; coverage tracked separately.
- ceilings.py: Action{PRACTICE,PROPOSE,SERVE} + Ceilings — human-set θ
  (practice=0, propose=.85, serve=.99). Frozen; with_override returns a NEW
  instance (no in-place self-authorization).
- gate.py: license_for() — deterministic gate, measured/required≥1 (≡ measured≥
  required; required=0 ⟹ always). Pure; never mutates/emits ceilings.

34 tests, each ADR invariant exercised by a test that fails under its violation:
#3 determinism/replay (idempotent, pre-rounded, deterministic decisions),
#4 no self-authorization (frozen ceilings; gate never emits/mutates them),
#1 proxy (zero serving coupling). Plus the §4a worked examples (38 clean
commitments clear propose; one wrong in 40 drops below; serve needs ~657).

Verified: 34/34 pass; architectural invariants 40/40; smoke 67/67; ruff clean;
no serving/eval import of the package.
2026-05-28 15:04:48 -07:00
Shay
13d016c90b docs(adr-0175): pre-implementation audit — correct reuse overclaims, record findings
Lookback/cross-reference audit before any code. No hard blockers. Corrects 3
overclaims of reuse and records the audit:
- reliability ledger is NEW (calibration/ is a grid-search param tuner, not a ledger)
- 0174 eliminate/reevaluate/contemplate is reading-coupled -> 'repoint to solving'
  needs generalization, not drop-in reuse
- teaching corridor evidence (MathReaderRefusalEvidence) is reader-refusal-coupled
  -> solver-practice proposals need a new evidence type
Constraints recorded: wrong=0 pinned on serving lane by ~25 tests + train_sample
runner -> practice MUST be a separate lane (phasing updated). Alignments noted:
ADR-0165 (lexemes-not-grammar) fulfilled by thin-front-end/thick-solver split;
INV-07 governance = no-self-authorization pre-wired; MAX_TOTAL_BRANCHES precedent
for bounded deterministic search; seal exists.
2026-05-28 14:55:53 -07:00
Shay
e3c28773ff docs(adr-0175): pin conservative_floor (Wilson lower bound) + N_min
Resolves Open Question #1. conservative_floor(s,k) = one-sided Wilson lower
bound over COMMITTED trials (k=correct+wrong; refusals excluded so coverage
never penalizes reliability). Constants: z=2.576 (single global pessimism
knob), N_min=10. Range [0,1) — never returns exactly 1.0. float64 rounded
half-to-even to 1e-9 for cross-backend replay. z (estimator skepticism) and
per-class theta (action's required reliability, human-set) are independent
dials; engine touches neither. Worked cost-to-clear table + asymmetry example
included.
2026-05-28 14:51:50 -07:00
Shay
8c2e469be0 docs(adr-0175): calibrated attempt-and-eliminate learning architecture
Proposed ADR + session derivation doc capturing the 2026-05-28 design
discussion that took GSM8K Phase 5b from 'build another matcher' to a
self-calibrating problem solver.

Session doc (docs/sessions/SESSION-2026-05-28-...): the full journey —
problem (per-shape matchers can't compound; 79% need mul, 0% single-step),
dead-ends (brute-force spurious matches; 0021 is the only single-sentence
case and it's idiosyncratic), and the four pivots that converged on the
solution.

ADR-0175 (Proposed): the decision —
- two regimes: serving (wrong=0, unchanged) vs sealed practice
  (attempt-and-eliminate; wrong is the learning signal)
- proof-carrying seal: practice never writes serving; ratification only
- deterministic attempt/refuse gate: reliability(C) / theta_required >= 1
  (NOT RL; regimes collapse the reward side so only reliability is quantified)
- per-class calibration ledger of replayable COUNTS + conservative lower
  bound; human-set theta ceilings raised only on evidence
- checkability ladder (gold > convergent self-verification > consistency-only),
  privilege proportional to reversibility; provenance + gold tether against
  correlated self-delusion
- diagnostic refusal routes skill vs knowledge vs ambiguity; three
  compounding stores (vault/packs/pruning); self-proving acquisition narrows
  human input without bypassing the gate
- five proof-obligation invariants (wrong=0 on serving, no spurious banking,
  determinism, no self-authorization, retractability)

Supersedes the matcher-oriented ADR-0174 5b sub-phases; repoints the
0174 eliminate/reevaluate/contemplate substrate from reading to solving.
Open question: shape of conservative_floor + N_min.
2026-05-28 14:45:17 -07:00
Shay
5830c1fb8c docs(adr-0174): write Phase 5b scope — operation-capability buildout
Grounded in a ground-truth measurement of the 47 train_sample refusals
against GSM8K's own <<a*b=c>> calculator annotations:
- 37/47 (79%) use multiplication; 43/47 use mul-or-div; 0/47 single-step
- multiplication is the foundational general capability (breadth = the
  anti-overfitting signal), but necessary-not-sufficient: no case flips
  from an operation matcher alone

Solver already supports {add,subtract,transfer,multiply,divide,apply_rate,
compare_additive,compare_multiplicative}; the gap is the reader->injector->
Operation front-end (matcher extracts 0 anchors on real sentences).

Sub-phase sequence (biggest-chunk-first, measure-the-flip-gated):
- 5b.1 single-sentence multiplicative aggregate (cleanest proof, ~2-4)
- 5b.2 shallow 2-3 step composition (25/47, the real chunk)
- 5b.3 deep multi-step 4-7 (22/47) under held-hypothesis elimination
Generality guard: flipped cases must hold under ADR-0114a perturbation/OOD.
Explicitly NOT widening discrete_count injector (overfitting + wrong=0 hazard).
2026-05-28 13:48:58 -07:00
Shay
3fd317290b feat(adr-0174-phase5a): retire inert GSM8K scoring-path reader
The recognizer/candidate-graph path is the single canonical reader.
Retires the flag-gated incremental-reader dispatch that admitted 0/50 on
train_sample and only added a dead fall-through:

- remove _try_comprehension_reader, _try_reader_for_question, _tokenize_sentence
  and both dispatch blocks from generate/math_candidate_graph.py
- delete generate/comprehension/lifecycle_runtime_adapter.py (402 LOC,
  used only by the question-reader dispatch)
- drop the comprehension_reader_questions config flag and the parse_and_solve
  / _score_one_candidate_graph config threading
- remove the --use-reader runner plumbing + flag-ON/OFF delta report from
  the train_sample runner; refresh report.json (drops stale use_reader field
  and a stale refusal-reason; verdicts unchanged at 3/47/0)
- remove the now-dead use_reader field from teaching/coverage.py
  CoverageReport + the core teaching coverage CLI flag
- delete tests/test_reader_coexistence.py (flag-ON/OFF premise dissolved);
  fix 3 ADR-0174 build_report calls and 2 subprocess invocations

lifecycle.py and audit.py are KEPT — they are load-bearing for the ADR-0172
math-contemplation teaching corridor (audit_problem -> teaching/math_*),
which a pre-deletion trace surfaced. The parent ADR's plan to delete
lifecycle.py was wrong; only its GSM8K scoring dispatch was inert.

Net -1,038 LOC (code + tests). Behavior-preserving:
- train_sample 3/47/0, byte-identical verdicts to pre-5a baseline
- determinism holds; smoke/packs/runtime/cognition/teaching lanes green
- contemplation corridor + lifecycle/audit tests pass

Pre-existing (NOT introduced here; reproduce on base with changes stashed):
5 out-of-curated-lane stale committed-artifact / stale-assertion failures
(test_math_evidence_e2e, test_adr_0126_runner_wiring, G3/coverage_probe
report-match, test_refusal_taxonomy_lane rebuild).
2026-05-28 13:38:44 -07:00
Shay
62a3e23318 docs(adr-0174): amend Phase 5 scope — invert premise, split 5a/5b
Pre-implementation investigation (lookback discipline) found the original
Phase 5 text inverted against shipped code:
- math_parser.py already out of runtime + candidate-graph scoring path
- lifecycle.py admits 0/50 (inert parallel parser, not the reader to promote)
- correct>=25 is a semantic gate structural collapse cannot meet

Decision (Invert + split): recognizer/candidate-graph path is the canonical
reader; lifecycle.py is retired. Phase 5a = structural retirement (net -LOC,
3/47/0 byte-identical, wrong=0). Phase 5b = semantic narrowness removal (the
real lift, own sub-phases, per-layer wrong=0 obligations).
2026-05-28 13:19:30 -07:00
Shay
d1dbda24fc
Merge pull request #429 from AssetOverflow/feat/adr-0174-phase4-contemplate
feat(adr-0174-phase4): in-loop contemplate + en_core_names_v1 pack
2026-05-28 13:07:05 -07:00
Shay
aa15dc1f3d feat(adr-0174-phase4): in-loop contemplate + en_core_names_v1 pack
ADR-0174 Phase 4 — deterministic search adapter for evidence that
disambiguates surviving hypothesis sets. First load-bearing use case:
gendered-pronoun resolution via the en_core_names_v1 pack — turns
the Phase 3a multi-actor defense from refuse-on-ambiguity into
admit-via-evidence when an unambiguous gendered name binds the
pronoun to one antecedent.

generate/comprehension/contemplate.py (new, ~310 lines):
  - Resolution dataclass (closed-set kind + source + evidence shape)
  - VALID_RESOLUTION_KINDS = {eliminate, admit_unknown}
  - VALID_RESOLUTION_SOURCES = {vault, pack, audit_history}
  - contemplate() orchestrator — adapters consulted in precedence
    order: vault > pack > audit_history (ADR-0174 §Open Q#3)
  - _consult_packs() — gendered-pronoun resolution implementation
  - _consult_vault() and _consult_audit_history() — stubs (Phase 4b)
  - _PRONOUN_GENDER closed map (she/he gendered; they/them epicene)
  - _load_names_pack() with @lru_cache; refusal-preferring on
    absent pack

language_packs/data/en_core_names_v1/ (new pack):
  - gender.jsonl — 59 unambiguously-gendered English first names
    (30 female, 29 male), alphabetically sorted, JSONL with schema
    {name: str, gender: 'female'|'male'}.  Covers names appearing
    in train_sample/v1 GSM8K problems (Alice, Bob, Daniel, Malcolm,
    Erica, Jan, Tina, etc.).  Deliberately excludes ambiguous-
    gender names (Jordan, Alex, Casey, Pat, Taylor, Morgan, Sam,
    Chris, Robin, Riley).
  - manifest.json — pack metadata with sha256 checksum
    (f65836e7a25a9db8aae984d259b60e161574ff3b4bb135a924aa767a794fbd21),
    entry count, schema declaration, ambiguity discipline,
    expansion pathway through HITL corridor, wrong=0 protection
    contract.

generate/math_candidate_graph.py:
  - Phase 4 wiring at the multi-actor defense site (was: refuse
    on len(_distinct_priors) > 1; now: invoke contemplate first,
    fall through to defense when contemplate returns None).
  - On contemplate.kind='admit_unknown' from pack source: extract
    chosen antecedent from evidence, override _antecedent, clear
    _multi_actor_ambiguous, proceed to admit-via-PronounResolution.
  - On contemplate=None: fire new 'ambiguous_unresolvable'
    contemplate trace event AND original 'no_antecedent_ambiguous'
    lookback event, drop candidates.

tests/test_adr_0174_phase4_contemplate.py (new):
  27 acceptance tests covering: primitive contract (empty/single-
  survivor noop), Resolution dataclass invariants (5 refusal
  paths), names pack load + content spot-checks, pronoun gender
  lookup (gendered + epicene), 6 gendered-pronoun resolution
  cases (she/he success, same-gender refusal, unknown-name
  refusal, epicene refusal, no-matching-gender refusal), end-to-
  end wiring through parse_and_solve, determinism (two calls
  byte-identical, evidence sorted), closed-set contracts,
  wrong=0 + case-0050 canary.

tests/test_adr_0174_phase3_lookback.py + phase3b_compound_clause.py:
  Updated the multi-actor defense tests to use SAME-GENDER
  antecedents (Alice + Mary) so Phase 4 contemplate cannot
  disambiguate via gender pack — the Phase 3a defense still
  fires. (For mixed-gender antecedents the new behavior is
  correct admit-via-evidence; that's tested in Phase 4 suite.)

End-to-end answer-correctness caveat (documented in test
docstrings):
  Phase 4 trace events fire correctly when the recognizer-
  injection path encounters multi-actor pronoun cases that the
  pack disambiguates.  However the regex parser ALSO produces
  candidates for simpler pronoun-subject shapes (without
  intervening prepositional phrases); those compete in the
  Cartesian product and the contemplate-resolved binding may be
  shadowed.  This is the latent regex-path pronoun hazard tracked
  in project-adr-0174-multi-actor-pronoun-hazard memory.  Full
  answer lift on train_sample requires regex-path defense (Phase 5
  regex retirement work).

Acceptance:
- 285/285 ADR-0174 + math_problem_graph tests pass
- Smoke 67/67, packs 141/141
- train_sample 3/47/0 preserved (wrong=0 held)
- Phase 4 trace event fires end-to-end on multi-PP pronoun-subject
  case: contemplate/resolved with chosen=Alice, evidence pack
  Alice=female + Bob=male

References: ADR-0174 §In-loop contemplation, CLAUDE.md §Lookback
Review Discipline, docs/handoff/ADR-0174-PHASE-3B-4-COMBINED-SCOPE.md,
docs/handoff/phase-3b-4-skeleton/ (skeleton dispatch source),
project-adr-0174-multi-actor-pronoun-hazard memory.
2026-05-28 12:09:52 -07:00
Shay
49ce76a525
Merge pull request #428 from AssetOverflow/feat/adr-0174-phase3b-compound-clause
feat(adr-0174-phase3b): compound-clause held hypotheses
2026-05-28 11:59:15 -07:00
Shay
4b277d4e84 feat(adr-0174-phase3b): compound-clause held hypotheses
ADR-0174 Phase 3b — emit N anchors for compound-clause discrete-count
sentences sharing one subject + one verb. Architectural substrate;
score on train_sample preserved at 3/47/0 (compound cases like 0027
admit past the recognizer-injection refusal but the rest of the
problem still has downstream complexity — fractions, percent — that
needs Phase 4 + solver work).

generate/comprehension/state.py:
  HYPOTHESIS_CAP raised 4 → 8. Case 0040 emits 5 anchors; cap=8
  gives headroom (7-item lists) without becoming permissive.

generate/recognizer_match.py:
  _try_extract_compound_discrete_count_anchors() — new extractor
  emitting tuple of anchors for compound sentences. Refusal-
  preferring on:
    - no conjunctive separator (single-anchor path)
    - multiplicative/percent/fraction markers
    - head verb not in whitelist
    - any tail clause without grounded (count, observed_noun) pair
    - exceeding HYPOTHESIS_CAP
    - unaccounted digit in tail (wrong=0 hazard defense surfaced by
      2026-05-28 implementation review: bogusnoun would silently fail
      to produce anchor while leaving the digit unaccounted, admitting
      partial state)
  Wired into _match_discrete_count_statement dispatch as fallback when
  single-anchor extraction fails.

tests/test_adr_0174_phase3b_compound_clause.py:
  11 acceptance tests passing — pure conjunctive lists (proper-noun
  + pronoun-subject + single-actor antecedent), refusal-preferring
  discipline (mixed-verb, multiplicative-tail, non-whitelisted-head,
  partial-grounding all-or-nothing), HYPOTHESIS_CAP enforcement,
  multi-actor pronoun defense preserved on compound, wrong=0 +
  case-0050 canary.

tests/test_adr_0174_phase1_held_hypothesis_state.py:
  Updated test_hypothesis_cap_is_four → test_hypothesis_cap_is_eight
  with rationale for the raise.

Phase 3b implementation lookback review (per CLAUDE.md doctrine):
  - Surfaced silent-partial-admission hazard in tail extraction;
    fixed with digit-accounting check before commit
  - Surfaced LATENT regex-path multi-actor pronoun hazard (not
    introduced by Phase 3b; documented in test docstring with
    cross-reference to project-adr-0174-multi-actor-pronoun-hazard
    memory for follow-up)
  - case 0040 ('He now has...') remains refused — 'now' adverb between
    subject and verb defeats the existing canonical regex. Adverb-
    stripping is separate scope (not Phase 3b).

Acceptance:
- 258/258 ADR-0174 + math_problem_graph tests pass
- Smoke 67/67, packs 141/141
- train_sample 3/47/0 preserved (wrong=0 held)
- Case 0027 'Malcolm has 240 followers on Instagram and 500 followers
  on Facebook' now admits via the compound extractor — verified by
  refusal moving to the next sentence (which has 'half' fraction)
2026-05-28 11:49:57 -07:00
Shay
1f7a1c4ac6
Merge pull request #427 from AssetOverflow/feat/adr-0174-phase3-lookback-reevaluate
feat(adr-0174-phase3a): lookback re-evaluation operator + pronoun resolution substrate
2026-05-28 11:35:10 -07:00
Shay
619cd62227 fix(adr-0174-phase3a): multi-actor pronoun hazard defense + test backfills + ADR amendment
All findings from the 2026-05-28 Phase 1-3a lookback review addressed
in one commit on the Phase 3a branch:

Wrong=0 hazard defense (the load-bearing fix):
- generate/math_candidate_graph.py: Phase 3a wiring now collects the
  set of distinct proper-noun subjects seen in prior context. When
  more than one exists, refuses with no_antecedent_ambiguous trace
  event rather than guessing the most-recent (which was gender-blind
  single-binding — wrong attribution in multi-actor problems).
- Refusals from the statement loop now preserve _statement_trace via
  reader_trace in CandidateGraphResult (pre-existing latent issue:
  Phase 2/3 trace events were dropped on early statement refusal).
- New tests assert: ambiguous case refuses with correct trace; single-
  actor case still resolves normally.

Test coverage backfills (closes the 13 untested predicate-name gaps):
- TestCheckConstraintsInitialPredicateNames — 3 tests asserting the
  exact predicate name on initial.value_grounds / initial.unit_grounds
  / initial.entity_grounds failure paths.
- TestCheckConstraintsOperationPredicateNames — 3 tests asserting
  operation.verb_grounds / operation.value_grounds / operation.unit_grounds
  failure-predicate-name parity.
- TestCheckConstraintsComposedInitialPath — 4 tests for the RAT-1
  composed_initial path which was entirely untested in Phase 2
  (parity manually verified during lookback review; now automated).

ADR amendment (honest doc vs impl drift):
- docs/decisions/ADR-0174-held-hypothesis-comprehension.md: appended
  'Implementation Notes' section documenting:
  - reevaluate signature differs from spec text (shipped is more
    composable; treat as amended)
  - Phase 2 wires per-candidate, not per-token (per-token is Phase 5)
  - Lookback recompute is candidate-level, not token-level
  - Hypothesis.constraint_state is never populated by Phase 2
  - Multi-actor pronoun hazard defense rationale
  - Honest LOC accounting: Phases 1-3a net +1,500 lines (Phase 5
    delivers the projected net removal)
  - Test coverage backfill summary

Cosmetic:
- lookback.py:297 unreachable raise — added # type: ignore[unreachable]
  with comment explaining defensive future-proofing for Phase 3b.

Acceptance verified:
- 124/124 Phase 1+2+3a + reader tests pass (was 95/95 before backfills)
- Smoke 67/67, packs 141/141
- train_sample 3/47/0 preserved (wrong=0 invariant held)
- Multi-actor hazard live-tested: parse_and_solve refuses the
  Alice/Bob/She case with no_antecedent_ambiguous trace event

See CLAUDE.md §Lookback Review Discipline and memory
feedback-lookback-review-discipline for the doctrine that surfaced
all of these issues at the right time.
2026-05-28 10:49:20 -07:00
Shay
5d1f1001f4 feat(adr-0174-phase3a): lookback re-evaluation operator + pronoun resolution substrate
ADR-0174 Phase 3a — substrate for held-hypothesis lookback.
Score unchanged at 3/47/0 (this PR is correctly-engineered
infrastructure; eval impact gated on ADR-0163.x recognizer expansion
documented in the follow-up brief).

Adds generate/comprehension/lookback.py:
- VALID_REFINEMENT_KINDS, VALID_UNRESOLVED_SLOTS — closed sets
  contracted with reader_trace consumer
- PronounResolution refinement dataclass (pronoun + resolved_to +
  evidence_source, all validated)
- Refinement Union (Phase 3b will widen with CompoundClauseExpansion)
- ReevaluateResult dataclass with admit/eliminate consistency
- reevaluate(hypothesis, refinement) operator — applies refinement,
  re-runs check_constraints, returns refined Hypothesis or None.
- _rebuild_candidate_with_resolved_actor — rebuilds
  CandidateOperation / CandidateInitial replacing the semantic actor
  field (op.actor / initial.entity) while preserving matched_actor_token
  / matched_entity_token as the pronoun (so grounding still passes
  against the held statement's source span).

Modifies generate/recognizer_match.py:
- _try_extract_discrete_count_anchor: pronoun-subject statements now
  emit anchors with subject_role=<pronoun> + requires_pronoun_resolution
  marker, rather than refusing at the _REFUSED_SUBJECT_TOKENS check.
  The other narrowness layers (clause split, verb whitelist) still
  refuse; only the pronoun layer changes.

Modifies generate/math_candidate_graph.py:
- After inject_from_match, when any parsed_anchor carries
  requires_pronoun_resolution, the candidates are held as Hypothesis
  objects with unresolved=('actor_pronoun',). The lookback path then
  resolves via the existing _discourse_prior_subjects map and runs
  PronounResolution refinements through reevaluate.  Resolved
  hypotheses flow into per_sentence_choices as if the regex parser
  had produced them; unresolved hypotheses drop cleanly (refusal-
  preferring).  Emits 'lookback' JSON trace events with
  outcome ∈ {admitted, eliminated, no_antecedent}.

Tests:
- tests/test_adr_0174_phase3_lookback.py — 17 acceptance tests
  covering operator semantics on Operation/Initial, dataclass
  invariants, closed-set constants, end-to-end wiring on synthetic
  problems, and wrong=0 preservation on train_sample.

Phase 3.1 follow-up brief:
- docs/handoff/PHASE-3.1-FOLLOWUP-RECOGNIZER-EXPANSION.md documents
  the empirical finding that the train_sample bottleneck is
  verb-coverage (recognizer scope, ADR-0163.x) not lookback
  (ADR-0174 scope). 11 verbs identified for HITL contemplation pass.
  Recommends sequencing: Phase 3a now (substrate), ADR-0163.x verb
  expansion next, Phase 3b after coverage matures.

Acceptance verified:
- 17/17 Phase 3a tests pass
- 95/95 existing tests pass (Phase 1 + Phase 2 + brief_11 + reader_phase2)
- Smoke 67/67, packs 141/141, lanes 8/8
- wrong=0 preserved, score unchanged 3/47/0 (intentional per brief)

Stacks on Phase 2 (PR #420). Rebases onto main after #416 + #420 land.
2026-05-28 10:49:20 -07:00
Shay
54d719fbdb
Merge pull request #420 from AssetOverflow/feat/adr-0174-phase2-constraint-propagation
feat(adr-0174-phase2): continuous constraint propagation in comprehension reader
2026-05-28 10:49:06 -07:00
Shay
3357c5fc71 feat(adr-0174-phase2): continuous constraint propagation in comprehension reader
ADR-0174 Phase 2 — hoist _initial_admissible / roundtrip_admissible into
hypothesis-based constraint checks with structured elimination tracing.
Admission semantics are byte-equivalent to today; the change is structural.

Adds generate/comprehension/constraint_propagation.py:
- VALID_PREDICATE_NAMES: closed set of 17 sub-check names spanning
  initial / composed_initial / operation admissibility predicates.
  Adding new names requires an ADR amendment (structural contract with
  reader_trace consumer).
- ConstraintResult dataclass: admitted bool + predicates_run trace +
  elimination_reason. Validates admitted-vs-reason consistency.
- Elimination dataclass: confidence_rank + predicate + reason for one
  hypothesis being eliminated.  Serialisable as a reader_trace event.
- hypothesis_from_initial / hypothesis_from_operation: adapters wrapping
  CandidateInitial / CandidateOperation as Phase-1 Hypothesis objects
  with caller-supplied confidence_rank.
- _check_initial / _check_composed_initial / _check_operation:
  decomposed sub-check implementations of the existing admissibility
  predicates with first-failure short-circuit (matches current
  semantics).  Each sub-check populates predicates_run with (name, ok|
  fail|skip) so the consumer sees exactly which predicate decided.
- check_constraints: dispatches on candidate type.
- eliminate_violating: bulk filter; returns (survivors, eliminations);
  survivors are re-densified to satisfy ProblemReadingState's
  open_hypotheses post_init invariant (dense-from-0 ranks);
  eliminations carry the original confidence_rank for trace fidelity.

Wires into generate/math_candidate_graph.py at the recognizer
injection site (line 825+): replaces inline _initial_admissible /
roundtrip_admissible dispatch with eliminate_violating. Elimination
events become JSON entries in reader_trace with layer=
'constraint_propagation', phase=2, predicate, reason, sentence_index.

Phase 2 acceptance verified:
- 24/24 ADR-0174 Phase 2 tests pass (emission, parity with existing
  predicates on 9 admit/reject cases, redensification, dataclass
  invariants, integration).
- 71/71 existing reader + Phase 1 tests still pass.
- Smoke 67/67, packs 141/141, lanes 8/8.
- train_sample/v1 byte-identical across two runs with use_reader=True.
- Score preserved: correct=3 refused=47 wrong=0 — semantics identical
  because the decomposed sub-checks short-circuit on the same predicates
  the inline checks would have caught.

Trace-event behavior: today's injectors are conservative enough that
zero eliminations occur on train_sample/v1 (no false positives, no
mid-pipeline failures).  The wiring is exercised by
test_phase2_event_shape_when_synthesized which proves the trace shape
on a synthetic CandidateInitial that fails initial.unit_grounds.  When
Phase 3 begins emitting partial hypotheses from apply_word, the
elimination path will fire on real candidates and the trace will
populate.

Stacks on Phase 1 (feat/adr-0174-phase1-held-hypothesis-state, PR
#416).  Merges cleanly into main after PR #416 lands.
2026-05-28 10:16:33 -07:00
Shay
7a09b70a5e
Merge pull request #416 from AssetOverflow/feat/adr-0174-phase1-held-hypothesis-state
feat(adr-0174-phase1): held-hypothesis state primitive in comprehension reader
2026-05-28 10:15:32 -07:00
Shay
4b78843f92
Merge pull request #426 from AssetOverflow/fix/contradictory-initial-possessions-wrong-zero-hazard
fix(math-graph): refuse contradictory initial possessions (wrong=0 hazard)
2026-05-28 10:09:58 -07:00
Shay
36cf14c436 docs(handoff): phase 3b+4 skeleton pack — tests + en_core_names_v1 starter
Self-contained skeleton dispatch under docs/handoff/phase-3b-4-skeleton/.
Nothing is collected by pytest, nothing is loaded by runtime.
Implementer moves files to live locations when wiring.

Contents:

  README.md
    Implementer workflow, target locations per file, pack notes.

  test_adr_0174_phase3b_compound_clause.py
    13 acceptance tests covering: pure conjunctive lists (2-clause
    proper noun, 5-clause pronoun w/ single antecedent), refusal
    discipline (mixed-verb, multiplicative-tail, non-whitelisted-verb,
    partial-grounding), HYPOTHESIS_CAP raise to 8, pronoun + multi-
    actor defense preservation, wrong=0 + case-0050 canary. All
    @pytest.mark.skip until implementation lands; imports inside
    test bodies so collection succeeds before target modules exist.

  test_adr_0174_phase4_contemplate.py
    16 acceptance tests covering: contemplate() primitive contract
    (empty/single-survivor noop), Resolution dataclass invariants
    (kind/source closed sets), gendered-pronoun resolution (the
    killer use case — 4 tests covering she/he/same-gender/unknown-
    name), evidence-source precedence (vault>pack>audit_history),
    determinism, trace event shape, wrong=0 + case-0050 canary.

  en_core_names_v1/gender.jsonl
    59 unambiguously-gendered English first names (30 female,
    29 male), alphabetically sorted, JSONL format with schema
    {name: str, gender: 'female'|'male'}. Covers names appearing
    in train_sample/v1 GSM8K problems (Alice, Bob, Daniel,
    Malcolm, Erica, Jan, Tina, etc.) plus common English
    first-name baseline.

  en_core_names_v1/manifest.json
    Pack metadata with sha256 checksum (verified against gender.jsonl
    bytes), entry count, schema declaration, ambiguity discipline
    (Jordan/Alex/Casey/Pat/Taylor/Morgan/Sam/Chris/Robin/Riley
    explicitly excluded), expansion pathway through HITL corridor,
    wrong=0 protection contract for the contemplate adapter.

Pack integrity verified:
  checksum match: True
  entry count match: True (59/59)
  no duplicates
  alphabetically sorted (deterministic diff)

References: docs/handoff/ADR-0174-PHASE-3B-4-COMBINED-SCOPE.md,
ADR-0174 (held-hypothesis comprehension), CLAUDE.md §Semantic Pack
Discipline.
2026-05-28 10:01:05 -07:00
Shay
6b946ec873 docs(handoff): ADR-0174 Phase 3b + Phase 4 combined scope
Phase 3b (compound-clause held hypotheses) is prerequisite for Phase
4 (in-loop contemplate) to have anything to operate on. Combined
scope rather than separate briefs because:

- Phase 3b alone: 0 lift on train_sample (multi-actor defense
  refuses, solver gaps prevent admission)
- Phase 3b + Phase 4: 2-4 case lift via gendered-pronoun resolution
- Phase 3b + 4 + solver multi-qty (separate ADR): 6-8 case lift

First concrete Phase 4 use case: gendered pronoun resolution via a
new en_core_names_v1 pack. Turns the multi-actor defense from
refuse-on-ambiguity into admit-via-evidence when an unambiguous
gendered name exists for one antecedent.

Architecture overview, Phase 3b extractor design, Phase 4
contemplate adapters (vault > packs > audit_history), wrong=0
hazard surfaces, sequencing (3b then 4 stacked), truth tests, and
deliberately-excluded scope (solver work, verb expansion, per-token
apply_word integration — all separate ADRs).

HYPOTHESIS_CAP raise from 4 to 8 needed for Phase 3b (case 0040
has 5 anchors). Documented in scope.

References: ADR-0174 (held-hypothesis comprehension), CLAUDE.md
§Lookback Review Discipline, memories for multi-actor pronoun
hazard and case-0050.
2026-05-28 09:54:45 -07:00
Shay
d17fec6801 fix(math-graph): refuse contradictory initial possessions (wrong=0 hazard)
MathProblemGraph.__post_init__ now raises MathGraphError when two
InitialPossession entries share the same (entity, unit) key but
declare different quantity values.

Pre-fix behavior surfaced by 2026-05-28 ADR-0174 Phase 3 post-merge
diagnostic: math_solver.solve() line 207 used last-write-wins dict
assignment when consolidating initial state. Two contradictory
inputs would silently overwrite without trace:

  'Sam has 5 marbles. Sam has 3 marbles. How many marbles does Sam have?'
   → returned 3.0 (wrong=0 violation: definite answer from
     contradictory input)

Post-fix: same input refuses with 'no branch produced a solvable
graph' — refusal-preferring discipline as wrong=0 doctrine requires.

Identical duplicates (same value) are admitted as redundant (no
contradiction). Different units for same actor admitted. Different
actors for same unit admitted. Single-value cases (the dominant
real-world pattern) unchanged.

This is an extraction-layer hazard discovered while investigating
Phase 3b scope: Phase 3b compound-clause held hypotheses would
emit multiple CandidateInitial entries per sentence, exercising
exactly this consolidation path. Fixing the silent overwrite NOW
ensures Phase 3b admission doesn't silently produce wrong answers.

Acceptance:
- 4 new tests in TestContradictoryInitialPossessionsRefuse
- 165/165 test_math_problem_graph tests pass (was 161/161)
- Smoke 67/67, packs 141/141 unchanged
- train_sample 3/47/0 unchanged (no real case exercised the
  overwrite — but the hazard was latent)

References: CLAUDE.md §Lookback Review Discipline (the doctrine
that surfaced this), CLAUDE.md §Non-Negotiable Field Invariant
(make illegal states difficult to represent).
2026-05-28 09:51:14 -07:00
Shay
796b446b49 docs(handoff): require multi-actor pronoun safety test in VE-A/B acceptance
Adds second mandatory hazard test to VE-A/B acceptance criteria:
multi-actor pronoun ambiguity must trigger no_antecedent_ambiguous
refusal per ADR-0174 Phase 3a defense.

Verb expansion widens the cases that reach Phase 3a lookback wiring;
without this test the multi-actor wrong=0 hazard could fire silently
in production. Surfaced by 2026-05-28 Phase 1-3a lookback review.

References: project-adr-0174-multi-actor-pronoun-hazard memory,
CLAUDE.md §Lookback Review Discipline.
2026-05-28 09:19:46 -07:00
Shay
ee63ce4d5d docs(claude-md): institutionalize lookback review discipline
Adds 'Lookback Review Discipline' section before 'Architectural Scan
Exclusions'.  Multi-PR architectural work was accumulating latent
defects — three Phase 1-3a PRs shipped without a substrate audit
between phases, and a multi-actor pronoun wrong=0 hazard sat in
Phase 3a from the moment it was written until an explicit user-
requested review surfaced it.

Mandatory lookback triggers:
1. Before starting Phase N+1 of a multi-phase ADR
2. Before merging a stacked PR sequence into main
3. After any 3+ PR sequence on the same module/architectural surface

Review template covers: doc drift, test coverage gaps, parity gaps,
wrong=0 hazard surfaces, cross-PR consistency, honest LOC accounting.

Output is structured: solid / gaps / drift / hazards. Hazards require
fix-before-next-phase decision.

Cost: 20-40 minutes per 3-PR substrate. Skipping costs more (every PR
built on undetected hazard becomes implicated when it fires).
2026-05-28 09:10:02 -07:00
Shay
23bc789e06 docs(handoff): verb-coverage expansion dispatch pack (VE-A/B/C)
Operationalises the recommendation from PHASE-3.1-FOLLOWUP-RECOGNIZER-EXPANSION.md
into three independent dispatchable PRs:

- VE-A: acquisition widening (gain, earn, save, accumulate, acquire) — Opus
- VE-B: new depletion class (donate, give, lose, spend, eat) — Opus
- VE-C: refusal evidence for non-arithmetic verbs (instrumentation only) — Sonnet

Each PR includes:
- Hazard pinning: explicit case 0050 test must pass after widening
- Lift evidence: at least one train_sample case moves refused → correct
- Phase 3a substrate fires: the lifted case shows a 'lookback' trace event
- wrong=0 preserved across train_sample AND case 0050

Operator decisions needed before dispatch: which specific lemmas to
admit per class, whether to introduce the depletion class at all, and
whether to ship VE-C as evidence groundwork.

Verb classification rationale per lemma documented in the brief.
Hazard surfaces called out per lemma (delta-of-attribute for 'gained',
direction inversion for 'lost', monetary-vs-time ambiguity for 'saved').

No timelines; operator dispatches when ready.
2026-05-28 08:56:46 -07:00
Shay
da9f89ae16 chore(eval): regenerate train_sample/v1 report.json after 86d4e98 multi-word unit fix
86d4e98 (multi-word unit grounding fix) changed refusal reasons for two
cases without changing the 3/47/0 count:
- case 0019 (currency_amount): refusal moves from 'requires 3 vet
  appointments which cost $400 each' to 'After the first appointment,
  John paid $100 for pet insurance...' — first sentence now passes,
  refusal moves to subsequent sentence
- case 0023 (Nicole/Pokemon cards): refusal moves from 'Nicole collected
  400 Pokemon cards' (now passes via multi-word unit grounding) to
  'Cindy collected twice as many, and Rex collected half of...'

Counts unchanged: correct=3 refused=47 wrong=0. Updates report.json to
match current behavior so subsequent eval runs are byte-deterministic
from the committed snapshot.
2026-05-28 08:09:51 -07:00
Shay
a713d2db33 feat(adr-0174-phase1): held-hypothesis state primitive in comprehension reader
ADR-0174 Phase 1 — substrate only, no admission behavior change.

Adds to generate/comprehension/state.py:
- HYPOTHESIS_CAP (=4, structural assertion per ADR-0174 §Constraints)
- VALID_HYPOTHESIS_CONFIDENCE_RANKS (closed set, no probabilistic ranking)
- Hypothesis dataclass (frozen, slots) — candidate, category_assignments,
  constraint_state, confidence_rank, unresolved. The 'candidate' field is
  typed as object to avoid circular import on math_roundtrip /
  math_candidate_graph candidate types; Phase 2 will pin canonical_bytes
  contract over real candidates.
- UnknownHeld dataclass — token, position, narrowed_categories (frozenset).
  Substrate for Phase 3 'hold instead of refuse' on unknown words; Phase 1
  introduces only the type.
- ProblemReadingState.open_hypotheses + unknown_held fields, both default
  to () (empty tuple). Defaults preserve today's single-committed behavior
  exactly. Confidence-rank uniqueness + density-from-0 enforced at
  __post_init__ as structural invariants.
- Canonical-bytes serializer extended to handle frozenset (sorted list).

Phase 1 acceptance verified:
- 29/29 ADR-0174 Phase 1 tests pass (construction, validation, cap
  enforcement, canonical-bytes determinism, frozenset stability).
- 42/42 existing reader tests pass (test_brief_11_audit +
  test_reader_phase2) — default-empty fields preserve byte-identity.
- Smoke 67/67, packs 141/141.
- train_sample/v1 byte-identical across two runs with use_reader=True.
- wrong=0 invariant held: 3/47/0 unchanged.

No apply_word body changes. The 'thread the hypothesis set' requirement
at Phase 1 is satisfied by field defaults that propagate through every
ProblemReadingState construction site in lifecycle.py without code edits.

Phase 2 (continuous constraint propagation) and Phase 3 (lookback
re-evaluation) will populate these fields with real hypothesis data and
wire the EMIT / ELIMINATE / HOLD operators.
2026-05-28 08:09:00 -07:00
Shay
f90f0cf471 docs(adr-0174): held-hypothesis comprehension with lookback and in-loop contemplation (proposed)
Extends ADR-0164's incremental comprehension reader from single-committed
state to held-hypothesis state, adding lookback re-evaluation and in-loop
contemplation. Diagnoses why the ADR-0164 reader is wired but inert
(all-or-nothing refusal at first unknown token / unexpected category).

Architecture: apply_word produces ProblemReadingState.open_hypotheses
(small ranked set, HYPOTHESIS_CAP=4 initial). Three operators per token:
EMIT (extend compatible hypotheses), ELIMINATE (constraint violations
remove hypotheses immediately), HOLD (uncommitted hypotheses survive at
lower confidence). At finalize(), |survivors|=0 refuses, |survivors|=1
admits, |survivors|>=2 invokes in-loop contemplate() over vault + packs
+ audit history. Ambiguity contemplation cannot resolve refuses cleanly,
preserving wrong=0.

Collapses three parallel parsing systems into one held-hypothesis
reader: removes regex parser runtime path (math_parser.py),
per-category injector dispatch table (recognizer_anchor_inject._INJECTORS),
and duplicate per-sentence-choices scaffolding. Net ~1,900 lines
removed; reader grows by ~600 for hypothesis state + reevaluate +
contemplate.

Preserves ADR-0164 lexicon and category set, ADR-0165 regex scope,
ADR-0150/0152/0155/0161 HITL corridor, binding graph + solver
substrate, capability-axis lanes, replay-equivalence gate. Trust
boundary for in-loop contemplation: read-only over vault/packs/audit;
ratification still rides offline HITL.

Status: Proposed. Six phases (no timelines) gated on wrong=0 and
capability-axis 100% at each transition. Five open questions resolved
before Phase 1 PR.
2026-05-28 07:56:57 -07:00
Shay
86d4e98d5c fix(roundtrip): multi-word units ground when every component appears in source
_unit_grounds() previously refused multi-word units like 'Pokemon cards'
even when both component words appeared as tokens in the source span.
The function checked unit_token against the haystack as a single key,
but the tokenizer splits source into per-word tokens — 'Pokemon cards'
was never going to match.

Fix is conjunctive by design: every component word must appear in the
haystack. A missing component refuses, preserving wrong=0.

Truth-test: case 0023 (Nicole/Pokemon cards) previously refused with
'recognizer matched but produced no injection' on its first sentence.
After this fix, sentence 1 passes injection cleanly; the case now
refuses on sentence 2 (Cindy/Rex compositional clause) — a more
honest refusal reason that reflects the actual remaining gap.

Score unchanged at 3/47/0 (no overall lift; correctness win).
smoke 67/67, packs 141/141, lanes 8/8 all green.
2026-05-28 07:49:24 -07:00
823 changed files with 102970 additions and 3255 deletions

View file

@ -1,7 +1,11 @@
name: smoke
# Fast PR gate — runs the smoke suite (~2-3 min) on every PR push.
# Covers: chat runtime, pipeline, architectural invariants.
# Fast PR gate — runs the smoke suite on every PR push.
# Covers: chat runtime, pipeline, architectural invariants, audio sensorium
# (tests/test_audio_*.py — compiler/eval-gates/mount/CRDT-merge/teachers, ~3s),
# and identity falsifiability (tests/test_pack_measurements_phase2.py, ADR-0043
# — ratified packs diverge directionally; pack-invariant refusal floor; no
# fabrication). The falsifiability lane adds ~4 min but blocks-on-regression.
#
# Full pytest runs post-merge to main (see full-pytest.yml).
# Regressions caught here block the PR; anything outside the smoke
@ -49,4 +53,6 @@ jobs:
tests/test_achat.py \
tests/test_runtime_config.py \
tests/test_cognitive_turn_pipeline.py \
tests/test_architectural_invariants.py
tests/test_architectural_invariants.py \
tests/test_audio_*.py \
tests/test_pack_measurements_phase2.py

4
.gitignore vendored
View file

@ -25,6 +25,10 @@ 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

@ -21,7 +21,7 @@ status predicate evaluates to `reasoning-capable` with no open gaps.
| Domain | Status | ADR | Packs | Open gaps |
| --- | --- | --- | --- | --- |
| `philosophy_theology` | reasoning-capable | ADR-0085 | 2 | 0 |
| `mathematics_logic` | expert | ADR-0097 | 1 | 0 |
| `mathematics_logic` | audit-passed | ADR-0097 | 1 | 0 |
| `physics` | audit-passed | ADR-0100 | 1 | 0 |
| `systems_software` | audit-passed | ADR-0101 | 1 | 0 |
| `hebrew_greek_textual_reasoning` | reasoning-capable | ADR-0102 | 4 | 0 |
@ -42,6 +42,7 @@ 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

107
CLAUDE.md
View file

@ -58,6 +58,16 @@ 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:
@ -70,6 +80,15 @@ 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
@ -104,6 +123,23 @@ 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)
- `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/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).
- `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
@ -293,6 +329,77 @@ Current near-term sequence:
Avoid broad docs-first churn, dashboard work, or large infrastructure unless it
unlocks one of these steps.
The afferent sensorium/modalities arc (ADR-0013 -> 0181/0197/0208/0209; ADR-0198
reserves the efferent/motor half) is a **sanctioned parallel track** — not part
of the near-term sequence above and not licensed to displace it. It is disjoint
from the GSM8K serving path (no `generate.derivation` / `core.reliability_gate`
import), so it cannot regress the serving metric; its efferent half stays gated
behind ADR-0198's fail-closed boundary and a dedicated motor governance ADR
(ratified afferent ADRs carry `Accepted (ratified ...)`; ADR-0198 stays a
partially-implemented spike).
## Lookback Review Discipline
Multi-PR architectural work accumulates latent defects when each PR
is reviewed only against its own acceptance criteria. A hazard
introduced in PR N can sit dormant until PR N+2 exercises it — by
which point the substrate is harder to fix and three PRs are
implicated rather than one.
**Mandatory lookback review** is triggered at three points:
1. **Before starting the next phase of a multi-phase ADR.** Before
any code on Phase N+1, audit Phase N's shipped substrate. Check
for: ADR-doc vs implementation drift, untested predicate paths,
wrong=0 hazard surfaces, cross-phase trace/event/rank consistency,
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
review-each-PR-individually pattern misses cross-PR consistency
issues. Audit the whole stack as one unit before any merge.
3. **After any 3+ PR sequence on the same module or architectural
surface.** When work concentrates on one area, regression risk
compounds. Audit before claiming the surface is "stable" or
"ready for the next layer."
**What a lookback review covers** (template — adjust per scope):
- **Documentation drift.** Does what shipped match what the ADR / brief
said would ship? Signature differences, scope reductions, missing
pieces — flag them.
- **Test coverage gaps.** Run the test suite under coverage. For every
predicate/branch in a closed-set contract (like
`VALID_PREDICATE_NAMES`), confirm at least one test asserts the
specific elimination/admission path. Vacuous tests (assertions
that pass under broken impl) are coverage gaps.
- **Parity gaps.** When a new implementation claims byte-equivalence
with an existing one, exercise BOTH on the same inputs and confirm
identical outputs — including failure modes, not just success.
- **wrong=0 hazard surface.** Every new code path: under what input
conditions could it admit a candidate the prior path would have
refused? Trace upstream to confirm no input class can trigger it.
If a class CAN trigger it, build the defensive refusal NOW, before
the next phase makes it load-bearing.
- **Cross-PR consistency.** Trace event shapes, rank handling,
determinism contracts, dataclass invariants — do they compose
cleanly across PRs?
- **Honest LOC accounting.** Did this phase net add or net remove
lines? ADR claims of "removes ~N lines" only count post-collapse;
intermediate phases that ADD substrate before removal happens
should be called out.
**Output.** The review produces a structured report with findings
categorized as: solid, gaps (no risk), drift (need amendment), and
hazards (live wrong=0 risks). Hazards require a fix-before-next-phase
decision.
**Cost.** A lookback review on a 3-PR substrate typically takes
20-40 minutes of focused tool calls. Skipping it costs more: every
PR built on an undetected hazard becomes implicated when the hazard
fires, and the fix has to land across multiple PRs instead of one.
## Architectural Scan Exclusions
The invariant tests in `tests/test_architectural_invariants.py` perform

View file

@ -0,0 +1,49 @@
# HANDOFF gpt55 2026-06-03
Branch: `codex/ntimes-completeness-guard`
Final commit SHA: reported in the thread final after commit creation.
## File Status
| file | path | status | purpose | ready-to-merge |
|---|---|---|---|---|
| recognizer anchor injector | `generate/recognizer_anchor_inject.py` | FINAL | Refusal-only guard preventing no-reference `<N> times ...` comparative multipliers from being injected as `CandidateInitial(value=N, unit="times")`. | Y |
| completeness guard tests | `tests/test_candidate_graph_completeness_guard.py` | FINAL | Pins the §9 hard negative matrix, with solve/refusal controls. | Y |
| question-layer gap survey | `docs/analysis/question-layer-gap-survey.md` | FINAL | Canonical audited 44-refusal partition and backlog interpretation. | Y |
| solver operation coverage | `docs/analysis/solver-operation-coverage.md` | FINAL | Canonical read-only audit of existing solver op coverage and representation gaps. | Y |
| composition capability scope | `docs/analysis/composition-capability-scope.md` | FINAL | Canonical execution-lane v2 scope for ADR-0174 Phase 5b emission/representation and §9 guard precondition. | Y |
| comprehension primitive inventory | `docs/analysis/comprehension-primitive-inventory.md` | FINAL | Canonical consolidated inventory and cross-subject leverage map from the execution lane. | Y |
| handoff | `HANDOFF-gpt55-2026-06-03.md` | FINAL | Merge/readiness manifest for this thread. | Y |
## Validation
Completed on branch `codex/ntimes-completeness-guard`:
- `uv run python -m pytest tests/test_candidate_graph_completeness_guard.py -q`
- `21 passed`
- `uv run python -m pytest tests/test_adr_0131_G2_comparatives.py tests/test_adr_0131_G2a_comparative_verb_widening.py -q`
- `30 passed`
- `uv run python -m pytest tests/test_aggregate_total_question_forms.py tests/test_discrete_count_open_noun_class.py tests/test_adr_0163_d2_discrete_count_injection.py -q`
- `59 passed`
- Train-sample probe through `generate.math_candidate_graph.parse_and_solve`
- `6 correct / 44 refused / 0 wrong`
- `refused_nonzero_count = 0`
- admitted IDs: `0003`, `0014`, `0018`, `0021`, `0024`, `0042`
## New Since Last Message
- Broadened the §9 guard from phrase-specific `times as many|more` matching to the structural captured shape: cardinal immediately followed by `times` inside the discrete-count injector.
- Added no-reference hard negatives for `3 times the number of apples` and `3 times the apples`.
- Added explicit safe-refusal controls for no-reference `twice`, no-reference `double`, and case 605.
- Added canonical execution-lane `composition-capability-scope.md`.
- Added canonical execution-lane `comprehension-primitive-inventory.md`.
- Added this handoff manifest.
## Not Done / WIP
- No 5b emission/representation slice was implemented.
- No <=20-case validation sub-corpus was authored.
- No solver operation kinds or binding-graph node types were added.
- No serving/eval/claims-ledger files were changed.
- Superseded `0001` / `0002` / `0003` patch files from the Opus handoff directory were not committed.

View file

@ -27,6 +27,20 @@ When facing a design decision, the world offers two visible options: use what al
---
## Native Substrate Direction — Python, Rust, Zig
CORE is not moving toward a wholesale Zig rewrite. The architecture is moving toward a stricter native-substrate boundary:
- **Python** remains the semantic source of truth: cognition runtime, teaching/review workflows, pack ratification, eval harnesses, and Workbench/operator tooling.
- **Rust** remains the incumbent native algebra backend: Cl(4,1) products, versor operations, CGA inner product, exact recall, and diffusion surfaces already proven by parity gates.
- **Zig** is a candidate material for the next native substrate layer: Delta-CRDT arenas/deltas/merge kernels, deterministic modality compilers such as `audio_core_v1`, stable C ABI surfaces, edge-native ingestion, and selected exact recall challenge kernels only after parity and benchmark proof.
The rule is component law, not language preference. Zig may enter where explicit allocation, deterministic buffer ownership, C ABI clarity, and edge-native deployment materially strengthen CORE. Zig must not replace review-gated semantics, introduce approximate recall, hide repair in native code, or turn teacher/shadow models into substrate.
Decision package: [`docs/zig/README.md`](docs/zig/README.md). Adoption gates: [`docs/zig/adoption-gates.md`](docs/zig/adoption-gates.md).
---
## The Truth-Seeking Schema
Co-equal with the algebraic substrate. CORE's epistemic schema is a foundational architectural commitment: every claim that enters the runtime field carries a typed position in a revision graph (`SPECULATIVE`, `COHERENT`, `CONTESTED`, `FALSIFIED`); coherence — not source authority — is the only admission signal; no claim is ever locked, even when COHERENT; identity cannot be rewritten by content; and exactly one mutation path admits knowledge, enforced by a CI-level architectural-invariant test.
@ -64,7 +78,7 @@ English establishes the operational base. Hebrew and Koine Greek bring the hidde
```bash
pip install -e ".[dev]"
pytest tests/test_versor_closure.py # the core invariant — must pass first
pytest tests/ # full suite (~4 minutes, 1099 tests)
pytest tests/ # full suite (~8,337 tests; some pre-existing reds — see docs/test-debt-quarantine.md)
```
### Watch the flywheel turn — one command
@ -294,11 +308,16 @@ ADR-0114a's 10 anti-overfitting proof obligations are all discharged for the
it does not confabulate. The zero-confabulation property holds against the external
benchmark.
**ADR-0120 (first `expert` promotion contract) is the next gate.** It will set the
numeric expert threshold and ε, require all 10 ADR-0114a obligations as hard gates,
and sign the first `expert_claims` entry — or defer honestly if the correct_rate
gate is not yet met. **No domain is at `expert` today.** That status string remains
reserved namespace.
**ADR-0120 (the first `expert` promotion contract) has since been built and
exercised.** On 2026-05-23 `mathematics_logic` was signed and briefly promoted to
`expert` — then **auto-reverted to `audit-passed`** when its evidence bundle
drifted: a *non-gating* GSM8K coverage metric moved, which changed the
evidence-derived digest and invalidated the signature. That revert is the
contract's fail-closed property working as designed — CORE revoked its own expert
claim rather than carry a stale one. **No domain is at `expert` today**, and when
`expert` is held at all it rests on CORE-authored lanes, not external GSM8K. Full
record: [ADR-0200](docs/decisions/ADR-0200-expert-claim-reconciliation.md) and
[`docs/claims_ledger.md`](docs/claims_ledger.md).
To run the GSM8K math eval lane:

View file

@ -1,5 +1,19 @@
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 cga_inner, outer_product, is_null, null_project, embed_point
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 .holonomy import holonomy_encode, holonomy_similarity
from .rotor import word_transition_rotor

View file

@ -19,13 +19,34 @@ No cosine similarity. No L2 norm. No approximate indexing.
"""
import numpy as np
from .cl41 import geometric_product, scalar_part, basis_vector, N_COMPONENTS
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
# 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:
"""
@ -38,10 +59,19 @@ def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
def outer_product(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
"""
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.
"""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.)
"""
XY = geometric_product(X, Y)
YX = geometric_product(Y, X)
@ -62,18 +92,25 @@ def null_project(X: np.ndarray) -> np.ndarray:
return embed_point(euclidean)
def embed_point(x: np.ndarray) -> np.ndarray:
def embed_point(x: np.ndarray, *, dtype: "np.typing.DTypeLike" = np.float32) -> 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=np.float32)
x = np.asarray(x, dtype=dtype)
assert len(x) == 3, "embed_point expects a 3D vector"
x_sq = float(np.dot(x, x))
result = np.zeros(N_COMPONENTS, dtype=np.float32)
result = np.zeros(N_COMPONENTS, dtype=dtype)
result[1:4] = x
# n_o + 0.5|x|^2 n_inf
@ -82,3 +119,108 @@ def embed_point(x: np.ndarray) -> np.ndarray:
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

@ -50,6 +50,14 @@ DEFAULT_RESOLVABLE_PACK_IDS: tuple[str, ...] = (
"en_core_spatial_v1",
"en_core_causation_v1",
"en_core_polarity_v1",
# Foundation-curriculum syntax substrate. Mounted AFTER the
# high-frequency content packs so it is purely additive: every lemma
# it owns is disjoint from earlier mounted packs. Two colliders were
# renamed at authoring time so syntax never steals prior resolution
# (`agent`→`agent_role`, owned by en_core_meta_v1; `comparison`→
# `comparison_relation`, owned by en_core_cognition_v1). `negation`
# is kept bare: no *mounted* pack owned it, so syntax now grounds it.
"en_core_syntax_v1",
"en_core_relations_v1",
"en_core_relations_v2",
# ADR-0073c — synthetic English anchor lemmas for cross-lang collapse.

View file

@ -4,6 +4,7 @@ from dataclasses import dataclass, replace
import hashlib
import json
import re
import warnings
from collections.abc import Sequence
from typing import Any, List
@ -37,6 +38,8 @@ 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,
format_correction_event_jsonl,
@ -51,7 +54,8 @@ from teaching.discovery import (
format_candidate_jsonl,
)
from teaching.discovery_sink import DiscoveryCandidateSink
from engine_state import EngineStateStore
from engine_state import EngineStateStore, get_git_revision
from core.engine_identity import engine_identity_for_config
from recognition.anti_unifier import derive_recognizer
from recognition.outcome import FeatureBundle
from recognition.registry import RecognizerRegistry
@ -385,6 +389,28 @@ 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
@ -447,6 +473,13 @@ class ChatResponse:
# Comma-separated violated boundary/commitment IDs when normative
# clearance is VIOLATED or SUPPRESSED; empty string otherwise.
normative_detail: str = ""
# ADR-0206 — Response Governance Bridge reach level for this turn.
# Mirrors the TurnEvent field so callers (CLI, demos, tests) can read
# the governed reach level from ChatResponse. Scaffold contract:
# always "strict" (govern_response is STRICT-only until widening is
# built). ``"strict"`` default preserves byte-identity for callers
# that construct ChatResponse without this field.
reach_level: str = "strict"
# ADR-0072 (R5) — operator-visible register identity per turn.
# Mirrors the TurnEvent fields so callers (CLI, demos, tests) can
# read the register state from ChatResponse without re-parsing the
@ -495,6 +528,29 @@ 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,
@ -653,6 +709,11 @@ 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)
)
@ -666,6 +727,27 @@ 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()
@ -673,11 +755,46 @@ 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(
@ -711,7 +828,130 @@ class ChatRuntime:
for c in candidates_to_save
]
store.save_discovery_candidates(candidates_to_save)
store.save_manifest(self._turn_count)
# 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,
)
def record_recognition_example(
self,
@ -764,9 +1004,164 @@ 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
@ -2335,9 +2730,24 @@ class ChatRuntime:
refusal_emitted=refusal_emitted,
hedge_injected=hedge_injected,
)
main_epistemic_state = epistemic_state_for_grounding_source(
main_grounding_source
).value
main_epistemic = epistemic_state_for_grounding_source(main_grounding_source)
main_epistemic_state = main_epistemic.value
# ADR-0206 — Response Governance Bridge seam (cognition path).
# govern_response is STRICT-only (scaffold), so shape_surface is the
# identity transform and response_surface is returned verbatim —
# byte-identical to the pre-bridge path. This is live wiring, not
# dead code: the response surface now flows through the policy
# consumer, and the ONLY thing keeping it strict is the STRICT
# return value (proven by the live-wiring test). The risk-reward
# widening pathway and the math-serving seam are deferred to their
# own PRs (ADR-0206 §5); wrong==0 is untouched here.
main_reach_policy = govern_response(epistemic_state=main_epistemic)
main_reach_level = main_reach_policy.level.value
response_surface = shape_surface(
main_reach_policy,
committed_surface=response_surface,
decode_state=main_epistemic,
)
main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value
main_normative_detail = normative_detail_from_verdicts(verdicts_bundle)
turn_event = TurnEvent(
@ -2371,6 +2781,7 @@ class ChatRuntime:
epistemic_state=main_epistemic_state,
normative_clearance=main_normative_clearance,
normative_detail=main_normative_detail,
reach_level=main_reach_level,
)
self.turn_log.append(turn_event)
self._emit_turn_event(turn_event)
@ -2421,6 +2832,7 @@ class ChatRuntime:
epistemic_state=main_epistemic_state,
normative_clearance=main_normative_clearance,
normative_detail=main_normative_detail,
reach_level=main_reach_level,
refusal_reason=refusal_surface if refusal_emitted else "",
dispatch_trace=dispatch_trace,
)

View file

@ -25,6 +25,26 @@ 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

@ -9,7 +9,9 @@
//! The CGA inner product in Cl(4,1) is structurally diagonal with
//! ±1 metric values, so per-versor scoring collapses to
//!
//! sum_i metric[i] * v[i] * q[i]
//! ```text
//! sum_i metric[i] * v[i] * q[i]
//! ```
//!
//! which is 32 multiplies + 32 adds, not the 1024-op full
//! geometric_product the reference scalar path computes. Bit-
@ -150,3 +152,205 @@ pub fn vault_reproject_parallel(versors: &[[f32; 32]]) -> Vec<[f32; 32]> {
use crate::cga::null_project_raw;
versors.par_iter().map(|v| null_project_raw(v)).collect()
}
// ===========================================================================
// Delta-CRDT substrate (ADR-0180 §2.1 / §2.2)
// ===========================================================================
//
// `LocalArena` is the thread-local, share-nothing write cache each modality
// adapter accumulates into (ADR-0180 §2.1). It never touches global state, so
// it needs no locks: concurrency safety comes from each thread owning its own
// arena, not from synchronisation.
//
// `SemilatticeDelta` is the join-semilattice contract (ADR-0180 §2.2): the
// merge of deltas is commutative, associative, and idempotent. The Merge
// Kernel folds deltas into a single content-addressed, deduplicated, totally
// ordered set — the order-invariant state the Vault consumes. Ordering is by
// content (the versor's IEEE-754 bit pattern + provenance bytes), never
// arrival order, per the §2.2 content-addressed-tiebreak amendment
// (2026-05-29). That is precisely what makes
// `hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest)` (§4.3) reachable:
// the merged state cannot depend on the order deltas arrived in.
//
// This is the pure-CPU Rust substrate (§1.5.5); no MLX/UMA handshake and no
// Python binding land here — those are downstream (ADR-0180 §4.1 item 2, and
// ADR-0181 PR-5 respectively).
use std::cmp::Ordering;
/// One write accumulated into an arena: a Cl(4,1) versor plus opaque
/// provenance bytes. Provenance is part of the content key, so two writes of
/// the same versor under different provenance are distinct semilattice
/// elements (both retained); two byte-identical writes collapse (the
/// idempotence leg of the join semilattice, ADR-0180 §2.2).
#[derive(Clone, Debug)]
pub struct ArenaEntry {
pub versor: [f32; 32],
pub provenance: Vec<u8>,
}
impl ArenaEntry {
pub fn new(versor: [f32; 32], provenance: Vec<u8>) -> Self {
Self { versor, provenance }
}
}
/// Total, arrival-independent content order over arena entries.
///
/// f32 has no total `Ord` (NaN is unordered), so we compare the raw IEEE-754
/// bit patterns — which is exactly what "content-addressed" means here:
/// byte-identical versors sort together and deduplicate, and `-0.0`/`+0.0`
/// (distinct bytes) are treated as distinct content, as a byte-addressed merge
/// requires. Ties on the versor fall through to the provenance bytes, giving a
/// total order (ADR-0180 §2.2 amendment, mirroring ADR-0181 §2.2's merge key).
fn content_cmp(a: &ArenaEntry, b: &ArenaEntry) -> Ordering {
for i in 0..32 {
let o = a.versor[i].to_bits().cmp(&b.versor[i].to_bits());
if o != Ordering::Equal {
return o;
}
}
a.provenance.cmp(&b.provenance)
}
#[inline]
fn content_eq(a: &ArenaEntry, b: &ArenaEntry) -> bool {
content_cmp(a, b) == Ordering::Equal
}
/// A snapshot of newly-ingested entries (ADR-0180 §2.2). A `Delta` is always
/// held in content-addressed order with byte-identical duplicates removed, so
/// it is a canonical join-semilattice element regardless of the order its
/// entries were inserted in.
#[derive(Clone, Debug, Default)]
pub struct Delta {
entries: Vec<ArenaEntry>,
}
impl Delta {
/// Canonicalise an arbitrary entry list into a Delta: sort by content,
/// drop byte-identical duplicates. `sort_by` is stable, but the dedup key
/// is the *whole* content, so stability is immaterial to the result.
pub fn from_entries(mut entries: Vec<ArenaEntry>) -> Self {
entries.sort_by(content_cmp);
entries.dedup_by(|a, b| content_eq(a, b));
Self { entries }
}
pub fn entries(&self) -> &[ArenaEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Canonical little-endian serialization of this Delta — the cross-language
/// G1 contract (ADR-0180 / ADR-0196). Byte-for-byte identical to the
/// Python reference `vault.crdt.canonical_bytes`; parity is pinned by
/// `tests/test_crdt_hash_parity.rs`. Layout:
///
/// ```text
/// u64 entry_count
/// per entry (canonical order):
/// 32 x f32 versor components (IEEE-754, little-endian, 4 bytes each)
/// u64 provenance_length
/// bytes provenance
/// ```
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(self.entries.len() as u64).to_le_bytes());
for entry in &self.entries {
for component in &entry.versor {
out.extend_from_slice(&component.to_le_bytes());
}
out.extend_from_slice(&(entry.provenance.len() as u64).to_le_bytes());
out.extend_from_slice(&entry.provenance);
}
out
}
}
/// The join-semilattice contract for Delta-CRDT state (ADR-0180 §2.2).
///
/// Implementors must satisfy, with content-addressed equality (`content_cmp`):
/// * commutativity `a.join(b) == b.join(a)`
/// * associativity `a.join(b).join(c) == a.join(b.join(c))`
/// * idempotence `a.join(a) == a`
///
/// These are exercised as failable tests in `tests/test_arena.rs`; if `join`
/// ever orders by arrival instead of content, or stops deduplicating, those
/// tests fail loudly (CLAUDE.md §Schema-Defined Proof Obligations).
pub trait SemilatticeDelta: Sized {
fn join(&self, other: &Self) -> Self;
}
impl SemilatticeDelta for Delta {
fn join(&self, other: &Self) -> Self {
let mut merged =
Vec::with_capacity(self.entries.len() + other.entries.len());
merged.extend_from_slice(&self.entries);
merged.extend_from_slice(&other.entries);
Delta::from_entries(merged)
}
}
/// Thread-local, share-nothing write cache for one modality adapter
/// (ADR-0180 §2.1). Adapters push entries here lock-free; nothing is ever
/// written to global state from an arena. `snapshot` emits the order-invariant
/// `Delta` the Merge Kernel folds.
#[derive(Clone, Debug, Default)]
pub struct LocalArena {
entries: Vec<ArenaEntry>,
}
impl LocalArena {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// Lock-free local write. Push order is irrelevant: `snapshot`
/// canonicalises into content-addressed order.
pub fn push(&mut self, versor: [f32; 32], provenance: Vec<u8>) {
self.entries.push(ArenaEntry::new(versor, provenance));
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Emit a canonical Delta of everything accumulated so far. Does not drain
/// the arena — flush/GC policy is the Merge Kernel's concern, not the
/// arena's.
pub fn snapshot(&self) -> Delta {
Delta::from_entries(self.entries.clone())
}
}
/// The Merge Kernel (ADR-0180 §2.2): fold a batch of deltas into a single
/// content-addressed, deduplicated, totally ordered entry set. The result is
/// invariant under any permutation of `deltas` (commutativity + associativity)
/// and under duplicate deltas (idempotence) — the property §4.3's
/// `hash(Sequential) == hash(Concurrent)` proof rides on.
///
/// Implemented as a single canonicalisation of the union rather than a
/// `fold(join)` chain; `tests/test_arena.rs` pins that the two are
/// content-equal, so the cheap path can never silently diverge from the
/// semilattice fold it stands in for.
pub fn merge_kernel(deltas: &[Delta]) -> Delta {
let mut all = Vec::new();
for d in deltas {
all.extend_from_slice(&d.entries);
}
Delta::from_entries(all)
}

View file

@ -0,0 +1,13 @@
// @generated by tests/fixtures/crdt/_generate.py — DO NOT EDIT.
// Expected canonical_bytes hex per case (mirrors merge_fixtures.json):
// the Python-canonical Delta-CRDT G1 contract (ADR-0180 / ADR-0196)
// the Rust substrate must reproduce byte-for-byte.
// (include!d mid-module, so plain // comments — not //! — are required.)
pub const EXPECTED_CANONICAL_BYTES_HEX: &[(&str, &str)] = &[
("single_entry", "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"),
("dedup_within_delta", "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"),
("distinct_provenance_retained", "0200000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000006c656674000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000007269676874"),
("three_delta_merge", "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000071000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000006d000000000000404000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000007a0000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"),
("signed_zero_distinct", "020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000700000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000070"),
];

175
core-rs/tests/test_arena.rs Normal file
View file

@ -0,0 +1,175 @@
//! Delta-CRDT substrate proof obligations (ADR-0180 §2.1 / §2.2).
//!
//! Each test is written to FAIL LOUDLY under the specific violation it names
//! (CLAUDE.md §Schema-Defined Proof Obligations): a schema/trait is only a
//! proven property when a test would break if the property were silently
//! removed. The map:
//!
//! * commutativity / associativity / idempotence — the three join-semilattice
//! legs ADR-0180 §2.2 claims `Delta::join` satisfies.
//! * permutation-invariant `merge_kernel` — the load-bearing property §4.3's
//! `hash(Sequential) == hash(Concurrent)` rides on. Fails if `from_entries`
//! stops sorting (i.e. orders by arrival).
//! * distinct-provenance retention — guards the dedup from collapsing on the
//! versor alone, which would silently drop epistemic state (a wrong=0-style
//! data-loss hazard at the merge layer).
use core_rs::vault::{merge_kernel, ArenaEntry, Delta, LocalArena, SemilatticeDelta};
/// Deterministic distinct versor per seed.
fn versor(seed: u8) -> [f32; 32] {
let mut v = [0f32; 32];
for (i, slot) in v.iter_mut().enumerate() {
*slot = (seed as f32) * 0.5 + (i as f32) * 0.125;
}
v
}
fn entry(seed: u8, prov: &str) -> ArenaEntry {
ArenaEntry::new(versor(seed), prov.as_bytes().to_vec())
}
/// Canonical, comparable view of a Delta: (versor bits, provenance) per entry,
/// in the Delta's own order. Two Deltas are content-equal iff these match,
/// including order — so this also pins that ordering is content-addressed.
fn keys(d: &Delta) -> Vec<([u32; 32], Vec<u8>)> {
d.entries()
.iter()
.map(|e| {
let mut bits = [0u32; 32];
for (i, b) in bits.iter_mut().enumerate() {
*b = e.versor[i].to_bits();
}
(bits, e.provenance.clone())
})
.collect()
}
fn delta(entries: Vec<ArenaEntry>) -> Delta {
Delta::from_entries(entries)
}
// --- the three join-semilattice legs (ADR-0180 §2.2) ----------------------
#[test]
fn join_is_commutative() {
let a = delta(vec![entry(1, "p1"), entry(3, "p3")]);
let b = delta(vec![entry(2, "p2"), entry(3, "p3")]);
// Fails if join carries arrival order: a-first vs b-first would differ.
assert_eq!(keys(&a.join(&b)), keys(&b.join(&a)));
}
#[test]
fn join_is_associative() {
let a = delta(vec![entry(1, "p1")]);
let b = delta(vec![entry(2, "p2")]);
let c = delta(vec![entry(3, "p3")]);
assert_eq!(keys(&a.join(&b).join(&c)), keys(&a.join(&b.join(&c))));
}
#[test]
fn join_is_idempotent() {
let a = delta(vec![entry(1, "p1"), entry(2, "p2")]);
// a ∘ a == a — fails if dedup is removed (length would double).
let joined = a.join(&a);
assert_eq!(keys(&joined), keys(&a));
assert_eq!(joined.len(), 2);
}
// --- the load-bearing property for §4.3 -----------------------------------
#[test]
fn merge_kernel_is_permutation_invariant() {
let d0 = delta(vec![entry(5, "a"), entry(1, "b")]);
let d1 = delta(vec![entry(3, "c")]);
let d2 = delta(vec![entry(9, "d"), entry(2, "e"), entry(7, "f")]);
let forward = merge_kernel(&[d0.clone(), d1.clone(), d2.clone()]);
let reversed = merge_kernel(&[d2.clone(), d1.clone(), d0.clone()]);
let shuffled = merge_kernel(&[d1.clone(), d0.clone(), d2.clone()]);
// hash(Sequential) == hash(Concurrent): merged state is independent of the
// order deltas arrived in. Fails the instant `from_entries` orders by
// arrival instead of content.
assert_eq!(keys(&forward), keys(&reversed));
assert_eq!(keys(&forward), keys(&shuffled));
}
#[test]
fn merge_kernel_dedups_duplicate_deltas() {
let d = delta(vec![entry(4, "x"), entry(6, "y")]);
// Re-ingesting the same delta is a no-op (idempotence at the kernel).
let once = merge_kernel(&[d.clone()]);
let twice = merge_kernel(&[d.clone(), d.clone()]);
assert_eq!(keys(&once), keys(&twice));
assert_eq!(twice.len(), 2);
}
#[test]
fn merge_kernel_equals_semilattice_fold() {
let deltas = [
delta(vec![entry(8, "a"), entry(2, "b")]),
delta(vec![entry(5, "c")]),
delta(vec![entry(2, "b"), entry(9, "d")]), // overlaps the first delta
];
let folded = deltas
.iter()
.fold(Delta::default(), |acc, d| acc.join(d));
// The cheap union-then-canonicalise path must equal the explicit
// semilattice fold, or the kernel has silently diverged from the trait.
assert_eq!(keys(&merge_kernel(&deltas)), keys(&folded));
}
// --- data-loss / over-dedup guard -----------------------------------------
#[test]
fn distinct_provenance_is_not_collapsed() {
// Same versor, different provenance => two distinct semilattice elements.
// Fails if dedup keys on the versor alone (which would drop state).
let same_versor = vec![entry(7, "alpha"), entry(7, "beta")];
let merged = delta(same_versor);
assert_eq!(merged.len(), 2);
// Byte-identical content (same versor + same provenance) => collapses.
let identical = vec![entry(7, "alpha"), entry(7, "alpha")];
assert_eq!(delta(identical).len(), 1);
}
#[test]
fn merge_result_is_content_sorted() {
let d = merge_kernel(&[delta(vec![entry(9, "z"), entry(1, "a"), entry(5, "m")])]);
let ks = keys(&d);
let mut sorted = ks.clone();
sorted.sort();
assert_eq!(ks, sorted, "merge output must be in content-addressed order");
}
// --- LocalArena (ADR-0180 §2.1) -------------------------------------------
#[test]
fn arena_snapshot_independent_of_push_order() {
let mut a = LocalArena::new();
a.push(versor(3), b"p3".to_vec());
a.push(versor(1), b"p1".to_vec());
a.push(versor(2), b"p2".to_vec());
let mut b = LocalArena::new();
b.push(versor(2), b"p2".to_vec());
b.push(versor(3), b"p3".to_vec());
b.push(versor(1), b"p1".to_vec());
// Two arenas fed the same writes in different orders snapshot to the same
// canonical Delta (ADR-0180 §2.1: push order is irrelevant).
assert_eq!(keys(&a.snapshot()), keys(&b.snapshot()));
}
#[test]
fn arena_snapshot_does_not_drain() {
let mut a = LocalArena::new();
a.push(versor(1), b"p1".to_vec());
let _ = a.snapshot();
// Flush/GC is the Merge Kernel's concern, not the arena's; snapshot must
// be non-destructive so a delayed merge (the §3.2 window) cannot lose it.
assert_eq!(a.len(), 1);
assert_eq!(a.snapshot().len(), 1);
}

View file

@ -0,0 +1,105 @@
//! ADR-0180 / ADR-0196 gate G1 (slice ZC-0) — Rust↔Python byte-parity.
//!
//! Proves the Rust Delta-CRDT substrate (`core_rs::vault`) produces
//! `canonical_bytes` identical to the Python reference `vault/crdt.py` over the
//! shared golden corpus. Expected hex is generated *from* the Python reference
//! (single source of truth) into `tests/fixtures/crdt_parity_expected.rs`, so
//! the two languages cannot silently disagree. Fails loudly if the Rust
//! serialization, content ordering, or dedup ever diverges from the locked
//! Python contract (CLAUDE.md §Schema-Defined Proof Obligations).
use core_rs::vault::{merge_kernel, ArenaEntry, Delta};
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/crdt_parity_expected.rs"
));
fn v(idx: usize, val: f32) -> [f32; 32] {
let mut a = [0.0f32; 32];
a[idx] = val;
a
}
fn e(idx: usize, val: f32, prov: &str) -> ArenaEntry {
ArenaEntry::new(v(idx, val), prov.as_bytes().to_vec())
}
fn delta(entries: Vec<ArenaEntry>) -> Delta {
Delta::from_entries(entries)
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{:02x}", b));
}
s
}
fn expected(name: &str) -> &'static str {
EXPECTED_CANONICAL_BYTES_HEX
.iter()
.find(|entry| entry.0 == name)
.map(|entry| entry.1)
.unwrap_or_else(|| panic!("no expected fixture for case {name}"))
}
fn assert_parity(name: &str, deltas: Vec<Delta>) {
let merged = merge_kernel(&deltas);
assert_eq!(hex(&merged.canonical_bytes()), expected(name), "case {name}");
}
#[test]
fn parity_single_entry() {
assert_parity("single_entry", vec![delta(vec![e(0, 1.0, "a")])]);
}
#[test]
fn parity_dedup_within_delta() {
assert_parity(
"dedup_within_delta",
vec![delta(vec![e(0, 1.0, "a"), e(0, 1.0, "a")])],
);
}
#[test]
fn parity_distinct_provenance_retained() {
assert_parity(
"distinct_provenance_retained",
vec![delta(vec![e(5, 2.0, "left"), e(5, 2.0, "right")])],
);
}
#[test]
fn parity_three_delta_merge() {
assert_parity(
"three_delta_merge",
vec![
delta(vec![e(1, 3.0, "z"), e(0, 1.0, "a")]),
delta(vec![e(2, 2.0, "m"), e(0, 1.0, "a")]),
delta(vec![e(10, 5.0, "q")]),
],
);
}
#[test]
fn parity_signed_zero_distinct() {
assert_parity(
"signed_zero_distinct",
vec![delta(vec![e(0, 0.0, "p"), e(0, -0.0, "p")])],
);
}
#[test]
fn parity_permutation_invariant_matches_python() {
// Reordering input deltas must not change the merged bytes (and thus the
// Python-pinned hash). Mirrors the Python permutation-invariance test.
let d0 = delta(vec![e(1, 3.0, "z"), e(0, 1.0, "a")]);
let d1 = delta(vec![e(2, 2.0, "m"), e(0, 1.0, "a")]);
let d2 = delta(vec![e(10, 5.0, "q")]);
let forward = hex(&merge_kernel(&[d0.clone(), d1.clone(), d2.clone()]).canonical_bytes());
let reversed = hex(&merge_kernel(&[d2, d1, d0]).canonical_bytes());
assert_eq!(forward, reversed);
assert_eq!(forward, expected("three_delta_merge"));
}

64
core/array_codec.py Normal file
View file

@ -0,0 +1,64 @@
"""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

@ -41,6 +41,14 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
"tests/test_runtime_config.py",
"tests/test_cognitive_turn_pipeline.py",
"tests/test_architectural_invariants.py",
# ADR-0043 — identity falsifiability: ratified identity packs must
# produce distinct, directionally-correct articulations, with a
# pack-invariant grounding/refusal floor and zero fabrication. Lives
# only under ``full`` historically, so a divergence regression cleared
# the PR gate and surfaced only post-merge. Promoted into smoke so
# the falsifiability claim blocks-on-regression rather than
# detect-after-merge.
"tests/test_pack_measurements_phase2.py",
),
"runtime": (
"tests/test_chat_runtime.py",
@ -106,6 +114,29 @@ _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",
@ -157,6 +188,9 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
"math": (
"tests/test_adr_0126_train_sample_runner.py",
),
"deductive": (
"tests/test_deductive_logic_entail.py",
),
"full": ("tests/",),
}
@ -2038,14 +2072,10 @@ def cmd_teaching_coverage(args: argparse.Namespace) -> int:
return 1
report_path = lane_dir / "report.json"
use_reader = bool(args.use_reader)
if args.run or not report_path.exists():
import subprocess
runner_module = f"evals.{lane}.{split}.{version}.runner"
runner_args = [sys.executable, "-m", runner_module]
if use_reader:
runner_args.append("--use-reader")
try:
subprocess.run(
runner_args,
@ -2069,14 +2099,13 @@ def cmd_teaching_coverage(args: argparse.Namespace) -> int:
lane=lane,
split=split,
version=version,
use_reader=use_reader,
baseline_path=baseline_path,
)
if args.json:
print(json.dumps(report.as_dict(), indent=2, sort_keys=True))
else:
print(f"Lane: {report.lane}/{report.split}/{report.version} (use_reader={report.use_reader})")
print(f"Lane: {report.lane}/{report.split}/{report.version}")
if report.delta:
print(
f"Counts: correct={report.counts.correct} "
@ -2346,6 +2375,10 @@ def cmd_doctor(args: argparse.Namespace) -> int:
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)
@ -2453,6 +2486,66 @@ def cmd_eval(args: argparse.Namespace) -> int:
return 0
def cmd_eval_sensorium(args: argparse.Namespace) -> int:
"""Run deterministic sensorium modality evidence reports."""
from evals.sensorium import build_sensorium_report
modality = getattr(args, "modality", "vision") or "vision"
try:
report = build_sensorium_report(modality)
except ValueError as exc:
_die(str(exc), code=2)
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"modality : {report['modality']}")
print(f"pack_id : {report['pack_id']}")
print(f"gate_engaged : {report['gate_engaged']}")
print(f"gate_closed : {report['gate_closed']}")
print(f"cases : {report['total']}")
print(f"passed : {report['passed']}")
print(f"failed : {report['failed']}")
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["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
# ---------------------------------------------------------------------------
@ -4631,10 +4724,6 @@ def build_parser() -> argparse.ArgumentParser:
teaching_coverage.add_argument(
"--version", default="v1", help="lane version (default: v1)",
)
teaching_coverage.add_argument(
"--use-reader", action="store_true",
help="pass --use-reader to the runner (matches train_sample default)",
)
teaching_coverage.add_argument(
"--run", action="store_true",
help="re-run the lane's runner even if report.json exists",
@ -4890,6 +4979,12 @@ def build_parser() -> argparse.ArgumentParser:
eval_cmd.add_argument("--json", action="store_true", help="emit machine-readable JSON")
eval_cmd.add_argument("--save", action="store_true", help="write result to lane results/ directory")
eval_cmd.add_argument("--report", metavar="PATH", help="write JSON report to file")
eval_cmd.add_argument(
"--modality",
choices=["audio", "vision", "event-vision", "sensorimotor"],
default="vision",
help="sensorium lane modality to evaluate (default: vision)",
)
eval_cmd.add_argument(
"--audit",
metavar="PATH",

View file

@ -22,6 +22,7 @@ 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 (
@ -47,6 +48,7 @@ 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
@ -122,7 +124,7 @@ class CognitiveTurnPipeline:
) -> None: # runtime: ChatRuntime (no import cycle)
self.runtime = runtime
self._last_node_id: str | None = None
self.teaching_store = teaching_store or TeachingStore()
self.teaching_store = teaching_store if teaching_store is not None else TeachingStore()
if recognizer is not None:
self._recognizer = recognizer
elif hasattr(runtime, "first_admitted_recognizer"):
@ -304,6 +306,8 @@ 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,
@ -393,13 +397,14 @@ 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)
# 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
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
)
# ADR-0023 — admissibility trace + ratification provenance.
# Comb pass 2026-05-21 — direct attribute access; the fields
@ -690,6 +695,39 @@ 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."""
@ -721,6 +759,19 @@ 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

@ -0,0 +1,57 @@
"""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

@ -0,0 +1,159 @@
"""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

@ -0,0 +1,250 @@
"""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

@ -0,0 +1,63 @@
"""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

@ -0,0 +1,148 @@
"""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

@ -0,0 +1,77 @@
"""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,16 +276,73 @@ class RuntimeConfig:
# ADR-0151 — generate TeachingChainProposals from enriched candidates on load.
auto_proposal_enabled: bool = False
# ADR-0164 Phase 1 — incremental comprehension reader for question sentences.
# When True, the candidate-graph path uses the comprehension reader
# (generate/comprehension/lifecycle.py) to parse question sentences BEFORE
# consulting the regex question patterns (Pattern A/B/C in
# generate/math_candidate_parser.py). On reader refusal, falls through to
# the existing regex parser — the reader is purely additive at Phase 1.
# Default False: flag-OFF behaviour is byte-identical to today.
# Phase 3 (per ADR-0164 §Phasing) removes the regex question parser entirely;
# that work is deferred — this PR is the Phase 1 stopgap.
comprehension_reader_questions: 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"

View file

@ -2,8 +2,10 @@
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

@ -0,0 +1,111 @@
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"]

134
core/engine_identity.py Normal file
View file

@ -0,0 +1,134 @@
"""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

@ -0,0 +1,87 @@
"""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

@ -0,0 +1,171 @@
"""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

@ -0,0 +1,65 @@
"""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

@ -0,0 +1,110 @@
"""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

@ -0,0 +1,378 @@
"""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

@ -0,0 +1,33 @@
"""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

@ -0,0 +1,211 @@
"""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

@ -0,0 +1,40 @@
"""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

@ -0,0 +1,272 @@
"""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

@ -0,0 +1,195 @@
"""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

@ -0,0 +1,32 @@
"""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

@ -0,0 +1,40 @@
"""ADR-0199 — cross-domain learning arena.
The shared engine + interfaces every base subject plugs into. Domains live
outside this package (e.g. ``evals/gsm8k_math/practice``); this package never
imports a concrete domain.
"""
from __future__ import annotations
from core.learning_arena.engine import run_practice
from core.learning_arena.protocols import (
Attempt,
BaseAttempt,
DomainProblem,
DomainSolver,
GoldTether,
Problem,
Tier2Verifier,
)
from core.learning_arena.report import (
REFUSAL_DIAGNOSES,
EliminationRecord,
PracticeReport,
bucket_counts,
)
__all__ = [
"run_practice",
"Attempt",
"BaseAttempt",
"DomainProblem",
"DomainSolver",
"GoldTether",
"Problem",
"Tier2Verifier",
"REFUSAL_DIAGNOSES",
"EliminationRecord",
"PracticeReport",
"bucket_counts",
]

View file

@ -0,0 +1,120 @@
"""ADR-0199 §2.2 — the domain-agnostic practice engine.
This is the extraction of the GSM8K ``run_practice`` fold into a subject-neutral
core. It is the **only** new per-domain code path a subject needs to reach: a
subject supplies a :class:`DomainSolver` + :class:`GoldTether` and gets a
:class:`PracticeReport` whose ``.ledger`` is the ``dict[str, ClassTally]`` the
reliability gate (``propose_from_ledger``) consumes unchanged.
Invariants (the load-bearing ADR-0199 mandates, enforced structurally here):
- **L-1 (one floor).** Reliability is computed only via :class:`ClassTally`
(which calls the single pinned ``conservative_floor``). This module defines
no pessimism constant of its own.
- **L-3 (seal).** ``run_practice`` returns a report and mutates nothing. It
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.
"""
from __future__ import annotations
from typing import Callable, Sequence
from core.learning_arena.protocols import (
Attempt,
DomainProblem,
DomainSolver,
GoldTether,
Tier2Verifier,
)
from core.learning_arena.report import EliminationRecord, PracticeReport
from core.reliability_gate import ClassTally
def _default_diagnose(_reason: str) -> str:
"""Conservative default: assume a missing piece (ADR-0175 §8).
A domain supplies its own router (e.g. a refusal-reason vocabulary) via the
``diagnose`` parameter; absent one, refusals are attributed to a knowledge
gap rather than silently dropped.
"""
return "knowledge_gap"
def run_practice(
problems: Sequence[DomainProblem],
solver: DomainSolver,
tether: GoldTether,
*,
diagnose: Callable[[str], str] = _default_diagnose,
tier2_verifier: Tier2Verifier | None = None,
) -> PracticeReport:
"""Sealed practice: attempt -> gold-tether score -> per-class ledger.
For each problem, in input order: the solver attempts it; the verdict is
``refused`` when the attempt is uncommitted, else ``correct``/``wrong`` per
the tether's independent gold check. Counts and per-class :class:`ClassTally`
accumulate; each wrong yields an :class:`EliminationRecord`; each refusal is
routed by ``diagnose``.
"""
counts = {"correct": 0, "wrong": 0, "refused": 0}
ledger: dict[str, ClassTally] = {}
diagnoses: dict[str, str] = {}
elims: list[EliminationRecord] = []
for problem in problems:
cls = problem.class_name
attempt: Attempt = solver.attempt(problem)
gold_correct = False
if not attempt.committed:
verdict = "refused"
else:
gold_correct = tether.is_correct(attempt, problem)
verdict = "correct" if gold_correct else "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,
)
elif verdict == "wrong":
tally = tally.record(
wrong=1,
t2_verified=t2_verified,
t2_agrees_gold=t2_agrees_gold,
)
elims.append(
EliminationRecord(
case_id=attempt.case_id,
class_name=cls,
attempted=attempt.answer,
gold=tether.gold_answer(problem),
reason=attempt.reason or "",
)
)
else: # refused
tally = tally.record(refused=1)
diagnoses[attempt.case_id] = diagnose(attempt.reason or "")
ledger[cls] = tally
return PracticeReport(
counts=counts,
ledger=ledger,
refusal_diagnoses=diagnoses,
elimination_records=tuple(elims),
)

View file

@ -0,0 +1,118 @@
"""ADR-0199 §2.2 — the cross-domain learning-arena interfaces.
A subject becomes a learning arena by supplying four domain-specific pieces
(``DomainSolver``, a gold anchor set, capability classes, a Tier-2 verifier)
and reusing the shared engine (:mod:`core.learning_arena.engine`) and the
shared reliability gate (:mod:`core.reliability_gate`) unchanged.
These protocols are structural (PEP 544). A domain provides concrete classes;
the engine never imports a concrete domain. The first instance is GSM8K math
(``evals/gsm8k_math/practice/v1/runner.py``), re-expressed against this
contract with no behavior change.
Note on the ADR's illustrative signatures: the ADR sketched
``is_correct(attempt, problem_id)``. We pass the whole ``DomainProblem`` (which
carries its ``problem_id``) so a tether can reach class/payload without a
separate lookup table strictly more general, same contract.
"""
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):
"""One problem in a practice arena.
``class_name`` is the capability axis this problem exercises (the ledger
key); it is resolved up front by a domain adapter that may consult gold.
``payload`` is opaque to the engine only the domain's solver/tether read
it.
"""
problem_id: str
class_name: str
payload: Any
@runtime_checkable
class Attempt(Protocol):
"""The result of a single attempt.
``committed is False`` means the engine refused (always safe; excluded
from reliability's denominator per ADR-0175 §4). ``derivations`` are the
2 structurally-distinct paths a Tier-2 verifier inspects; ``trace_sha256``
is replayable provenance carrying no raw content beyond hashes.
"""
committed: bool
answer: Any
reason: str
case_id: str
derivations: tuple[Any, ...]
trace_sha256: str
@runtime_checkable
class DomainSolver(Protocol):
"""Attempts a grounded derivation over the subject's operations.
This is where intelligence lives (ADR-0175 Pivot-2). The engine calls
:meth:`attempt` once per problem and never inspects how the answer was
reached beyond the :class:`Attempt` fields.
"""
domain_id: str
def attempt(self, problem: DomainProblem) -> Attempt: ...
@runtime_checkable
class GoldTether(Protocol):
"""The Tier-1 truth anchor for a subject.
ADR-0199 mandate 2: the truth ``is_correct`` consults must come from a
source **independent of the solver's derivation** (proof obligation L-2).
For dataset domains the gold is the dataset's own answer; for software it
is execution; etc.
"""
domain_id: str
def is_correct(self, attempt: Attempt, problem: DomainProblem) -> bool: ...
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."""
problem_id: str
class_name: str
payload: Any = None
@dataclass(frozen=True, slots=True)
class BaseAttempt:
"""Concrete :class:`Attempt` for domains that need no extra fields."""
committed: bool
answer: Any = None
reason: str = ""
case_id: str = ""
derivations: tuple[Any, ...] = field(default_factory=tuple)
trace_sha256: str = ""

View file

@ -0,0 +1,80 @@
"""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.
The three refusal-diagnosis axes are the universal ADR-0175 §8 router
(skill / knowledge / ambiguity), not a domain quantity so they live here.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
from core.reliability_gate import ClassTally
# ADR-0175 §8 — the universal "name the missing piece" axes.
REFUSAL_DIAGNOSES: tuple[str, ...] = ("skill_gap", "knowledge_gap", "genuine_ambiguity")
@dataclass(frozen=True, slots=True)
class EliminationRecord:
"""A wrong practice attempt that gold caught — the pruning signal (§9)."""
case_id: str
class_name: str
attempted: float | None
gold: float
reason: str
def bucket_counts(diagnoses: Mapping[str, str]) -> dict[str, int]:
out = {d: 0 for d in REFUSAL_DIAGNOSES}
for d in diagnoses.values():
out[d] = out.get(d, 0) + 1
return out
@dataclass(frozen=True, slots=True)
class PracticeReport:
counts: Mapping[str, int]
ledger: Mapping[str, ClassTally]
refusal_diagnoses: Mapping[str, str]
elimination_records: tuple[EliminationRecord, ...]
def as_dict(self) -> dict[str, Any]:
return {
"schema_version": 1,
"adr": "0175",
"regime": "practice",
"counts": dict(self.counts),
"per_class": {
cls: {
"correct": t.correct,
"wrong": t.wrong,
"refused": t.refused,
"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())
},
"refusal_diagnoses": dict(sorted(self.refusal_diagnoses.items())),
"diagnosis_counts": bucket_counts(self.refusal_diagnoses),
"elimination_records": [
{
"case_id": r.case_id,
"class_name": r.class_name,
"attempted": r.attempted,
"gold": r.gold,
"reason": r.reason,
}
for r in self.elimination_records
],
}

View file

@ -298,6 +298,15 @@ class TurnEvent:
epistemic_state: str = "undetermined"
normative_clearance: str = "unassessable"
normative_detail: str = ""
# ADR-0206 — Response Governance Bridge reach level for this turn.
# The reach policy that governed the response surface, as a
# lower_snake_case string mirroring core.response_governance.ReachLevel
# without importing that module here (preserving identity.py's
# low-coupling shared-value-type role). Scaffold contract: always
# "strict" — govern_response emits STRICT-only until the risk-reward
# widening loop is built (ADR-0206 §3). Default "strict" so callers
# that omit the field stay byte-identical and conservatively governed.
reach_level: str = "strict"
# ADR-0153 (W-020a) — canonical SHA-256 trace hash for this turn,
# back-stamped by ``CognitiveTurnPipeline.process`` after
# ``compute_trace_hash`` runs. Empty string on construction;

View file

@ -0,0 +1,38 @@
"""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

@ -0,0 +1,60 @@
"""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

@ -0,0 +1,39 @@
"""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

@ -0,0 +1,80 @@
"""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

@ -0,0 +1,102 @@
"""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

@ -0,0 +1,85 @@
"""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"]

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