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
316 lines
14 KiB
Python
316 lines
14 KiB
Python
"""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
|