feat(cli): add shellcade-kit check wide-glyph width-contract lint#54
Merged
Conversation
A wide-glyph writer — (*Frame).SetWide / (*Frame).SetGraphemeWide (internal/game/grid.go ~128-173) — reserves TWO terminal columns for one glyph and never measures it: width-2 is the author's contract. If the base code point's East-Asian-Width is not Wide (W) or Fullwidth (F), the terminal advances the cursor by one column while the SDK reserved two, desyncing every column to that cell's right. This is the bug that corrupted the pokies reels in production (a keycap base U+0037, EAW Neutral, fed to a wide writer). Add `shellcade-kit lint-width <path>...`: it parses Go game source with go/parser, finds wide-writer calls whose base code point is a determinable literal (a rune literal for SetWide, a string literal's first code point for SetGraphemeWide), reports each base whose EAW is not W/F with file:line, and exits non-zero on any violation. Non-literal bases are skipped — the lint reports only what it can prove. The EAW judgement embeds the W/F ranges of the UCD EastAsianWidth.txt; the repo has no general EAW classifier to reuse (host/render only hard-codes a single Fullwidth fold + ASCII fallback) and the public module is deliberately dependency-free. `shellcade-kit check <gamedir>` now runs this lint over the Go source before the build + conformance run, since a width-contract desync never faults and so cannot be observed by conformance alone. Table-driven tests cover the EAW classifier, the table's sort invariant, and the file-level lint against a passing and a failing fixture; the failing fixture includes the exact pokies keycap base. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BCook98
marked this pull request as ready for review
June 17, 2026 02:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A wide-glyph writer in the SDK —
(*Frame).SetWideand(*Frame).SetGraphemeWide(internal/game/grid.go~128-173) — reserves two terminal columns for one glyph and never measures the glyph it is handed: width-2 is the author's contract. If the base code point's East-Asian-Width is not Wide (W) or Fullwidth (F), the terminal advances the cursor by one column while the SDK reserved two, desyncing every column to that cell's right for the rest of the row.This is the bug that corrupted the pokies reels in production: a keycap base code point
U+0037(7️⃣, base EAW Neutral) was fed to a wide writer. The desync never faults, so the conformance harness cannot observe it.Fix
Adds a source lint and wires it into the CLI's subcommand dispatch:
shellcade-kit lint-width <path>...(newcmd/shellcade-kit/lintwidth.go): parses Go game source withgo/parser, finds calls to the wide writers whose base code point is a determinable compile-time literal (a rune literal forSetWide; a string literal's first code point — the cluster base — forSetGraphemeWide), and reports each base whose EAW is not W/F withfile:line. It exits non-zero on any violation, so it is a one-command merge gate. Non-literal bases (variables, call results) are skipped — the lint reports only what it can prove.shellcade-kit check <gamedir>now runs this lint over the Go source before the build + conformance run, since a width-contract desync never faults and so conformance alone cannot catch it. (main.go,check().)cmd/shellcade-kit/eaw.go): I searchedinternal/and the existing width logic first. The repo has no general EAW/UCD classifier to reuse — its only width logic (host/render) hard-codes a single Fullwidth fold plus an ASCII fallback table, not a general classifier — and the public module is deliberately dependency-free. So rather than pull ingolang.org/x/text/widthor hand-roll a full UCD parser, I embed the Wide+Fullwidth ranges of the UCDEastAsianWidth.txtas a sorted, binary-searched range table; everything outside it is single-column (the set the wide writers must refuse).Naming note for reviewers
The task framed this as "add a
checksubcommand". The CLI already has achecksubcommand (the artifact conformance harness, which operates on a built.wasmor a game dir, not arbitrary paths). To avoid breaking that and to keep the lint usable on explicit file/dir paths, the standalone entry is namedlint-width, and the width lint is additionally wired intocheckfor Go source directories. Soshellcade-kit check .now runs it as a fail-fast pre-step. If you'd prefer a different surface (e.g. only fold it intocheck, or a different command name), easy to adjust.Verification
All run locally in the worktree and observed to pass:
go build ./...— OKgo vet ./cmd/shellcade-kit/— OKgo test ./cmd/shellcade-kit/— OK (full package, 4.2s), including the newTestEAWClass,TestEAWTableSorted,TestLintWidthFile,TestLintWidthExitsNonZeroOnViolationgofmt -lon all touched files — cleanlint-width pass.go→ exit 0, "no width-contract violations"lint-width fail.go→ exit 1, fourfile:lineviolations (incl. the keycap baseU+0037)check <go-dir-with-violation>→ exit 1, fails at the lint step before any buildNot run:
tinygowasm build and the end-to-end conformance path for a passingcheck(would need the TinyGo toolchain); the lint pre-step and its failure path are fully exercised above without a build.Tests / fixtures
cmd/shellcade-kit/lintwidth_test.go— table-driven: EAW classifier (incl. the pokies7), table sort/overlap invariant, file-level lint over a passing and a failing fixture, and the non-zero-exit entry point.cmd/shellcade-kit/testdata/widthlint/{pass,fail}.go.txt— passing/failing fixtures (stored.go.txtsogo build ./...doesn't try to compile them); the failing fixture includes the exact pokies keycap base.Reviewers should focus on
lint-widthstandalone + fold intocheck).eaw.govs. the UCD —TestEAWTableSortedguards the binary-search invariant but the ranges themselves are a manual transcription.(row, col, glyph, style)signatures ingrid.go).🤖 Generated with Claude Code