core/docs/decisions/ADR-0131.G.5-aggregate-answer-composition.md
Shay 7f67cea400
feat(ADR-0131.G.5): aggregate answer composition — combined/together cues wired, axis lane 20/20, wrong==0 (#197)
Closes the vocabulary gap: `combined` and `together` added to `_Q_TOTAL_RE`
and `_Q_ENTITY_RE` tail alternations. Both map to `entity=None` semantics;
the solver's existing sum path is unchanged.

Ships:
- Parser one-line regex extension (`generate/math_candidate_parser.py`)
- 20-case curated axis lane (`G5_aggregate/v1/`) — 5 shapes × 4 cues
- Runner + byte-equal report (20/20 pass, wrong=0)
- 25 tests covering cue vocab, 2/3-entity sums, degenerate aggregate,
  refusals, byte-equality, B3 regression guard, GSM8K safety rail
- ADR-0131.G.5

No admission movement on GSM8K probe (statement-parse bottleneck unchanged).
2026-05-23 19:42:55 -07:00

89 lines
3.4 KiB
Markdown

# ADR-0131.G.5 — Aggregate Answer Composition
**Status:** Accepted
**Parent:** [ADR-0131.G — GSM8K Coverage Probe](ADR-0131.G-gsm8k-coverage-probe.md)
**Date:** 2026-05-23
## Context
The aggregate-answer path — questions like "How many apples do they have
altogether?" — was functionally complete before this ADR. The parser
(`_Q_TOTAL_RE` in `generate/math_candidate_parser.py`) already emitted
`Unknown(entity=None, unit=<unit>)` for aggregate cues, and the solver
(`generate/math_solver.py`) already summed all terminal state entries
matching the questioned unit when `entity is None`.
What was missing:
1. **Vocabulary gap (now closed):** `"combined"` and `"together"` were
absent from `_Q_TOTAL_RE`'s tail alternation, causing questions using
those cues to be refused even when the solver would have produced the
correct sum.
2. **No pinned lane:** no curated axis cases proved the 2-entity,
3-entity, and degenerate aggregate paths end-to-end through
`parse_and_solve`.
## Decision
### Closed aggregate-cue vocabulary
Exactly four cues are admitted:
| Cue | Example tail |
|-----|-------------|
| `in total` | "How many apples do they have in total?" |
| `altogether` | "How many apples do they have altogether?" |
| `combined` | "How many apples do they have combined?" |
| `together` | "How many apples do they have together?" |
All four map to `entity=None` semantics — the solver sums all state
entries whose unit matches the questioned unit, across all entities.
### Solver path (pre-existing)
The `entity is None` branch in `_resolve_unknown` was not changed. It
sums `v for (_, unit), v in state.items() if unit == unknown.unit`.
This ADR extends the cue vocabulary and pins the lane, not the solver.
### Axis lane
20 curated cases at `evals/math_capability_axes/G5_aggregate/v1/cases.jsonl`:
| Shape | Count | Purpose |
|-------|-------|---------|
| 2-entity sum, no operations | 4 | one case per cue |
| 3-entity sum, no operations | 4 | one case per cue |
| 2-entity sum with add/subtract op | 4 | mixed cues |
| Single-entity degenerate | 4 | regression guard |
| Refusal: outside closed cue | 4 | wrong==0 probe |
Refusal cases use question forms outside the closed `_Q_TOTAL_RE`
pattern (e.g., "How many apples does everyone have?", "What is the
total number of coins?") to verify the parser correctly refuses
paraphrases not in the closed cue set.
### Gate
`wrong == 0` on every axis case. GSM8K `admitted_wrong == 0` preserved
(no admission movement expected — all 50 sample cases still refuse at
statement parsing; question-layer work cannot lift that).
## Deferred
- **Implicit aggregation without a cue word:** "How many apples do Sam
and Tom have?" requires coreference resolution (named-entity →
pronoun-equivalent grouping). Out of scope for the closed-cue model.
- **Rate-based aggregation:** "How many dollars did they earn in total?"
where the unit derives from a rate operation. Requires rate-verb
support in the statement parser.
- **GSM8K admission lift:** all 50 sample cases fail at statement
parsing (rate verbs, compound sentences, implicit entities).
Question-layer cue extensions cannot move that number.
## Evidence
- Axis runner: `evals/math_capability_axes/G5_aggregate/v1/runner.py`
- Report: `evals/math_capability_axes/G5_aggregate/v1/report.json`
- Tests: `tests/test_adr_0131_G5_aggregate.py`
- B3 lane unchanged.
- GSM8K `admitted_wrong == 0` preserved.