Skip to content

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 into
ThinkFlowLab:mainfrom
HugoXK:dev/1-project-panel-code-point-cuts
Nov 1, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-project-panel-code-point-cuts

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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 land
between 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 cut from
core/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:

  1. core/src/host/project-assistant.js:132 — the assistant card's askOf title (77 units).
  2. core/src/host/project-board.js:105 — oneLine (200), the one-line value a blocked or
    finished board card carries.
  3. core/src/host/project-board.js:163 — titleOf (200), the decision card's title.
  4. 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).
  5. core/src/host/project-team.js:67 — the dossier row's memory.head (4,000).

The sixth site is the same defect one layer above the panel, which the panel cannot fix:

  1. core/src/host/memory.js:1501 — memoryDigest bounded the head with slice(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 issue
    reports. 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 no
longer 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 a memoryDigest that returns a 4,001-unit head, the row is 3,999 units
and 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 only

The three test files with the six source hunks reverted: 52 pass / 6 fail, and the six
failures are exactly the new cases:

not ok 20 - #2005: a long card title in the digest is shortened at a code point, never inside an emoji
not ok 39 - #2005: a decision title is bounded at a code point, never inside an emoji
not ok 43 - #2005: a blocked card reason is bounded at a code point, never inside an emoji
not ok 56 - #2005: project-team caps a bot name at a code point, never inside an emoji
not ok 57 - #2005: project-team cuts the memory head at a code point, never inside an emoji
not ok 58 - #2005: project-team cuts a head handed to it directly at a code point

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).

reverted bound the one red case
project-assistant.js 77 → slice the assistant card title
project-board.js oneLine → slice the blocked card reason
project-board.js titleOf → slice the decision title
project-team.js capped → slice the roster bot name
memory.js head → slice the memory head through the reader
project-team.js:67 → slice the head handed to the dossier directly

Lines (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 / :167
  • core/src/host/project-team.js: capped :22 / :24; the dossier head :67 / :69
  • core/src/host/memory.js: the head in memoryDigest :1501 / :1503 (the issue names the
    same 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)
  • tests: 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 — the text.js row gains the six bounds and says why the
    head is bounded where memoryDigest reads it
  • patches/ds-bot/0001-core-fixes.patch regenerated (it carries the four source files and
    the three test files); patches/ds-bot/0008-client-bundle.patch unchanged, no client file

What I ran

TZ=UTC DSH_HARNESS=<repo>/core/dev/harness ARCH_DOCS_BASE=193281239a4a8d3c06b74afa10555fc1057696e8 npm run verify   # exit 0
  • core 2420/2420, social 541/541, tree 127/127, timeline 135/135, workflow 336/336,
    .github 563/563, interop 37/37, live-e2e 24/24; oxlint 0 errors — the 73 warnings are
    the count the base reports too (19328123: 73, 0), so this change adds none
  • arch-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 second generate
    left git status empty
  • changelog: 21 fragment(s) in changelog.d/ are well formed (mine:
    changelog.d/2005-project-panel-code-points.fixed.md)
  • generate twice before that, the patch mirrors written by it and not by hand

What I did not run

  • No browser DOM check of the panels: each bound is a pure string function on the host and
    is asserted through the endpoint the client calls (project-team through the channel,
    the board and digest cases through their own modules).
  • No smoke test (dev/bench.sh smoke): the change touches no runtime path, only how six
    strings are bounded.

Verified on the head

Every number above was re-run on a clean checkout of the pushed head
146fb1581ac7881af5bfaa14044968ef954bd22e (tree 4b1548fc1b62cbbeff4d089d252f5857946366a6, base
193281239a4a8d3c06b74afa10555fc1057696e8, 16 files, +225/−16), not on a local commit. The push
path rebuilt that head from the same tree as the local 03b256c6, so the tree is identical and only
the 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"
}

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.
@Birfy

Birfy commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 146fb15
Written-by: Reviewer
Attestation: 146fb15 role=Reviewer

Ran at this tip:

