Implements the 4-phase documentation reorganization master plan. - Consolidation: Merged brief/, handoff/, planning/, and decisions/ into briefs/, handoffs/, plans/, and adr/ respectively (101 ADRs relocated) - Root Cleanup: Relocated HANDOFF-gpt55-*.md and key top-level docs (runtime_contracts.md, etc.) to canonical folders. Added superseded alerts. - Indices & Navigation: Created docs/README.md navigation document, docs/sessions/README.md index, docs/adr/README.md index - Note: Also includes prior commit adding ADR-0200+ corpus hygiene governance (ADR-0225, dependency map, backfilled cross-references)
12 KiB
ADR-0131.G.4 — Capability axis: multi-clause composition (conjoined subjects, conjoined objects, embedded quantifiers)
Status: Proposed Date: 2026-05-23 Author: CORE agents + reviewers Parent: ADR-0131.G Depends on: ADR-0126 (candidate graph), ADR-0127 (substance qualifier policy), ADR-0131.3, ADR-0132..ADR-0135
Context
GSM8K paragraphs frequently introduce starting state via within-sentence composition the per-statement candidate parser refuses today:
| Baseline refusal sentence | Capability missing |
|---|---|
Aaron and his brother Carson each saved up $40 ... |
conjoined subject + distributive each |
Francine has five full boxes of crayons and 5 loose crayons |
conjoined object NPs sharing a verb |
Ella has 4 bags with 20 apples in each bag and six bags with 25 apples in each bag |
embedded quantifier + conjunction |
This is the highest-risk axis of ADR-0131.G's four near-term capability extensions. Multi-clause emission means the round-trip filter does more work; multi-candidate ambiguity makes confabulation risk higher. Refusal-first stays paramount: admission gains must be small and load-bearing.
Decision
Land four within-sentence multi-clause extractors in
generate/math_candidate_parser.py. All emit
CandidateInitial records (initial state, not operations — the
shapes here introduce starting holdings, they do not mutate state):
| Extractor | Shape (closed set) | Emission |
|---|---|---|
_conj_subject_each_candidates |
<A> and [his/her/their <kin>] <B> each <verb> <N> <unit> |
two CandidateInitial (one per actor), same (N, unit) |
_conj_object_candidates |
<E> has <N1> <unit1> and <N2> <unit2> |
two CandidateInitial for the same entity; same-unit conjuncts refuse |
_embedded_quantifier_candidates |
<E> has <N> <container> with <M> <unit> in each [<container>] |
one derived CandidateInitial with value = N*M, unit = <unit> |
_embedded_quantifier_candidates (conj branch) |
<E> has <N1> <C> with <M1> <U> in each ... and <N2> <C> with <M2> <U> in each ... |
one SUM CandidateInitial with value = N1*M1 + N2*M2, unit = <U>; mixed-unit conjuncts refuse |
Wired into the existing extract_initial_candidates public entry
point — the binding graph (math_candidate_graph._filtered_statement_choices)
already consumes through that path; no graph-side edit required (the
read-only audit concluded composed candidates are reachable through
existing edges).
Closed-set discipline
- Distributive
eachonly. Surface markerstogether,in total,altogetherimmediately abort emission (explicit contradiction with distributive reading). Pinned bytest_refuses_each_with_together/test_collective_without_each_refuses. - Two-way conjunction only. Three-way
A and B and C each ...is out of closed-set shape and refuses by non-match. - Same-unit conjoined object refuses. Two same-unit conjuncts
on the same entity (
Sam has 5 dimes and 3 dimes) would silently collide under the solver'sstate[(entity, unit)]overwrite semantics (math_solver.py:206); refusing keepswrong == 0. - Ambiguous
eachscope refuses.Ella has 4 bags with 20 apples in each box—box≠bags⇒ refuse. - Mixed-unit conjoined embedded refuses. Apples + pears cannot be summed.
- No cross-sentence state. Multi-sentence inputs are processed per-sentence; pronouns / coreference across sentences stay refused (out of within-sentence axis scope).
CandidateInitial anchor widening
CandidateInitial.__post_init__ whitelists a narrow set of
initial-state-introducing verbs needed for the conjoined-subject-each
shape (saved, earned, got, received, bought, made,
paid, plus their inflected variants). The widening is keyed on
lowercase tokens; the _token_in check in
math_candidate_graph._initial_admissible confirms the anchor word
appears in source. Verb-class widening for the general parser
remains G.1's scope; G.4 widens only what conjoined-subject-each
needs.
Derived-value provenance
Embedded-quantifier and conjoined-embedded emissions carry a
derived value (N*M and N1*M1 + N2*M2 respectively) that does
not appear as a single source token. The round-trip filter's
"value grounds in source" check (_value_grounds) is satisfied by
anchoring matched_value_token on the per-container M (or
first per-container M1 for the sum). This is a deliberate,
documented widening of the source-grounding spirit: the components
of the derived value all appear in the source, and the parser
commits to the canonical arithmetic composition. Refusing on
component-mismatch (mixed units, wrong container scope) and refusing
on indefinite quantifiers in any value slot together keep the
derivation honest. The alternative — emitting two flat candidates
for conjoined-embedded — was rejected: under the solver's
overwrite-on-collision semantics it would silently drop one
conjunct's contribution, breaching wrong == 0.
No graph-side edits
math_candidate_graph.py is unchanged. Multi-candidate emission
flows through the existing per-sentence choice space + Cartesian
product; conjoined-subject-each and conjoined-object emissions land
in _filtered_statement_choices like any other initial candidate.
The solver's state[(entity, unit)] model naturally accommodates
distinct entities (each-shape) and distinct units (object-shape);
collision-prone shapes refuse at the parser. This decision is the
"read-only audit only edit if composed candidate is unreachable"
posture from the brief.
Curated coverage cases (G.4 axis lane)
evals/math_capability_axes/G4_multi_clause/v1/cases.jsonl —
32 cases:
| Category | Cases | Notes |
|---|---|---|
conj_subject_each |
6 | incl. kin-appositive, word-form value |
conj_object |
6 | distinct-unit conjuncts only |
embedded_quantifier |
6 | with + without explicit container2 |
conj_embedded |
6 | same-unit only |
refusal |
8 | together / altogether / 3-way / cross-entity / scope-mismatch / mixed-unit / cross-sentence / same-unit collision |
Runner emits a deterministic report.json; wrong == 0 is the gate.
Deferred (out of scope for G.4)
- Cross-sentence coreference (
Aaron has 5. He gives 2 to Bob.) — needs per-discourse state; pinned as refusal probe. - Ellipsis (
Aaron has 5 apples, Carson 3) — needs verb reconstruction; pinned out-of-scope. - Three-way+ conjunctions (
A and B and C) — combinatorial explosion + ambiguity; deferred to a future axis. - Collective readings (
A and B saved $40 together) — explicitly refused; collective semantics needs a different binding-graph node type. - Currency / unit prefix (
$40) — refused at the value slot (the$is not a_VALUEcharacter). Deferred to G.3 numeric-literals axis, which is the natural place for currency / percentage / decimal literal handling. Documented impact on the GSM8K probe gate (case 0026 stays refused). - Same-unit conjoined object summation — would require either parser-side sum (analogous to conj-embedded) or solver-side state-merge; deferred until a sum-shaped CandidateInitial proves necessary outside this axis.
- Solver / binding-graph changes. If a multi-clause case parses but does not solve, that's a downstream gap and gets its own ADR.
GSM8K-probe gate (chosen)
G.4 gates on:
Multi-clause statement-clause refusals in the candidate-graph probe (
evals/gsm8k_math/train_sample/v1/report.json) strictly decrease.
Counter (in test_gsm8k_candidate_graph_multi_clause_refusals_decreased)
matches refused cases citing a statement-clause refusal whose
embedded sentence text contains a multi-clause anchor pattern
(each <init-verb>, with N <unit> in each, or has N <unit> and N <unit>).
| Probe report | Baseline (origin/main 481e0c3) |
After G.4 | Δ |
|---|---|---|---|
Multi-clause statement-clause refusals (report.json) |
2 | 1 | −1 |
wrong (report.json) |
0 | 0 | 0 |
admission_rate (report.json) |
0/50 | 0/50 | 0 |
Legacy train_sample_coverage_report.json |
byte-identical | byte-identical | 0 |
Baseline-2 cases: gsm8k-train-sample-v1-0026 (Aaron and his brother Carson each saved up $40 ... — refused on $40 value
slot; deferred to G.3) and -0042 (Ella has 4 bags with 20 apples in each bag and six bags with 25 apples in each bag. — now parses,
refusal moves to question layer). admission_rate does not rise
because downstream layers (question-form admission for derived
initial states) are out of G.4 scope.
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 G.4 does not touch. Refreshed and pinned
via test_gsm8k_legacy_probe_safety_rail_intact.
Invariants
g4_wrong_count_is_zero— every G.4 axis case passes or refuses; no case admits a wrong shape. Pinned bytest_runner_wrong_count_is_zero.g4_closed_set_refusals_hold— all 8 refusal probes admit zero multi-clause candidates. Pinned bytest_refusal_cases_emit_no_admitted_multi_clause(parametric).g4_distributive_each_only—each ... togetherandeach ... altogetherrefuse. Pinned bytest_refuses_each_with_together.g4_cross_sentence_refuses— multi-clause extractors do not fire across sentence boundaries. Pinned bytest_cross_sentence_pronoun_refuses_multi_clause.g4_report_deterministic—report.jsonis byte-equal across back-to-back runs.gsm8k_safety_rail_intact—admitted_wrong == 0on both GSM8K probe reports.gsm8k_multi_clause_refusal_strictly_decreased— chosen G.4 gate.
Acceptance evidence
evals/math_capability_axes/G4_multi_clause/v1/runner.pyexits 0 withwrong == 0on all 32 curated cases.tests/test_adr_0131_G4_multi_clause.py(26 tests): per-shape emission, refusal-set, distributive-only policy, cross-sentence refusal, runner byte-equality, GSM8K-probe gate.- Candidate-graph probe
report.json: multi-clause statement refusal count 2 → 1 (case 0042 moves from statement to question refusal). - Legacy probe
train_sample_coverage_report.jsonrefreshed and byte-identical. - B3 lane + ADR-0126 candidate-graph tests + ADR-0131.G probe tests all pass (95/95 across the regression sweep).
Consequences
- The candidate-graph topology can now see four multi-clause initial-state shapes the per-statement parser previously refused. Downstream question-form admission for derived initial states (case 0042) becomes a natural next unblock.
- The same Cartesian-product / "branches that disagree → refuse" decision rule handles the new multi-candidate emissions; no graph-side edits, no admissibility weakening.
- Highest-risk axis lands without breaching
wrong == 0: multi-candidate emission stays tightly scoped, refuses on every documented adversarial probe, and the derived-value emissions refuse on every shape-mismatch (mixed unit, scope-mismatch, collision-prone same-unit). - Future axes inherit the same axis-lane harness layout under
evals/math_capability_axes/.
Out of scope
- Solver changes. If a multi-clause case parses but does not solve, the gap is downstream; file a follow-up ADR (no solver stubs, no admissibility relaxation).
- Currency / numeric-literal handling. Case 0026 (
$40) stays refused; the G.3 numeric-literals axis is the natural place. - Three-way / ellipsis / cross-sentence shapes. Deferred per the closed-set discipline.
- Probe runner contract. ADR-0131.G pinned
run_laneas the legacy probe's contract; G.4 does not change that. The candidate-graph probe (report.json) is the measurement surface that moves.