Repository navigation
host: five Project-panel bounds still cut text on UTF-16 units, so a dossier or a board card can carry half an emoji - #2012
Merged
Birfy merged 1 commit intoNov 1, 2026
Conversation
ThinkFlowLab#2005) The assistant digest's 77-unit card title, the board's 200-unit one-line value and decision title, and the dossier's capped fields now go through cut(). The dossier's 4,000-unit memory head is bounded in memoryDigest, where it is read: a cut at the dossier alone could only turn half a pair into U+FFFD.
Collaborator
|
Verdict: PASS at 146fb15 Ran at this tip: Review points:
|
Contributor
Author
|
Scope note: six UTF-16 |
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.
Fixes #2005
Owner: Dev-1
Reviewer: Birfy
Claim-Id: 9ce27c65909b4c5c982d5f505017a3a6
Written-by: Dev-1
What changed
Six host bounds that the Project panels read were
slice(0, n), so a bound could landbetween the two UTF-16 units of one emoji and the panel drew half a pair — a lone high
surrogate, which no client can render. Every one of them now goes through
cutfromcore/src/shared/text-boundary.js(#1916, #1963): the cut steps back onto a code point,so the emoji is dropped whole, the result is well formed and the budget does not move.
The five bounds the issue lists:
core/src/host/project-assistant.js:132— the assistant card'saskOftitle (77 units).core/src/host/project-board.js:105—oneLine(200), the one-line value a blocked orfinished board card carries.
core/src/host/project-board.js:163—titleOf(200), the decision card's title.core/src/host/project-team.js:22—capped(), the roster's name 200 / role 120 /persona 4,000 / model 200 / color 64 in one place (the five call sites at
:60-:64).core/src/host/project-team.js:67— the dossier row'smemory.head(4,000).The sixth site is the same defect one layer above the panel, which the panel cannot fix:
core/src/host/memory.js:1501—memoryDigestbounded the head withslice(0, 4000)before any panel read it. Measured on this base with
'a'.repeat(3999) + '😀'(4,001 units): the base gives 4,000 units,
isWellFormed() === false, tail"a\ud83d";cutting that broken head again at the dossier can only turn the lone half into
…+U+FFFD (4,000 units, well formed, tail
"a\uFFFD"), which is the symptom the issuereports.
cut(raw, 4000)at the reader gives the issue's expected value: 3,999 units,well formed, emoji dropped whole.
The reader now hands over a well-formed head, so the dossier's own bound (
:67) is nolonger reached on the end-to-end path. It stays because that module promises every string
it answers is capped, and it is pinned by a direct call rather than left untested: with
install(rt)and amemoryDigestthat returns a 4,001-unit head, the row is 3,999 unitsand well formed. The alternative was deleting the line as unreachable; a contract that a
future reader path can still break is kept and tested instead.
No payload shape, endpoint, key or limit changes: the six numbers are the same, and the
only visible difference is that a bound landing inside an emoji now returns the text with
the whole character dropped instead of half of it.
Red first, on this base (
19328123), tests onlyThe three test files with the six source hunks reverted: 52 pass / 6 fail, and the six
failures are exactly the new cases:
On this tree the same three files are 58/58 green.
Each site reds alone
Reverting one bound at a time, everything else left in place, on this base: each revert
turns exactly one case red (57 pass / 1 fail every time).
project-assistant.js77 →sliceproject-board.jsoneLine→sliceproject-board.jstitleOf→sliceproject-team.jscapped→slicememory.jshead →sliceproject-team.js:67→sliceLines (base
193281239a4a→ this tree)core/src/host/project-assistant.js::132(base) /:134(tree)core/src/host/project-board.js:oneLine:105/:108;titleOf:163/:167core/src/host/project-team.js:capped:22/:24; the dossier head:67/:69core/src/host/memory.js: the head inmemoryDigest:1501/:1503(the issue names thesame line as
:1494, which is where it sat before host: a seat change renames the room on the host, and the guests already seated keep the old name #2008 moved the file)core/test/project-assistant.test.js:546;core/test/project-board.test.js:425(decision title) and
:502(blocked reason);core/test/project-team.test.js:196(name),:208(head through the reader) and:221(head handed directly)docs/subsystems/core-host.md:41— thetext.jsrow gains the six bounds and says why thehead is bounded where
memoryDigestreads itpatches/ds-bot/0001-core-fixes.patchregenerated (it carries the four source files andthe three test files);
patches/ds-bot/0008-client-bundle.patchunchanged, no client fileWhat I ran
.github563/563, interop 37/37, live-e2e 24/24; oxlint 0 errors — the 73 warnings arethe count the base reports too (
19328123: 73, 0), so this change adds nonearch-docs: OK (16 changed file(s))10 patches apply cleanly in order;core/ equals ds-bot/ + patches/ds-bot/;patches/ds-bot/ is what generate writes (generate is a no-op)— the secondgenerateleft
git statusemptychangelog: 21 fragment(s) in changelog.d/ are well formed(mine:changelog.d/2005-project-panel-code-points.fixed.md)generatetwice before that, the patch mirrors written by it and not by handWhat I did not run
is asserted through the endpoint the client calls (
project-teamthrough the channel,the board and digest cases through their own modules).
dev/bench.sh smoke): the change touches no runtime path, only how sixstrings are bounded.
Verified on the head
Every number above was re-run on a clean checkout of the pushed head
146fb1581ac7881af5bfaa14044968ef954bd22e(tree4b1548fc1b62cbbeff4d089d252f5857946366a6, base193281239a4a8d3c06b74afa10555fc1057696e8, 16 files, +225/−16), not on a local commit. The pushpath rebuilt that head from the same tree as the local
03b256c6, so the tree is identical and onlythe commit sha differs; the local sha is mentioned here only as the branch tip I built from.
Checkpoint
{ "pr": 2012, "at": "146fb1581ac7881af5bfaa14044968ef954bd22e", "claim": "Dev-1", "issue": 2005, "summary": "Six Project-panel bounds used slice(), so a bound could end between the two units of one emoji and the panel drew half a pair. All six now go through cut(), which steps back onto a code point and drops the emoji whole, while the six limits stay 77, 200, 200, the roster caps and 4,000. The sixth site is memoryDigest, which cut the head before any panel read it, so a panel cut alone could only leave U+FFFD. Each site has a case that reds when only its own bound is reverted, and the dossier head the reader no longer reaches is pinned by a direct call. verify is green on the new base.", "files": [ "core/src/host/project-assistant.js", "core/src/host/project-board.js", "core/src/host/project-team.js", "core/src/host/memory.js", "core/test/project-assistant.test.js", "core/test/project-board.test.js", "core/test/project-team.test.js", "docs/subsystems/core-host.md", "patches/ds-bot/0001-core-fixes.patch", "changelog.d/2005-project-panel-code-points.fixed.md" ], "checks": { "ran": "TZ=UTC npm run verify (ARCH_DOCS_BASE=193281239a4a8d3c06b74afa10555fc1057696e8)", "result": "green", "at": "146fb1581ac7881af5bfaa14044968ef954bd22e" }, "next": "none — ready for review" }