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)
6.2 KiB
ADR-0192 — Open the discrete_count counted-noun class (firewall-backed)
Status: Proposed (implemented in this PR). Widens the ADR-0163.D.2 discrete_count matcher. Builds directly on ADR-0191 — the completeness firewall is the precondition that makes this safe. Substrate PR: 0 metric delta by design; the value is 8× more statements parsing into solver state, wrong=0-proven on the full real corpus.
One line. The discrete_count matcher gated the counted noun against a CLOSED ratified set (
observed_counted_nouns): "Betty has 24 marbles" matched only because "marbles" was ratified, while "Randy has 60 mango trees" / "Sam has 12 red apples" produced no anchor purely because the noun was unseen. This opens the single-anchor possession/acquisition path to an open noun phrase, keeping every other narrowness layer. Wrong=0 is held downstream by the ADR-0191 completeness guard + round-trip + branch disagreement — not by the curated noun list.
1. The gap (microscope finding, 2026-05-30)
The full-corpus microscope (scripts/gsm8k_microscope.py) ranked the serving
reader's refusals across all 7,473 real GSM8K train questions.
discrete_count_statement is the dominant wall: 3,850 first-wall refusals
("recognizer matched but produced no injection"). Dissecting why the matcher
emits no anchor:
| sub-shape | count | extractable? |
|---|---|---|
subj verb N <multi-word / adj+noun> ("Randy has 60 mango trees") |
~1,004 | yes — matcher too narrow |
| count on a prepositional object ("sold clips to 48 friends") | ~550 | no — correctly conservative |
| attributive number ("a 120-page book") | ~120 | no — verb not possession/acquisition |
| number is a unit (rate/currency/time) | ~380 | no — different category |
| relational / "other" | ~1,400 | no — needs composition |
Pinned blocker: the matcher only extracts when the counted noun is in
spec.observed_counted_nouns (a closed ratified set). "Betty has 24 marbles" matched (ratified); "Randy has 60 mango trees" / "Sam has 12 red apples" / "Randy has 60 trees on his farm" all emitted anchors=0 solely
because the noun (or noun phrase) was unseen — not because of the trailing PP
(the regex already allowed trailing content) and not because the shape was
ambiguous.
2. Decision
Open the counted-noun slot of the single-anchor discrete_count extractor
(_extract_discrete_count_re_open in generate/recognizer_match.py):
- The noun slot matches either a ratified
observed_counted_nounsentry (closed branch — preserves casing canonicalization and capitalized compounds like "Pokemon cards") OR an OPEN lowercase noun phrase: 1–3 consecutive lowercase word tokens, none a boundary/stop word (prepositions, conjunctions, determiners, comparatives). (?-i:...)makes the open branch lowercase-only so it never captures a following proper noun; the stop-word lookahead bounds the phrase so it never swallows a trailing prepositional phrase ("mango trees on his farm" → "mango trees").- Every other narrowness layer is unchanged: proper-noun subject, possession/acquisition verb whitelist, single numeric token, no clause-split. The compound-enumeration path stays closed.
Why this is safe (the firewall is the precondition)
The closed noun set existed to prevent open-vocabulary mis-parses from
reaching the solver. ADR-0191 moved that guarantee downstream: an open-vocab
mis-parse now hits the completeness guard (every source quantity must be
consumed), the round-trip filter (every slot must ground in source), and
branch-disagreement refusal. So wrong=0 is held by the firewall, not by
the noun list. The dangerous shapes are still refused before the open noun
even applies — "is reading a 120-page book" refuses because "is" is not a
possession/acquisition verb; "has many apples" refuses on the count token;
"has 60 apples and 30 oranges" refuses on the single-count / clause-split
layers.
3. Evidence
- Substrate gain: 61 → 494 discrete_count anchors extracted+injected over the full real corpus (8×), all clean.
- wrong=0 holds on the full 7,473-question corpus — 494 statements parse, zero confabulations. This is the direct proof that open-vocabulary recognition is safe under the ADR-0191 firewall.
- 0 metric delta (
train_samplebyte-identical 4/46/0; full-corpus correct unchanged at 4). The widening makes statements parse; the problems still refuse downstream at the composition wall (multi-statement chaining + question-target). This is expected: statement parsing is necessary, not sufficient. Refusal families shift accordingly — problems advance from the discrete_count first-wall to later walls. - Tests: new
tests/test_discrete_count_open_noun_class.py(open-vocab now extracts; noun phrase stops before prepositions; dangerous shapes still refuse). The one closed-contract assertion (test_unobserved_counted_noun_refused) is updated to the new open contract. All other discrete_count narrowness tests unchanged and passing.
4. Consequences
- This is substrate, deliberately landed with no metric movement. Its value is (a) the foundation every discrete_count composition will consume — a statement cannot be composed before it parses — and (b) the empirical proof that the firewall makes open-vocabulary recognition wrong=0-safe, retiring the closed-set constraint for the simple possession/acquisition shape.
- The remaining discrete_count walls (prepositional-object counts, attributive numbers, rate/currency) are correctly still refused — they are not simple possession and must not be admitted by this path.
- The next layer is composition (multi-statement same-unit aggregate + question-target parsing) which now has parsing statements to consume.
5. Follow-ups
- Re-run
scripts/gsm8k_microscope.py --corpus <train.jsonl>after the composition layer lands to confirm wrong=0 holds and the metric moves. - Compound-enumeration ("N1 noun1 and N2 noun2") noun class remains closed; open it only after the single-anchor open path is proven in serving.