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.
282 lines
13 KiB
Markdown
282 lines
13 KiB
Markdown
# 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` | `<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 <unit>" 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.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 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.
|