Skip to content

feat(cli): add shellcade-kit check wide-glyph width-contract lint#54

Merged
BCook98 merged 1 commit into
mainfrom
uplift/glyph-width-lint
Jun 17, 2026
Merged

feat(cli): add shellcade-kit check wide-glyph width-contract lint#54
BCook98 merged 1 commit into
mainfrom
uplift/glyph-width-lint

Conversation

@BCook98

@BCook98 BCook98 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Problem

A wide-glyph writer in the SDK — (*Frame).SetWide and (*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>... (new cmd/shellcade-kit/lintwidth.go): parses Go game source with go/parser, finds calls to the wide writers whose base code point is a determinable compile-time literal (a rune literal for SetWide; a string literal's first code point — the cluster base — for SetGraphemeWide), and reports each base whose EAW is not W/F with file: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().)
  • EAW machinery (cmd/shellcade-kit/eaw.go): I searched internal/ 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 in golang.org/x/text/width or hand-roll a full UCD parser, I embed the Wide+Fullwidth ranges of the UCD EastAsianWidth.txt as 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 check subcommand". The CLI already has a check subcommand (the artifact conformance harness, which operates on a built .wasm or 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 named lint-width, and the width lint is additionally wired into check for Go source directories. So shellcade-kit check . now runs it as a fail-fast pre-step. If you'd prefer a different surface (e.g. only fold it into check, or a different command name), easy to adjust.

Verification

All run locally in the worktree and observed to pass:

  • go build ./... — OK
  • go vet ./cmd/shellcade-kit/ — OK
  • go test ./cmd/shellcade-kit/ — OK (full package, 4.2s), including the new TestEAWClass, TestEAWTableSorted, TestLintWidthFile, TestLintWidthExitsNonZeroOnViolation
  • gofmt -l on all touched files — clean
  • Built the binary and ran it against the fixtures:
    • lint-width pass.go → exit 0, "no width-contract violations"
    • lint-width fail.go → exit 1, four file:line violations (incl. the keycap base U+0037)
    • check <go-dir-with-violation> → exit 1, fails at the lint step before any build

Not run: tinygo wasm build and the end-to-end conformance path for a passing check (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 pokies 7), 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.txt so go build ./... doesn't try to compile them); the failing fixture includes the exact pokies keycap base.

Reviewers should focus on

  • The naming decision above (lint-width standalone + fold into check).
  • The completeness/accuracy of the embedded EAW W/F range table in eaw.go vs. the UCD — TestEAWTableSorted guards the binary-search invariant but the ranges themselves are a manual transcription.
  • That arg-index 2 is the correct base-codepoint position for both writers (matches the (row, col, glyph, style) signatures in grid.go).

🤖 Generated with Claude Code

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
BCook98 marked this pull request as ready for review June 17, 2026 02:22
@BCook98
BCook98 merged commit 22d9385 into main Jun 17, 2026
6 checks passed
@BCook98
BCook98 deleted the uplift/glyph-width-lint branch June 17, 2026 02:22
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