Skip to content

backlog: file #1221 -- a doc lint crashes printing a hit that contains a glyph - #330

Merged
wshallwshall merged 3 commits into
mainfrom
w3-file-tallylint
Aug 11, 2026
Merged

backlog: file #1221 -- a doc lint crashes printing a hit that contains a glyph#330
wshallwshall merged 3 commits into
mainfrom
w3-file-tallylint

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Files BACKLOG #1221 -- a doc lint that crashes while printing its own findings.

scripts/docs/asvs_tally_lint.py raises UnicodeEncodeError: 'charmap' codec can't encode character when
a hit's text contains a glyph. It fires only on the failure path, so it is invisible while the lint
is green
, and it converts a legible FAIL into a traceback at exactly the moment someone needs to read
which document tripped it. A gate that is correct until it has something to say, and then unreadable.

Measured, and this is what moves it from theoretical to likely. The lint walks rglob("*.md") over
docs/, and main's docs/BACKLOG.md carries 799 cp1252-unsafe codepoints across 29 distinct
characters
-- including U+2192 x140. So the crash is reachable from any hit whose line carries an
arrow, in the primary corpus, with 140 chances.

Why this is not #1030 evidence, and needs its own number. #1030 asks that the corpus be kept
cp1252-clean. That cannot fix this, because this lint's corpus is required to contain cp1252-unsafe
characters:
docs/BACKLOG.md carries the sanctioned banner alphabet -- CLAUDE.md section 11's one
machine-parsed holdout, which backlog_status_check.py requires -- and all five of those glyphs are
cp1252-unsafe. #1030 is about the CONTENT; this is about the INSTRUMENT. Both are needed; neither
substitutes.

It wants a decision, not a patch: should a repo script force UTF-8 on its own stdout, or should the
crash be treated as the corpus's problem? The first is already an in-tree idiom --
messagefoundry/__main__.py:52-55 hardens stdout/stderr via
getattr(stream, "reconfigure", None) with errors="replace", and comments the environments that lack
or reject it (PYTHONLEGACYWINDOWSSTDIO, pytest capture). PYTHONIOENCODING is a workaround, not a fix:
CI and a developer shell will not both set it.

How to prove any fix: make the lint fire on a document containing a glyph and read the output. A
green run exercises none of this, which is the entire point.

Ledger-only. Verified: #1221 free in the live ledger and the archive, merges clean onto current main.

…tains a glyph

The coordinator asked whether this belongs under #1030 or needs its own
number. It needs its own, and the reason is measured rather than argued.

#1030 is 'keep the corpus cp1252-clean, because per-file gating lets the
class recur'. THAT CANNOT FIX THIS, because the corpus this lint reads is
REQUIRED to contain cp1252-unsafe characters: asvs_tally_lint walks
rglob('*.md') over docs/ (:245) and explicitly reads docs/BACKLOG.md
(:63-64), whose sanctioned banner alphabet is CLAUDE.md section 11's one
machine-parsed holdout and every glyph of which is cp1252-unsafe. Its own
comment at :153 records it already redding on backlog content.

So a clean corpus is not achievable HERE even in principle. #1030 is
about the CONTENT; this is about the INSTRUMENT. Both are needed and
neither substitutes for the other.

The defect shape is the interesting half: it fires ONLY on the failure
path -- printing the hits -- so it is INVISIBLE WHILE THE LINT IS GREEN,
and it converts a legible FAIL into a traceback at exactly the moment
someone needs to read which document tripped it. A gate that is correct
until it has something to say, and then unreadable.

The item states the real decision rather than prescribing a patch: force
UTF-8 on the script's own stdout -- an idiom already proven in-tree, since
messagefoundry/__main__.py hardens stdout/stderr for the whole CLI and the
harness and a bench script carry the same remedy -- or treat the crash as
the corpus's problem. PYTHONIOENCODING=utf-8 is a workaround, not a fix,
because CI and a developer shell will not both set it.

And it names where to prove the fix: MAKE THE LINT FIRE on a document
containing a glyph and read the output. A green run exercises none of it,
which is the whole point.

Fourth cp1252 surface in one day -- a U+21D2 in a ci.yml comment, three
pre-existing U+2192 already on main in that same file, one in the
coordinator playbook, and now this. The first three are #1030's subject;
this one is not.

Gates: 485 items each declaring exactly one status; ledger_check clean.
cp1252 dogfood on my own added lines: the only unsafe character is
U+1F522, the sanctioned banner glyph, which is the exemption recorded
under #1030.
… lint names

The coordinator measured the corpus after I filed, and it moves the item
from 'fires on the failure path in principle' to 'fires on a large
fraction of possible failures in the primary corpus'.

main's docs/BACKLOG.md -- the file asvs_tally_lint names at :63-64 --
carries 799 cp1252-unsafe codepoints across 29 distinct characters,
including U+2192 x140. So the crash is reachable from any hit whose line
carries an arrow, and there are 140 chances from that character alone.

I re-derived the census rather than transcribing it, and that surfaced a
decoding correction now recorded in the item: the first pass reported
U+D83D x193 and U+DD22 x171, which are UTF-16 SURROGATE HALVES rather
than characters. Read as codepoints they are U+1F522 x171 and U+1F6A7
x16. Same underlying data, but anyone grepping for U+D83D would find
nothing and reasonably conclude the census was wrong. Count codepoints,
not UTF-16 units.

The item also now carries the in-tree hardening precedent with its edge
cases, which is worth more than the idiom alone: __main__.py:52-55 uses
getattr(stream, 'reconfigure', None) and calls it with errors='replace',
with a comment naming the environments that lack or reject reconfigure --
PYTHONLEGACYWINDOWSSTDIO and pytest capture. Whoever builds this inherits
that scar tissue instead of rediscovering it.

Gates: 485 items each declaring exactly one status; #1221 still open.
#1220 landed while #1221 was in flight, and both insert at the same
docs/BACKLOG.md line, so the tail conflicts. Resolved keep-both, #1220
then #1221, in numeric order.

KEEP-BOTH HAS BEEN THE WRONG ANSWER TWICE TONIGHT, so I checked the
property that makes it right here rather than reaching for it:

  conflict blocks : 1
  OURS   carries  : {1221} only
  THEIRS carries  : {1220} only
  intersection    : EMPTY

Two disjoint appends of different items, neither referencing the other's
text. That is a different situation from #302, where keep-both would have
resurrected 38 archived items, and from #1220's own subject, where it
ships seam 19 describing two contract changes. The phrase is not the
answer; the property is.

RE-DERIVED ON THE RESULT rather than trusting the resolution, as the
coordinator asked:

  live 250 | archive 236 | namespace 486 | in BOTH files 0
  duplicate headings in live: 0
  #1220 and #1221 both live, neither archived
  backlog_status_check: 486 items, each declaring exactly one status
  ledger_check: clean
  doc guards: 48 passed (link resolution, banner status, dast claims)

484 -> 486, nothing duplicated, which is what a correct two-append merge
must produce.

My own earlier advice to batch these was semantically right and
mechanically incomplete: I checked whether either item invalidated the
other's claims and never asked whether their HUNKS collided. They insert
at the same line. Two PRs bought the same parallelism at the cost of
exactly this one resolve.
@wshallwshall
wshallwshall merged commit 8077a03 into main Aug 11, 2026
32 of 33 checks passed
@wshallwshall
wshallwshall deleted the w3-file-tallylint branch August 11, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant