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.
13 KiB
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. quarter → factor=0.25,
third → factor=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:
- add / subtract / transfer (existing — ADR-0126)
_compare_nested_candidates(most specific — requires both(more|fewer|less) ... thanandN times <REF>tail)_compare_multiplicative_candidates(anchor-as-value formstwice/thrice/half, thenN times as many)_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.jsonl —
24 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 preserveswrong == 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 inCOMPARE_MULTIPLICATIVE_ANCHORS(for thedouble the Xsentence 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 hasop.operand.direction ∈ {more, fewer, times, fraction}. Pinned bytest_direction_literals_closed_set.compare_additive_wrong_zero/compare_multiplicative_wrong_zero— every additive / multiplicative coverage case passes throughroundtrip_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 bytest_nested_emits_both_flat_candidates.closed_set_refusal_holds— none of the five documented refusal paraphrases admit any comparative candidate. Pinned bytest_refusal_cases_emit_no_admitted_comparative.gsm8k_safety_rail_intact—admitted_wrong == 0on both the legacy and candidate-graph GSM8K probe reports.gsm8k_comparative_refusal_strictly_decreased— chosen G.2 gate (see above).report_deterministic—report.jsonis byte-equal across two back-to-back runs.
Acceptance evidence
evals/math_capability_axes/G2_comparatives/v1/runner.pyexits 0 withwrong == 0on 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.jsonshows comparative-clause refusal count drop from 2 → 1.evals/gsm8k_math/train_sample/v1/train_sample_coverage_report.jsonrefreshed 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-0009moves 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_laneas the legacy probe's contract; G.2 does not change that. The candidate-graph probe (report.jsonviarunner.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.