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
255 lines
10 KiB
Python
255 lines
10 KiB
Python
"""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")
|