← board

The corpus ladder scan passes one -Fu root, so cross-package imports read as compiler walls

Found by Track A, 2026-08-17, while fixing bug-a-package-and-sibling-module-resolution-is-the-corpus-wall.

The defect

The ladder scan passes only the scanned file's own candidate root on -Fu. tinycss2/bytes.py does from webencodings import ... — a cross-package import. With one root it fails and is recorded as a wall; with both roots it resolves immediately and the wall moves to undefined variable (CodecInfo), the known mimic_codecs gap already tracked.

Why this is worth a ticket rather than a tweak

That single row was 6-7 files, the largest entry in the wall table, and it was used to rank webencodings as the top lever and file a Track A resolution ticket. The compiler bug it implied does not exist.

A measurement artefact that survives is worse than a missing measurement, because it is actionable and wrong: it does not merely fail to inform, it actively dispatches work. This one would have kept doing so on every re-scan.

Fix

Pass every fetched corpus root on -Fu, not just the file's own — that is what CPython does with sys.path, and a cross-package import is ordinary Python, not an edge case. Then re-run the ladder and re-rank; several rows may move.

Track A deliberately did not change it: the scan is Track B's instrument, and redefining another lane's measurement mid-campaign is not Track A's call. Correct handling, recorded here so the reasoning survives.

Gate

Re-run the ladder with all roots; webencodings should leave the first-wall table and CodecInfo should appear. Publish the corrected table — the old one is cited in at least two tickets.

2026-08-17 (frank3) — FIXED, ladder re-run, table republished

Confirmed as filed, and it was my scan and my table — the shell loop passed -Fu$(dirname $f), i.e. only the scanned file's own package directory.

The fix is a checked-in tool, not a corrected command

tools/nilpy_ladder.py. The point of this ticket is that the instrument gets re-run, so the path rule now lives in a file with the reasoning attached rather than in a command somebody retypes. For each fetched corpus two roots go on -Fu:

library_candidates/<name>/          so `import webencodings` finds the package
library_candidates/<name>/<name>/   so a sibling `from .constants import X` resolves

which is what CPython's sys.path gives a source checkout. Corpora are detected by the <name>/<name>/__init__.py layout, so reportlab (an oracle for a different probe, src/reportlab/...) is excluded without a hardcoded list.

One thing the rewrite had to handle: the scan died on errors="strict" decoding, because a diagnostic can echo a source line and these corpora carry non-UTF-8 bytes. errors="replace" — a scan that stops at the first odd byte is its own measurement artefact.

The corrected table

compile: 4/48
    8  undefined variable (digits)
    7  undefined variable (CodecInfo)
    4  missing module: xml_dom
    3  Nil Python: class Filter cannot inherit from itself
    3  missing module: six_moves
    2  missing module: bisect
    2  missing module: genshi_core
    2  missing module: xml_etree_elementtree
    2  missing module: xml_sax_xmlreader
    2  unexpected character (a Unicode identifier)
    1  each: Mapping base, colorsys, copy, lxml, sys, urllib_request,
       xml_sax_saxutils, MULTILINE, lookup

webencodings (6) and constants (4) are gone from the table entirely — both were the artefact, exactly as this ticket predicted. CodecInfo appears, as predicted. The gate is met.

What the corrected ranking actually says

The two top rows are single root causes with wide transitive reach, and both are already-filed tickets:

row files cause
digits (8) all of html5lib, through constants.py [[bug-n-assigning-to-a-name-that-collides-with-a-pascal-shim-attribute-fails]]
CodecInfo (7) all of tinycss2 + webencodings, through webencodings/__init__.py [[bug-n-a-temporary-receiver-resolves-to-the-shim-type-not-the-user-class]]

15 of the 44 failing files sit behind those two tickets. Neither is a missing module and neither is package resolution — which is the substantive correction, because the old table ranked package/sibling resolution as the top lever and it does not appear in the corrected one at all.

New from this scan and filed: [[bug-n-a-unicode-identifier-is-rejected-by-the-lexer]] (p25 — 2 files, and both sit behind CodecInfo anyway, so nothing is unblocked by it alone).

The old numbers are cited in four places, all corrected

feature-nilpy-six-and-warnings-shims, bug-a-package-and-sibling-module-resolution-is-the-corpus-wall, doc-n-fu-is-how-a-python-package-is-found, feature-nilpy-thirdparty-libraries-as-targets — each now carries a pointer to this table. That was the half of the gate that mattered: a corrected instrument nobody re-runs leaves the wrong numbers in circulation.

Log