Skip to content

id(web): world presenter widens every U+FE0F cluster to 2 — a letter+VS16 cell (a️) is Rust-valid at columns 1 but renderWorldSurface refuses the whole screen #3265

Closed

Description

@dezren39

Summary

id(web): the TS world presenter's graphemeWidth (pkgs/id/web/src/world-surface.ts:321) returns width 2 for every grapheme cluster that contains U+FE0F (variation selector 16):

if (codePoints.includes(0xfe0f)) return 2;

Rust's unicode-width 0.2.2 — the validator the server side uses (pkgs/id/src/world_presentation.rs, parse_surface assigns each cell's columns from UnicodeWidthStr::width(grapheme)) — widens a base+FE0F cluster to 2 only for its emoji-presentable bases (digits, #, *, ©, ®, ™, suits, ▶/◀, ↔/↩/⤴, 〰/〽/㊗/㊙, actual emoji, …). For ordinary bases the cluster stays width 1, and a lone U+FE0F is width 0.

So a Rust-valid screen whose row contains e.g. a️ (U+0061 U+FE0F) is built by parse_surface with columns: 1 — and the web presenter then computes TS width 2 ≠ 1 and renderWorldSurface returns false for the whole screen, which falls back to raw text. This is exactly the #1581/#761 failure mode (one mis-measured cell rejects the entire frame), in a class those fixes and the #2962 differential never scoped: #2962 covers single code points alone and +U+FE0E; it never tested multi-codepoint clusters ending in U+FE0F.

Evidence (main c0fb853a; both files sha256-identical in the sandbox and laptop clones)

TS side — verbatim extraction of WIDE_RANGES/codePointWidth/graphemeWidth from world-surface.ts (sha256 5e04c698…), run under bun on the laptop; Rust side — standalone probe pinned to unicode-width = "=0.2.2" (the crate/version world_presentation.rs uses), same inputs. Probe: ~/git/fleet/probe-2026-10-31-laneA-vs16/ (laptop).

cluster Rust 0.2.2 TS graphemeWidth
a + FE0F 1 2
x / Z / é / Ω + FE0F 1 2
→ (U+2192) + FE0F 1 2
! / ? / % / space + FE0F 1 2
lone FE0F 0 2
1 / # / * / 0 + FE0F 2 2 (agree)
© / ® / ™ + FE0F 2 2 (agree)
♠ / ♥ / ♦ / ▶ / ◀ / ↔ / ↩ / ⤴ / 〰 / 〽 / ㊗ / ㊙ + FE0F 2 2 (agree)
⚠ (U+26A0) + FE0F, ☝ + FE0F, ❤ + FE0F, 😀 + FE0F 2 2 (agree)
ZWJ alone (U+200D) 0 1 (same function, minor: Rust rejects width-0 cells, TS would admit one at columns: 1)

Existing tests only cover the agreeing cases: world-surface.test.ts:271-272 exercises U+26A0+FE0F (⚠️) among its wide glyphs; no test pairs FE0F with a non-emoji-presentable base.

Why it matters

The two validators are a matched pair by design — the TS file's own comments insist "The two validators must not disagree" (world-surface.ts, on the symbol-cap removal). Any world program that emits a letter, digit-free symbol, or arrow followed by VS16 (copy-pasted text and styled labels do this in the wild) produces a screen the server accepts and the browser refuses wholesale, degrading the world view to raw text with no error pointing at the cell. The fix is small and well-scoped: gate the FE0F shortcut on the same emoji-presentable base set unicode-width uses (the file already maintains exactly such a table for the FE0E narrowing case, TEXT_PRESENTATION_RANGES, plus the extra VS16-only bases like digits/#/* — the Rust probe above enumerates the agreeing set to seed it).

Acceptance criteria

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a run 2026-10-31T19:05:00-05:00 (Sat 2026-10-31 19:05 CDT)
    • Found via: filed this run
    • Task: Filed this issue — TS world presenter widens every U+FE0F cluster to 2, disagreeing with Rust unicode-width 0.2.2 for non-emoji bases; verified with a laptop differential probe (real TS graphemeWidth extraction vs unicode-width =0.2.2) on sha-matched files at main c0fb853.
    • Expectation: effort/s — triage confirms the VS16-eligible base gating, then a fix + parity tests in world-surface.test.ts.
    • Links: probe ~/git/fleet/probe-2026-10-31-laneA-vs16/ (laptop); queue packet packet-3265-triage.json
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. 37 remaining items

  5. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. added a commit that references this issue on Nov 2, 2026
  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions