- scripts/gsm8k_frontier_report.py + test (stable buckets; rate_with_currency surfaced) - docs/recognizer-registry.md + math_candidate_graph.py comments repaired (current refusal doctrine; old skip-only marked historical) - generate/math_roundtrip.py: add 'a','an' to RATE_ANCHORS (with doc update) - generate/recognizer_anchor_inject.py: inject_rate_with_currency (narrow ProperName actor, Rate/apply_rate CandidateOperation, rejects unsafe); registered in _INJECTORS; module docs updated - tests/test_*_rate_injection*.py + frontier test (8+ unit cases, confusers, synthetic wiring, real-report frontier pin) - ratification doc (pre-code) - lookback (post-impl, truthful) All required local commands exercised (pytest green for new + prior extract/invariants; frontier script shows rate bucket; runner per brief; shas captured). wrong=0 held. No sealed movement. Proxy still expected !passed (correct_min=10). See ratification and lookback for scope, hazards, exact outputs.
114 lines
5.2 KiB
Markdown
114 lines
5.2 KiB
Markdown
# Ratified Recognizer Registry (ADR-0163 Phase D)
|
|
|
|
The recognizer registry projects accepted exemplar-corpus proposals
|
|
from the append-only proposal log into a tuple the math
|
|
candidate-graph consults before refusing on an empty per-statement
|
|
choice list. It is the connective tissue between Phase C's operator
|
|
review surface and Phase D/E's admission expansion.
|
|
|
|
## Projection rule
|
|
|
|
A proposal enters the registry iff **all** of:
|
|
|
|
- `source.kind == "exemplar_corpus"` (Phase C's source kind)
|
|
- `review_state == "accepted"` (operator ratification — never agent-side)
|
|
- `proposed_chain.recognizer_spec` parses as a
|
|
`teaching.recognizer_synthesis.RecognizerSpec`
|
|
|
|
Pending, rejected, withdrawn, and non-exemplar proposals are
|
|
invisible. Malformed accepted proposals raise `RegistryLoadError`
|
|
with the offending `proposal_id` — silent drops are forbidden.
|
|
|
|
Registry order is `(review_date, proposal_id)` ascending — stable
|
|
across runs.
|
|
|
|
## Match contract
|
|
|
|
`generate.recognizer_match.match(statement, registry)` returns at most
|
|
one `RecognizerMatch` per call (first-match-wins over registry order).
|
|
Each per-category matcher is rules-only — no LLM, no embedding, no
|
|
learned classifier. A module-import test pins the no-ML constraint.
|
|
|
|
`parsed_anchors` carry numeric tokens extracted **from the statement
|
|
text**, not from the spec. For `descriptive_setup_no_quantity`,
|
|
`parsed_anchors` is the empty tuple by design — the recognizer admits
|
|
the statement as setup context, contributing no math state.
|
|
|
|
## Narrowness invariant
|
|
|
|
Per ADR-0163 §Phase C The Synthesis Rule property (b), the
|
|
recognizer is the **narrowest** commitment that subsumes the seeds.
|
|
The matcher inherits that narrowness verbatim:
|
|
|
|
- A currency symbol outside the spec's `observed_currency_symbols`
|
|
does not match `rate_with_currency`.
|
|
- A window unit outside `observed_window_units` does not match
|
|
`temporal_aggregation`.
|
|
- A statement with any digit, number word, or indefinite quantifier
|
|
does not match `descriptive_setup_no_quantity`.
|
|
|
|
Widening happens through the corridor — wider exemplar corpus →
|
|
Phase C synthesis on wider seeds → operator ratifies the wider
|
|
proposal — never by editing the matcher's permissiveness.
|
|
|
|
## Wiring point (current doctrine — post wrong=0 correction)
|
|
|
|
`generate/math_candidate_graph.py:parse_and_solve` consults the
|
|
registry at the per-statement choice loop, **before** the existing
|
|
`no admissible candidate for statement` refusal.
|
|
|
|
When the registry recognizes the statement:
|
|
- the per-category injector (`generate/recognizer_anchor_inject.inject_from_match`)
|
|
is consulted;
|
|
- if the injector emits one or more `CandidateInitial` / `CandidateOperation`
|
|
that survive admissibility, those candidates are added to the per-sentence
|
|
choice space exactly as parser output would be;
|
|
- if the injector emits nothing (or all candidates are dropped by later
|
|
pronoun/lookback guards), the graph **refuses explicitly** with the
|
|
reason `"recognizer matched but produced no injection for statement: ... (category=...)"`.
|
|
|
|
**Never silently drop** a recognized math statement as "zero state".
|
|
The historical skip-only rule ("drop it, Cartesian product unchanged")
|
|
was retired because it admitted incomplete graphs at the problem level
|
|
(the solver could answer from the remaining statements and produce a
|
|
number that was not the answer to the full input). The current code
|
|
and this document treat "recognized + no typed emission" as a refusal
|
|
case. Old skip-only language appears only in historical notes below.
|
|
|
|
Empty registry → the guard is a no-op and the pre-registry refusal
|
|
behavior is preserved byte-identically.
|
|
|
|
## Ratification boundary (ADR-0161 §5)
|
|
|
|
The agent does not ratify the live proposal log. Phase D tests
|
|
build a synthetic in-memory `RatifiedRecognizer` tuple from the
|
|
Phase C pending proposals' content (`tests/_phase_d_fixture.py`).
|
|
The matcher and candidate-graph wiring exercise the same
|
|
RecognizerSpec bytes the operator will later ratify, with zero
|
|
modification to `teaching/proposals/proposals.jsonl`.
|
|
|
|
The operator's ratification path is the existing
|
|
`core teaching review <proposal_id> --accept --review-date <YYYY-MM-DD>`
|
|
— no new CLI surface lands with Phase D.
|
|
|
|
## Phase E / D.2 follow-up (historical note)
|
|
|
|
Early Phase D wiring (the registry + skip-only guard) was intentionally
|
|
"skip-only by construction" so that adding recognizer categories could
|
|
not regress wrong=0. That doctrine was corrected once the "recognized
|
|
but uninjected → incomplete graph" hazard was understood (see the
|
|
explicit refusal branch and comments in `math_candidate_graph.py` and
|
|
the ADR-0167 / Brief 11 lineage).
|
|
|
|
Current follow-up work (Workstream A Inc 2 and successors) adds the
|
|
per-category injectors that turn `parsed_anchors` for `rate_with_currency`
|
|
(and later categories) into grounded `CandidateOperation` / `Rate` /
|
|
`apply_rate` primitives that the existing solver already knows how to
|
|
execute. When an injector is present and emits, the statement
|
|
contributes real solver state; when it cannot, refusal (not silent drop)
|
|
is the outcome.
|
|
|
|
Historical skip-only descriptions are preserved only as "rejected
|
|
behavior" markers in this document and in code comments. Grep for the
|
|
old phrases on the active source surfaces should surface only such
|
|
markers.
|