feat(evals): measure the reader/writer construction inventories against each other
Phase 5 item 1 asked for the overlap to be sized "by measuring the reader's construction set against the writer's rather than by growing corpora blindly". New lane `evals/construction_inventory` does that: it sweeps the writer's whole parameter space (every RhetoricalMove x IntentTag x predicate x quantifier x tense x aspect, both public entry points, 32292 cells), quotients it by the reader's OWN function-word skeleton, and comprehends each construction under three vocabularies. writer constructions 1739 reader constructions 19 (mint-site AST-guarded) overlap, faithful 6 accepts but MIS-READS 22 refuses 1711 vocabulary-dependent 0 Faithfulness, not acceptance, is the criterion — and that reverses two claims in the plan's §6 RESULT, using a metric that was already on the page: * g_read_rate is 1/293 but **g_args_rate is 0.0**. The one surface that "reads", `all molecules are defined as compounds`, is comprehended as `subset(molecule, defined_as_compound)` — the reader chunked the writer's verb phrase into a class name. It accepted; it did not comprehend. * "one construction wide" was wrong both ways: zero on that corpus, six over the writer's actual output space. A corpus cannot report an inventory's size. Three findings re-order Phase 5 item 1: 1. The reader FABRICATES on 22 constructions — neither reads nor refuses. `every dog is a mammal` -> member(every_dog, mammal); `furthermore, all dogs are mammals` -> asserted(furthermore). Both are ordinary user English, not writer artefacts. Root cause: _RESERVED lacks the function words the writer emits, and _parse_propositional accepts any single token as a fact. 2. It reaches SERVED output. deduction_surface recites `Given: furthermore; p implies q; p.` — a premise the user never stated — and chat/runtime.py realizes declarative turns into the held self, so a fabricated atom is vault-writable. Widening the inventory first would widen the fabrication surface with it. 3. ADR-0265's defect class survives inside ADR-0265's designated owner of clause grammar: the four aspect arms of _inflect_predicate bind `negated` to a wildcard and never read it, so `dog has been defined as mammal` serves both the assertion and the denial (10530/16146 points). Unreachable today — no producer sets aspect — so a loaded gun, not a casualty. It survived because ADR-0265's invariant is structural (is `negated` threaded?) and cannot see an arm that receives the flag and ignores it. A behavioural sweep can. The two fixes are written already, as the mutations that turn the defect pins red. They are NOT applied here: they change what CORE comprehends from user input, which is a serving change on the truth path — authorization-gated, ADR first, on the ADR-0261 §5.1 refuse-don't-drop precedent. Guards, so the tables cannot rot: reader constructions are pinned to an AST count of the reader's 10 mint sites; the declared tense/aspect axes are pinned to _inflect_predicate's match arms; the committed corpus is pinned to the fillers in use. 14 pins, 13 mutations observed RED. [Verification]: in-worktree, CPython 3.12.13, uv sync --locked. deductive 517 (was 503, +14 — count moved, registration confirmed) smoke 641 (unchanged; the file is registered in `deductive`) lane SHAs 11/11 match, no pin edited pyright 0 errors on all new files mutations 13/13 red, including both fabrication fixes
This commit is contained in:
parent
8927c5632f
commit
c69f99485d
12 changed files with 1214 additions and 0 deletions
|
|
@ -270,6 +270,14 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
# reason smoke stayed at 621 across two PRs that added 13 tests.
|
||||
# ~0.4s; it belongs with the other grammar pins.
|
||||
"tests/test_realizer_quantifier_agreement.py",
|
||||
# Phase 5 item 1 — the reader/writer construction overlap, plus the
|
||||
# fabrication pins. Gated rather than left to the `full` lane because
|
||||
# three of its pins are wrong=0 hazards on a serving path: the reader
|
||||
# accepts `every dog is a mammal` as an assertion about `every_dog`,
|
||||
# and recites `Given: furthermore; ...` back to the user. Those must
|
||||
# not be able to worsen silently. ~9.5s (sweeps the writer's whole
|
||||
# parameter space three times, once per vocabulary).
|
||||
"tests/test_construction_inventory.py",
|
||||
),
|
||||
"full": ("tests/",),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -914,6 +914,63 @@ a producer exists.
|
|||
Phase 5's *diversity* work (items 1–3 above) remains open and is now correctly
|
||||
scoped by §6's RESULT: the frontier is **construction-inventory overlap**.
|
||||
|
||||
**RESULT (second unit) — the inventories measured against each other.**
|
||||
New lane `evals/construction_inventory` (contract + 14 pins, 13 mutations
|
||||
observed red). It sweeps the writer's whole parameter space — every
|
||||
`RhetoricalMove` × `IntentTag` × predicate × quantifier × tense × aspect, both
|
||||
public entry points, 32292 cells — quotients it by the reader's own function-word
|
||||
skeleton, and comprehends each construction under three vocabularies.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| writer constructions | **1739** |
|
||||
| reader constructions | **19** (mint-site AST-guarded) |
|
||||
| **overlap, faithful** | **6** |
|
||||
| reader accepts but mis-reads | **22** |
|
||||
| reader refuses | 1711 |
|
||||
| vocabulary-dependent | **0** |
|
||||
|
||||
The overlap is the same six from both sides — `X is a Y`, `the X is a Y`, and
|
||||
the four categoricals — and it is **entirely set-theoretic**. The writer cannot
|
||||
express one ordering, propositional or interrogative construction; the reader
|
||||
has no template for the bare transitive clause that is the writer's whole
|
||||
default output. Two of the reader's constructions need a `?`, which no writer
|
||||
template contains, so they are unreachable **by shape** — corpus work cannot
|
||||
close that gap, only a template can.
|
||||
|
||||
Three findings change what item 1 should be:
|
||||
|
||||
1. **Growing the inventory is not the first move — the reader fabricates.** On
|
||||
22 constructions it neither reads nor refuses: it invents. `every dog is a
|
||||
mammal` → `member(every_dog, mammal)`; `all dogs are defined as mammals` →
|
||||
`subset(dog, defined_as_mammal)`; `furthermore, all dogs are mammals` →
|
||||
`asserted(furthermore)`. One root cause: `_RESERVED` does not contain the
|
||||
function words the writer emits, and `_parse_propositional` accepts any
|
||||
single token as a fact. **The first and third are ordinary user English.**
|
||||
|
||||
2. **It reaches served output.** `chat/deduction_surface.py` recites
|
||||
`Given: furthermore; p implies q; p.` — a premise the user never stated —
|
||||
and `chat/runtime.py` *realizes* declarative turns into the held self, so a
|
||||
fabricated atom is vault-writable. Widening the inventory before closing this
|
||||
widens the fabrication surface with it.
|
||||
|
||||
3. **ADR-0265's defect class survives inside ADR-0265's own designated owner of
|
||||
clause grammar.** The four aspect arms of `_inflect_predicate` bind `negated`
|
||||
to a wildcard and never read it, so `dog has been defined as mammal` is
|
||||
emitted for both the assertion and the denial — 10530 of 16146 parameter
|
||||
points. Not reachable today (no producer sets `aspect`), so a loaded gun, not
|
||||
a casualty. It survived because ADR-0265's invariant is *structural* — does
|
||||
`negated` reach the call? — and cannot see an arm that receives the flag and
|
||||
ignores it. **A behavioural sweep catches what a threading invariant cannot.**
|
||||
|
||||
**Item 1 is therefore re-ordered: close the fabrications first, then widen.**
|
||||
Both fixes are small and both are already written as mutations that turn the
|
||||
defect pins red (`every`/`each` into `_RESERVED`; discourse connectives stripped
|
||||
in `_split_clauses`). But they change what CORE *comprehends* from user input,
|
||||
which is a serving change on the truth path — **authorization-gated, and an ADR
|
||||
before any code**, on the ADR-0261 §5.1 precedent that a reader must refuse
|
||||
rather than drop or invent.
|
||||
|
||||
---
|
||||
|
||||
## 5. What is deliberately not estimated
|
||||
|
|
@ -1008,6 +1065,32 @@ The graph-model ADR is **not** cancelled — §1.8's type mismatch is still real
|
|||
but nothing measured here obliges it, and it should not be opened until a case
|
||||
exists that fails for that reason and no other.
|
||||
|
||||
#### CORRECTION to the block above, from the Phase 5 measurement it asked for
|
||||
|
||||
Two claims made here are wrong, and the metric that disproves both was already
|
||||
on the page when they were written.
|
||||
|
||||
**"the reader independently reads `all molecules are defined as compounds`" —
|
||||
no, it mis-reads it.** `g_read_rate` is `1/293` but **`g_args_rate` is `0.0`**.
|
||||
The reader comprehends that surface as `subset(molecule, defined_as_compound)`:
|
||||
it chunked the writer's verb phrase into a class name. It accepted, it did not
|
||||
comprehend. The contract for this lane separates `read` from `args` for exactly
|
||||
this reason and the separation did its job; the RESULT above simply put its
|
||||
weight on the wrong one of the two.
|
||||
|
||||
**"one construction wide" — wrong in both directions.** On this corpus the
|
||||
*faithful* overlap is **zero**, because the one case that reads is the mis-read
|
||||
above. Measured over the writer's whole output space instead of a 293-case
|
||||
corpus, the true overlap is **six** — the corpus never exercised five of them.
|
||||
A corpus cannot report the size of an inventory. Only the inventory can, which
|
||||
is what §Phase 5's RESULT now does.
|
||||
|
||||
The generalisable lesson, and the reason this is kept rather than edited away:
|
||||
**acceptance is not comprehension.** A reader scored on whether it returned a
|
||||
graph, rather than on whether the graph is the one it was given, is rewarded for
|
||||
guessing — a `wrong=0` hazard wearing a green metric. `g_read_rate` should never
|
||||
again be quoted without `g_args_rate` beside it.
|
||||
|
||||
---
|
||||
|
||||
## 7. Verification protocol
|
||||
|
|
|
|||
1
evals/construction_inventory/__init__.py
Normal file
1
evals/construction_inventory/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Construction-inventory lane: how wide is the reader/writer overlap?"""
|
||||
173
evals/construction_inventory/contract.md
Normal file
173
evals/construction_inventory/contract.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Construction-Inventory Eval Lane — Contract
|
||||
|
||||
**Lane:** `construction_inventory`
|
||||
**Version:** v1
|
||||
**Created:** 2026-07-27
|
||||
**Plan:** `docs/plans/grammar-unification-2026-07-26.md` (Phase 5, item 1)
|
||||
|
||||
## What this lane measures
|
||||
|
||||
How many **constructions** the reader and the writer share — the quantity §6's
|
||||
RESULT identified as the arc's frontier, and asked to be measured directly
|
||||
"rather than by growing corpora blindly."
|
||||
|
||||
## Why `grammar_roundtrip` could not answer this
|
||||
|
||||
`grammar_roundtrip` measures a **corpus**. Its G-direction reports
|
||||
`g_read_rate = 1/293`, and §6 read that one case as the shared construction,
|
||||
concluding the overlap was "one construction wide."
|
||||
|
||||
Both halves of that were wrong, and the lane's own metrics already said so:
|
||||
|
||||
- **`g_args_rate` is `0.0`.** The one surface that "reads" —
|
||||
`all molecules are defined as compounds` — is comprehended as
|
||||
`subset(molecule, defined_as_compound)`. The reader chunked the writer's verb
|
||||
phrase into a class name. It is a **mis-read**, so on that corpus the faithful
|
||||
overlap is **zero**, not one.
|
||||
- **The true overlap is six**, and the corpus never exercised five of them.
|
||||
293 cases cannot report the size of an inventory; only the inventory can.
|
||||
|
||||
The lesson generalises: *acceptance is not comprehension*. A lane that counts
|
||||
`read` without counting `args` will reward a reader for guessing.
|
||||
|
||||
## The construction quotient
|
||||
|
||||
A construction is defined by the reader's own dispatch. `reader.py` reads
|
||||
"STRUCTURE symbolically ... via templates keyed on FUNCTION WORDS + ORDER", so
|
||||
replacing every non-function word with `*` yields a form that is, by the
|
||||
reader's design, indistinguishable to its parser. The function-word set is read
|
||||
off `reader._RESERVED` — the lane cannot pick a quotient that flatters a number,
|
||||
and it widens automatically when the reader learns a word.
|
||||
|
||||
**Known limit:** `_chunk_class` refuses on unrecognized plurals, which depends on
|
||||
the token rather than the shape. So the skeleton determines the verdict *up to
|
||||
morphology*. The lane sweeps three count-noun vocabularies (regular, `-f/-ves`,
|
||||
suppletive) and counts a construction as shared only when all three agree;
|
||||
disagreement is reported as `w2r_filler_dependent`, currently **0**.
|
||||
|
||||
## The two directions
|
||||
|
||||
| direction | question | how |
|
||||
|---|---|---|
|
||||
| **W → R** | of everything the writer can emit, what does the reader read *truly*? | sweep the writer's whole parameter space, quotient to constructions, comprehend each |
|
||||
| **R → W** | of everything the reader can read, what can the writer emit? | instantiate each reader construction, quotient, test membership in the writer's construction set |
|
||||
|
||||
The writer's space is enumerated from live source — `RhetoricalMove`,
|
||||
`IntentTag`, `PREDICATE_DISPLAY`, `PREDICATIVE_NOMINAL`, `PLURAL_QUANTIFIERS`,
|
||||
and the tense/aspect literals in `_inflect_predicate`'s match arms (AST-pinned).
|
||||
Nothing is a hand-copied list of "what the writer can say."
|
||||
|
||||
Both writer entry points are swept: `render_semantic` (the **serving** writer,
|
||||
called by `core/cognition/pipeline.py`) and `render_step` (the clause owner it
|
||||
delegates to, also reached by the eval-only `realize_target`).
|
||||
|
||||
## Acceptance is split three ways
|
||||
|
||||
| bucket | meaning |
|
||||
|---|---|
|
||||
| `faithful` | the reader recovered the writer's proposition and minted **nothing else** — the only bucket that counts as overlap |
|
||||
| `fabricating` | accepted, but minted an argument the writer never wrote, or manufactured a query from a declarative |
|
||||
| `refused` | the honest outcome: no template, and the reader says so |
|
||||
|
||||
## Metrics
|
||||
|
||||
| metric | definition |
|
||||
|---|---|
|
||||
| `writer_constructions` | distinct constructions the writer can emit |
|
||||
| `overlap_constructions` | shared **and faithful** under every count-noun filler — the Phase 5 number |
|
||||
| `w2r_fabricating` | writer constructions the reader mis-reads — a `wrong=0` hazard, ratcheted **down** |
|
||||
| `w2r_filler_dependent` | shared under some vocabularies but not others (morphology gaps) |
|
||||
| `reader_constructions` | the reader's inventory, guarded against rot by mint-site AST count |
|
||||
| `r2w_expressible` | reader constructions the writer can emit |
|
||||
| `negation_collapsed_points` | writer parameter points whose denial is byte-identical to its assertion — ADR-0265 sentinel, ratcheted **down** |
|
||||
|
||||
## v1 baseline — measured on `main` @ `8927c563`
|
||||
|
||||
```
|
||||
writer_cells 32292
|
||||
writer_constructions 1739
|
||||
w2r_faithful 6
|
||||
w2r_fabricating 22
|
||||
w2r_refused 1711
|
||||
w2r_filler_dependent 0
|
||||
w2r_faithful_rate 0.00345
|
||||
cells_faithful 12
|
||||
cells_fabricating 150
|
||||
|
||||
reader_constructions 19
|
||||
r2w_expressible 6
|
||||
r2w_rate 0.315789
|
||||
reader_interrogative_constructions 2
|
||||
|
||||
overlap_constructions 6 <-- the Phase 5 number
|
||||
negation_collapsed_points 10530 <-- defect, see below
|
||||
```
|
||||
|
||||
## What this lane found
|
||||
|
||||
**1. The overlap is six, and all six are set-theoretic.**
|
||||
`X is a Y`, `the X is a Y`, `all Xs are Ys`, `no Xs are Ys`, `some Xs are Ys`,
|
||||
`some Xs are not Ys`. The writer cannot express a single ordering, propositional
|
||||
or interrogative construction; the reader has no template for the bare
|
||||
transitive clause (`X supports Y`) that is the writer's entire default output.
|
||||
The two halves meet only on set membership and subsumption.
|
||||
|
||||
**2. Three of the reader's constructions are unreachable by SHAPE, not by
|
||||
vocabulary.** `member_query` and `subset_query` require `?`; no writer template
|
||||
contains one. Corpus work cannot close that gap — a template can.
|
||||
|
||||
**3. The reader fabricates on 22 constructions, and it is on a serving path.**
|
||||
Three distinct leaks, one root cause: `_RESERVED` does not contain the
|
||||
function-word vocabulary the writer actually emits.
|
||||
|
||||
| leak | example | minted |
|
||||
|---|---|---|
|
||||
| determiner glued to entity | `every dog is a mammal` | `member(every_dog, mammal)` |
|
||||
| verb phrase chunked as class | `all dogs are defined as mammals` | `subset(dog, defined_as_mammal)` |
|
||||
| connective promoted to proposition | `furthermore, all dogs are mammals` | `asserted(furthermore)` |
|
||||
|
||||
The first and third are **ordinary user English**, not writer artefacts. And the
|
||||
third reaches served output:
|
||||
|
||||
```
|
||||
"if p then q. p. therefore q." -> Given: p implies q; p. ...
|
||||
"furthermore, if p then q. p. therefore q." -> Given: furthermore; p implies q; p. ...
|
||||
```
|
||||
|
||||
CORE recites a premise the user never stated. `chat/runtime.py` additionally
|
||||
*realizes* declarative turns into the held self, so `asserted(furthermore)` is
|
||||
writable to the vault.
|
||||
|
||||
**4. ADR-0265's defect class survives in the module ADR-0265 named as the single
|
||||
owner of clause grammar.** The four aspect arms of `_inflect_predicate` bind
|
||||
`negated` to a wildcard and never read it:
|
||||
|
||||
```
|
||||
aspect=perfective affirm: dog has been defined as mammal
|
||||
denied: dog has been defined as mammal <-- identical
|
||||
```
|
||||
|
||||
10530 of 16146 writer parameter points collapse this way. It is **not reachable
|
||||
today** — no producer sets `aspect` on the serving path — so it is a loaded gun,
|
||||
not a casualty. It survived because ADR-0265's invariant is *structural* (is
|
||||
`negated` threaded to the call?) and cannot see an arm that receives the flag and
|
||||
ignores it. This lane's sentinel is behavioural, and does.
|
||||
|
||||
## What this lane does NOT prove
|
||||
|
||||
The overlap is a count of **shapes**, not a quality judgement. Six shared
|
||||
constructions that are all set-theoretic is a narrow grammar, and nothing here
|
||||
says the shared six are *well* written — `render_step` could emit stilted
|
||||
English inside every one of them and this lane would be satisfied.
|
||||
|
||||
It also says nothing about `MeaningGraph`/`PropositionGraph` type compatibility
|
||||
(§1.8). That question is still open and still un-forced: no case measured here
|
||||
fails for that reason and no other.
|
||||
|
||||
## Mutation guarantees
|
||||
|
||||
Every pin in `tests/test_construction_inventory.py` has been run against a
|
||||
mutation of its own subject and observed RED — 12 of 12, recorded in the Phase 5
|
||||
RESULT block of the plan. The two fabrication fixes (`every`/`each` into
|
||||
`_RESERVED`; discourse connectives stripped in `_split_clauses`) are among the
|
||||
mutations, so the defect pins are known to move when the defect is fixed.
|
||||
46
evals/construction_inventory/corpora.py
Normal file
46
evals/construction_inventory/corpora.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Lexical fillers — the controls that keep this a construction measurement.
|
||||
|
||||
The reader refuses for two unrelated reasons: it has no template for a shape
|
||||
(*construction*), or it cannot singularize a token (*morphology*). Folding those
|
||||
together produces a number that moves when someone adds a noun, which is not
|
||||
what Phase 5 needs to size.
|
||||
|
||||
So every construction is probed with several vocabularies. A construction counts
|
||||
as shared only when **all count-noun fillers agree**; a construction that reads
|
||||
under one filler and refuses under another is reported as ``filler_dependent``,
|
||||
which is the signature of a morphology gap rather than a missing template.
|
||||
|
||||
The mass-noun filler is deliberately excluded from the headline and reported on
|
||||
its own. Mass nouns have no plural, so the writer's own rule keeps them singular
|
||||
under a quantifier ("all evidence supports truth") — and the reader's
|
||||
categorical template *requires* a plural head. That is a real inventory
|
||||
mismatch, but it is a different one from "the reader has no template", and
|
||||
averaging it into the overlap rate would hide both.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from evals.construction_inventory.reader_space import LexicalFiller
|
||||
|
||||
#: Count-noun fillers — these determine the headline overlap.
|
||||
COUNT_FILLERS: tuple[LexicalFiller, ...] = (
|
||||
LexicalFiller("regular", "dog", "mammal", "dogs", "mammals", "p", "q"),
|
||||
LexicalFiller("f_plural", "wolf", "canine", "wolves", "canines", "r", "s"),
|
||||
LexicalFiller("suppletive", "child", "person", "children", "people", "u", "v"),
|
||||
)
|
||||
|
||||
#: Mass-noun filler — diagnostic only, never part of the headline.
|
||||
MASS_FILLER = LexicalFiller("mass", "evidence", "truth", "evidence", "truth", "m", "n")
|
||||
|
||||
ALL_FILLERS: tuple[LexicalFiller, ...] = (*COUNT_FILLERS, MASS_FILLER)
|
||||
|
||||
#: Third lexical slot, used by the frame intents that take a ``secondary``.
|
||||
SECONDARY: dict[str, str] = {
|
||||
"regular": "cat",
|
||||
"f_plural": "fox",
|
||||
"suppletive": "adult",
|
||||
"mass": "meaning",
|
||||
}
|
||||
|
||||
|
||||
__all__ = ("ALL_FILLERS", "COUNT_FILLERS", "MASS_FILLER", "SECONDARY")
|
||||
4
evals/construction_inventory/public/v1/cases.jsonl
Normal file
4
evals/construction_inventory/public/v1/cases.jsonl
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{"id": "filler-regular", "kind": "count_filler", "x": "dog", "y": "mammal", "plural_x": "dogs", "plural_y": "mammals", "secondary": "cat", "note": "regular -s plural; the default vocabulary"}
|
||||
{"id": "filler-f-plural", "kind": "count_filler", "x": "wolf", "y": "canine", "plural_x": "wolves", "plural_y": "canines", "secondary": "fox", "note": "-f/-ves; exercises IRREGULAR_SINGULARS in both directions"}
|
||||
{"id": "filler-suppletive", "kind": "count_filler", "x": "child", "y": "person", "plural_x": "children", "plural_y": "people", "secondary": "adult", "note": "suppletive plural; no suffix rule can reach it"}
|
||||
{"id": "filler-mass", "kind": "mass_filler", "x": "evidence", "y": "truth", "plural_x": "evidence", "plural_y": "truth", "secondary": "meaning", "note": "DIAGNOSTIC ONLY, excluded from the headline: mass nouns have no plural, so the writer keeps them singular under a quantifier and the reader's categorical template requires a plural head. A real inventory mismatch, but a different one from 'no template'."}
|
||||
128
evals/construction_inventory/reader_space.py
Normal file
128
evals/construction_inventory/reader_space.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""The reader's construction inventory, with a structural guard against rot.
|
||||
|
||||
A hand-written table of "what the reader reads" is worth nothing on its own —
|
||||
it drifts the first time someone adds a template, and then the overlap number
|
||||
silently measures a stale list. So the table is paired with a guard keyed on
|
||||
something the reader cannot add a construction without touching: a **mint
|
||||
site**, one of the ``relations.append`` / ``queries.append`` calls that are the
|
||||
only ways a proposition or query enters a ``Comprehension``.
|
||||
|
||||
Every construction below names the mint site it flows through. The guard
|
||||
(``test_construction_inventory.py``) AST-scans ``reader.py``, counts mint sites,
|
||||
and requires that count to equal the number of distinct sites named here — so a
|
||||
new template breaks the test until it is entered in this table. That is the
|
||||
``feedback-pin-registered-in-no-suite`` lesson applied one level up: a table
|
||||
that cannot go stale beats a table someone promises to update.
|
||||
|
||||
Constructions are keyed on SHAPE, not on predicate: ``all Xs are Ys`` and ``no
|
||||
Xs are Ys`` are two constructions through one mint site, because the reader
|
||||
dispatches on the quantifier token and a writer must emit that token to reach
|
||||
either.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
#: The reader's mint sites — the only routes into a ``Comprehension``.
|
||||
#: Named, not numbered, so a diff shows which route a change touched.
|
||||
MINT_RELATION_CATEGORICAL = "relations:categorical"
|
||||
MINT_RELATION_MEMBER = "relations:member"
|
||||
MINT_RELATION_LESS = "relations:less"
|
||||
MINT_RELATION_PROPOSITIONAL = "relations:propositional"
|
||||
MINT_QUERY_COMPARE = "queries:compare"
|
||||
MINT_QUERY_SORT = "queries:sort"
|
||||
MINT_QUERY_MEMBER = "queries:member"
|
||||
MINT_QUERY_SUBSET = "queries:subset"
|
||||
MINT_QUERY_THEREFORE_CATEGORICAL = "queries:therefore_categorical"
|
||||
MINT_QUERY_THEREFORE_PROPOSITIONAL = "queries:therefore_propositional"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReaderConstruction:
|
||||
"""One surface shape the reader claims to read, plus a canonical instance.
|
||||
|
||||
``instance`` is a template over the lane's lexical fillers, so the same
|
||||
construction is probed with several vocabularies and a morphology refusal
|
||||
shows up as filler-dependence rather than as a missing construction.
|
||||
"""
|
||||
|
||||
construction_id: str
|
||||
instance: str
|
||||
mint_site: str
|
||||
interrogative: bool = False
|
||||
|
||||
|
||||
#: The inventory. Order is stable so report diffs are readable.
|
||||
READER_CONSTRUCTIONS: tuple[ReaderConstruction, ...] = (
|
||||
# --- categorical facts: one mint site, four dispatch tokens -------------
|
||||
ReaderConstruction("subset_fact", "all {plural_x} are {plural_y}", MINT_RELATION_CATEGORICAL),
|
||||
ReaderConstruction("disjoint_fact", "no {plural_x} are {plural_y}", MINT_RELATION_CATEGORICAL),
|
||||
ReaderConstruction("intersects_fact", "some {plural_x} are {plural_y}", MINT_RELATION_CATEGORICAL),
|
||||
ReaderConstruction("some_not_fact", "some {plural_x} are not {plural_y}", MINT_RELATION_CATEGORICAL),
|
||||
# --- membership facts ---------------------------------------------------
|
||||
ReaderConstruction("member_fact", "{x} is a {y}", MINT_RELATION_MEMBER),
|
||||
ReaderConstruction("member_fact_definite", "the {x} is a {y}", MINT_RELATION_MEMBER),
|
||||
# --- ordering facts: one mint site, two comparator directions -----------
|
||||
ReaderConstruction("less_fact", "{x} is below {y}", MINT_RELATION_LESS),
|
||||
ReaderConstruction("greater_fact", "{x} is above {y}", MINT_RELATION_LESS),
|
||||
# --- propositional facts ------------------------------------------------
|
||||
ReaderConstruction("implies_fact", "if {p} then {q}", MINT_RELATION_PROPOSITIONAL),
|
||||
ReaderConstruction("negated_atom_fact", "not {p}", MINT_RELATION_PROPOSITIONAL),
|
||||
ReaderConstruction("or_fact", "{p} or {q}", MINT_RELATION_PROPOSITIONAL),
|
||||
ReaderConstruction("atom_fact", "{p}", MINT_RELATION_PROPOSITIONAL),
|
||||
# --- queries ------------------------------------------------------------
|
||||
ReaderConstruction("member_query", "is {x} a {y}?", MINT_QUERY_MEMBER, interrogative=True),
|
||||
ReaderConstruction("subset_query", "are all {plural_x} {plural_y}?", MINT_QUERY_SUBSET, interrogative=True),
|
||||
ReaderConstruction("compare_query", "compare {x} with {y}", MINT_QUERY_COMPARE),
|
||||
ReaderConstruction("sort_query", "sort ascending", MINT_QUERY_SORT),
|
||||
ReaderConstruction("sort_range_query", "order from lowest to highest", MINT_QUERY_SORT),
|
||||
ReaderConstruction(
|
||||
"therefore_categorical", "therefore all {plural_x} are {plural_y}",
|
||||
MINT_QUERY_THEREFORE_CATEGORICAL,
|
||||
),
|
||||
ReaderConstruction(
|
||||
"therefore_propositional", "therefore {q}", MINT_QUERY_THEREFORE_PROPOSITIONAL,
|
||||
),
|
||||
)
|
||||
|
||||
#: Distinct mint sites named above — the quantity the AST guard checks.
|
||||
READER_MINT_SITES: frozenset[str] = frozenset(c.mint_site for c in READER_CONSTRUCTIONS)
|
||||
|
||||
|
||||
def render_instance(construction: ReaderConstruction, filler: "LexicalFiller") -> str:
|
||||
"""Instantiate a construction's canonical surface with *filler*'s vocabulary."""
|
||||
return construction.instance.format(
|
||||
x=filler.x, y=filler.y,
|
||||
plural_x=filler.plural_x, plural_y=filler.plural_y,
|
||||
p=filler.p, q=filler.q,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LexicalFiller:
|
||||
"""One vocabulary for probing constructions.
|
||||
|
||||
Held separate from the constructions so that a refusal caused by *morphology*
|
||||
(an unrecognized plural) is distinguishable from a refusal caused by the
|
||||
*construction*. A construction counts as readable only when every filler
|
||||
agrees; disagreement is reported as ``filler_dependent`` rather than being
|
||||
averaged away.
|
||||
"""
|
||||
|
||||
name: str
|
||||
x: str
|
||||
y: str
|
||||
plural_x: str
|
||||
plural_y: str
|
||||
p: str
|
||||
q: str
|
||||
|
||||
|
||||
__all__ = (
|
||||
"LexicalFiller",
|
||||
"READER_CONSTRUCTIONS",
|
||||
"READER_MINT_SITES",
|
||||
"ReaderConstruction",
|
||||
"render_instance",
|
||||
)
|
||||
255
evals/construction_inventory/runner.py
Normal file
255
evals/construction_inventory/runner.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
"""Construction-inventory eval lane.
|
||||
|
||||
Sizes Phase 5 by measuring the reader's construction set against the writer's,
|
||||
in both directions, on the criterion that matters: **faithfulness, not
|
||||
acceptance**.
|
||||
|
||||
Why acceptance is the wrong criterion
|
||||
-------------------------------------
|
||||
``grammar_roundtrip`` reports ``g_read_rate`` alongside ``g_args_rate`` for
|
||||
exactly this reason, and the two disagree: on ``main`` the G-direction reads
|
||||
1 of 293 surfaces and recovers **0** argument pairs. The single surface that
|
||||
"reads" is ``all molecules are defined as compounds``, which the reader accepts
|
||||
as ``subset(molecule, defined_as_compound)`` — a class it invented from the
|
||||
writer's verb phrase. Counting that as overlap would have Phase 5 growing an
|
||||
inventory toward a number that rewards mis-reading.
|
||||
|
||||
So this lane splits acceptance three ways:
|
||||
|
||||
``faithful``
|
||||
the reader recovered the proposition the writer emitted, and minted nothing
|
||||
else. This is the only bucket that counts as overlap.
|
||||
``fabricating``
|
||||
the reader accepted the surface and minted at least one argument the writer
|
||||
never wrote. This is a ``wrong=0`` hazard on a serving path
|
||||
(``chat/runtime.py`` realizes declarative turns into the held self;
|
||||
``chat/deduction_surface.py`` recites premises back to the user), and it is
|
||||
ratcheted **downward**.
|
||||
``refused``
|
||||
the honest outcome — the reader has no template and says so.
|
||||
|
||||
Framework contract: ``run_lane(cases, config=None) -> LaneReport``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from generate.meaning_graph.reader import Refusal, comprehend
|
||||
|
||||
from evals.construction_inventory.corpora import (
|
||||
COUNT_FILLERS,
|
||||
MASS_FILLER,
|
||||
SECONDARY,
|
||||
)
|
||||
from evals.construction_inventory.reader_space import (
|
||||
READER_CONSTRUCTIONS,
|
||||
LexicalFiller,
|
||||
render_instance,
|
||||
)
|
||||
from evals.construction_inventory.skeleton import skeleton
|
||||
from evals.construction_inventory.writer_space import writer_cells
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaneReport:
|
||||
metrics: dict[str, Any] = field(default_factory=dict)
|
||||
case_details: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
FAITHFUL = "faithful"
|
||||
FABRICATING = "fabricating"
|
||||
REFUSED = "refused"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Verdict:
|
||||
"""What the reader did with one writer construction."""
|
||||
|
||||
bucket: str
|
||||
detail: str
|
||||
|
||||
|
||||
def _classify(surface: str, filler: LexicalFiller, secondary: str) -> Verdict:
|
||||
"""Read *surface* back and decide whether the reading is faithful.
|
||||
|
||||
The writer emitted a proposition over ``{x, y}`` (or ``{x, secondary}`` for
|
||||
the frame intents). Any argument outside that set is something the reader
|
||||
invented — a discourse connective promoted to a proposition, a verb phrase
|
||||
chunked into a class, a determiner glued onto an entity. Any *query* is also
|
||||
invention: the writer never emits an interrogative, so a question can only
|
||||
have been manufactured.
|
||||
"""
|
||||
reading = comprehend(surface)
|
||||
if isinstance(reading, Refusal):
|
||||
return Verdict(REFUSED, reading.reason)
|
||||
|
||||
permitted = {filler.x, filler.y, secondary}
|
||||
relations = reading.meaning_graph.relations
|
||||
|
||||
if reading.queries:
|
||||
minted = ",".join(q.predicate for q in reading.queries)
|
||||
return Verdict(FABRICATING, f"query_from_declarative:{minted}")
|
||||
|
||||
invented = sorted(
|
||||
{arg for rel in relations for arg in rel.arguments if arg not in permitted}
|
||||
)
|
||||
if invented:
|
||||
return Verdict(FABRICATING, f"invented_args:{'|'.join(invented)}")
|
||||
|
||||
recovered = any(
|
||||
tuple(rel.arguments) in ((filler.x, filler.y), (filler.x, secondary))
|
||||
for rel in relations
|
||||
)
|
||||
if not recovered:
|
||||
return Verdict(FABRICATING, "proposition_lost")
|
||||
return Verdict(FAITHFUL, ",".join(sorted({r.predicate for r in relations})))
|
||||
|
||||
|
||||
def _sweep_filler(filler: LexicalFiller) -> dict[str, Any]:
|
||||
"""Sweep the whole writer space under one vocabulary."""
|
||||
secondary = SECONDARY[filler.name]
|
||||
by_skeleton: dict[str, str] = {}
|
||||
cells_per_skeleton: Counter[str] = Counter()
|
||||
# Negation sentinel (ADR-0265): key each parameter point WITHOUT its polarity
|
||||
# so the two polarities land in one bucket. A point whose negated surface is
|
||||
# byte-identical to its affirmative is a proposition served as its own denial.
|
||||
polarity: dict[str, dict[bool, str]] = {}
|
||||
n_cells = 0
|
||||
|
||||
for cell in writer_cells(filler.x, filler.y, secondary):
|
||||
n_cells += 1
|
||||
sk = skeleton(cell.surface)
|
||||
by_skeleton.setdefault(sk, cell.surface)
|
||||
cells_per_skeleton[sk] += 1
|
||||
point = f"{cell.entry}|{cell.label.replace('neg=True', '').replace('neg=False', '')}"
|
||||
polarity.setdefault(point, {})[cell.negated] = cell.surface
|
||||
|
||||
negation_collapsed = sorted(
|
||||
point for point, both in polarity.items()
|
||||
if len(both) == 2 and both[True] == both[False]
|
||||
)
|
||||
|
||||
verdicts = {sk: _classify(s, filler, secondary) for sk, s in by_skeleton.items()}
|
||||
return {
|
||||
"filler": filler.name,
|
||||
"cells": n_cells,
|
||||
"skeletons": by_skeleton,
|
||||
"cells_per_skeleton": cells_per_skeleton,
|
||||
"verdicts": verdicts,
|
||||
"negation_collapsed": negation_collapsed,
|
||||
}
|
||||
|
||||
|
||||
def _reader_direction(writer_skeletons: frozenset[str]) -> dict[str, Any]:
|
||||
"""Which of the reader's constructions can the writer emit at all?"""
|
||||
rows = []
|
||||
for construction in READER_CONSTRUCTIONS:
|
||||
instance = render_instance(construction, COUNT_FILLERS[0])
|
||||
sk = skeleton(instance)
|
||||
reading = comprehend(instance)
|
||||
rows.append({
|
||||
"construction_id": construction.construction_id,
|
||||
"mint_site": construction.mint_site,
|
||||
"instance": instance,
|
||||
"skeleton": sk,
|
||||
"writer_can_emit": sk in writer_skeletons,
|
||||
"reader_reads_own": not isinstance(reading, Refusal),
|
||||
"interrogative": construction.interrogative,
|
||||
})
|
||||
return {"rows": rows}
|
||||
|
||||
|
||||
def _rate(num: int, den: int) -> float:
|
||||
return round(num / den, 6) if den else 0.0
|
||||
|
||||
|
||||
def run_lane(cases: Any = None, config: Any = None) -> LaneReport: # noqa: ARG001
|
||||
"""Measure the two construction inventories against each other."""
|
||||
sweeps = [_sweep_filler(f) for f in COUNT_FILLERS]
|
||||
mass = _sweep_filler(MASS_FILLER)
|
||||
|
||||
# A construction is shared only if EVERY count-noun filler agrees it is.
|
||||
per_filler_faithful = [
|
||||
{sk for sk, v in s["verdicts"].items() if v.bucket == FAITHFUL} for s in sweeps
|
||||
]
|
||||
per_filler_fabricating = [
|
||||
{sk for sk, v in s["verdicts"].items() if v.bucket == FABRICATING} for s in sweeps
|
||||
]
|
||||
faithful_all = set.intersection(*per_filler_faithful)
|
||||
faithful_any = set.union(*per_filler_faithful)
|
||||
fabricating_any = set.union(*per_filler_fabricating)
|
||||
filler_dependent = faithful_any - faithful_all
|
||||
|
||||
base = sweeps[0]
|
||||
all_skeletons = frozenset(base["skeletons"])
|
||||
reader_dir = _reader_direction(frozenset().union(*(frozenset(s["skeletons"]) for s in sweeps)))
|
||||
|
||||
n_constructions = len(all_skeletons)
|
||||
refusal_census = Counter(
|
||||
v.detail for v in base["verdicts"].values() if v.bucket == REFUSED
|
||||
)
|
||||
fabrication_census = Counter(
|
||||
v.detail.split(":")[0] for v in base["verdicts"].values() if v.bucket == FABRICATING
|
||||
)
|
||||
|
||||
cells_fabricating = sum(
|
||||
base["cells_per_skeleton"][sk] for sk in fabricating_any if sk in base["cells_per_skeleton"]
|
||||
)
|
||||
cells_faithful = sum(
|
||||
base["cells_per_skeleton"][sk] for sk in faithful_all if sk in base["cells_per_skeleton"]
|
||||
)
|
||||
|
||||
reader_rows = reader_dir["rows"]
|
||||
expressible = [r for r in reader_rows if r["writer_can_emit"]]
|
||||
unreadable_own = [r for r in reader_rows if not r["reader_reads_own"]]
|
||||
interrogative = [r for r in reader_rows if r["interrogative"]]
|
||||
|
||||
metrics: dict[str, Any] = {
|
||||
# --- W -> R: the writer's space, judged by the reader ---------------
|
||||
"writer_cells": base["cells"],
|
||||
"writer_constructions": n_constructions,
|
||||
"w2r_faithful": len(faithful_all),
|
||||
"w2r_fabricating": len(fabricating_any),
|
||||
"w2r_refused": n_constructions - len(faithful_any) - len(fabricating_any),
|
||||
"w2r_filler_dependent": len(filler_dependent),
|
||||
"w2r_faithful_rate": _rate(len(faithful_all), n_constructions),
|
||||
"w2r_fabricating_rate": _rate(len(fabricating_any), n_constructions),
|
||||
"cells_faithful": cells_faithful,
|
||||
"cells_fabricating": cells_fabricating,
|
||||
# --- R -> W: the reader's inventory, judged by the writer ------------
|
||||
"reader_constructions": len(reader_rows),
|
||||
"r2w_expressible": len(expressible),
|
||||
"r2w_rate": _rate(len(expressible), len(reader_rows)),
|
||||
"reader_constructions_unreadable_by_reader": len(unreadable_own),
|
||||
"reader_interrogative_constructions": len(interrogative),
|
||||
# --- the headline -----------------------------------------------------
|
||||
"overlap_constructions": len(faithful_all),
|
||||
# --- sentinels --------------------------------------------------------
|
||||
"negation_collapsed_points": len(base["negation_collapsed"]),
|
||||
"mass_noun_faithful": len(
|
||||
[1 for v in mass["verdicts"].values() if v.bucket == FAITHFUL]
|
||||
),
|
||||
}
|
||||
|
||||
details: list[dict[str, Any]] = [
|
||||
{
|
||||
"kind": "writer_construction",
|
||||
"skeleton": sk,
|
||||
"example": base["skeletons"][sk],
|
||||
"cells": base["cells_per_skeleton"][sk],
|
||||
"bucket": base["verdicts"][sk].bucket,
|
||||
"detail": base["verdicts"][sk].detail,
|
||||
"shared_by_all_fillers": sk in faithful_all,
|
||||
}
|
||||
for sk in sorted(all_skeletons, key=lambda s: -base["cells_per_skeleton"][s])
|
||||
]
|
||||
details.extend({"kind": "reader_construction", **row} for row in reader_rows)
|
||||
details.append({"kind": "refusal_census", **dict(refusal_census)})
|
||||
details.append({"kind": "fabrication_census", **dict(fabrication_census)})
|
||||
|
||||
return LaneReport(metrics=metrics, case_details=details)
|
||||
|
||||
|
||||
__all__ = ("FABRICATING", "FAITHFUL", "REFUSED", "LaneReport", "Verdict", "run_lane")
|
||||
61
evals/construction_inventory/skeleton.py
Normal file
61
evals/construction_inventory/skeleton.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""The construction quotient: a surface reduced to what the reader parses by.
|
||||
|
||||
The reader is documented as reading "STRUCTURE symbolically from the token
|
||||
sequence via domain-agnostic templates keyed on FUNCTION WORDS + ORDER". Take
|
||||
that literally and it defines an equivalence relation on surfaces: replace every
|
||||
token the reader does *not* key on with a placeholder, and two surfaces with the
|
||||
same result are — by the reader's own design — indistinguishable to its parser.
|
||||
|
||||
That relation is what makes "how many constructions?" a countable question
|
||||
instead of a judgement call. The quotient is not chosen by this module; it is
|
||||
read off ``reader._RESERVED``, so it cannot be gerrymandered to flatter a
|
||||
number, and it widens automatically when the reader learns a new function word.
|
||||
|
||||
**The one place the abstraction leaks**, stated here because a silent leak in a
|
||||
measurement instrument is worse than the defect it measures: ``_chunk_class``
|
||||
refuses on an unrecognized plural, which depends on the *token*, not on whether
|
||||
the token is reserved. So the skeleton determines the reader's verdict **up to
|
||||
morphology**. The lane handles this by sweeping several lexical fillers per
|
||||
construction and only counting a construction as shared when every filler
|
||||
agrees — see ``runner.py``. Morphology refusals are then visible as
|
||||
filler-dependent disagreement rather than being silently folded into the
|
||||
construction count.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from generate.meaning_graph.reader import _RESERVED
|
||||
|
||||
#: Function words the reader keys on that ``_RESERVED`` does not list.
|
||||
#:
|
||||
#: ``_parse_propositional`` dispatches on ``if`` and ``then``, but neither is in
|
||||
#: ``_RESERVED`` — so ``_chunk`` will happily swallow them into a noun phrase.
|
||||
#: That asymmetry is a reader defect (see contract.md, "What this lane found"),
|
||||
#: not an artefact of this module; it is recorded here so the quotient reflects
|
||||
#: the tokens the parser *actually* branches on rather than the ones it declares.
|
||||
_UNDECLARED_KEYWORDS: frozenset[str] = frozenset({"if", "then"})
|
||||
|
||||
#: The reader's full function-word vocabulary, derived from source.
|
||||
READER_FUNCTION_WORDS: frozenset[str] = frozenset(_RESERVED) | _UNDECLARED_KEYWORDS
|
||||
|
||||
#: Stands in for any token the reader treats as content.
|
||||
CONTENT = "*"
|
||||
|
||||
_PUNCT = ".,;:?!"
|
||||
|
||||
|
||||
def skeleton(surface: str) -> str:
|
||||
"""Reduce *surface* to its reader-visible construction.
|
||||
|
||||
Content tokens collapse to ``*``; function words and attached punctuation
|
||||
survive, because both change how the reader splits and dispatches.
|
||||
"""
|
||||
out: list[str] = []
|
||||
for token in surface.lower().split():
|
||||
core = token.strip(_PUNCT)
|
||||
trailing = token[len(core):] if core and token.startswith(core) else ""
|
||||
out.append((core if core in READER_FUNCTION_WORDS else CONTENT) + trailing)
|
||||
return " ".join(out)
|
||||
|
||||
|
||||
__all__ = ("CONTENT", "READER_FUNCTION_WORDS", "skeleton")
|
||||
128
evals/construction_inventory/writer_space.py
Normal file
128
evals/construction_inventory/writer_space.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""The writer's construction space, enumerated from live source.
|
||||
|
||||
Every axis below is pulled from the table or enum the writer actually consults,
|
||||
so the space grows the moment the writer grows. Nothing here is a hand-copied
|
||||
list of what the writer "can say" — that is the failure mode this lane exists to
|
||||
avoid, and the reason ``docs/plans/grammar-unification-2026-07-26.md`` §6 could
|
||||
report a rate without knowing what the rate was about.
|
||||
|
||||
Two axes cannot be read off a table because the writer does not keep one:
|
||||
|
||||
* **quantifier** — ``render_step`` accepts any string. ``PLURAL_QUANTIFIERS`` is
|
||||
authoritative for the plural half; the singular determiners are a *probe set*,
|
||||
labelled as such, not an inventory claim.
|
||||
* **tense / aspect** — these are literals in ``_inflect_predicate``'s ``match``
|
||||
arms. They are declared here for readability and pinned against the source by
|
||||
``test_construction_inventory.py``, which AST-scans those arms and fails if a
|
||||
value appears there and not here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator
|
||||
|
||||
from generate.graph_planner import RhetoricalMove
|
||||
from generate.intent import IntentTag
|
||||
from generate.lexicon import PLURAL_QUANTIFIERS, PREDICATE_DISPLAY, PREDICATIVE_NOMINAL
|
||||
from generate.semantic_templates import render_semantic
|
||||
from generate.templates import render_step
|
||||
|
||||
#: Every predicate the writer can humanize. ``PREDICATIVE_NOMINAL`` contributes
|
||||
#: ``is_a``, which has no ``PREDICATE_DISPLAY`` entry (it humanizes by the
|
||||
#: underscore rule) and is the *only* predicate that reaches the reader's
|
||||
#: membership template, so omitting it would erase the overlap being measured.
|
||||
WRITER_PREDICATES: tuple[str, ...] = tuple(
|
||||
sorted(set(PREDICATE_DISPLAY) | set(PREDICATIVE_NOMINAL))
|
||||
)
|
||||
|
||||
#: Singular determiners — a probe, not an inventory (see module docstring).
|
||||
SINGULAR_DETERMINER_PROBES: tuple[str, ...] = ("the", "every", "each", "a")
|
||||
|
||||
WRITER_QUANTIFIERS: tuple[str | None, ...] = (
|
||||
None,
|
||||
*sorted(PLURAL_QUANTIFIERS),
|
||||
*SINGULAR_DETERMINER_PROBES,
|
||||
)
|
||||
|
||||
#: Pinned against ``_inflect_predicate``'s match arms by AST scan.
|
||||
WRITER_TENSES: tuple[str | None, ...] = (None, "past", "future")
|
||||
WRITER_ASPECTS: tuple[str | None, ...] = (None, "perfective", "imperfective")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WriterCell:
|
||||
"""One point in the writer's parameter space, and the surface it emits."""
|
||||
|
||||
entry: str
|
||||
surface: str
|
||||
negated: bool
|
||||
predicate: str
|
||||
quantifier: str | None
|
||||
label: str
|
||||
|
||||
|
||||
def _step_cells(subject: str, obj: str) -> Iterator[WriterCell]:
|
||||
for move, predicate, negated, quant, tense, aspect in itertools.product(
|
||||
tuple(RhetoricalMove),
|
||||
WRITER_PREDICATES,
|
||||
(False, True),
|
||||
WRITER_QUANTIFIERS,
|
||||
WRITER_TENSES,
|
||||
WRITER_ASPECTS,
|
||||
):
|
||||
surface = render_step(
|
||||
move, subject, predicate, obj,
|
||||
negated=negated, quantifier=quant, tense=tense, aspect=aspect,
|
||||
)
|
||||
yield WriterCell(
|
||||
entry="render_step",
|
||||
surface=surface,
|
||||
negated=negated,
|
||||
predicate=predicate,
|
||||
quantifier=quant,
|
||||
label=f"{move.name}/{predicate}/neg={negated}/q={quant}/{tense}/{aspect}",
|
||||
)
|
||||
|
||||
|
||||
def _semantic_cells(subject: str, obj: str, secondary: str) -> Iterator[WriterCell]:
|
||||
for intent, predicate, negated in itertools.product(
|
||||
tuple(IntentTag), WRITER_PREDICATES, (False, True)
|
||||
):
|
||||
surface = render_semantic(
|
||||
intent, subject, predicate, obj, secondary=secondary, negated=negated,
|
||||
)
|
||||
yield WriterCell(
|
||||
entry="render_semantic",
|
||||
surface=surface,
|
||||
negated=negated,
|
||||
predicate=predicate,
|
||||
quantifier=None,
|
||||
label=f"{intent.name}/{predicate}/neg={negated}",
|
||||
)
|
||||
|
||||
|
||||
def writer_cells(subject: str, obj: str, secondary: str) -> Iterator[WriterCell]:
|
||||
"""Every surface the writer emits for one lexical filler triple.
|
||||
|
||||
Both public entry points are swept. ``render_semantic`` is the **serving**
|
||||
writer (``core/cognition/pipeline.py``); ``render_step`` is the clause owner
|
||||
it delegates to and is also reached directly by the eval-only
|
||||
``realize_target``. Measuring only the serving entry would understate the
|
||||
grammar's reach; measuring only the clause owner would overstate what ships.
|
||||
The lane reports both and keeps ``entry`` on every cell.
|
||||
"""
|
||||
yield from _step_cells(subject, obj)
|
||||
yield from _semantic_cells(subject, obj, secondary)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SINGULAR_DETERMINER_PROBES",
|
||||
"WRITER_ASPECTS",
|
||||
"WRITER_PREDICATES",
|
||||
"WRITER_QUANTIFIERS",
|
||||
"WRITER_TENSES",
|
||||
"WriterCell",
|
||||
"writer_cells",
|
||||
)
|
||||
|
|
@ -69,6 +69,17 @@ only the *vocabulary* is split — a materially different remedy from both being
|
|||
low. Collapsing them into one boolean would hide the distinction that decides
|
||||
this arc's direction (plan §6).
|
||||
|
||||
> **Never quote `g_read_rate` without `g_args_rate`.** They currently disagree:
|
||||
> `g_read_rate = 0.003413` with `g_args_rate = 0.0`. The single surface that
|
||||
> "reads" is `all molecules are defined as compounds`, which the reader accepts
|
||||
> as `subset(molecule, defined_as_compound)` — it chunked the writer's verb
|
||||
> phrase into a class name. **Acceptance is not comprehension**, and a reader
|
||||
> scored on whether it returned a graph rather than on whether the graph is the
|
||||
> one it was given is rewarded for guessing. Plan §6's RESULT drew a conclusion
|
||||
> from `g_read_rate` alone and had to be corrected; see
|
||||
> `evals/construction_inventory/contract.md`, which measures the same question
|
||||
> against the writer's whole output space instead of a 293-case corpus.
|
||||
|
||||
## Corpora
|
||||
|
||||
| corpus | source | size |
|
||||
|
|
|
|||
316
tests/test_construction_inventory.py
Normal file
316
tests/test_construction_inventory.py
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
"""Pins for the construction-inventory lane (grammar-unification Phase 5, item 1).
|
||||
|
||||
Three kinds of test live here, and the difference matters:
|
||||
|
||||
**Measurements** record what the two inventories currently share. ``overlap`` is
|
||||
ratcheted *upward* — that is the number Phase 5 exists to raise.
|
||||
|
||||
**Defect pins** record something that is wrong. Each says so in its docstring and
|
||||
states which direction it may be revised. ``w2r_fabricating`` may only be
|
||||
revised *downward*; raising it to accommodate a new fabrication would be
|
||||
recording a regression as progress.
|
||||
|
||||
**Structural guards** are AST-keyed so the tables cannot rot. A table of "what
|
||||
the reader reads" that someone must remember to update is worth nothing — the
|
||||
guards make forgetting fail loudly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from generate.graph_planner import RhetoricalMove
|
||||
from generate.meaning_graph.reader import Refusal, comprehend
|
||||
from generate.templates import render_step
|
||||
|
||||
from evals.construction_inventory.corpora import COUNT_FILLERS, MASS_FILLER, SECONDARY
|
||||
from evals.construction_inventory.reader_space import (
|
||||
READER_CONSTRUCTIONS,
|
||||
READER_MINT_SITES,
|
||||
render_instance,
|
||||
)
|
||||
from evals.construction_inventory.runner import run_lane
|
||||
from evals.construction_inventory.skeleton import skeleton
|
||||
from evals.construction_inventory.writer_space import WRITER_ASPECTS, WRITER_TENSES
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Measurements
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_overlap_is_six_constructions() -> None:
|
||||
"""The headline. Phase 5 item 1 raises this; it may be revised UPWARD only.
|
||||
|
||||
Six constructions are shared: ``X is a Y``, ``the X is a Y``, and the four
|
||||
categoricals (``all`` / ``no`` / ``some`` / ``some ... not``). All six are
|
||||
set-theoretic. The writer cannot express a single ordering, propositional or
|
||||
interrogative construction, and the reader has no template for the bare
|
||||
transitive clause that is the writer's entire default output.
|
||||
"""
|
||||
metrics = run_lane().metrics
|
||||
assert metrics["overlap_constructions"] == 6
|
||||
assert metrics["w2r_faithful"] == 6
|
||||
assert metrics["r2w_expressible"] == 6
|
||||
|
||||
|
||||
def test_the_overlap_is_the_same_six_seen_from_both_sides() -> None:
|
||||
"""Internal consistency: the two directions must name the same set.
|
||||
|
||||
``w2r_faithful`` counts writer constructions the reader reads truly;
|
||||
``r2w_expressible`` counts reader constructions the writer can emit. These
|
||||
are computed by different code over different corpora, and if they ever
|
||||
disagree, one of the two inventories is measuring something else.
|
||||
"""
|
||||
report = run_lane()
|
||||
faithful = {
|
||||
d["skeleton"] for d in report.case_details
|
||||
if d.get("kind") == "writer_construction" and d["bucket"] == "faithful"
|
||||
}
|
||||
expressible = {
|
||||
d["skeleton"] for d in report.case_details
|
||||
if d.get("kind") == "reader_construction" and d["writer_can_emit"]
|
||||
}
|
||||
assert faithful == expressible
|
||||
|
||||
|
||||
def test_reader_side_rate_is_pinned() -> None:
|
||||
"""The reader's own inventory is 19 constructions; the writer reaches 6."""
|
||||
metrics = run_lane().metrics
|
||||
assert metrics["reader_constructions"] == 19
|
||||
assert metrics["r2w_rate"] == 0.315789
|
||||
|
||||
|
||||
def test_the_shared_six_do_not_depend_on_vocabulary() -> None:
|
||||
"""Morphology control: the overlap must be the same under every filler.
|
||||
|
||||
If a construction reads for ``dogs`` but not for ``wolves`` or ``children``,
|
||||
the lane would be reporting a morphology gap as a construction gap. Zero
|
||||
filler-dependence is what licenses calling this a *construction* count.
|
||||
"""
|
||||
assert run_lane().metrics["w2r_filler_dependent"] == 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Defect pins
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_reader_fabricates_on_twenty_two_writer_constructions() -> None:
|
||||
"""DEFECT PIN. Revise DOWNWARD only — never up to admit a new fabrication.
|
||||
|
||||
On these the reader neither reads nor refuses: it accepts the surface and
|
||||
mints an argument the writer never wrote. That breaks the reader's own
|
||||
stated contract ("a fabricated reading is the only failure") and it is on a
|
||||
serving path, so it is a ``wrong=0`` hazard, not a cosmetic one.
|
||||
"""
|
||||
metrics = run_lane().metrics
|
||||
assert metrics["w2r_fabricating"] == 22
|
||||
assert metrics["cells_fabricating"] == 150
|
||||
|
||||
|
||||
def test_reader_invents_propositions_from_ordinary_english() -> None:
|
||||
"""DEFECT PIN. These are user-typable sentences, not writer artefacts.
|
||||
|
||||
``every``/``each`` are not in ``_RESERVED``, so they are chunked into the
|
||||
entity id and the asserted fact is about an entity that does not exist. A
|
||||
later query about ``dog`` then answers UNKNOWN even though the user said it.
|
||||
"""
|
||||
reading = comprehend("every dog is a mammal")
|
||||
assert not isinstance(reading, Refusal)
|
||||
assert [tuple(r.arguments) for r in reading.meaning_graph.relations] == [
|
||||
("every_dog", "mammal")
|
||||
]
|
||||
|
||||
reading = comprehend("each metal is a conductor")
|
||||
assert not isinstance(reading, Refusal)
|
||||
assert [tuple(r.arguments) for r in reading.meaning_graph.relations] == [
|
||||
("each_metal", "conductor")
|
||||
]
|
||||
|
||||
|
||||
def test_reader_promotes_a_discourse_connective_to_a_proposition() -> None:
|
||||
"""DEFECT PIN. ``_parse_propositional`` accepts ANY single token as a fact.
|
||||
|
||||
``_split_clauses`` splits on commas and strips only a leading ``and``/``or``,
|
||||
so a one-word discourse opener becomes its own clause, and the bare-atom rule
|
||||
mints it. CORE then recites it back to the user as a premise — see
|
||||
``test_fabricated_premise_reaches_a_served_deduction_surface``.
|
||||
"""
|
||||
reading = comprehend("furthermore, all dogs are mammals")
|
||||
assert not isinstance(reading, Refusal)
|
||||
minted = [(r.predicate, tuple(r.arguments)) for r in reading.meaning_graph.relations]
|
||||
assert ("asserted", ("furthermore",)) in minted
|
||||
|
||||
|
||||
def test_fabricated_premise_reaches_a_served_deduction_surface() -> None:
|
||||
"""DEFECT PIN — this is the one that shows the hazard is not theoretical.
|
||||
|
||||
The verdict is unaffected here, but the *recital* is: CORE tells the user
|
||||
they said ``furthermore``. Revise only by removing the fabrication.
|
||||
"""
|
||||
from chat.deduction_surface import deduction_grounded_surface
|
||||
|
||||
clean = deduction_grounded_surface("if p then q. p. therefore q.")
|
||||
leaked = deduction_grounded_surface("furthermore, if p then q. p. therefore q.")
|
||||
|
||||
assert clean == "Given: p implies q; p. Your premises entail: q."
|
||||
assert leaked == "Given: furthermore; p implies q; p. Your premises entail: q."
|
||||
|
||||
|
||||
def test_aspect_arms_drop_negation() -> None:
|
||||
"""DEFECT PIN — ADR-0265's defect class, surviving in the module ADR-0265
|
||||
named as the single owner of clause grammar.
|
||||
|
||||
The four aspect arms of ``_inflect_predicate`` match on ``(aspect, ...)``
|
||||
with ``negated`` bound to a wildcard and never consult it, so a denial is
|
||||
rendered as its own assertion. ADR-0265's invariant is *structural* — it
|
||||
checks that ``negated`` is threaded to each call — and cannot see an arm that
|
||||
receives the flag and ignores it. This pin is behavioural and can.
|
||||
|
||||
Not reachable today: no producer sets ``aspect`` on the serving path (only
|
||||
``generate/realizer.py`` forwards ``step.aspect``, which is always None). It
|
||||
is a loaded gun, not a casualty — the distinction ADR-0265 got wrong once
|
||||
already, so it is stated explicitly rather than assumed.
|
||||
|
||||
Revise DOWNWARD only; ``0`` is the fixed state.
|
||||
"""
|
||||
assert run_lane().metrics["negation_collapsed_points"] == 10530
|
||||
|
||||
for aspect in ("perfective", "imperfective"):
|
||||
affirmed = render_step(
|
||||
RhetoricalMove.ASSERT, "dog", "is_defined_as", "mammal",
|
||||
negated=False, aspect=aspect,
|
||||
)
|
||||
denied = render_step(
|
||||
RhetoricalMove.ASSERT, "dog", "is_defined_as", "mammal",
|
||||
negated=True, aspect=aspect,
|
||||
)
|
||||
assert affirmed == denied, f"aspect={aspect} now distinguishes polarity — revise DOWN"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Structural guards
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _mint_sites(path: Path) -> list[tuple[str, int]]:
|
||||
"""Every ``relations.append`` / ``queries.append`` in *path*."""
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
found: list[tuple[str, int]] = []
|
||||
for node in ast.walk(tree):
|
||||
if (
|
||||
isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr == "append"
|
||||
and isinstance(node.func.value, ast.Name)
|
||||
and node.func.value.id in ("relations", "queries")
|
||||
):
|
||||
found.append((node.func.value.id, node.lineno))
|
||||
return found
|
||||
|
||||
|
||||
def test_reader_inventory_covers_every_mint_site() -> None:
|
||||
"""STRUCTURAL GUARD. A new reader template must be entered in the table.
|
||||
|
||||
A ``Comprehension`` can only be populated through a ``relations.append`` or
|
||||
``queries.append`` call, so the count of those calls bounds the number of
|
||||
constructions. When someone adds a template, this goes red until
|
||||
``READER_CONSTRUCTIONS`` names its mint site — which is the only thing
|
||||
keeping the overlap number honest as the reader grows.
|
||||
"""
|
||||
sites = _mint_sites(_REPO / "generate" / "meaning_graph" / "reader.py")
|
||||
assert len(sites) == 10, f"reader mint sites changed: {sites}"
|
||||
assert len(READER_MINT_SITES) == 10
|
||||
|
||||
|
||||
def test_every_reader_construction_is_read_by_the_reader() -> None:
|
||||
"""STRUCTURAL GUARD. The table must describe the reader, not an aspiration.
|
||||
|
||||
A construction the reader refuses would inflate ``reader_constructions`` and
|
||||
depress ``r2w_rate`` for a reason that has nothing to do with the writer.
|
||||
"""
|
||||
for construction in READER_CONSTRUCTIONS:
|
||||
for filler in COUNT_FILLERS:
|
||||
instance = render_instance(construction, filler)
|
||||
reading = comprehend(instance)
|
||||
assert not isinstance(reading, Refusal), (
|
||||
f"{construction.construction_id} under {filler.name}: "
|
||||
f"{instance!r} -> {reading}"
|
||||
)
|
||||
|
||||
|
||||
def _match_arm_literals(path: Path, function: str) -> set[str]:
|
||||
"""String literals appearing in *function*'s ``match`` case PATTERNS."""
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
literals: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if not (isinstance(node, ast.FunctionDef) and node.name == function):
|
||||
continue
|
||||
for stmt in ast.walk(node):
|
||||
if not isinstance(stmt, ast.Match):
|
||||
continue
|
||||
for case in stmt.cases:
|
||||
for pat in ast.walk(case.pattern):
|
||||
if isinstance(pat, ast.MatchValue) and isinstance(pat.value, ast.Constant):
|
||||
if isinstance(pat.value.value, str):
|
||||
literals.add(pat.value.value)
|
||||
return literals
|
||||
|
||||
|
||||
def test_declared_writer_axes_cover_the_inflection_arms() -> None:
|
||||
"""STRUCTURAL GUARD. The sweep must not miss a tense or aspect.
|
||||
|
||||
``WRITER_TENSES`` / ``WRITER_ASPECTS`` are declared for readability, so they
|
||||
are pinned against the arms of ``_inflect_predicate``. A new aspect added to
|
||||
the writer without being added to the sweep would silently shrink the space
|
||||
this lane claims to measure.
|
||||
"""
|
||||
literals = _match_arm_literals(_REPO / "generate" / "templates.py", "_inflect_predicate")
|
||||
declared = {v for v in (*WRITER_TENSES, *WRITER_ASPECTS) if v is not None}
|
||||
assert literals - declared == set(), f"undeclared inflection axis values: {literals - declared}"
|
||||
|
||||
|
||||
def test_committed_corpus_matches_the_fillers_in_use() -> None:
|
||||
"""STRUCTURAL GUARD. The reviewable corpus must be the one that ran.
|
||||
|
||||
``corpora.py`` holds the fillers as typed objects and ``public/v1/cases.jsonl``
|
||||
is what a reviewer reads. Two copies of one fact is how this codebase got two
|
||||
grammars; since the runner cannot import a JSON file as a dataclass without
|
||||
losing the typing, they stay separate and this guard keeps them identical.
|
||||
"""
|
||||
path = _REPO / "evals" / "construction_inventory" / "public" / "v1" / "cases.jsonl"
|
||||
committed = [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
|
||||
|
||||
in_use = [
|
||||
{
|
||||
"id": f"filler-{f.name.replace('_', '-')}",
|
||||
"kind": "count_filler" if f in COUNT_FILLERS else "mass_filler",
|
||||
"x": f.x, "y": f.y,
|
||||
"plural_x": f.plural_x, "plural_y": f.plural_y,
|
||||
"secondary": SECONDARY[f.name],
|
||||
}
|
||||
for f in (*COUNT_FILLERS, MASS_FILLER)
|
||||
]
|
||||
assert [{k: v for k, v in row.items() if k != "note"} for row in committed] == in_use
|
||||
|
||||
|
||||
def test_writer_emits_no_interrogative_construction() -> None:
|
||||
"""STRUCTURAL GUARD. Three reader query templates are unreachable BY SHAPE.
|
||||
|
||||
``member_query`` and ``subset_query`` need a ``?``; no writer template
|
||||
contains one, so no amount of vocabulary work reaches them. Recording this
|
||||
as a shape fact keeps Phase 5 from trying to close that gap with corpora.
|
||||
"""
|
||||
report = run_lane()
|
||||
writer_skeletons = {
|
||||
d["skeleton"] for d in report.case_details if d.get("kind") == "writer_construction"
|
||||
}
|
||||
assert not any("?" in sk for sk in writer_skeletons)
|
||||
|
||||
interrogative = [c for c in READER_CONSTRUCTIONS if c.interrogative]
|
||||
assert len(interrogative) == 2
|
||||
for construction in interrogative:
|
||||
instance = render_instance(construction, COUNT_FILLERS[0])
|
||||
assert skeleton(instance) not in writer_skeletons
|
||||
Loading…
Reference in a new issue