Repository navigation
Description
Summary
The web world presenter (pkgs/id/web/src/world-surface.ts) still diverges from the Rust validator for four grapheme classes. CLOSED #1431 fixed emoji-modifier and tag sequences (PR #1542), but two classes that were folded into #1431 as evidence (comment 6446198995) were not touched by that fix, and two further classes in the same function were found this run. In every case a screen the Rust side validates and the SSH presenter renders (world_presentation.rs:219 parse, :350 render_ansi, both measuring with UnicodeWidthStr::width under unicode-width 0.2.2, pinned in pkgs/id/Cargo.lock) is either refused whole by the web presenter — renderWorldSurface returns false, so world.ts:175-176 falls back to dumping the raw frame text — or, worse, accepted at the wrong column count, producing a misaligned grid.
Verified on main 601af5a (2026-10-27, lane-B creator run)
Method: TS probe on the laptop driving the verbatim world-surface.ts (sha256 3ee9823c identical both sides; Bun 1.4.2, minimal DOM stub, renderWorldSurface called directly) against a Rust probe crate on the laptop using the pinned unicode-width =0.2.2 + unicode-segmentation (the exact crates world_presentation.rs uses). Controls behaved: x Rust 1 / TS renders at 1; U+1F600 Rust 2 / renders at 2, refused at 1; the #1542 controls U+1F44D U+1F3FD and the England tag flag render at 2.
1. ⬛ U+2B1B and ⭕ U+2B55 — wide in Rust, missing from WIDE_RANGES
- Rust: both are 1 grapheme, width 2.
- TS: refused at
columns=2, wrongly accepted atcolumns=1(WIDE_RANGES, world-surface.ts:60-127, has no U+2Bxx entry;codePointWidthfalls through to 1). - This class was reported on id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431 (evidence comment 6446198995) and survived its fix.
2. Narrow base + skin-tone modifier — ☝🏻 U+261D U+1F3FB
- Rust: 1 grapheme, width 2 (unicode-width measures an emoji-modifier sequence at 2 even when the base alone is narrow).
- TS: refused at
columns=2, accepted atcolumns=1. PR fix(id/web): measure emoji-modifier and tag sequences as their base — world presenter (#1431) #1542's modifier rule (world-surface.ts:162-167) strips the modifier and measures the base alone, so a narrow base measures 1 — correct for the bases id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431 tested, wrong for narrow bases. - Also reported on id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431 and survived its fix.
3. Composed Hangul jamo clusters — U+1100 U+1161 (LV) and U+1100 U+1161 U+11A8 (LVT)
- Rust: each is 1 grapheme, width 2 (a jamo syllable measures as the precomposed syllable U+AC00 does — and the precomposed form renders correctly in TS).
- TS: refused at both
columns=2andcolumns=1—codePointWidthsums choseong 2 + jungseong 1 + jongseong 1. Any decomposed-Hangul screen (NFD text, jamo input methods) falls back whole. - New this run; the id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431-era sweep tested jamo only as single code points, which match.
4. One grapheme longer than 16 UTF-16 units, and a lone spacing mark
e+ 17× U+0301 (combining acute): Rust 1 grapheme, width 1. TS refuses atcolumns=1— pinned to thecell.symbol.length > 16check (world-surface.ts, cell validation inrenderWorldSurface): the same grapheme at UTF-16 length 15 and 16 renders, at 17 and 18 it is refused. The Rust validator imposes no symbol-length limit — only exactly-one-grapheme and width 1–2 — so the cap is a TS-only rule that rejects Rust-valid cells.- Lone U+0903 (Devanagari spacing combining mark, category Mc): Rust width 1; TS width 0 via the
/^\p{M}$/urule (world-surface.ts:133,142), so the cell is refused at both column counts. (Inside a cluster after a base consonant both sides agree at the base's width; only the standalone mark diverges.)
Why it matters
The presenter is the web half of a two-presenter contract: the same validated ScreenSurface must render on SSH and web. Every divergence either drops the whole screen to a raw-text dump (classes 1–4, at the correct column count) or silently misaligns it (classes 1–2, at the wrong count — a guest that disagrees with the Rust validator, or a future Rust change, would pass the web check it should fail). ⬛/⭕ are ordinary emoji a world program can plausibly draw as tiles; decomposed Hangul and accented Latin are ordinary text.
Acceptance criteria
renderWorldSurfaceagrees withUnicodeWidthStr::width(unicode-width 0.2.2) for: U+2B1B, U+2B55 (2, and refused at 1); U+261D U+1F3FB (2, refused at 1); composed jamo LV and LVT clusters (2); a lone U+0903 cell (1).- The symbol-length rule is reconciled with the Rust side: either the TS cap is removed/raised so a 1-grapheme, width-1 symbol of 17+ UTF-16 units renders, or an explicit matching cap is added to
world_presentation.rswith a comment naming the shared bound — the two validators must not disagree silently. - A parity test in
world-surface.test.tscovers each class above at its correct column count (renders) and at the wrong one (refused), in the style of the existing PR #759's world-surface width check mis-measures astral-plane, ZWJ and ⚠️ glyphs (JS width 1 vs Rust unicode-width 2) — must not close #724 as-is #761/id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431 tests. - Existing presenter tests still pass; the fix(id/web): measure emoji-modifier and tag sequences as their base — world presenter (#1431) #1542 modifier/tag controls (U+1F44D U+1F3FD, England flag) still render at 2.
Notes
- Follow-up remainder of CLOSED id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431 / PR fix(id/web): measure emoji-modifier and tag sequences as their base — world presenter (#1431) #1542 (classes 1–2 were its evidence comment 6446198995); classes 3–4 are new findings in the same function from this run's sweep. Distinct from OPEN id(e2e): no Playwright coverage of the world surface — presenter, input, records, resize untested in a browser (#1076 slice 3 gap) #1322 (Playwright e2e coverage of the presenter — that is test-infrastructure, not a width defect).
- Probe artifacts:
hidden_files/probe-2026-10-27-laneB-surface2/in the goal workspace (probe.ts, boundary.ts, main.rs, out.log, rust-out.log); laptop copies under~/git/fleet/probe-2026-10-27-laneB-surface2/.
Activity
developing-today commentedon Oct 28, 2026
CODE-618
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
hidden_files/probe-2026-10-27-laneB-surface2/(probe.ts, boundary.ts, main.rs, out.log, rust-out.log)37 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · parked
dezren39 commentedon Oct 31, 2026
🤖 Agent note · claimed
Merge pull request #2945 from developing-today/queue/1842-1581-graphe…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done
fixed_merged9db9563b67bf5dc2f78fb0bb8c7adba00d7ecb37; id(web): world presenter still mis-measures ⬛/⭕, narrow-base skin tones, composed Hangul jamo and long graphemes — #1431's folded-in classes survived its fix, and the 16-unit symbol cap has no Rust twin #1581 auto-closed.world-surface.test.ts8/8 pass; full web suite 490 pass / 1 fail / 9 skipped, the single failure (search-panel accessibility) proven pre-existing on clean main14679ee25;tsc --noEmitclean; Codex 4/4 review threads resolved (final head81797ebfeis the U+FE0E fix for the last finding).idworkflow never ran on this head — its runs (including main's) are stuck in the runner queue (dispatch run 41174573772 queued since 05:56Z). Merged under the standing directive after verifying the change itself on the laptop; the queued CI is an infra backlog, not a diff failure.