Commit graph

43 commits

Author SHA1 Message Date
Shay
57a62043d1
feat(gsm8k): xhigh capability sprint 13 robust lift (#827)
* test(gsm8k): pin xhigh sprint13 confusers and completeness

Add the Sprint 13 contract bundle with target-chain pins, sealed-wrong
neighbors, fraction-word refusals, and non-vacuous completeness cases for
extra mile and extra-actor obligations.

* feat(gsm8k): add xhigh sprint13 verified organs

Gate A2t bounded_rate_projection admits 0016 and 0034 with honest
affine/percent derivations. Gate A2u closed_reference_affine_aggregate
admits 0027 and 0039 with statement-scoped numeric obligations and
repaired comparative provenance. Serving lift: 26/24/0 -> 30/20/0 with
wrong=0 preserved.

* docs(gsm8k): add xhigh sprint13 lookback

Record XHIGH draft recovery, unsafe evidence findings, repairs, Path A
salvage decision, validation outputs, and Sprint 14 recommendation.
2026-06-18 16:10:15 -07:00
Shay
6daa5dfdea
feat(gsm8k): capability paradigm sprint 12 singleton contract lift (#825)
* feat(gsm8k): capability paradigm sprint 12 singleton contract lift

Add Gate A2r nested_fraction_remainder_total (0004) and Gate A2s
loose_crayon_box_capacity (0007) under explicit ClusterContracts with
hard 0026/0047 neighbor confuser refusal. Serving lifts 24/26/0 to
26/24/0 with wrong=0 preserved; report.json and sealed artifacts
untouched.

* fix(gsm8k): bind nested fraction outer camp

* fix(gsm8k): bind loose box actor and item units

* test(gsm8k): cover sprint12 binding confusers
2026-06-18 15:10:10 -07:00
Shay
50e8de03fe
feat(gsm8k): capability paradigm sprint 11 cluster-contract lift (#824) 2026-06-18 12:52:32 -07:00
Shay
a37f390009
feat(derivation): capability paradigm sprint 10 frontier lift (#823)
* feat(derivation): capability paradigm sprint 10 frontier lift

Add Gate A2o affine_comparative_inversion_total (0009) and Gate A2p
sequential_comparative_scale (0006). Reject wholesale multiplicative_aggregate
and defer 0013 piecewise calendar until month day-count grounds in text.

Serving: 21/29/0 → 23/27/0, wrong=0 preserved. report.json and sealed
artifacts untouched.

* fix(gsm8k): bind affine inversion total question subject

* fix(gsm8k): bind sequential scale page question subject

* test(gsm8k): cover sprint10 subject binding

* fix(gsm8k): license affine inversion aggregate units

* fix(gsm8k): bind sequential scale factors to reading chain

* test(gsm8k): cover sprint10 aggregate and scale confusers
2026-06-18 12:15:52 -07:00
Shay
80753e13de
fix(gsm8k): harden sprint 9 temporal and affine gates (#822)
Patch post-merge wrong=0 hazards from #820 review without reverting
Sprint 9 organs: bind affine/temporal answers to asked subjects and body
actors, anchor overtime thresholds to per-shift/day surfaces, fix affine
completeness double-counting, narrow percent_partition group skip, and
expand temporal question/rental verb recognition.
2026-06-18 10:57:36 -07:00
Shay
63a9bea823
feat(derivation): capability paradigm sprint 9 temporal tariff + affine lift (#820)
Gate A2m promotes narrow temporal_tariff organs for overtime shift earnings
(0001) and bundle-overflow rental tariffs (0017). Gate A2n promotes affine
fraction-delta chains for 0010-class references (reference × N/M + K).

Serving moves 18/32/0 → 21/29/0 with wrong=0 preserved. Hardens
percent_partition to refuse subgroup-as-total confusers surfaced in review.
2026-06-18 09:59:46 -07:00
Shay
282c5126de
feat(derivation): capability paradigm sprint 8 R6 affine/fraction lift (#819) 2026-06-18 01:52:25 -07:00
Shay
5d529b425f
feat(derivation): capability paradigm sprint 7 experience-guided lift (#818)
* feat(derivation): capability paradigm sprint 7 experience-guided lift

Add Gate A2i round_trip_trip_duration (0030/cv-0006) and Gate A2j
giveaway_target_residual (0035/cv-0021) as narrow self-verified promotion
bridges discovered via Experience Flywheel decomposition of the
joint-refusal cluster.

Serving: 14/36/0 → 16/34/0, wrong=0 preserved. Holdout_dev admits 0.

* fix(gsm8k): bind giveaway residual to owner

* test(gsm8k): cover giveaway owner binding
2026-06-17 22:47:02 -07:00
Shay
e2f3d37373
feat(derivation): capability paradigm sprint 6 experience-guided lift (#817)
* feat(derivation): capability paradigm sprint 6 experience-guided lift

Experience Flywheel + scout on post-#816 main showed lift_refused_to_correct=0
(Sprint 5 already served prior deltas). Decompose the MA cluster and R5 pin into
two narrow organs:

- Gate A2g duration_segment_total (0015 / cv-0022)
- Gate A2h survey_rate_earnings (0045)

Ephemeral train_sample: 12/38/0 → 14/36/0, wrong=0 preserved.
Holdout_dev: 0 new admissions. report.json and sealed artifacts untouched.

* fix(gsm8k): require compatible duration units

* fix(gsm8k): harden survey earnings verification

* test(gsm8k): cover sprint6 review hardening
2026-06-17 22:22:53 -07:00
Shay
aa6173af58
feat(derivation): capability paradigm sprint 5 question-bound product lift (#815)
Practice/scout showed 0003/0021 refuse on DCS no-injection while sealed
resolve_pooled commits correctly. Build Gate A2f typed revenue/weight product
organs narrower than disabled product_bridge; lifts train_sample 10/40/0 to
12/38/0 with wrong=0 and zero holdout_dev admissions.
2026-06-17 20:51:17 -07:00
Shay
e8f5a347d5
feat(derivation): capability strike batch 4 goal-residual lift (#814)
* feat(derivation): capability strike batch 4 goal-residual lift

Scout-guided Batch 4 re-wires resolve_promotable_goal_residual serving
promotion (Gate A2e) for train_sample 0037. product_bridge stays disabled
per held-out wrong=0 bisection.

Ephemeral train_sample: 9/41/0 → 10/40/0, wrong=0 preserved.
report.json untouched.

* test(gsm8k): refresh composition corpus prose for A2e

* fix(derivation): refuse comparative goal-residual questions

* test(derivation): guard goal-residual against comparative more-than
2026-06-17 20:21:18 -07:00
Shay
80240ea9b8
feat(derivation): Workstream A inc 1 — lexeme reader + cleaned exemplars + proxy rebaseline (#796)
* feat(derivation): Workstream A inc 1 — lexeme-only reader components for fractions/comparatives + cleaned Phase B exemplars + proxy rebaseline (ratified, post-review fixes)

- generate/derivation/extract.py: lexeme-only passes (surface tokens only; no synthesized values or non-surface source_tokens for "half of", "X/Y of", "X more/less than"). Composition deferred per module contract (ADR-0165). Hygiene and EX-6 preserved. Docstring updated.
- teaching/admissibility_exemplars/*_v1.jsonl: grown + cleaned (removed category-polluted seeds, deduplicated ma IDs, fixed comparative exemplar to surface tokens per lexeme rule).
- evals/gsm8k_math/train_sample/v1/report.json: rebaselined after fixes (runner executed; wrong=0 held).
- docs/analysis/*-2026-06-1[6-7].md + lookback: ratif + honest post-review lookback (local paths removed, scope claims match actual clean diff, extractor/exemplar fixes documented).

Governing: the two 2026-06-16/17 ratifications (included; local session path replaced with stable ref). Scope per ratif: reader (lexeme) + exemplar growth (clean) + rebaseline + tests + lookback. No sealed, no CLOSE (deferred), 0 wrong.

Added tests in test_adr_0179_extract.py for new lexeme paths + grounding.

Branch prepared clean from origin/main; diff limited to scoped files only (no posture ratif, no runtime_contracts drift).

See updated lookback and PR body for full evidence (6/44/0 or current post-fix,  tests green, etc.).

(Addresses review: Semantic Rigor restored, branch hygiene, exemplar quality, tests, docs.)

* fix(review): resolve all verifiability blockers

- generate/derivation/extract.py: conflict markers removed (was from stash); lexeme-only implementation confirmed clean.
- tests/test_adr_0179_extract.py: added TestWorkstreamAReaderLexemeOnly class with direct tests for half-of, fraction-of, more/less components, source_token grounding, no-synthesis invariants.
- evals/gsm8k_math/train_sample/v1/report.json: fresh rebaseline via runner with fixed code (6/44/0, wrong=0).
- docs/analysis/gsm8k-workstream-a-increment-1-lookback-2026-06-17.md: updated to exactly match the actual diff and actual report numbers on this head.
- No conflict markers remain in extract, tests, lookback, ratifs, or exemplars (verified by grep).
- Branch head now clean and matches all claims in PR body/lookback.

This is the verifiable head for the lead engineer review.

* fix(test): make lexeme-only half test case-insensitive on source_token to match actual extraction (preserves input casing like other EX tests)

* fix(review): commit current report.json (6/44/0 from runner) and updated lookback so they appear in branch diff; lookback now exactly matches actual files in git diff --name-only and actual test count (29) and report numbers.

* fix(review): precise fractional doc in extract contract (normalizes factors while preserving source_token; comparative deferred); remove stale comparative sum/diff comments; lookback exact reality (29/29 tests, no deep-dive in diff, no sum/diff claim); mark remaining exemplars REVIEW-REQUIRED/ambiguous with explicit notes. Small final patch.

* fix(exemplars): remove three REVIEW-REQUIRED ambiguous-but-machine-admissible seeds (rwc-v1-0022, rwc-v1-0023, ma-v1-0024) from ingested JSONL to eliminate synthesis-poisoning risk per final review. Update lookback wording for actual seed count and rationale. Small final patch.

* docs(lookback): align exemplar category list with actual diff (only dcs/rwc/ma touched in this increment; no ca/ta/dsnq changes)
2026-06-16 21:57:05 -07:00
Shay
ad75a2c79a test(derivation): add ADR-0184 semantic replay equivalence harness
ADR-0184 S4b — the replay/provenance equivalence boundary, the
precondition for any semantic-primary path. After #685 the in-repo
legacy path delegates to the semantic boundary, so old-vs-new is
vacuous; the durable reference is a pinned canonical trace artifact
(evals/gsm8k_math/equivalence/v1/, 937 problems) whose authority is
the #684/#685 cross-tree differentials (documented provenance chain).

- state/provenance.py: replay_is_faithful — the replay bridge's law as
  a checkable structural property (no verify/pool import; covered by
  the #685 authority scan).
- equivalence/trace.py: canonical traces (worlds, candidates, order,
  multiplicity, classifications, resolutions) + authority_violations
  re-deriving the pool commit law from trace content.
- scripts/verify_semantic_equivalence.py: check / --update gate
  mirroring verify_lane_shas.py.
- tests/test_adr_0184_s4b_replay_equivalence.py: full-corpus live-vs-
  pinned equality, corpus-wide authority + faithfulness, and 17
  single-mutation non-vacuity proofs.

Pure additions; verify.py/pool.py untouched; no serving, CLAIMS,
metrics, or lane pins moved.
2026-06-10 14:35:22 -07:00
Shay
951bcc0252 feat(derivation): define semantic-ledger candidate source boundary
ADR-0184 §7 S4: add generate/derivation/state/source.py — the single
boundary through which semantic-ledger worlds become pool candidates —
and swap pool.py's accumulation source to semantic_state_candidates().
accumulate.py keeps its public surfaces as thin compatibility wrappers
(ADR-0184 §10). Byte-identical over the 937-problem differential;
verify.py / pool.py remain the sole commit authority (structural
no-authority-import scan over state/, non-vacuous).

Also: docs/analysis design note for the S3–S5 boundary stack, and a
drive-by ruff F541 fix in r1_reconstruction.py (rf->r, zero behavior).
2026-06-10 13:17:53 -07:00
Shay
9dcb00248c feat(derivation): route accumulation through ADR-0184 S2 semantic ledger
ADR-0184 S1 (state/bind.py, state/change.py) landed the reusable referent and
change-cue helpers. S2 adds the first explicit state-transition substrate and
routes both accumulation readings through it:

- state/model.py   frozen SET/GAIN/LOSS transition model over one entity/unit
                   StateKey, with structural validation (closed op set; unit is a
                   str with "" = unitless, mirroring the extractor's Quantity.unit
                   contract; entity optional, mirroring leading_subject_token).
- state/ledger.py  build_accumulation_ledger() — the proven single-referent
                   gain/loss reading, expressed as a SemanticLedger.
- state/replay.py  replay_accumulation_ledger() — the only bridge to
                   GroundedDerivation; refuses any non-accumulation ledger shape
                   (non-SET start, cross-key mutation).

accumulate.py's _build_accumulation / _build_accumulation_anchor_skip now build a
ledger and replay it instead of constructing the arithmetic chain inline; the
public compose_accumulation()/accumulation_candidates() surfaces are unchanged.

Behavior-equivalent: a byte-for-byte differential over 937 real GSM8K problems
(accumulation_candidates, compose_accumulation, pooled_candidates, resolve_pooled)
shows 0 differences vs main. Semantic worlds never commit directly — replay emits
GroundedDerivation and verify.py / pool.py remain the sole authority. Sealed: no
serving/runtime/chat import; no CLAIMS or eval-lane-SHA movement.

Refs ADR-0184 section 7 (S2).
2026-06-10 12:00:23 -07:00
Shay
c5f0c90738 feat(derivation): math-serving reach seam — select_self_verified is policy-aware (ADR-0206 §5)
Completes the ADR-0206 §5 math-serving deferral in its own careful PR. The GSM8K
wrong=0 serving gate, select_self_verified, now takes a ReachPolicy. This changes
NO serving behavior today — it is the safe, byte-identical, sanctioned first edit to
the most wrong=0-critical line, with a live-wiring test, so a future VERIFIED
widening has a precise, proven integration point.

Why it can't actually widen yet (the tension scoping surfaced): GSM8K wrong=0 is
ABSOLUTE (zero wrong, ever); a reliability license is STATISTICAL (a 0.99 Wilson
floor); math answers aren't disclosed like the cognition path (E). So widening the
math serve on a statistical license would eventually serve a SILENT wrong. ADR-0206
§4 foresaw this: VERIFIED is "the only state that will license widening past gold,"
and it is reserved pending a canonical-comparison pass (the soundness≠correctness
gap) that is unbuilt.

Design (safe by construction):
- select_self_verified(..., policy=STRICT_POLICY). STRICT (the default every one of
  the 5 callers passes) is the prior logic verbatim — unique answer → Resolution;
  zero-verify or disagreement → refuse. Byte-identical: the pinned serving-lane SHAs
  (demo_composition 3a3d09f3, fabrication 01e1b6b7, math_teaching_corpus eaf160d1)
  are unchanged; Resolution is unchanged; no import cycle.
- A wider reach resolves a disagreement ONLY via _canonically_verified — the VERIFIED
  gate — whose body returns None (capability unbuilt). So the widening is
  STRUCTURALLY inert: a disagreement refuses regardless of policy. wrong=0 holds by
  construction, not caller discipline. A statistical reliability license is NOT
  consulted by the math path (the cognition/math asymmetry is deliberate: cognition
  discloses, math is absolute).
- test_seam_is_live_wiring injects the gate to prove the consumer fires under a wider
  reach — and STRICT still refuses even then.

Verified: smoke (90), invariants (56), governance + selfverify + ms2 (354 incl. the
new seam tests), GSM8K serving regression byte-identical (the one red,
test_serving_unchanged_by_search expecting stale 6/44, fails identically on clean
main — a pre-existing stale artifact, not this change). Three-lens adversarial review
(byte-identity/seal, structural wrong=0 inertness, no-overclaim): all held.

The real unlock — a VERIFIED canonical-comparison producer — is scoped in
docs/analysis/VERIFIED-canonical-comparison-scoping-2026-06-06.md (recommended:
back-substitution / constraint-satisfaction on a checkable problem class). Still
deferred: SITUATE (stakes), the live FEED-BACK loop, reach_level JSONL emission.
2026-06-06 15:46:14 -07:00
Shay
7155f5ab34 feat: add gsm8k r1 reconstruction 2026-06-04 13:25:11 -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
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
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
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
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
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
Shay
872ed3b52d feat(adr-0175-phase3b): bounded multiplicative search in the sealed practice lane
ADR-0175 Phase 3b — the first live attempt generator. Runs only in the sealed
practice lane, only on cases the engine refused; every proposal is gated by the
Phase 3a self-verification gate.

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

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

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

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

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

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

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

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

Verified: 16/16; ruff clean; smoke 67/67; no serving import.
2026-05-28 15:19:02 -07:00