Repository navigation
host: room turns cut card, stub and transcript text on a code point boundary - #1961
Conversation
…oundary `compactStatus`, `fitRoomLines` and the `oneLine` behind the card, board and presence fields all ended a bounded prefix with a bare `slice`, so a bound that landed between the halves of a surrogate pair stored and sent a lone high surrogate. The stub is what every writer of `room.log` keeps (ThinkFlowLab#1251), so the half reached the encoder from disk as U+FFFD. `shared/text-boundary.js` `cut` already answers this everywhere else (ThinkFlowLab#1908/ThinkFlowLab#1912/ThinkFlowLab#1916); these three sites call it instead. The bound stays a cap: a pair straddling it is dropped whole, and text inside the bound is untouched.
|
Verdict: PASS at 1d6e92c Ran at this tip: Red first, at this tip: base blob Scope: 8 files, exactly the body's table ( CI at this tip: 16 check-runs, all Not run here: the full core suite — this checkout has no Metadata: |
Fixes #1960
Owner: Dev-2
Reviewer: Birfy
Written-by: Dev-2
Claim-Id: 311b65a9317c4d539e8ca9a4734966df
Branch: dev/2-p305-room-context-code-point-bound
Base: 54dad6b
Tip SHA: 1d6e92c
Tree: 29db63e18ba5e1d8d4168ea207f7eee646b4e06b
Three character bounds in
core/src/host/room-context.jsended a bounded prefix with a bareslice, so a bound that landed between the halves of a surrogate pair kept the high half and produced a string that is not well formed. The stub is the one that reaches disk: since #1251 every writer ofroom.logruns its line throughprepareRoomLine→compactStatus, so a half cut there is stored instate.jsonand reaches a UTF-8 encoder as U+FFFD on the next load.54dad6b8compactStatus— the hundred-unit stub every writer ofroom.logstores:95flat.slice(0, 100).trim():96cut(flat, 100).text.trim()fitRoomLines— the transcript clip atbudget - used - gap:142text.slice(0, room - 1):143cut(text, room - 1).textoneLine— the card, board and presence fields (max - 1):45flat.slice(0, Math.max(0, max - 1)):46cut(flat, Math.max(0, max - 1)).textThis tree is one
importline longer than the base (cutfrom../shared/text-boundary.js), so every line below it reads +1 against54dad6b8.#1959did not touch this file, so the base column is exact.cut(text, max)(core/src/shared/text-boundary.js) answers{ text, cut }with the longest prefix that does not split a pair. The budget is unchanged everywhere —100for the stub,room - 1for the clip,max - 1foroneLine— and only the exit changes: text inside the bound comes back untouched, and a pair straddling it is dropped whole instead of kept in half. No signature, constant or record shape moves, andoneLine's own semantics (flatten, fold</>and control characters to spaces, squeeze whitespace, trim, then cut, then append…when it truncated) stay as they were.Base and rebase
The branch was cut from
37e81623and is rebased onto54dad6b8(the merge of#1959), not merged.#1959added itsoneLinesentence to the task-record paragraph ofdocs/subsystems/core-host.md; this PR's sentence is in the Room context budget (#1203) paragraph above it, so the rebase applied clean with no conflict and both sentences stand, each in the paragraph whose subject it covers. The one newchangelog.d/fragment is1960-room-context-code-point-bound.fixed.md;#1959's1958-…fragment is untouched.Red first
Three cases, one per site; the third is the stored-value case and is asserted on what a fresh runtime reads back from
state.json, not on the object the writer kept in memory. With the base blob swapped in (git show 54dad6b8:core/src/host/room-context.js > core/src/host/room-context.js, the two test files at this branch):(with the branch's
room-context.js:# pass 3 / # fail 0.)Case 3 walks the real path:
postRoomMessage→append→prepareRoomLine→rt.save()writesstate.json, then a new runtime loads that file and the assertion runs on the reloaded line —isWellFormed(), a UTF-8 round trip, and a turn rendered from the reloaded log byfitRoomLineswithprepareRoomLine. Each case also carries the controls: a string of exactly 80 units ending in a whole😀comes back untouched, and a ZWJ family emoji inside the bound survives whole.Mutation — each site has its own tooth
One site reverted at a time, the other two in place:
:96stub back toslicethe reloaded stub is not well formed):143clip back toslicethe clipped line is not well formed):46oneLinetail back toslicethe board line is not well formed)Each run is
# pass 2 / # fail 1, so no case is satisfied by the other two fixes and the controls stay green under all three.Why
core/test/room-membership.test.jsis in the 8 filesCase 3 needs a real store round trip, and the runtime scaffolding that writes and reloads
state.json—host(),rt.load(),rt.save(),postRoomMessage— lives only incore/test/room-membership.test.js. The file's change is two import lines (readFileSync;fitRoomLinesandprepareRoomLinefromroom-context.js) plus one new test at the end. No existing assertion is edited. The alternative, a second copy of that scaffold inroom-context.test.js, would duplicate the harness rather than assert on the store the product writes.Verification
TZ=UTC ARCH_DOCS_BASE=54dad6b856c5ea8c387b2a62ea9143e67126acd3 npm run verify→ exit 0, on a clean tree (git status --porcelainempty) at this tip's tree29db63e1…. Counts, this tree / base54dad6b8:.github/scriptsThe base core count is measured, not inferred: a clean worktree at
54dad6b8with the same harness runs2358 tests / 2358 pass / 0 fail, so this PR adds exactly the three cases above.arch-docs: OK (8 changed file(s))againstARCH_DOCS_BASE=54dad6b8…— the 8 files are the twocore/src/testchanges plus their threepatches/ds-bot/files/mirrors, the doc, and the changelog fragmentds-bot-patches: pinFei2411/DeepSeek-Bot@2b10c70, 10 patches apply in order,core/=ds-bot/+ patches,canonicalclean,generatea no-op; the three mirrors are byte-identical tocore/(cmpclean onsrc/host/room-context.js,test/room-context.test.js,test/room-membership.test.js)build-client --checkok;changelog.mjs check→4 fragment(s) in changelog.d/ are well formedNot run: the smoke test — this checkout has no
dev/bench.shand no running FlyBot instance to drive. Also not run: the CIpackagejob, typecheck and actionlint.Checkpoint
{
"pr": 1961, "claim": "Dev-2", "issue": 1960,
"summary": "The three character bounds in core/src/host/room-context.js now cut through shared/text-boundary.js cut(): the hundred-unit status stub every writer of room.log stores, the transcript clip, and the oneLine fields behind card, board and presence lines. Budgets and oneLine's flatten, trim and ellipsis are unchanged; a pair straddling a bound is dropped whole, never kept in half. The stored-stub case asserts on the line a fresh runtime reloads from state.json, not the in-memory one. room-membership.test.js gains two imports and one test because the store round-trip scaffold lives only there.",
"files": ["core/src/host/room-context.js", "core/test/room-context.test.js", "core/test/room-membership.test.js", "docs/subsystems/core-host.md", "changelog.d/1960-room-context-code-point-bound.fixed.md", "patches/ds-bot/files/src/host/room-context.js", "patches/ds-bot/files/test/room-context.test.js", "patches/ds-bot/files/test/room-membership.test.js"],
"checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=54dad6b856c5ea8c387b2a62ea9143e67126acd3 npm run verify; node scripts/ds-bot-patches.mjs check; node .github/scripts/changelog.mjs check", "result": "green", "at": "1d6e92c3c7daa383f07c8d8b933bffcfb7603988" },
"next": "none — ready for review"
}