docs(matcher-extension): dispatch pack (ME-1) (#399)
Follow-up to PR #398 — lights up the dormant consumption path by extending _match_rate_with_currency to publish composition_shape + pre-composed CandidateInitial in parsed_anchors. Scope: one matcher extension (_match_rate_with_currency) for the currency-per-unit composition shape ("$X each"). Other composition shapes (multi-quantity, additive, subtractive) deferred to ME-3/4/5 follow-ups. Subject binding decision pinned: Option A (refuse when same-sentence subject is absent). Forbids Option B (placeholder subject — fabricates attribution). Defers Option C (cross-sentence subject) to its own ME-2 brief. Honest consequence: case 0019 stays refused in THIS PR (requires Option C). Truth-test #1 replaced with a synthetic "Maria bought 3 vet appointments at $400 each" canary that has same-sentence subject. The flywheel turns one revolution: ratify → compile → load → consume → admit, end-to-end on the synthetic canary. Operator profile: Opus (load-bearing wrong=0; pre-composed CandidateInitial; case 0050 mandatory pin). Test surface enumerated (4 test files, 12+ tests): - test_matcher_extension_currency_per_unit (8 narrowness tests) - test_matcher_extension_case_0050_hazard_pin (mandatory) - test_matcher_extension_end_to_end_admission (truth test) - test_matcher_extension_train_sample_baseline_preserved - test_matcher_extension_public_split_preserved 6-row truth test pinned. Anti-regression invariants enumerated. Forbidden surface includes Option B + new SAFE category entries + solver mutation. Recommended next dispatch sequence: ME-1 → ME-2 (case 0019) → ME-3/4/5 (remaining composition shapes).
This commit is contained in:
parent
78ddab79b4
commit
7a0b1ed931
1 changed files with 441 additions and 0 deletions
441
docs/handoff/MATCHER-EXTENSION-DISPATCH-PACK.md
Normal file
441
docs/handoff/MATCHER-EXTENSION-DISPATCH-PACK.md
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
# Matcher Extension — Dispatch Pack (ME-1)
|
||||||
|
|
||||||
|
**Goal:** Light up the dormant consumption path in PR #398 by extending
|
||||||
|
the recognizer matcher to publish `composition_shape` + a pre-composed
|
||||||
|
`CandidateInitial` in `parsed_anchors`. With this PR, the math
|
||||||
|
compounding loop's `ratify → admit` closure becomes operational for
|
||||||
|
the `each`/`per`-framed cost composition shape — and case 0019
|
||||||
|
admits.
|
||||||
|
|
||||||
|
**Parent briefs:**
|
||||||
|
- `docs/handoff/COMPOSITION-FRAME-CONSUMPTION-WIRING-BRIEF.md` (PR #396)
|
||||||
|
- `docs/handoff/CONSUMPTION-WIRING-DISPATCH-PACK.md` (PR #397)
|
||||||
|
- PR #398 — the consumption-wiring implementation (merged into context)
|
||||||
|
|
||||||
|
**Parent ADRs:** ADR-0163 (recognizer registry), ADR-0167, ADR-0168 / 0168.1,
|
||||||
|
ADR-0169 / 0169.1, ADR-0170 (injector contract widening), ADR-0172
|
||||||
|
|
||||||
|
**Type:** Implementation dispatch; not a doctrine ADR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why this brief exists
|
||||||
|
|
||||||
|
PR #398 shipped the consumption infrastructure (compile, loaders,
|
||||||
|
manifest, injector consultation). All 38 new tests pass. But the
|
||||||
|
truth-test rows #1 and #3 (case 0019 admits; train_sample 3/47 →
|
||||||
|
≥4/46) **partial**: the wiring is positioned correctly but stays
|
||||||
|
dormant because the existing matchers don't publish
|
||||||
|
`composition_shape` in `parsed_anchors`.
|
||||||
|
|
||||||
|
PR #398 §"Scope Boundary" named this finding. This brief is the
|
||||||
|
follow-up.
|
||||||
|
|
||||||
|
The matchers in `generate/recognizer_match.py`:
|
||||||
|
|
||||||
|
| Matcher | Currently | Needs to |
|
||||||
|
|---|---|---|
|
||||||
|
| `_match_rate_with_currency` | Extracts `(currency_symbol, amount, per_unit)` per anchor; **detection + extraction** | Detect the outer count in the same sentence; emit `composition_shape` + `composed_initial` when count is present |
|
||||||
|
| `_match_currency_amount` | Detection-only (returns empty anchors); refuses if per-unit framing present | **Out of scope** — this matcher's discriminator deliberately rejects per-unit framing |
|
||||||
|
| `_match_multiplicative_aggregation` | Detection-only; matches "M outer × N inner" shape | **Out of scope** (Phase B follow-up) — multiplicative_aggregation maps to a different solver shape than `currency_per_unit_rate` |
|
||||||
|
|
||||||
|
So the minimum-scope extension is **`_match_rate_with_currency`**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bundling rule
|
||||||
|
|
||||||
|
**One PR.** Single matcher extension + injector wire for the
|
||||||
|
per-unit-cost composition shape. Don't bundle multi-quantity_composition
|
||||||
|
or additive_composition cases into this brief — those are separate
|
||||||
|
matcher targets and each carries its own subject-binding risk.
|
||||||
|
|
||||||
|
Branch: `feat/matcher-extension-currency-per-unit-composition`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operator profile
|
||||||
|
|
||||||
|
**Opus** (load-bearing wrong=0 surface; the matcher extension produces
|
||||||
|
a `CandidateInitial` whose value is the product of two recognized
|
||||||
|
quantities — same hazard surface as CC-2, with case 0050 as mandatory
|
||||||
|
pin).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dispatch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin main && \
|
||||||
|
git worktree add /tmp/wt-matcher origin/main && \
|
||||||
|
cd /tmp/wt-matcher && \
|
||||||
|
git checkout -b feat/matcher-extension-currency-per-unit-composition
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reads required FIRST
|
||||||
|
|
||||||
|
- `generate/recognizer_match.py`:
|
||||||
|
- `_match_rate_with_currency` (lines 290–344) — the extension target
|
||||||
|
- `RecognizerMatch` dataclass (line 139) — `parsed_anchors` is `tuple[Mapping[str, Any], ...]`
|
||||||
|
- `generate/recognizer_anchor_inject.py`:
|
||||||
|
- `inject_from_match` (lines 73–98) — already consults composition_registry on `composition_shape`
|
||||||
|
- `_consult_composition_registry` (lines 100–170) — the contract: matcher must publish `composition_shape` + `composed_initial` OR `composed_operation`
|
||||||
|
- `generate/comprehension/composition_registry.py` — `is_affirmed`, `is_falsified`, allowlist
|
||||||
|
- `generate/math_candidate_parser.py::CandidateInitial` — required fields:
|
||||||
|
`(initial, source_span, matched_anchor, matched_value_token, matched_unit_token, matched_entity_token)`
|
||||||
|
- `generate/math_problem_graph.py::InitialPossession, Quantity`
|
||||||
|
- Case 0019 audit row in `evals/gsm8k_math/train_sample/v1/audit_brief_11.json`
|
||||||
|
- The literal text of case 0019 in `evals/gsm8k_math/train_sample/v1/cases.jsonl`
|
||||||
|
- `tests/test_composition_consult_in_injector.py` — the synthetic-anchor tests
|
||||||
|
showing the consumption path admits when given populated anchors
|
||||||
|
- `tests/test_consumption_case_0050_hazard_pin.py` — the case-0050
|
||||||
|
parametrized canary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The target case
|
||||||
|
|
||||||
|
**Case 0019:**
|
||||||
|
|
||||||
|
> "John adopts a dog from a shelter. The dog ends up having health
|
||||||
|
> problems and this requires 3 vet appointments, which cost $400
|
||||||
|
> each. After 2.5 years of these appointments John discovers that his
|
||||||
|
> dog's appointments are only 50% covered by insurance. How much did
|
||||||
|
> John pay total?"
|
||||||
|
|
||||||
|
The sentence that triggers the composition is:
|
||||||
|
|
||||||
|
> "The dog ends up having health problems and this requires 3 vet
|
||||||
|
> appointments, which cost $400 each."
|
||||||
|
|
||||||
|
Recognized data the extended matcher must bind:
|
||||||
|
|
||||||
|
- outer count: **3**
|
||||||
|
- unit cost: **$400**
|
||||||
|
- per-unit token: **each**
|
||||||
|
- currency symbol: **$**
|
||||||
|
|
||||||
|
Pre-composed admission target:
|
||||||
|
|
||||||
|
```python
|
||||||
|
CandidateInitial(
|
||||||
|
initial=InitialPossession(
|
||||||
|
entity=<resolved_subject>, # see "Subject binding" below
|
||||||
|
quantity=Quantity(value=1200, unit="dollars"),
|
||||||
|
),
|
||||||
|
source_span="3 vet appointments, which cost $400 each",
|
||||||
|
matched_anchor="cost",
|
||||||
|
matched_value_token="1200", # the composed value as a token
|
||||||
|
matched_unit_token="dollars",
|
||||||
|
matched_entity_token=<resolved_subject>,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`composition_shape = "bound(count) × bound(unit_cost)"`
|
||||||
|
(must match the surface_pattern an operator ratifies in
|
||||||
|
`compositions/multiplicative_composition.jsonl`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Subject binding (the load-bearing decision)
|
||||||
|
|
||||||
|
The CandidateInitial requires a non-empty `entity`. Case 0019's
|
||||||
|
sentence ("The dog ends up having health problems…") has no human
|
||||||
|
subject in the same sentence — "John" lives in sentence 0.
|
||||||
|
|
||||||
|
**Three options, pick one (recommend Option A):**
|
||||||
|
|
||||||
|
### Option A — Refuse when same-sentence subject is absent
|
||||||
|
|
||||||
|
The matcher checks the statement for a recognized proper-noun subject.
|
||||||
|
If absent (case 0019's situation), the matcher returns `None` for the
|
||||||
|
composition extension — falling back to the existing detection-only
|
||||||
|
behavior. **The composition admission does NOT fire for case 0019.**
|
||||||
|
|
||||||
|
This is honest refusal-preferring: cross-sentence subject binding is
|
||||||
|
its own hazard surface (the wrong subject = wrong arithmetic
|
||||||
|
attribution).
|
||||||
|
|
||||||
|
If you choose Option A, case 0019 stays refused — the brief's truth
|
||||||
|
test #1 is still **partial** for THIS PR. Pick a different canary
|
||||||
|
sentence with same-sentence subject (e.g. construct a synthetic case
|
||||||
|
where the subject IS in the same sentence).
|
||||||
|
|
||||||
|
### Option B — Use a placeholder subject (DISCOURAGED)
|
||||||
|
|
||||||
|
Insert `entity="anonymous"` or similar. **Strongly discouraged** —
|
||||||
|
this fabricates a subject and pollutes the candidate graph. The
|
||||||
|
solver may produce a "correct" total but attribute it wrongly, which
|
||||||
|
is structurally the same shape as case 0050 hazard (correct-looking
|
||||||
|
trace, wrong attribution).
|
||||||
|
|
||||||
|
Forbidden unless explicitly approved.
|
||||||
|
|
||||||
|
### Option C — Cross-sentence subject lookup
|
||||||
|
|
||||||
|
Make the matcher consult prior-sentence state to find the most recent
|
||||||
|
proper-noun entity. This requires either:
|
||||||
|
|
||||||
|
- threading sentence-state context into the recognizer match path
|
||||||
|
(currently the matcher only sees one sentence at a time), or
|
||||||
|
- extending `RecognizerMatch` with an optional `prior_subject` field
|
||||||
|
populated upstream
|
||||||
|
|
||||||
|
Either is a larger surface than a single PR should land. **Defer to
|
||||||
|
its own brief.**
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
**Ship Option A**, document the case-0019 gap as "requires Option C"
|
||||||
|
in the PR body, and demonstrate the truth-test fires via a
|
||||||
|
**purpose-built test case** that has same-sentence subject. Example:
|
||||||
|
|
||||||
|
> "Maria bought 3 vet appointments, which cost $400 each."
|
||||||
|
|
||||||
|
For Maria-with-3-appointments: count=3, unit_cost=400, subject=Maria
|
||||||
|
(same sentence) → CandidateInitial(entity="Maria", quantity=$1200).
|
||||||
|
|
||||||
|
Add this case to `evals/gsm8k_math/train_sample/v1/cases.jsonl` as a
|
||||||
|
synthetic test case OR write a stand-alone Python test that builds
|
||||||
|
the recognizer match directly and verifies admission.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
### Modules touched (minimum scope)
|
||||||
|
|
||||||
|
1. **`generate/recognizer_match.py`**:
|
||||||
|
- Extend `_match_rate_with_currency` (or add a sibling helper
|
||||||
|
called from it) that:
|
||||||
|
- detects an outer count token in the same statement
|
||||||
|
- parses the count + per-unit cost
|
||||||
|
- resolves the subject under Option A (same-sentence
|
||||||
|
proper-noun extraction; refuse otherwise)
|
||||||
|
- constructs the pre-composed `CandidateInitial`
|
||||||
|
- publishes the anchor with keys `composition_shape`,
|
||||||
|
`composed_initial`, `currency_symbol`, `amount`, `per_unit`,
|
||||||
|
`outer_count`, `subject` (for debug/audit)
|
||||||
|
|
||||||
|
2. **`generate/recognizer_anchor_inject.py`** — **no changes**.
|
||||||
|
The consumption wire from PR #398 already handles `composition_shape`
|
||||||
|
+ `composed_initial`. This PR proves it lights up.
|
||||||
|
|
||||||
|
### Manifest / pack changes
|
||||||
|
|
||||||
|
**None.** The operator must separately ratify
|
||||||
|
`bound(count) × bound(unit_cost)` under `multiplicative_composition`
|
||||||
|
via `apply_composition_claim()` — that ratification already exists in
|
||||||
|
the canonical pack (or can be re-ratified). The matcher extension
|
||||||
|
makes the existing dormant ratification load-bearing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hard requirements
|
||||||
|
|
||||||
|
- **Refusal-preferring.** Subject absent → matcher does NOT emit a
|
||||||
|
composition anchor. The existing currency_per_unit_rate detection
|
||||||
|
path continues to fire (no regression).
|
||||||
|
- **Outer-count parse narrowness.** The outer count must be a single
|
||||||
|
integer or known number-word adjacent to a counted noun (mirror
|
||||||
|
`_match_discrete_count_statement`'s narrowness layers). Multi-count
|
||||||
|
or ambiguous → refuse the composition extension.
|
||||||
|
- **Per-unit token in observed set.** Only fire when the matched
|
||||||
|
per-unit token (e.g. "each", "per") is in the spec's
|
||||||
|
`observed_per_units` (existing narrowness from
|
||||||
|
`_match_rate_with_currency`).
|
||||||
|
- **`composition_shape` MUST match a SAFE category.** The matcher
|
||||||
|
emits `"bound(count) × bound(unit_cost)"` — the operator's
|
||||||
|
ratification of this exact pattern under
|
||||||
|
`multiplicative_composition` is what lights it up. Any other
|
||||||
|
shape string is forbidden.
|
||||||
|
- **Pre-composed value is `int` or `float` per `Quantity` invariants.**
|
||||||
|
Compute `outer_count * unit_cost`; if either is non-numeric,
|
||||||
|
refuse.
|
||||||
|
- **Source-span requirement.** `source_span` must be a verbatim
|
||||||
|
substring of the statement (existing CandidateInitial discipline).
|
||||||
|
- **Case 0050 hazard pin.** The matcher extension MUST NOT cause
|
||||||
|
case 0050 to admit. The extension's narrowness must reject case
|
||||||
|
0050's sentence shape (which has no per-unit framing of this
|
||||||
|
form).
|
||||||
|
- **No new eval lanes** (ADR-0166).
|
||||||
|
- **No solver / parser / decomposer mutation** (ADR-0169
|
||||||
|
§"Mutation boundary").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### `tests/test_matcher_extension_currency_per_unit.py`
|
||||||
|
|
||||||
|
1. **`test_outer_count_extracted_with_subject`** — synthetic sentence
|
||||||
|
"Maria bought 3 vet appointments, which cost $400 each."; matcher
|
||||||
|
returns anchor with composition_shape + composed_initial(value=1200, entity="Maria")
|
||||||
|
2. **`test_subject_absent_refuses_composition_extension`** — case 0019's
|
||||||
|
exact sentence "The dog ends up having health problems and this
|
||||||
|
requires 3 vet appointments, which cost $400 each."; matcher
|
||||||
|
returns the existing rate anchors but does NOT publish
|
||||||
|
composition_shape (Option A)
|
||||||
|
3. **`test_per_unit_token_outside_observed_set_refuses`** — narrowness
|
||||||
|
guard
|
||||||
|
4. **`test_multiple_counts_refuse_composition`** — narrowness guard
|
||||||
|
(refusal-preferring on ambiguity)
|
||||||
|
5. **`test_count_word_form_admits`** — "Maria bought three vet
|
||||||
|
appointments at $400 each" → composed_initial(value=1200)
|
||||||
|
6. **`test_negative_or_zero_count_refuses`**
|
||||||
|
7. **`test_currency_symbol_outside_observed_refuses`**
|
||||||
|
8. **`test_emits_canonical_unit_dollars`** — `Quantity.unit == "dollars"`
|
||||||
|
for `$` symbol
|
||||||
|
|
||||||
|
### `tests/test_matcher_extension_case_0050_hazard_pin.py`
|
||||||
|
|
||||||
|
1. **`test_case_0050_does_not_emit_composition_shape`** — feed case
|
||||||
|
0050's actual sentences through the extended matcher; assert no
|
||||||
|
anchor carries `composition_shape`. Mandatory; runs in CI.
|
||||||
|
|
||||||
|
### `tests/test_matcher_extension_end_to_end_admission.py`
|
||||||
|
|
||||||
|
The TRUTH TEST for this PR:
|
||||||
|
|
||||||
|
1. **`test_synthetic_maria_admits_via_composition_registry`** — start
|
||||||
|
from a clean pack, ratify `bound(count) × bound(unit_cost)` under
|
||||||
|
`multiplicative_composition`, run the candidate-graph pipeline
|
||||||
|
on the synthetic Maria sentence, assert the result is a correct
|
||||||
|
admission (not refusal).
|
||||||
|
2. **`test_train_sample_case_0019_partial`** — explicit test
|
||||||
|
documenting case 0019's refusal-preserving status under Option A
|
||||||
|
(refuses because cross-sentence subject; not a bug, an honest
|
||||||
|
gap). Comment names Option C as the next brief.
|
||||||
|
|
||||||
|
### `tests/test_matcher_extension_train_sample_baseline_preserved.py`
|
||||||
|
|
||||||
|
1. **`test_train_sample_counts_unchanged_or_better`** — full
|
||||||
|
`train_sample` reader-on eval; assert `correct >= 3`, `wrong == 0`,
|
||||||
|
`refused <= 47`. Any improvement is a bonus; no regression is
|
||||||
|
mandatory.
|
||||||
|
|
||||||
|
### `tests/test_matcher_extension_public_split_preserved.py`
|
||||||
|
|
||||||
|
1. **`test_gsm8k_math_public_150_150_preserved`** — `core eval
|
||||||
|
gsm8k_math --split public --json` returns `cases_total=150`,
|
||||||
|
`correct=150`, `wrong=0`, `refused=0`. Mandatory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Truth test (binding 6-row table for this PR)
|
||||||
|
|
||||||
|
| # | Assertion | How to verify |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Synthetic Maria sentence admits | `test_synthetic_maria_admits_via_composition_registry` passes |
|
||||||
|
| 2 | Case 0050 stays refused | `test_matcher_extension_case_0050_hazard_pin.py` passes |
|
||||||
|
| 3 | `train_sample` `correct ≥ 3`, no regression | full train_sample reader-on run; report green |
|
||||||
|
| 4 | `wrong == 0` preserved (train_sample) | same |
|
||||||
|
| 5 | `public` split 150/150 unchanged | `core eval gsm8k_math --split public --json` |
|
||||||
|
| 6 | All PR #398 tests still pass | `core test --suite packs -q` green |
|
||||||
|
|
||||||
|
Row #1 replaces the original case-0019 row from PR #397's truth test
|
||||||
|
(case 0019 stays refused under Option A; that's a Option C concern,
|
||||||
|
not this PR).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-regression invariants
|
||||||
|
|
||||||
|
- `wrong == 0` on `core eval gsm8k_math --split public` preserved
|
||||||
|
- Case 0050 stays refused under the parametrized hazard pin
|
||||||
|
- ADR-0166 — no new eval lanes
|
||||||
|
- ADR-0167 partition — matcher remains math-domain only
|
||||||
|
- Empty composition_registry runtime byte-identical to today
|
||||||
|
- `SAFE_COMPOSITION_CATEGORIES` enforced at write AND load
|
||||||
|
- Refusal-preferring discipline: subject-absent → no composition emission
|
||||||
|
- `engine_state/*` never committed
|
||||||
|
- Pinned-lane SHAs may move (intentional `correct` delta if admission
|
||||||
|
fires); call out the move in the PR body
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Forbidden surface
|
||||||
|
|
||||||
|
- Option B (placeholder subject) — fabricates entity attribution
|
||||||
|
- Option C (cross-sentence subject) — defer to its own brief
|
||||||
|
- New SAFE_COMPOSITION_CATEGORIES entries (this PR is read-only over
|
||||||
|
the existing allowlist)
|
||||||
|
- Mutating `_match_currency_amount` (its discriminator is correct;
|
||||||
|
don't conflate)
|
||||||
|
- Mutating `_match_multiplicative_aggregation` (different shape;
|
||||||
|
separate brief)
|
||||||
|
- Adding new ShapeCategory values
|
||||||
|
- Solver / parser / decomposer / arithmetic-operator mutation
|
||||||
|
- Manifest schema changes (PR #398 already added the optional
|
||||||
|
composition_checksum field)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What ships when this PR lands
|
||||||
|
|
||||||
|
- One matcher extension at `_match_rate_with_currency`
|
||||||
|
- 12+ new tests
|
||||||
|
- The first case (synthetic Maria) admits via the
|
||||||
|
ratify → compile → load → consume → admit chain
|
||||||
|
- `train_sample` counts improve OR stay at baseline (no regression)
|
||||||
|
- The dormant consumption path in PR #398 becomes load-bearing for
|
||||||
|
the per-unit-cost composition shape
|
||||||
|
- Case 0019 remains refused — but explicitly under documented
|
||||||
|
scope-boundary, not under structural unknown
|
||||||
|
|
||||||
|
## Sequencing
|
||||||
|
|
||||||
|
- **No upstream blocker** beyond PR #398 landing on main.
|
||||||
|
- **Subsequent briefs** (each its own PR):
|
||||||
|
- **ME-2:** Cross-sentence subject binding (Option C); admits case 0019
|
||||||
|
- **ME-3:** Multi-quantity_composition matcher (8 audit cases)
|
||||||
|
- **ME-4:** Additive composition matcher (independent named quantities)
|
||||||
|
- **ME-5:** Subtractive composition matcher
|
||||||
|
|
||||||
|
Each subsequent brief carries its own canary case + truth test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memory pointers
|
||||||
|
|
||||||
|
- [[project-ratification-consumption-gap-2026-05-27]] — the original finding
|
||||||
|
- [[feedback-ratify-vs-consume-loop-closure]] — pattern
|
||||||
|
- [[feedback-wrong-zero-hazard-case-0050]] — mandatory pin
|
||||||
|
- [[feedback-production-line-pattern]] — dispatch pattern
|
||||||
|
- [[feedback-parallel-agent-worktrees]]
|
||||||
|
- [[feedback-address-critiques-dont-waive]] — Option A is the honest
|
||||||
|
refusal-preferring choice; Option B is the convenient cheat that
|
||||||
|
this brief explicitly forbids
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Copy-paste dispatch line
|
||||||
|
|
||||||
|
```text
|
||||||
|
Read docs/handoff/MATCHER-EXTENSION-DISPATCH-PACK.md (entire file).
|
||||||
|
git fetch origin main && git worktree add /tmp/wt-matcher origin/main && cd /tmp/wt-matcher && git checkout -b feat/matcher-extension-currency-per-unit-composition
|
||||||
|
```
|
||||||
|
|
||||||
|
Operator instruction sequence:
|
||||||
|
1. Read this brief plus the three "parent briefs" + ADR-0169 + the four `Reads required FIRST` files.
|
||||||
|
2. Implement the matcher extension in `_match_rate_with_currency` (or a sibling helper) per Option A.
|
||||||
|
3. Write the 12 tests across the four test files.
|
||||||
|
4. Run the 6-row truth-test sequence end-to-end.
|
||||||
|
5. Confirm all 6 rows hold before opening PR.
|
||||||
|
6. PR title: `feat(matcher-extension): currency-per-unit composition admission (ME-1)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision summary
|
||||||
|
|
||||||
|
This PR closes the "consumption path is dormant" finding from PR #398
|
||||||
|
**for the simplest possible composition shape** (currency × per-unit
|
||||||
|
count). It does NOT close case 0019 — that requires cross-sentence
|
||||||
|
subject binding (Option C) and is the next brief.
|
||||||
|
|
||||||
|
The honest scoping: ship the matcher extension, prove the loop runs
|
||||||
|
end-to-end on a synthetic case where same-sentence subject binding
|
||||||
|
holds, leave case 0019 as the next domino. The flywheel turns one
|
||||||
|
revolution.
|
||||||
Loading…
Reference in a new issue