$ tar xzf p2012tip.tgz -C p2012tip --strip-components=1; tar xzf p2012base.tgz -C p2012base --strip-components=1
$ cd p2012tip/core && TMPDIR=/data/dsh/bot/workspace/p2012-tmp DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/project-assistant.test.js test/project-board.test.js test/project-team.test.js test/memory.test.js test/memory-search.test.js test/memory-search-edge.test.js test/text-boundary.test.js
# tests 140  # pass 140  # fail 0
$ # same seven files, tests untouched, with core/src/host/{memory,project-assistant,project-board,project-team}.js written back to their base 19328123 blobs
# tests 140  # pass 134  # fail 6

Review points:

  • 红先成立。 只把四个被改的 host 源文件换回 base 19328123 的 blob、测试保持 tip 版本,同一组 140 条里正好红 6 条,就是 host: five Project-panel bounds still cut text on UTF-16 units, so a dossier or a board card can carry half an emoji #2005 的六个点:digest 的 77 单位卡标题、decision 的 200 单位标题、blocked card 的 200 单位 reason、project-team 的 name 上限、dossier 的 4,000 单位 memory head、以及直接交给模块的 4,001 单位 head(第二条 bound 走不到 reader,靠直调钉住)。tip 上同一组 140/140 绿;相邻的 memory / memory-search / text-boundary 也在里面,说明限制常量没被顺手改小。
  • 范围与 compare 一致。 compare 193281239a…146fb1581a 是 1 ahead / 0 behind、1 个 commit、16 个文件 +225/−16,与 PR 正文点到的六处 bound、三份测试、docs/subsystems/core-host.md、changelog.d/2005-… 以及 patches/ds-bot/ 镜像完全对应;base 仍是当前 main,不欠 rebase。
  • tip check-runs。 15 条里 14 绿(tests-ok、package、merge-gate、judge、四条 plugins、Node 24、agent-audience、risk tier、reviewed tip),唯一的红是 enqueue-guard,理由是 no Verifier report names the tip 146fb1581ac7 —— 本条即那份 report。
  • 档位 T2(core/src/host/** 命中 risk-rules 的 T2 path)。PR 来自 fork HugoXK/FlyBot,guard 日志已写明 agent-pr 与 Owner:/Reviewer:、checkpoint 规则对 fork 不问,所以正文里那两行与无标签不构成拒绝;mergeable: true、非 draft、maintainer_can_modify: true。
  • 限制。 本机只跑了受影响的 core 套件,没有重跑全量 npm run verify 和 smoke;全量证据取自 tip 的 tests-ok 与 Node 24 两条绿,来源是 GitHub check-runs。

@Birfy
Birfy added this pull request to the merge queue Nov 1, 2026
@github-actions github-actions Bot added pass risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff labels Nov 1, 2026
Merged via the queue into ThinkFlowLab:main with commit e2103b9 Nov 1, 2026
@Birfy
Birfy deleted the dev/1-project-panel-code-point-cuts branch November 1, 2026 09:11
@HugoXK

HugoXK commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2012 (Fixes #2005), tip 146fb1581ac7881af5bfaa14044968ef954bd22e.

  • base 193281239a4a8d3c06b74afa10555fc1057696e8, tree 4b1548fc1b62cbbeff4d089d252f5857946366a6, single commit, 16 files +225/−16
  • non-draft, maintainer_can_modify: true, mergeable: true / clean
  • checks: 13 success; enqueue-guard red for the one expected reason — no Verifier report names the tip 146fb1581ac7
  • checkpoint parses clean against live .github/scripts/enqueue-rules.mjs at 19328123 (checkpointReasons: [], 10 paths ≤ 12, block under 1500 characters)
  • at / checks.at are this head, measured on a clean checkout of it (core 2420/2420, verify exit 0). The local commit 03b256c6 with the same tree is noted as an aside only.

Scope note: six UTF-16 slice bounds on Project-panel strings → the shared cut(), limits unchanged (77 / 200 / 200 / roster caps / 4,000); the sixth site is memoryDigest, which bounded the head before any panel read it. Each of the six reds alone when only its own bound is reverted.

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

Labels

pass risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: five Project-panel bounds still cut text on UTF-16 units, so a dossier or a board card can carry half an emoji

2 participants