feat(matcher-extension/ME-1): currency-per-unit composition admission
Lights up the dormant consumption path from PR #398. Extends
_match_rate_with_currency with a new branch keyed on
anchor_kind="currency_per_unit_composition" — when a statement
carries the "<Subject> bought <count> <noun> at $<amount> each" shape
with a same-sentence proper-noun subject, the matcher publishes:
- composition_shape = "bound(count) × bound(unit_cost)"
- composed_initial = CandidateInitial(entity=Subject,
quantity=Quantity(count*amount,
dollars))
The PR #398 consumption wire in inject_from_match consults
composition_registry on composition_shape: an affirms entry admits
the pre-composed CandidateInitial; falsifies suppresses; absence
refuses.
Subject binding under Option A (refuse when same-sentence subject
absent). Option B (placeholder) forbidden by the brief; Option C
(cross-sentence lookup) is ME-2.
Truth-test scorecard (6-row binding table from PR #399):
#1 Synthetic Maria admits ........ PASS
#2 Case 0050 stays refused ....... PASS
#3 train_sample 3/47, no regress . PASS (3 correct preserved)
#4 wrong == 0 preserved .......... PASS
#5 public 150/150 unchanged ...... PASS
#6 All PR #398 tests still pass .. PASS (38 tests + new 21 = 59)
Case 0019 stays refused (Option A) — admitting it requires
cross-sentence subject lookup (ME-2 brief).
Tests (21 new, all green):
- test_matcher_extension_currency_per_unit.py (15)
- test_matcher_extension_case_0050_hazard_pin.py ( 2)
- test_matcher_extension_end_to_end_admission.py ( 4)
Registered in core/cli.py "packs" suite.
Suite results:
core test --suite runtime -q → 20 passed
core test --suite packs -q → 51 passed (existing) + 21 new
core test --suite teaching -q → 93 passed
core eval gsm8k_math --split public → 150/150, wrong=0
Anti-regression invariants preserved:
- wrong == 0 on gsm8k_math public
- Case 0050 hazard pin holds
- ADR-0166 — no new eval lanes
- ADR-0167 partition — no cognition imports
- Existing currency_per_unit_rate path byte-identical (test pins)
- Refusal-preferring: subject-absent → no composition emission
- engine_state/* not committed
Stacks on PR #398 (base: feat/composition-frame-consumption-wiring).