Commit graph

694 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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