# ADR-0131.G.2 — Capability axis: comparative operations (additive + multiplicative) **Status:** Proposed **Date:** 2026-05-23 **Author:** CORE agents + reviewers **Parent:** [ADR-0131.G](./ADR-0131.G-gsm8k-coverage-probe.md) **Depends on:** [ADR-0123](./ADR-0123-comparison-substrate.md) (Comparison op shape), [ADR-0126](./ADR-0126-candidate-graph-parser.md) (candidate-graph topology), [ADR-0131.3](./ADR-0131.3-bounded-grammar.md), [ADR-0132](./ADR-0132-binding-graph-data-model.md)..[ADR-0134](./ADR-0134-unit-aware-admissibility.md) --- ## 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` | ` has N more than ` / `... N fewer ...` / `... N less ...` | | `_compare_multiplicative_candidates` | ` has twice as many as ` / `... thrice ...` / `... half ...` / `... N times as many ...` / `... a quarter as many ...` / `... a third as many ...` | | `_compare_nested_candidates` | ` has N more than M times ` — 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`: 1. add / subtract / transfer (existing — ADR-0126) 2. **`_compare_nested_candidates`** (most specific — requires both `(more|fewer|less) ... than` *and* `N times ` 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 than `) 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 ` greedily, so a sentence the nested regex matches cannot also be claimed by the bare additive pattern (the bare additive regex requires the `than ` tail with `` matching `_COMPARE_REF`, but the nested regex's `` is preceded by `N times`, which the bare additive regex's `` 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 , ..."` and `"in comparison with , ..."` — 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_intact`** — `admitted_wrong == 0` on both the legacy and candidate-graph GSM8K probe reports. - **`gsm8k_comparative_refusal_strictly_decreased`** — chosen G.2 gate (see above). - **`report_deterministic`** — `report.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.