Commit graph

226 commits

Author SHA1 Message Date
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
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
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
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
7155f5ab34 feat: add gsm8k r1 reconstruction 2026-06-04 13:25:11 -07:00
Shay
3389cc68c3 feat: add deductive proof evidence gates 2026-06-04 08:37:51 -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
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
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
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
e1bcdf6286 Harden no-reference n-times comparative guard 2026-06-03 15:22:42 -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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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