core/tests/test_construction_inventory.py
Claude 9cf6a164d5
feat(evals,chat,tests): PR-12 — the Wilson re-count. 25 licensed bands become 4 (R-13)
Rank 5 on the docket, and the first item this arc whose expected outcome is
LOSING capability.

conservative_floor is a one-sided Wilson lower bound, and Wilson assumes
independent trials. CORE's pipeline is deterministic, so replaying an identical
case is not a second trial — it is the same trial with a guaranteed outcome.
The deduction sealer folded the raw practice corpus and counted every replay,
so bands recorded 720/720 committed whether that was 720 decisions or 28
decisions seen 26 times each. conservative_floor(720,720)=0.990868 cleared
theta_SERVE=0.99; conservative_floor(28,28)=0.808413 is not close.

RESULT: 25 licensed bands -> 4. Twenty-one demoted.

Survivors: en_conditional_chain, en_disjunctive, en_verb_fact,
en_verb_universal — the only bands whose corpus holds enough independent cases.

What the demotion did and did not do, stated exactly. NO ANSWER CHANGED. No
answer became wrong. `wrong` stayed 0 across all 25 bands. The engine is
exactly as correct as it was. What changed is the CLAIM attached to the answer:
21 bands now serve the same sound conclusion prefixed with "(reasoned, but I
haven't yet earned a verified track record on arguments of this shape)". The
reasoning did not get worse; the boast did.

The producer is hardened, which is the half that stops this recurring.
seal_ledger now refuses outright — assert_sealed_evidence_distinct compares the
ledger about to be written against the corpus's distinct-case count and raises
BEFORE anything is written. The curriculum sealer has carried this guarantee
since ADR-0264 R9; the deduction sealer had no equivalent, which is exactly how
the exposure arose. Catching it at seal time rather than in an audit matters:
an audit finds a padded ledger after it is committed, trusted, and gating a
live flag, and unwinding that then needs a ruling — it needed one.

A HOLLOW GUARD OF MY OWN, caught by sabotage rather than trusted. The first
version compared all_gold_problems() with distinct_gold_problems() — two
functions that agree with each other by construction, and neither of which is
what build_ledger folds. Reverting build_ledger to the raw corpus walked
straight past it AND re-sealed the inflated artifact. Same defect class the
ledger already suffered: a guard checking something adjacent to the thing it
protects. It now takes the BUILT LEDGER as input, because the invariant is
about the artifact. Re-running the identical attack: refused, wrote nothing.
Pinned as test_sealer_refuses_a_ledger_that_counts_replays.

A SECOND MISS, caught by the gate rather than by me. My blast-radius scan
grepped for deduction_serve_license / deduction_serve_ledger / 720 and found
nine files. It missed tests/test_construction_inventory.py, which asserts
deduction SURFACE STRINGS and never mentions the ledger — so a pattern search
over the wrong noun could not see it. The full gate found it. Recorded because
"I scanned for the blast radius" is worth exactly as much as the scan's key,
and mine was the wrong key.

That test is the #138 fabrication defect pin, and it is updated WITHOUT being
weakened. Both surfaces now carry the disclosure prefix, and the fabrication
survives untouched — which is a sharper finding than before: a disclosure
prefix looks like it might cover the hazard, and it does not. The user is still
told they said "furthermore". Exact-match assertions kept on the full strings
so neither the fabrication nor the licence state can drift unnoticed, plus an
isolating assertion that the only difference between clean and leaked recitals
is the fabricated premise.

Four records corrected in the same PR, each of which would otherwise have gone
on asserting capability that no longer exists:
  - CAPABILITY_LEDGERS' manifest note ("25 bands at 720/720 wrong=0")
  - the published CLAIMS.md claim — caught immediately by G-22's own pin, which
    is that fix earning its keep three days after landing
  - test_volume_honesty.py's module docstring, which said the exposure was
    "NOT silently repaired ... changing it is Shay's ratification, not a
    test's". R-13 IS that ratification, so the line went stale by being acted on
  - the exposure inventory's framing: it now pins the repair, not the gap

test_volume_honesty's sealed-vs-producer assertion is strictly stronger than it
was. It compared sealed committed counts to the producer's committed counts and
both were the inflated 720 — the assertion held perfectly while the number it
agreed on was the wrong number. Two records agreeing is not evidence that
either is right. It now pins committed == distinct, plus a non-vacuity check.

Nine tests encoded the unearned expectation and were updated to the honest one.

Closes G-19 and H-1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wcw2pnMBwyvmNyQg4uPEt4
2026-07-28 07:42:58 +00:00

338 lines
15 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.
**Updated 2026-07-28 (R-13), and deliberately not weakened.** The re-count
demoted ``conditional_single``, so both surfaces now carry the unverified-shape
disclosure. The prefix is licensing, not comprehension: it says *"I have not
earned a track record on this argument shape"*, which is a true statement about
evidence volume and says nothing about whether the recited premises are real.
**The fabrication survives the demotion untouched** — that is the finding, and it
is sharper now than before: a disclosure prefix might look like it covers the
hazard, and it does not. The user is still told they said ``furthermore``.
The assertion therefore stays exact-match on the full string, including the
prefix, so neither the fabrication nor the licence state can drift unnoticed.
"""
from chat.deduction_surface import deduction_grounded_surface
disclosure = (
"(reasoned, but I haven't yet earned a verified track record on "
"arguments of this shape) "
)
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 == disclosure + "Given: p implies q; p. Your premises entail: q."
assert leaked == disclosure + "Given: furthermore; p implies q; p. Your premises entail: q."
# The defect, isolated from the licence state: the ONLY difference between a
# clean and a leaked recital is the fabricated premise. If this ever passes
# because both surfaces became refusals, the exact-match assertions above fail
# first — which is why they are kept rather than replaced by this.
assert leaked.replace("furthermore; ", "") == clean
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