core/docs/decisions/ADR-0131.G.2-comparatives.md
Shay 657c74102b
fix(ADR-0131.G.2): rebase + mastery hardening — quarter/third fraction anchors, gate regex, boundary refusals (#196)
Rebases onto current main (dec98ea, post-G.1/G.3.1/G.4/promotion).

Parser:
- Extend _COMPARE_MULT_ANCHOR_RE anchor alternation to include 'quarter'
  and 'third'; add optional 'a\s+' article prefix so "a quarter as many"
  and "a third as many" parse. Both anchors are in COMPARE_MULTIPLICATIVE_ANCHORS
  and the round-trip factor-divisor table ("quarter":4, "third":3), so
  round-trip checks pass. quarter→0.25 (exact), third→1/3 (float).
- Add _ANCHOR_TO_FACTOR entries for quarter and third.

Gate regex (test_adr_0131_G2_comparatives.py):
- Widen _COMPARATIVE_STATEMENT_PATTERNS multiplicative pattern from
  '\d+\s+times' to '\w+\s+times' to match word-number forms ("four times")
  that would be missed by the digit-only pattern if a future GSM8K case
  contains one in a still-refused statement.

Cases (31 total, was 24):
- G2-mul-frac-005/006: two 'quarter' cases (fraction direction now has
  half×4 + quarter×2 + third×1 = 7 cases, was 4 all-half).
- G2-mul-frac-007: 'third' case.
- G2-refuse-006: hyphenated 'one-third' pins the closed-anchor boundary.
- G2-refuse-007: 'double as many' pins the deferred grammar shape.

Tests (25, was 21):
- Add quarter and third parametric entries to test_multiplicative_direction_admits.
- Add one-third and double-as-many refusal params to test_refusal_cases.
- Add quarter/third to test_direction_literals_closed_set.
- Update test_runner_per_category_minima comment to reflect new counts.

ADR: document quarter/third admission, updated case table, deferred list.
report.json: refreshed to 31 cases, wrong==0 preserved.
2026-05-23 19:28:09 -07:00

13 KiB
Raw Blame History

ADR-0131.G.2 — Capability axis: comparative operations (additive + multiplicative)

Status: Proposed Date: 2026-05-23 Author: CORE agents + reviewers Parent: ADR-0131.G Depends on: ADR-0123 (Comparison op shape), ADR-0126 (candidate-graph topology), ADR-0131.3, ADR-0132..ADR-0134


Context

The Comparison operand shape, the four direction literals (more, fewer, times, fraction), and the round-trip anchor tables (COMPARE_ADDITIVE_ANCHORS, COMPARE_MULTIPLICATIVE_ANCHORS) all landed with ADR-0123. The legacy first-match-wins parser (generate/math_parser.py) wires those for the bounded-grammar lane.

The candidate-emitting sentence parser (generate/math_candidate_parser.py:30) explicitly deferred comparative candidates to a "later phase of ADR-0126". Without that phase, multi-sentence GSM8K problems that contain a comparative clause are refused by the candidate-graph topology even when the comparative is well-formed — the comparative sentence emits zero candidates, the per-sentence choice space is empty, and the whole problem refuses with no admissible candidate for statement: 'X'.

This ADR is the deferred comparative phase, framed under ADR-0131.G's capability-first iteration discipline: comparatives are a single capability axis (not a list of GSM8K templates), exercised by curated sentences independent of GSM8K, with the probe's admission / refused-reasons numbers moving only as a side effect.


Decision

Land comparative candidate-emitting extractors in generate/math_candidate_parser.py:

Function Closed-set shapes admitted
_compare_additive_candidates <A> has N more <unit> than <B> / ... N fewer ... / ... N less ...
_compare_multiplicative_candidates <A> has twice as many <unit> as <B> / ... thrice ... / ... half ... / ... N times as many ... / ... a quarter as many ... / ... a third as many ...
_compare_nested_candidates <A> has N more <unit> than M times <REF> — emits two flat candidates (one additive, one multiplicative), letting the binding-graph layer (ADR-0134) pick an admissible composition or refuse

Direction vocabulary is closed to the four Comparison.direction literals. less is admitted as a surface synonym of fewer; its canonical Comparison value is direction='fewer' while matched_verb='less' (so the round-trip filter's verb-registry check finds less in COMPARE_ADDITIVE_ANCHORS, and its "verb appears in source" check succeeds).

quarter and third are admitted as fraction anchors (both are registered in COMPARE_MULTIPLICATIVE_ANCHORS and in the round-trip factor-divisor table). They admit an optional article ("a quarter", "a third") — the article is not captured; matched_verb is the anchor word itself, which is a substring of the source, so the round-trip verb-presence check passes. quarterfactor=0.25, thirdfactor=1/3 (float). Hyphenated forms (one-third) and double as many are explicitly deferred (pinned by refusal cases G2-refuse-006 / G2-refuse-007).

The reference slot widens beyond proper-nouns / "the X" to admit "the number/amount of " mass-noun constructions (_resolve_reference_token), so the GSM8K-style "...four times the number of chickens" composes. The canonical entity name is the full noun phrase; binding-graph referential integrity is preserved (distinct noun phrases stay distinct entities).

Specificity / precedence

Extractor order in extract_operation_candidates:

  1. add / subtract / transfer (existing — ADR-0126)
  2. _compare_nested_candidates (most specific — requires both (more|fewer|less) ... than and N times <REF> tail)
  3. _compare_multiplicative_candidates (anchor-as-value forms twice/thrice/half, then N times as many)
  4. _compare_additive_candidates (N more/fewer/less <unit> than <REF>)

Multiplicative anchors that overlap lexically with additive are disjoint at the lexical level: WORD_NUMBERS registers two/three but not twice/thrice, so the additive VALUE alternation cannot swallow a multiplicative anchor. Nested matching consumes its trailing than N times <REF> greedily, so a sentence the nested regex matches cannot also be claimed by the bare additive pattern (the bare additive regex requires the than <REF> tail with <REF> matching _COMPARE_REF, but the nested regex's <REF> is preceded by N times, which the bare additive regex's <REF> slot does not admit). Documented + tested in tests/test_adr_0131_G2_comparatives.py.

Curated coverage cases (G.2 axis lane)

evals/math_capability_axes/G2_comparatives/v1/cases.jsonl24 cases, exercised by an axis-specific runner (evals/math_capability_axes/G2_comparatives/v1/runner.py) that emits a deterministic report.json:

Category Cases Direction coverage
additive 8 more ×4, fewer ×4 (incl. one less surface)
multiplicative 11 times ×4, fraction ×7 (half×4, quarter×2, third×1)
nested 3 additive + multiplicative both flat-emitted
refusal 7 paraphrases + hyphenated + double as many boundary

Per-direction at-least-one-passing, nested-both-emitted, refusal zero-admitted, and wrong == 0 are pinned by tests/test_adr_0131_G2_comparatives.py. Report is byte-equal across runs.

Deferred (out of scope for G.2)

Documented refusal classes — these have no entry in COMPARE_*_ANCHORS, so admitting them would breach the round-trip filter's verb-registry check anyway:

  • "as many ... as" without a direction word ("Alice has as many apples as Bob") — equality is not in {more, fewer, times, fraction}.
  • "compared to <B>, <A> ..." and "in comparison with <B>, <A> ..." — prefix paraphrases; no anchor verb is consumed.
  • "the same number of ... as" — equality, see above.
  • "N times more" — legacy parser already refuses as ambiguous (ADR-0123 line 797); G.2 preserves that refusal.
  • Pronoun resolution in actor / reference slots — needs per-branch state (ADR-0126 P3 future work).
  • Past-tense / lemma widening (had, gets, took, buys) — adds surface variants without growing the capability axis; deferred to a separate axis to keep precedence narrow.
  • Unit ellipsis ("twice as many as Bob" without a unit) — without a unit the binding graph cannot pick a dimension to compare under ADR-0134's unit-aware admissibility; refusal preserves wrong == 0.
  • Hyphenated fractions (one-third, one-quarter) — not in the closed anchor alternation; the regex requires a bare token. Deferred to the numerics axis (G.3 / G.3.1) where the hyphenated-compound form is the natural extension domain.
  • "double as many" / "triple as many" — these anchors exist in COMPARE_MULTIPLICATIVE_ANCHORS (for the double the X sentence shape) but that shape requires different grammar. Deferred.

Nested composition is parser-side only

The parser emits two flat candidates for nested forms; it does not introduce a Comparison ∋ Comparison operand shape (which the data model doesn't support today). The binding-graph layer (ADR-0134) picks the admissible composition or refuses. Refusal of nested forms is the safe outcome and is what we observe today on gsm8k-train-sample-v1-0009: the comparative clause now parses (it no longer refuses at the statement layer), but the downstream composition has not yet been wired, so the case continues to refuse — at the question layer instead. That downstream gap is a follow-up ADR (compare_additive ∘ compare_multiplicative graph composition); this ADR explicitly does not stub the solver to bypass it.

GSM8K-probe gate (chosen)

G.2 gates on:

Comparative-clause refusals in the candidate-graph probe report (evals/gsm8k_math/train_sample/v1/report.json) strictly decrease.

Test: test_gsm8k_candidate_graph_comparative_clause_refusals_decreased (measures count of refused cases whose reason cites a statement clause containing either additive more/fewer/less ... than or multiplicative twice/N times/half as many anchors).

Probe report Baseline (origin/main 481e0c3) After G.2 Δ
Comparative-clause refusals (report.json) 2 1 1
admitted_wrong (report.json) 0 0 0
admission_rate (report.json) 0/50 0/50 0

The baseline-2 cases are gsm8k-train-sample-v1-0009 ("Jen has 10 more ducks than four times the number of chickens.") and -0016 ("Rudolph traveled 2 more than 5 miles"). G.2's extractor parses the comparative clause in 0009; 0016's "2 more than 5" is an arithmetic expression between numbers, not a between-entity comparison, and is deliberately out of G.2 scope (the additive shape requires an entity reference, not a numeric reference).

admission_rate does not rise because the downstream binding-graph composition for nested comparatives + the question layer's compatibility with comparison-derived state are out of G.2 scope. The honest claim is that the parser admits one more shape, not that the engine solves more GSM8K problems.

Legacy probe report (refreshed, byte-identical)

evals/gsm8k_math/train_sample/v1/train_sample_coverage_report.json runs through generate.math_parser.parse_problem (legacy first-match-wins), which already supported ADR-0123 comparative patterns. G.2 does not touch the legacy parser, so the legacy probe report is byte-identical to the baseline. Refreshed and pinned via test_gsm8k_legacy_probe_safety_rail_intact.


Invariants

  • compare_direction_closed_set — every emitted comparative candidate has op.operand.direction ∈ {more, fewer, times, fraction}. Pinned by test_direction_literals_closed_set.
  • compare_additive_wrong_zero / compare_multiplicative_wrong_zero — every additive / multiplicative coverage case passes through roundtrip_admissible. Pinned by the per-direction parametric tests.
  • nested_emits_both_flat — the nested-composition extractor emits exactly the two flat candidates (additive + multiplicative); whether either composes is the binding-graph layer's call. Pinned by test_nested_emits_both_flat_candidates.
  • closed_set_refusal_holds — none of the five documented refusal paraphrases admit any comparative candidate. Pinned by test_refusal_cases_emit_no_admitted_comparative.
  • gsm8k_safety_rail_intactadmitted_wrong == 0 on both the legacy and candidate-graph GSM8K probe reports.
  • gsm8k_comparative_refusal_strictly_decreased — chosen G.2 gate (see above).
  • report_deterministicreport.json is byte-equal across two back-to-back runs.

Acceptance evidence

  • evals/math_capability_axes/G2_comparatives/v1/runner.py exits 0 with wrong == 0 on all 24 curated cases (8 additive / 8 multiplicative / 3 nested / 5 refusal).
  • tests/test_adr_0131_G2_comparatives.py (21 tests): per-direction at-least-one-passing, nested-both-emitted, refusal-set zero-admit, closed-set discipline, runner byte-equality, GSM8K-probe gate.
  • evals/gsm8k_math/train_sample/v1/report.json shows comparative-clause refusal count drop from 2 → 1.
  • evals/gsm8k_math/train_sample/v1/train_sample_coverage_report.json refreshed and byte-identical (legacy parser untouched).
  • B3 lane (tests/test_adr_0131_3_bounded_grammar_lane.py) + candidate-graph runner tests + GSM8K probe tests all pass.

Consequences

  • The candidate-graph topology can finally see comparative statements as first-class operations. The downstream composition layer (ADR-0134 unit-aware admissibility + a future "graph composes nested comparison" ADR) becomes the next honest unblock.
  • gsm8k-train-sample-v1-0009 moves from "refuse at comparative clause" to "refuse at question layer" — a refusal-class shift, not an admission. That is the correct, honest classification: the case is still refused; the reason it's refused has changed.
  • Future axes (G.3 rate-verbs, G.4 multi-clause distributive subjects) inherit the same axis-lane harness shape under evals/math_capability_axes/.
  • Capability-first discipline is preserved: no GSM8K-specific templates were added; the new shapes are exercised by 24 hand-authored sentences plus whatever GSM8K has incidentally.

Out of scope

  • Solver changes. If a comparative parses but doesn't solve, that's a binding-graph composition gap, deferred to a follow-up ADR (no solver stubs).
  • Implicit-comparison shapes ("Alice has the same number of apples as Bob") — equality is not in {more, fewer, times, fraction}; admitting it requires extending the closed direction set, which is its own ADR.
  • Probe runner contract. ADR-0131.G pinned run_lane as the legacy probe's contract; G.2 does not change that. The candidate-graph probe (report.json via runner.py) is the measurement surface that actually moves under G.2.
  • Frontier head-to-head on comparative accuracy. Deferred until the composition layer admits at least one nested form end-to-end.