Harden no-reference n-times comparative guard
This commit is contained in:
parent
3e29559532
commit
e1bcdf6286
5 changed files with 420 additions and 0 deletions
47
HANDOFF-gpt55-2026-06-03.md
Normal file
47
HANDOFF-gpt55-2026-06-03.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# HANDOFF gpt55 2026-06-03
|
||||||
|
|
||||||
|
Branch: `codex/ntimes-completeness-guard`
|
||||||
|
|
||||||
|
Final commit SHA: reported in the thread final after commit creation.
|
||||||
|
|
||||||
|
## File Status
|
||||||
|
|
||||||
|
| file | path | status | purpose | ready-to-merge |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| recognizer anchor injector | `generate/recognizer_anchor_inject.py` | FINAL | Refusal-only guard preventing no-reference `<N> times ...` comparative multipliers from being injected as `CandidateInitial(value=N, unit="times")`. | Y |
|
||||||
|
| completeness guard tests | `tests/test_candidate_graph_completeness_guard.py` | FINAL | Pins the §9 hard negative matrix, with solve/refusal controls. | Y |
|
||||||
|
| question-layer gap survey | `docs/analysis/question-layer-gap-survey.md` | FINAL | Canonical audited 44-refusal partition and backlog interpretation. | Y |
|
||||||
|
| solver operation coverage | `docs/analysis/solver-operation-coverage.md` | FINAL | Canonical read-only audit of existing solver op coverage and representation gaps. | Y |
|
||||||
|
| composition capability scope | `docs/analysis/composition-capability-scope.md` | SUPERSEDED | Superseded by the execution lane canonical copy; intentionally not committed here. | N |
|
||||||
|
| handoff | `HANDOFF-gpt55-2026-06-03.md` | FINAL | Merge/readiness manifest for this thread. | Y |
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Completed on branch `codex/ntimes-completeness-guard`:
|
||||||
|
|
||||||
|
- `uv run python -m pytest tests/test_candidate_graph_completeness_guard.py -q`
|
||||||
|
- `21 passed`
|
||||||
|
- `uv run python -m pytest tests/test_adr_0131_G2_comparatives.py tests/test_adr_0131_G2a_comparative_verb_widening.py -q`
|
||||||
|
- `30 passed`
|
||||||
|
- `uv run python -m pytest tests/test_aggregate_total_question_forms.py tests/test_discrete_count_open_noun_class.py tests/test_adr_0163_d2_discrete_count_injection.py -q`
|
||||||
|
- `59 passed`
|
||||||
|
- Train-sample probe through `generate.math_candidate_graph.parse_and_solve`
|
||||||
|
- `6 correct / 44 refused / 0 wrong`
|
||||||
|
- `refused_nonzero_count = 0`
|
||||||
|
- admitted IDs: `0003`, `0014`, `0018`, `0021`, `0024`, `0042`
|
||||||
|
|
||||||
|
## New Since Last Message
|
||||||
|
|
||||||
|
- Broadened the §9 guard from phrase-specific `times as many|more` matching to the structural captured shape: cardinal immediately followed by `times` inside the discrete-count injector.
|
||||||
|
- Added no-reference hard negatives for `3 times the number of apples` and `3 times the apples`.
|
||||||
|
- Added explicit safe-refusal controls for no-reference `twice`, no-reference `double`, and case 605.
|
||||||
|
- Removed the local superseded `composition-capability-scope.md` from this branch's merge set.
|
||||||
|
- Added this handoff manifest.
|
||||||
|
|
||||||
|
## Not Done / WIP
|
||||||
|
|
||||||
|
- No 5b emission/representation slice was implemented.
|
||||||
|
- No <=20-case validation sub-corpus was authored.
|
||||||
|
- No solver operation kinds or binding-graph node types were added.
|
||||||
|
- No serving/eval/claims-ledger files were changed.
|
||||||
|
- The superseded composition scope is not part of this commit; use the execution lane canonical copy instead.
|
||||||
133
docs/analysis/question-layer-gap-survey.md
Normal file
133
docs/analysis/question-layer-gap-survey.md
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<!-- FILE: docs/analysis/question-layer-gap-survey.md -->
|
||||||
|
|
||||||
|
# Question-Layer Gap Survey
|
||||||
|
|
||||||
|
Status: Proposed analysis draft. No serving behavior is changed.
|
||||||
|
|
||||||
|
Source of truth for the metric is `docs/claims_ledger.md` and
|
||||||
|
`evals/gsm8k_math/train_sample/v1/report.json`: the current real
|
||||||
|
`train_sample` result is 6 correct / 44 refused / 0 wrong. This survey assigns
|
||||||
|
each of the 44 refused case ids to exactly one current failure group. It does
|
||||||
|
not claim that any group would pass if widened; it names the layer that refuses
|
||||||
|
today and estimates whether the missing capability is single-step or
|
||||||
|
composition-bound.
|
||||||
|
|
||||||
|
The requested Task A file, `docs/analysis/comprehension-primitive-inventory.md`,
|
||||||
|
was not present in this worktree. I used the embedded older handoff only as
|
||||||
|
orientation and grounded the grouping below in the report plus the live parser /
|
||||||
|
recognizer code.
|
||||||
|
|
||||||
|
## Code Map
|
||||||
|
|
||||||
|
Current refusal topology:
|
||||||
|
|
||||||
|
- `generate/math_candidate_graph.py` first extracts statement choices, then
|
||||||
|
consults the ratified recognizer registry when a numeric statement has no
|
||||||
|
parser candidate. If recognition succeeds but injection yields no typed
|
||||||
|
solver primitive, it now refuses explicitly instead of dropping the statement.
|
||||||
|
- `generate/recognizer_anchor_inject.py` is still a category dispatch surface.
|
||||||
|
Only `discrete_count_statement` and a narrow `multiplicative_aggregation`
|
||||||
|
entry can emit today; other categories return empty and therefore become
|
||||||
|
explicit refusals.
|
||||||
|
- `generate/math_candidate_parser.py` admits a closed question grammar:
|
||||||
|
total-across `How many ... do they have ...`, existential aggregate
|
||||||
|
`How many ... are there ...`, entity possession, activity `did`, and three
|
||||||
|
ADR-0163.D.4 patterns. If these emit no `CandidateUnknown`, the graph refuses
|
||||||
|
before branch enumeration.
|
||||||
|
- ADR-0174 explicitly deprecates the per-category injector dispatch table as
|
||||||
|
the long-term runtime admission path. Injector widening should therefore be
|
||||||
|
treated as stopgap or hypothesis-emitter work, not the primary strategic
|
||||||
|
direction.
|
||||||
|
|
||||||
|
## Assignment Table
|
||||||
|
|
||||||
|
Tractability scoring is case-level and arity-aware:
|
||||||
|
|
||||||
|
- High: one closed primitive or one local parser frame is plausibly missing.
|
||||||
|
- Medium: one local frame/schema is clear, but downstream composition is still
|
||||||
|
likely needed.
|
||||||
|
- Low: the current refusal is an early stop in a 2-4 capability derivation.
|
||||||
|
|
||||||
|
| Group | Count | Case ids | Current failing layer | Tractability | Representative report reason excerpts | Interpretation |
|
||||||
|
|---|---:|---|---|---|---|---|
|
||||||
|
| DCS high-arity composition wall | 18 | 0002, 0015, 0016, 0020, 0029, 0031, 0032, 0033, 0034, 0036, 0037, 0038, 0039, 0040, 0041, 0044, 0047, 0049 | Matcher -> injector -> composition | Low | `candidate_graph: recognizer matched but produced no injection for statement: 'She splits it up into 25-foot sections.' (category=discrete_count_statement)`; `candidate_graph: recognizer matched but produced no injection for statement: 'Malcolm is trying to find the fastest walk to school and is currently comparing two routes.' (category=discrete_count_statement)` | The recognizer often fires on a count-like token, but the actual derivation needs division, rest-state, route comparison, percent/rate, chained comparisons, target residuals, or per-entity attributes. Widening the discrete-count injector alone is metric-inert and risks incomplete readings. |
|
||||||
|
| Missing inverse/residual/comparative question frames | 5 | 0007, 0008, 0009, 0025, 0035 | Question parser / admissibility | Low | `candidate_graph: no admissible candidate for question: 'How many more boxes do they need if Francine has a total of 85 crayons?'`; `candidate_graph: no admissible candidate for question: 'How many more apples would Martha need to give away to be left with only 4 of them?'` | These are not just new surface phrasings for `Unknown(entity, unit)`. They ask for a missing operand, inverse relation, target residual, or conditional total. The question layer must bind the requested slot to a derivation, not merely extract a unit. |
|
||||||
|
| Rate, currency-rate, and tariff statements | 4 | 0001, 0011, 0017, 0022 | Recognizer -> injector/schema | Medium | `candidate_graph: recognizer matched but produced no injection for statement: 'Tina makes $18.00 an hour.' (category=rate_with_currency)`; `candidate_graph: recognizer matched but produced no injection for statement: 'He’s charging $50.00 per day or $500.00 for 14 days.' (category=temporal_aggregation)` | The local schema gap is visible: `rate_with_currency` and `temporal_aggregation` need typed rate/tariff hypotheses. Case-level admission still needs overtime, profit, historical+today aggregation, or piecewise tariff composition. |
|
||||||
|
| Non-quantitative relation category used as an early stop | 4 | 0012, 0023, 0027, 0046 | Matcher -> injector | Low | `candidate_graph: recognizer matched but produced no injection for statement: 'He put all of them in his aquarium but his fish ate half of them.' (category=descriptive_setup_no_quantity)`; `candidate_graph: recognizer matched but produced no injection for statement: 'Half of the students are girls, the other half are boys.' (category=descriptive_setup_no_quantity)` | The category name is accurate for the injector: it cannot emit a concrete primitive from the matched surface. The cases need fraction-of-prior, combined-total binding, partition, and percentage-of-subgroup reasoning. |
|
||||||
|
| Multiplicative aggregate beyond the narrow emitted shapes | 3 | 0006, 0013, 0045 | Recognizer -> injector / composition registry | Medium | `candidate_graph: recognizer matched but produced no injection for statement: 'Mandy started reading books with only 8 pages when she was 6 years old.' (category=multiplicative_aggregation)`; `candidate_graph: recognizer matched but produced no injection for statement: 'Each survey has 10 questions.' (category=multiplicative_aggregation)` | There is already a narrow product injector, but these rows need time/age chains, month segmentation, doubled rates, or survey-count composition. Single product emission may help local state, but full cases still depend on multi-step composition. |
|
||||||
|
| Financial currency amount / percent mutation statements | 3 | 0019, 0028, 0043 | Recognizer -> injector / mutation schema | Low | `candidate_graph: recognizer matched but produced no injection for statement: 'After the first appointment, John paid $100 for pet insurance that covers 80% of the subsequent visits.' (category=currency_amount)`; `candidate_graph: recognizer matched but produced no injection for statement: 'Her mother gave her an additional $4, and her father twice as much as her mother.' (category=currency_amount)` | The surface contains currency, but the required reading is coverage after first event, percent daily operating cost, revenue target, or comparative gift amount. A `CandidateInitial` currency emission would be an incomplete graph. |
|
||||||
|
| Fractional relational statements with no parser candidate | 3 | 0004, 0005, 0010 | Statement parser / admissibility | Low | `candidate_graph: no admissible candidate for statement: 'Half of the kids are going to soccer camp, and 1/4 of the kids going to soccer camp are going to soccer camp in the morning.'`; `candidate_graph: no admissible candidate for statement: 'Marion has 1/4 more than what Yun currently has, plus 7.'` | The parser has fraction literals and comparative operations, but these surfaces are relational fractions over prior or unknown quantities. They require held equations or derivation nodes, not a flat possession/operation candidate. |
|
||||||
|
| Duration and recurrence statement frames | 3 | 0030, 0048, 0050 | Statement parser / temporal composition | Medium | `candidate_graph: no admissible candidate for statement: 'It is a 2-hour drive each way.'`; `candidate_graph: no admissible candidate for statement: 'Mark does a gig every other day for 2 weeks.'` | These expose bounded duration-multiplier or temporal-frequency frames. Each frame is local and deterministic, but the cases still need composition across trip stages, weekly deltas to target, or per-event song duration totals. |
|
||||||
|
| Relational conjoined-subject each initial | 1 | 0026 | Statement parser / entity binding | Medium | `candidate_graph: no admissible candidate for statement: 'Aaron and his brother Carson each saved up $40 to go to dinner.'` | The parser has an `each` extractor for two named subjects, but this sentence uses a possessive relational subject (`his brother Carson`) and a purpose tail. The local parse is probably narrower than the concept; the full case remains multi-step because bill fraction and shared scoop count follow. |
|
||||||
|
|
||||||
|
Total assigned: 18 + 5 + 4 + 4 + 3 + 3 + 3 + 3 + 1 = 44.
|
||||||
|
|
||||||
|
## Backlog Interpretation
|
||||||
|
|
||||||
|
The audited partition is the stable artifact. A single `count x tractability`
|
||||||
|
sort is misleading here because count ranges from 1 to 18 while tractability is
|
||||||
|
coarse; it would place the known composition wall at the top as if it were an
|
||||||
|
incremental injector work order. In this survey, count is impact evidence, not
|
||||||
|
the sort key for near-term changes.
|
||||||
|
|
||||||
|
### Composition-Bound Work
|
||||||
|
|
||||||
|
These groups should feed the ADR-0174 held-hypothesis / derivation-composer
|
||||||
|
scope, not category-specific injector widening. They need multi-clause state,
|
||||||
|
referent binding, ratio/fraction relations, target-slot questions, or
|
||||||
|
event-scope composition before any answer can be safe.
|
||||||
|
|
||||||
|
| Group | Count | How to use the count |
|
||||||
|
|---|---:|---|
|
||||||
|
| DCS high-arity composition wall | 18 | Main evidence that discrete-count recognition is surfacing a composition wall, not an injector backlog. |
|
||||||
|
| Missing inverse/residual/comparative question frames | 5 | Question-layer evidence that unknowns must bind to derivation slots, not just noun units. |
|
||||||
|
| Rate, currency-rate, and tariff statements | 4 | Rate/tariff hypotheses are useful only if downstream overtime, profit, history, or piecewise composition can refuse partial readings. |
|
||||||
|
| Non-quantitative relation category used as an early stop | 4 | Evidence for relation/partition composition and for avoiding hard-stop loss of later structure. |
|
||||||
|
| Multiplicative aggregate beyond the narrow emitted shapes | 3 | Product-like anchors need day/month, age-chain, or survey-count composition before admission. |
|
||||||
|
| Financial currency amount / percent mutation statements | 3 | Currency is not enough; these need percent coverage, operating-cost, or spend/residual mutation. |
|
||||||
|
| Fractional relational statements with no parser candidate | 3 | Needs relational fraction/equation hypotheses, not flat possession candidates. |
|
||||||
|
| Duration and recurrence statement frames | 3 | Duration-multiplier and recurrence frames are local signals whose answers require temporal composition. |
|
||||||
|
|
||||||
|
### Bounded Near-Term Fixes
|
||||||
|
|
||||||
|
These are smaller, lower-blast-radius probes that may be useful as executable
|
||||||
|
follow-ups or regression tests. They should still preserve refusal-first
|
||||||
|
admission and should not be represented as the main path to the metric.
|
||||||
|
|
||||||
|
| Candidate fix | Case ids | Why bounded |
|
||||||
|
|---|---|---|
|
||||||
|
| Relational conjoined-subject `each` binding | 0026 | One row exposes a local entity-binding gap around `Aaron and his brother Carson each ...`; useful as a narrow parser/binding probe even though the full problem remains multi-step. |
|
||||||
|
| Descriptive/no-quantity early-stop handling | 0012, 0023, 0027, 0046 | The bounded mechanism is to avoid losing later structure when a descriptive relation cannot emit state; the cases behind it still require composition, so this is not a direct answer path. |
|
||||||
|
| Single-slot question-frame probes | subset of 0007, 0008, 0035 | A few question surfaces can become narrow probes for residual or divisor target-slot binding. They must be held as unknown slots and refused unless the derivation is complete. |
|
||||||
|
|
||||||
|
## Layer Notes
|
||||||
|
|
||||||
|
The high-count `discrete_count_statement` bucket should not be read as "add more
|
||||||
|
discrete-count injectors." The report is saying the matcher saw something
|
||||||
|
count-shaped before the engine had a safe composed reading. In the live code,
|
||||||
|
recognized-but-uninjected statements refuse explicitly because dropping them
|
||||||
|
would permit incomplete graphs. ADR-0174 points in the same direction:
|
||||||
|
injectors should become hypothesis emitters inside a held-hypothesis reader,
|
||||||
|
where branch disagreement, constraint propagation, and completeness can reject
|
||||||
|
partial readings.
|
||||||
|
|
||||||
|
The five question refusals are genuinely question-layer refusals in the narrow
|
||||||
|
code sense: `extract_question_candidates` emits no `CandidateUnknown`. But all
|
||||||
|
five ask for an inverse or residual target. A wider regex that only extracts
|
||||||
|
the noun would not identify the unknown slot and would still be unsafe under
|
||||||
|
`wrong = 0`.
|
||||||
|
|
||||||
|
## Open Questions for the Claude Lane
|
||||||
|
|
||||||
|
- Confirm whether `docs/analysis/comprehension-primitive-inventory.md` was meant
|
||||||
|
to be landed in this worktree or only supplied out-of-band; this draft could
|
||||||
|
be amended to cite its exact primitive table once present.
|
||||||
|
- Run an instrumented read of the 44 refused cases to distinguish "injector
|
||||||
|
returned empty because no parsed anchors existed" from "anchors existed but
|
||||||
|
constraint propagation eliminated them."
|
||||||
|
- For the five question refusals, collect the intended `Unknown` slot shape
|
||||||
|
without using gold answers: missing operand, residual-to-target, inverse
|
||||||
|
divisor, or aggregate total.
|
||||||
|
- Decide whether the next executable lane should prototype rate/tariff
|
||||||
|
hypotheses or question-target slots first; both are safer as held hypotheses
|
||||||
|
than as direct category admissions.
|
||||||
148
docs/analysis/solver-operation-coverage.md
Normal file
148
docs/analysis/solver-operation-coverage.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<!-- FILE: docs/analysis/solver-operation-coverage.md -->
|
||||||
|
|
||||||
|
# Solver Operation Coverage Audit
|
||||||
|
|
||||||
|
Status: Proposed analysis draft. No serving behavior is changed. This is a
|
||||||
|
read-only structural audit to de-risk ADR-0174 Phase 5b / Phase-4-style
|
||||||
|
composition work. Verdicts below are code-reading conclusions only; they must be
|
||||||
|
verified in the Claude lane with executable solver/binding-graph cases before
|
||||||
|
any promotion claim.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Read surfaces:
|
||||||
|
|
||||||
|
- `generate/math_problem_graph.py`
|
||||||
|
- `generate/math_solver.py`
|
||||||
|
- `generate/binding_graph/{model,adapter,admissibility,allocation,question_target,units}.py`
|
||||||
|
- ADR-0116, ADR-0117, ADR-0132, ADR-0133, ADR-0134, ADR-0135
|
||||||
|
- Skimmed ADR-0174 Phase 5b and ADR-0203/0204/0205 to confirm current
|
||||||
|
composition/proof-DAG framing.
|
||||||
|
|
||||||
|
Important correction to the relay: the current solver/graph vocabulary is not
|
||||||
|
only `{add, subtract, transfer, multiply, divide}`. `MathProblemGraph` and
|
||||||
|
`math_solver` both include the eight operation kinds:
|
||||||
|
|
||||||
|
```text
|
||||||
|
add, subtract, transfer, multiply, divide,
|
||||||
|
apply_rate, compare_additive, compare_multiplicative
|
||||||
|
```
|
||||||
|
|
||||||
|
ADR-0174 Phase 5b states the same: the solver is already waiting for these
|
||||||
|
operations; the gap is reader -> injector -> `Operation` front-end wiring plus
|
||||||
|
composition.
|
||||||
|
|
||||||
|
## Existing Operation Substrate
|
||||||
|
|
||||||
|
| Surface | Evidence | Consequence |
|
||||||
|
|---|---|---|
|
||||||
|
| Closed operation vocabulary | `generate/math_problem_graph.py` defines `VALID_OPERATION_KINDS` with eight kinds. | New arithmetic verbs are not needed for ordinary multiply/divide/rate/comparison chains. |
|
||||||
|
| Pack-bound solver dispatch | `generate/math_solver.py` maps all eight kinds to `en_arithmetic_v1` lemmas before solving. | Missing pack lemmas fail loudly; no hidden operation fallback. |
|
||||||
|
| Stateful solver semantics | `_apply` mutates `(actor, unit)` terminal state for `add`, `subtract`, `transfer`, `multiply`, `divide`; `_apply_rate` produces numerator-unit state from denominator-unit state; comparisons derive an actor state from a reference actor. | The solver is good at forward state trajectories, but weak at keeping multiple same-unit derived intermediates alive under one actor. |
|
||||||
|
| Unknown shape | `Unknown(entity, unit)` resolves either terminal state for one entity or total-across all entities with that unit. | Target questions that ask for a missing operand, number of iterations, or intermediate state are not represented by `Unknown` alone. |
|
||||||
|
| Binding-graph equation/data model | `BoundEquation(operation_kind=...)`, `BoundUnknown(question_form=...)`, semantic roles include `rate`, `duration`, `difference`, `ratio`. | The graph can name richer forms than the solver's final `Unknown`, but current adapter still comes from existing `MathProblemGraph` operation chains. |
|
||||||
|
| Unit admissibility | `check_admissibility` covers additive, multiplicative, divide, `apply_rate`, and comparison kinds. | Dimensional proofs exist for the current eight operation kinds; new node types would need explicit admissibility rules. |
|
||||||
|
| Question-target binding | `infer_question_form` recognizes `ratio`, `difference`, `rate`, `total`, and `count` from operation kinds touching the unknown. | It can label answer form, but it does not solve inverse targets or select intermediate operation indices yet. |
|
||||||
|
| Acyclicity | ADR-0203 adds `circular_dependency` refusal to the shared binding-graph constructor. | Any new equation/intermediate-node extension must remain a DAG, not a cyclic algebra system hidden inside the graph. |
|
||||||
|
|
||||||
|
## Phase-4 Target Chains
|
||||||
|
|
||||||
|
| Target chain | Verdict | Existing operations that can carry it | What is missing / risk |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Multi-step rate-sum | Expressible via composition for straight-line rate applications and sums. Piecewise tariffs/conditionals need scoped selection before admission. | `apply_rate` computes `X/Y * Y -> X`; `add`/`subtract` can aggregate generated same-unit totals. Binding admissibility has an `apply_rate` rule requiring one rate dep plus one duration/count dep. | The reader must emit the base duration/count quantities, rate hypotheses, and sum operations without clobbering unrelated same-unit state. Piecewise tariffs such as "$50/day or $500/14 days" need a tariff/choice scope or explicit branch-disagreement gate; that is a binding/composition problem, not a missing arithmetic verb. |
|
||||||
|
| Ratio chain | Expressible via composition for forward ratio chains. Inverse ratio equations need a target/equation extension. | `compare_multiplicative` supports "actor = factor * reference"; `multiply`/`divide` support scalar transformations; `infer_question_form` maps touching `compare_multiplicative` to `ratio`. | Forward chains like `A`, `B = 2A`, `C = 1/2(A+B)` can be represented if the reader emits the right reference actors and order. Inverse forms such as "ducks are 10 more than 4x chickens; ducks = 150; find total birds" require solving for an unknown reference, not just applying a forward operation. That needs equation/target solving or a new binding node shape, not a new `ratio` operation kind. |
|
||||||
|
| Accumulate-against-target | Needs new binding-graph target/equation capability; not expressible as a fixed existing operation chain in the current solver. | `add`, `subtract`, `multiply`, `divide`, and `apply_rate` can express the arithmetic once the iteration count or missing operand is known. | The current solver consumes a fully specified graph in source order, then resolves terminal `Unknown(entity, unit)`. It cannot represent "after how many weeks", "how many cups to sell to reach profit", or "how long to make back cost" as an unknown operand/iteration count. This needs a first-class target-slot/equation node or bounded inverse solver with refusal/disagreement, plus proof that no cyclic dependency is introduced. |
|
||||||
|
| Percent/fraction mutation | Mostly expressible via composition for forward mutations; needs intermediate-symbol/scope support for same-unit derived amounts and event subsets. | Percent/fraction values can be scalar `multiply`/`divide` factors, with `add`/`subtract` for mutation and `compare_multiplicative` for relative quantities. Binding admissibility already covers multiply/divide dimensions. | Some cases are safe forward mutations ("eat 75% of a pan", "lose half"). Others require original and derived same-unit quantities to coexist, e.g. principal plus interest, insured vs uninsured portions, operating cost as percent of startup cost. The current solver overwrites `(actor, unit)` for multiply/divide and `apply_rate`, so these need derived intermediate symbols/event scopes or separate binding nodes. A new `percent` operation kind is not structurally necessary; a new scoped intermediate/equation representation may be. |
|
||||||
|
|
||||||
|
## Structural Verdict
|
||||||
|
|
||||||
|
Phase 4/5b is mostly **not** blocked by missing arithmetic operation kinds. The
|
||||||
|
current operation vocabulary already covers the primitive arithmetic field:
|
||||||
|
addition/subtraction/transfer, scalar multiply/divide, rate application, and
|
||||||
|
additive/multiplicative comparison.
|
||||||
|
|
||||||
|
The real scope risk is representation:
|
||||||
|
|
||||||
|
- Can the reader emit a chain of typed operations from scattered clauses while
|
||||||
|
preserving all grounded quantities?
|
||||||
|
- Can the graph retain intermediate derived quantities when they share the same
|
||||||
|
actor/unit as their source?
|
||||||
|
- Can a question bind to a missing operand, iteration count, or intermediate
|
||||||
|
state instead of only terminal `Unknown(entity, unit)`?
|
||||||
|
- Can inverse/equation targets be solved under a disagreement rule and the
|
||||||
|
ADR-0203 acyclicity invariant?
|
||||||
|
|
||||||
|
That makes the likely build a **binding-target / intermediate-symbol /
|
||||||
|
derivation-composer extension**, not a broad new solver primitive pack.
|
||||||
|
|
||||||
|
## Chain-Specific Notes
|
||||||
|
|
||||||
|
### Multi-step rate-sum
|
||||||
|
|
||||||
|
`apply_rate` is first-class: the solver's `_apply_rate` reads the actor's
|
||||||
|
denominator-unit state and writes numerator-unit state. The binding-graph
|
||||||
|
adapter synthesizes a rate symbol with composite unit `<num>_per_<denom>`, and
|
||||||
|
admissibility checks that the denominator cancels. Therefore simple rate-sum is
|
||||||
|
expressible as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
duration/count fact -> apply_rate -> produced total
|
||||||
|
produced totals -> add/subtract -> final total
|
||||||
|
```
|
||||||
|
|
||||||
|
The unsafe part is not the operation. It is branch selection and scope:
|
||||||
|
overtime, tariffs, and "including today" require deciding which rate applies to
|
||||||
|
which event subset.
|
||||||
|
|
||||||
|
### Ratio chain
|
||||||
|
|
||||||
|
`compare_multiplicative` already gives a forward ratio operation. It refuses
|
||||||
|
when the reference actor has no quantity or multiple ambiguous units, which is
|
||||||
|
the right wrong=0 boundary. Binding target can label ratio-form questions.
|
||||||
|
|
||||||
|
The gap is inverse ratio. Current `Operation` is directional: it mutates the
|
||||||
|
actor from a known reference. If the reference is the unknown and the actor is
|
||||||
|
given, the solver has no inverse-equation mode.
|
||||||
|
|
||||||
|
### Accumulate-against-target
|
||||||
|
|
||||||
|
This is the clearest "needs new graph shape" target. A terminal state solver can
|
||||||
|
answer "what is the total after N weeks"; it cannot answer "what N reaches total
|
||||||
|
T" unless N is already present as a quantity and a divide operation has been
|
||||||
|
materialized by the reader. A safe implementation needs a target slot that names
|
||||||
|
the missing operand/iteration count and an admissibility/disagreement rule for
|
||||||
|
the inverse derivation.
|
||||||
|
|
||||||
|
### Percent/fraction mutation
|
||||||
|
|
||||||
|
Percent/fraction does not need a new arithmetic verb. It needs:
|
||||||
|
|
||||||
|
- scalar extraction (`75% -> 0.75`, `1/4 -> 0.25`);
|
||||||
|
- complement derivation when the text says "covers 80%" but the cost asks for
|
||||||
|
the uncovered part;
|
||||||
|
- event/subset scoping ("after the first appointment", "subsequent visits");
|
||||||
|
- intermediate symbols when original principal/cost and derived interest/cost
|
||||||
|
must both remain available.
|
||||||
|
|
||||||
|
The binding graph is the natural home for these intermediates because it already
|
||||||
|
has `BoundEquation`, dependency sets, unit proofs, question forms, and
|
||||||
|
acyclicity checks.
|
||||||
|
|
||||||
|
## Open Questions for the Claude Lane
|
||||||
|
|
||||||
|
- Does `apply_rate` overwriting `(actor, numerator_unit)` create a concrete
|
||||||
|
hazard in rate-sum cases where the actor already holds money? If yes, the
|
||||||
|
build needs derived result symbols before promotion.
|
||||||
|
- Should inverse target solving be introduced as a new `BoundUnknown` form, a
|
||||||
|
new `BoundEquation` operation kind, or a separate proof/derivation rule over
|
||||||
|
existing equations?
|
||||||
|
- Can forward ratio chains be admitted through `compare_multiplicative` without
|
||||||
|
widening the parser's reference-actor ambiguity beyond the existing refusal
|
||||||
|
discipline?
|
||||||
|
- What is the smallest executable probe set that distinguishes "new operation
|
||||||
|
kind required" from "same operation, new intermediate symbol required" for
|
||||||
|
percent/fraction mutations?
|
||||||
|
- When proof-DAG consumers and math binding graphs share
|
||||||
|
`SemanticSymbolicBindingGraph`, should proposition-specific operation kinds
|
||||||
|
remain isolated from math admissibility, or should the admissibility dispatcher
|
||||||
|
split into math/proof entrypoints before more equation kinds are added?
|
||||||
|
|
@ -304,6 +304,17 @@ def _build_initial_from_discrete_count(
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# A surface like "Jerry has 3 times as many apples", "3 times more
|
||||||
|
# apples", or "3 times the apples" is not an initial possession of
|
||||||
|
# "3 times"; it is an incomplete comparative-multiplicative clause.
|
||||||
|
# Letting this through as an initial consumes the scalar token and
|
||||||
|
# defeats the ADR-0191 completeness guard. Refuse here until a real
|
||||||
|
# compare_multiplicative operation can be emitted.
|
||||||
|
if counted_noun.lower() == "times" and _count_token_followed_by_times(
|
||||||
|
sentence, count_token
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
# CandidateInitial requires an anchor verb token recognized in its
|
# CandidateInitial requires an anchor verb token recognized in its
|
||||||
# post-init whitelist (has/have/had/owns/owned/holds/held/contains/
|
# post-init whitelist (has/have/had/owns/owned/holds/held/contains/
|
||||||
# contained — matched by the recognizer's narrowness rule). We pick
|
# contained — matched by the recognizer's narrowness rule). We pick
|
||||||
|
|
@ -433,6 +444,26 @@ def _locate_token(sentence: str, target_lc: str) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _count_token_followed_by_times(sentence: str, count_token: str) -> bool:
|
||||||
|
"""True when the count surface is immediately followed by ``times``.
|
||||||
|
|
||||||
|
The discrete-count recognizer can otherwise misread comparative
|
||||||
|
multiplier surfaces as an initial possession of ``<N> times``. This
|
||||||
|
check intentionally sits at the injector boundary: it only suppresses
|
||||||
|
the malformed initial candidate and does not create any new
|
||||||
|
admitting path.
|
||||||
|
"""
|
||||||
|
target = count_token.lower()
|
||||||
|
tokens = [
|
||||||
|
raw.strip(".,;:!?\"'()[]{}").lower()
|
||||||
|
for raw in sentence.split()
|
||||||
|
]
|
||||||
|
for i, tok in enumerate(tokens[:-1]):
|
||||||
|
if tok == target and tokens[i + 1] == "times":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _resolve_count_value(count_token: str, count_kind: str) -> int | None:
|
def _resolve_count_value(count_token: str, count_kind: str) -> int | None:
|
||||||
"""Map ``count_token`` to a numeric value.
|
"""Map ``count_token`` to a numeric value.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,3 +89,64 @@ def test_guard_is_refusal_only_not_answer_changing() -> None:
|
||||||
# Same value, same unit-bearing graph — guard does not mutate solving.
|
# Same value, same unit-bearing graph — guard does not mutate solving.
|
||||||
assert res.answer == 438.0
|
assert res.answer == 438.0
|
||||||
assert res.selected_graph is not None
|
assert res.selected_graph is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("factor", "comparative"),
|
||||||
|
[
|
||||||
|
("2", "as many"),
|
||||||
|
("3", "as many"),
|
||||||
|
("5", "as many"),
|
||||||
|
("two", "as many"),
|
||||||
|
("three", "as many"),
|
||||||
|
("five", "as many"),
|
||||||
|
("3", "more"),
|
||||||
|
("three", "more"),
|
||||||
|
("3", "the number of"),
|
||||||
|
("3", "the"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_n_times_without_reference_refuses(
|
||||||
|
factor: str, comparative: str
|
||||||
|
) -> None:
|
||||||
|
"""A dropped comparative multiplier must not admit as an initial
|
||||||
|
possession of ``<N> times``.
|
||||||
|
|
||||||
|
This is the Phase-5b hard negative: if future emission work makes
|
||||||
|
comparative-multiplicative surfaces reach the graph, the gate must
|
||||||
|
refuse incomplete ``<N> times ...`` clauses instead of consuming
|
||||||
|
the scalar as an ordinary count.
|
||||||
|
"""
|
||||||
|
res = parse_and_solve(
|
||||||
|
"Tom has 7 apples. "
|
||||||
|
f"Jerry has {factor} times {comparative} apples. "
|
||||||
|
"How many apples do they have together?"
|
||||||
|
)
|
||||||
|
assert res.answer is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_n_times_as_many_with_reference_still_solves() -> None:
|
||||||
|
"""The guard only blocks incomplete comparative clauses; a fully
|
||||||
|
referenced compare_multiplicative graph still solves."""
|
||||||
|
res = parse_and_solve(
|
||||||
|
"Tom has 7 apples. Jerry has 3 times as many apples as Tom. "
|
||||||
|
"How many apples do they have together?"
|
||||||
|
)
|
||||||
|
assert res.answer == 28.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"question",
|
||||||
|
[
|
||||||
|
"Tom has 7 apples. Jerry has twice as many apples. "
|
||||||
|
"How many apples do they have together?",
|
||||||
|
"Tom has 7 apples. Jerry has double the apples. "
|
||||||
|
"How many apples do they have together?",
|
||||||
|
"Ivan has 20 dice. Jerry has twice as many dice as Ivan. "
|
||||||
|
"How many dice do they have altogether?",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_existing_multiplier_refusals_stay_refused(question: str) -> None:
|
||||||
|
"""Existing safe multiplier refusals must not become admissions."""
|
||||||
|
res = parse_and_solve(question)
|
||||||
|
assert res.answer is None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue