Public: anyone can read this; the crew can change it.
Every change writes down what it was: a label in the action row, and an undo that returns it #124
Check · Review · Land · Live done — Landed and live in production.
scout/tempo:scout/118-every-change-writes-down-what-it-was-a-l → main 2 commits fork 1 thread · 1 unresolved
5 files
changed +124
−28
src/db.test.ts +76 −6
| @@ -71,7 +71,7 @@ test("undo of a renormalising move restores every renormalised card, not just th | ||
| 71 | 71 | const afterMove = db.listCards(boardId).filter((c) => c.column_id === columnId); |
| 72 | 72 | expect(afterMove.length).toBe(before.length + 1); |
| 73 | 73 | |
| 74 | expect(db.undo(boardId)).toBe(true); | |
| 74 | expect(db.undo(boardId)).toBe('moved "mover"'); | |
| 75 | 75 | const afterUndo = db.listCards(boardId).filter((c) => c.column_id === columnId && c.id !== mover.id); |
| 76 | 76 | expect(afterUndo.map((c) => [c.id, c.position])).toEqual(before.map((c) => [c.id, c.position])); |
| 77 | 77 | expect(db.getCard(boardId, mover.id)!.position).toBe(moverBefore.position); |
| @@ -91,8 +91,8 @@ test("ties are ordered consistently by position then id", () => { | ||
| 91 | 91 | expect(second.id).toBe(expectedSecond); |
| 92 | 92 | }); |
| 93 | 93 | |
| 94 | test("undo returns false with nothing to undo", () => { | |
| 95 | expect(db.undo("no-such-board")).toBe(false); | |
| 94 | test("undo returns null with nothing to undo", () => { | |
| 95 | expect(db.undo("no-such-board")).toBeNull(); | |
| 96 | 96 | }); |
| 97 | 97 | |
| 98 | 98 | test("listBoardsByOwner is a person's own boards only, never unowned ones or another person's", () => { |
| @@ -290,11 +290,11 @@ test("undoing a rename restores the old name and leaves a card moved before it u | ||
| 290 | 290 | db.renameBoard(boardId, "New name"); |
| 291 | 291 | expect(db.getBoard(boardId)!.name).toBe("New name"); |
| 292 | 292 | |
| 293 | expect(db.undo(boardId)).toBe(true); | |
| 293 | expect(db.undo(boardId)).toBe("renamed the board"); | |
| 294 | 294 | expect(db.getBoard(boardId)!.name).toBe("Board"); |
| 295 | 295 | expect(db.getCard(boardId, card.id)!.column_id).toBe(columns[1].id); |
| 296 | 296 | |
| 297 | expect(db.undo(boardId)).toBe(true); | |
| 297 | expect(db.undo(boardId)).toBe('moved "Card"'); | |
| 298 | 298 | expect(db.getCard(boardId, card.id)!.column_id).toBe(columnId); |
| 299 | 299 | }); |
| 300 | 300 | |
| @@ -307,6 +307,76 @@ test("an action row logged before restoreBoardName existed still undoes cleanly" | ||
| 307 | 307 | const card = assertCard(db.createCard(boardId, columnId, "Card")); |
| 308 | 308 | db.db.query("INSERT INTO action (board_id, payload) VALUES (?1, ?2)").run(boardId, JSON.stringify({ deleteCardIds: [card.id] })); |
| 309 | 309 | |
| 310 | expect(db.undo(boardId)).toBe(true); | |
| 310 | expect(db.undo(boardId)).toBe("a change"); | |
| 311 | 311 | expect(db.getCard(boardId, card.id)).toBeNull(); |
| 312 | 312 | }); |
| 313 | ||
| 314 | test("a legacy action row with no label undoes cleanly and reports a change", () => { | |
| 315 | const { boardId, columnId } = setup(); | |
| 316 | const card = assertCard(db.createCard(boardId, columnId, "Card")); | |
| 317 | db.db.query("DELETE FROM action WHERE board_id = ?1").run(boardId); | |
| 318 | db.db.query("INSERT INTO action (board_id, payload) VALUES (?1, ?2)").run(boardId, JSON.stringify({ deleteCardIds: [card.id] })); | |
| 319 | ||
| 320 | expect(db.peekUndo(boardId)).toBe("a change"); | |
| 321 | expect(db.undo(boardId)).toBe("a change"); | |
| 322 | expect(db.getCard(boardId, card.id)).toBeNull(); | |
| 323 | }); | |
| 324 | ||
| 325 | test("a label round-trips through the action row for each kind of mutation", () => { | |
| 326 | const { boardId, columnId } = setup(); | |
| 327 | ||
| 328 | const card = assertCard(db.createCard(boardId, columnId, "Ship it")); | |
| 329 | expect(db.peekUndo(boardId)).toBe("added a card"); | |
| 330 | expect(db.undo(boardId)).toBe("added a card"); | |
| 331 | ||
| 332 | const card2 = assertCard(db.createCard(boardId, columnId, "Ship it")); | |
| 333 | db.editCard(boardId, card2.id, "Ship it now"); | |
| 334 | expect(db.undo(boardId)).toBe('edited "Ship it"'); | |
| 335 | ||
| 336 | db.moveCard(boardId, card2.id, columnId, 0); | |
| 337 | expect(db.undo(boardId)).toBe('moved "Ship it"'); | |
| 338 | ||
| 339 | db.deleteCard(boardId, card2.id); | |
| 340 | expect(db.undo(boardId)).toBe('deleted "Ship it"'); | |
| 341 | ||
| 342 | const column = db.createColumn(boardId, "Doing 2"); | |
| 343 | if (typeof column === "string") throw new Error("expected a Column"); | |
| 344 | expect(db.undo(boardId)).toBe("added a column"); | |
| 345 | ||
| 346 | const column2 = db.createColumn(boardId, "Doing 2"); | |
| 347 | if (typeof column2 === "string") throw new Error("expected a Column"); | |
| 348 | db.renameColumn(boardId, column2.id, "Renamed"); | |
| 349 | expect(db.undo(boardId)).toBe('renamed the column "Doing 2"'); | |
| 350 | ||
| 351 | db.moveColumn(boardId, column2.id, "left"); | |
| 352 | expect(db.undo(boardId)).toBe('moved the column "Doing 2"'); | |
| 353 | ||
| 354 | db.deleteColumn(boardId, column2.id); | |
| 355 | expect(db.undo(boardId)).toBe('deleted the column "Doing 2"'); | |
| 356 | ||
| 357 | db.renameBoard(boardId, "Renamed board"); | |
| 358 | expect(db.undo(boardId)).toBe("renamed the board"); | |
| 359 | }); | |
| 360 | ||
| 361 | test("a title longer than 40 characters is elided once in the label itself", () => { | |
| 362 | const { boardId, columnId } = setup(); | |
| 363 | const longTitle = "x".repeat(60); | |
| 364 | const card = assertCard(db.createCard(boardId, columnId, longTitle)); | |
| 365 | db.deleteCard(boardId, card.id); | |
| 366 | const label = db.peekUndo(boardId)!; | |
| 367 | expect(label).toBe(`deleted "${"x".repeat(39)}…"`); | |
| 368 | }); | |
| 369 | ||
| 370 | test("peekUndo names the last change without consuming it", () => { | |
| 371 | expect(db.peekUndo("no-such-board")).toBeNull(); | |
| 372 | ||
| 373 | const { boardId, columnId } = setup(); | |
| 374 | expect(db.peekUndo(boardId)).toBe("added a column"); // the board's own three default columns | |
| 375 | ||
| 376 | const card = assertCard(db.createCard(boardId, columnId, "Card")); | |
| 377 | expect(db.peekUndo(boardId)).toBe("added a card"); | |
| 378 | expect(db.peekUndo(boardId)).toBe("added a card"); // reading twice does not consume it | |
| 379 | ||
| 380 | expect(db.undo(boardId)).toBe("added a card"); | |
| 381 | expect(db.peekUndo(boardId)).toBe("added a column"); | |
| 382 | }); |
src/db.ts +42 −16
| @@ -91,6 +91,14 @@ function randomId(length: number): string { | ||
| 91 | 91 | } |
| 92 | 92 | |
| 93 | 93 | type Payload = { |
| 94 | // A short human sentence for what this row's write did — "moved | |
| 95 | // \"Ship it\"", "deleted the column \"Doing\"" — built once, here, from | |
| 96 | // the data the write already holds. A card or column title inside it is | |
| 97 | // someone else's text: nothing in this file renders it, so every caller | |
| 98 | // (the page's `escape()`, the terminal's control-character strip) must | |
| 99 | // escape it before drawing. Optional only because a row written before | |
| 100 | // this field existed has none; undo() falls back to "a change" for those. | |
| 101 | label?: string; | |
| 94 | 102 | restoreColumns?: Column[]; |
| 95 | 103 | restoreCards?: Card[]; |
| 96 | 104 | deleteColumnIds?: string[]; |
| @@ -100,12 +108,20 @@ type Payload = { | ||
| 100 | 108 | restoreBoardName?: { id: string; name: string }; |
| 101 | 109 | }; |
| 102 | 110 | |
| 111 | // A title dropped into a label is elided once, here, rather than by every | |
| 112 | // renderer — a label is about to sit in an aria-label and a terminal line, | |
| 113 | // neither of which wants to wrap a long card title. | |
| 114 | const LABEL_TITLE_MAX = 40; | |
| 115 | function elide(title: string): string { | |
| 116 | return title.length > LABEL_TITLE_MAX ? `${title.slice(0, LABEL_TITLE_MAX - 1)}…` : title; | |
| 117 | } | |
| 118 | ||
| 103 | 119 | // Runs inside the same transaction as the write that logged the action, so |
| 104 | 120 | // undo never sees a row this trim already removed. Keeping only the |
| 105 | 121 | // newest MAX_UNDO_DEPTH means undoing past that horizon takes the |
| 106 | 122 | // existing "nothing to undo" path (undo() just finds no row). |
| 107 | function logAction(boardId: string, payload: Payload): void { | |
| 108 | db.query("INSERT INTO action (board_id, payload) VALUES (?1, ?2)").run(boardId, JSON.stringify(payload)); | |
| 123 | function logAction(boardId: string, label: string, payload: Omit<Payload, "label">): void { | |
| 124 | db.query("INSERT INTO action (board_id, payload) VALUES (?1, ?2)").run(boardId, JSON.stringify({ label, ...payload })); | |
| 109 | 125 | db.query( |
| 110 | 126 | `DELETE FROM action WHERE board_id = ?1 AND id NOT IN ( |
| 111 | 127 | SELECT id FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT ?2 |
| @@ -196,7 +212,7 @@ export function renameBoard(boardId: string, name: string): boolean { | ||
| 196 | 212 | const before = getBoard(boardId); |
| 197 | 213 | if (!before) return false; |
| 198 | 214 | db.query("UPDATE board SET name = ?2 WHERE id = ?1").run(boardId, name); |
| 199 | logAction(boardId, { restoreBoardName: { id: boardId, name: before.name } }); | |
| 215 | logAction(boardId, "renamed the board", { restoreBoardName: { id: boardId, name: before.name } }); | |
| 200 | 216 | bumpRevision(boardId); |
| 201 | 217 | bumpRevision(BOARDS_SCOPE); |
| 202 | 218 | return true; |
| @@ -244,7 +260,7 @@ export function createColumn(boardId: string, name: string, explicitPosition?: n | ||
| 244 | 260 | name, |
| 245 | 261 | position, |
| 246 | 262 | ); |
| 247 | logAction(boardId, { deleteColumnIds: [id] }); | |
| 263 | logAction(boardId, "added a column", { deleteColumnIds: [id] }); | |
| 248 | 264 | bumpRevision(boardId); |
| 249 | 265 | return { id, board_id: boardId, name, position }; |
| 250 | 266 | }).immediate(); |
| @@ -261,7 +277,7 @@ export function renameColumn(boardId: string, columnId: string, name: string): b | ||
| 261 | 277 | const before = getColumn(boardId, columnId); |
| 262 | 278 | if (!before) return false; |
| 263 | 279 | db.query("UPDATE board_column SET name = ?2 WHERE id = ?1").run(columnId, name); |
| 264 | logAction(boardId, { restoreColumns: [before] }); | |
| 280 | logAction(boardId, `renamed the column "${elide(before.name)}"`, { restoreColumns: [before] }); | |
| 265 | 281 | bumpRevision(boardId); |
| 266 | 282 | return true; |
| 267 | 283 | }).immediate(); |
| @@ -277,7 +293,7 @@ export function moveColumn(boardId: string, columnId: string, direction: "left" | ||
| 277 | 293 | const before = [columns[i], columns[j]]; |
| 278 | 294 | db.query("UPDATE board_column SET position = ?2 WHERE id = ?1").run(columns[i].id, columns[j].position); |
| 279 | 295 | db.query("UPDATE board_column SET position = ?2 WHERE id = ?1").run(columns[j].id, columns[i].position); |
| 280 | logAction(boardId, { restoreColumns: before }); | |
| 296 | logAction(boardId, `moved the column "${elide(columns[i].name)}"`, { restoreColumns: before }); | |
| 281 | 297 | bumpRevision(boardId); |
| 282 | 298 | return true; |
| 283 | 299 | }).immediate(); |
| @@ -289,7 +305,7 @@ export function deleteColumn(boardId: string, columnId: string): boolean { | ||
| 289 | 305 | if (!before) return false; |
| 290 | 306 | const cards = listCards(boardId).filter((c) => c.column_id === columnId); |
| 291 | 307 | db.query("DELETE FROM board_column WHERE id = ?1").run(columnId); |
| 292 | logAction(boardId, { restoreColumns: [before], restoreCards: cards }); | |
| 308 | logAction(boardId, `deleted the column "${elide(before.name)}"`, { restoreColumns: [before], restoreCards: cards }); | |
| 293 | 309 | bumpRevision(boardId); |
| 294 | 310 | return true; |
| 295 | 311 | }).immediate(); |
| @@ -309,7 +325,7 @@ export function createCard(boardId: string, columnId: string, title: string): Ca | ||
| 309 | 325 | title, |
| 310 | 326 | position, |
| 311 | 327 | ); |
| 312 | logAction(boardId, { deleteCardIds: [id] }); | |
| 328 | logAction(boardId, "added a card", { deleteCardIds: [id] }); | |
| 313 | 329 | bumpRevision(boardId); |
| 314 | 330 | return getCard(boardId, id)!; |
| 315 | 331 | }).immediate(); |
| @@ -329,7 +345,7 @@ export function editCard(boardId: string, cardId: string, title: string): boolea | ||
| 329 | 345 | const before = getCard(boardId, cardId); |
| 330 | 346 | if (!before) return false; |
| 331 | 347 | db.query("UPDATE card SET title = ?2, updated_at = datetime('now') WHERE id = ?1").run(cardId, title); |
| 332 | logAction(boardId, { restoreCards: [before] }); | |
| 348 | logAction(boardId, `edited "${elide(before.title)}"`, { restoreCards: [before] }); | |
| 333 | 349 | bumpRevision(boardId); |
| 334 | 350 | return true; |
| 335 | 351 | }).immediate(); |
| @@ -359,7 +375,7 @@ export function moveCard(boardId: string, cardId: string, columnId: string, inde | ||
| 359 | 375 | columnId, |
| 360 | 376 | position, |
| 361 | 377 | ); |
| 362 | logAction(boardId, { restoreCards: [before] }); | |
| 378 | logAction(boardId, `moved "${elide(before.title)}"`, { restoreCards: [before] }); | |
| 363 | 379 | bumpRevision(boardId); |
| 364 | 380 | return true; |
| 365 | 381 | } |
| @@ -377,7 +393,7 @@ export function moveCard(boardId: string, cardId: string, columnId: string, inde | ||
| 377 | 393 | columnId, |
| 378 | 394 | at + 1, |
| 379 | 395 | ); |
| 380 | logAction(boardId, { restoreCards }); | |
| 396 | logAction(boardId, `moved "${elide(before.title)}"`, { restoreCards }); | |
| 381 | 397 | bumpRevision(boardId); |
| 382 | 398 | return true; |
| 383 | 399 | }).immediate(); |
| @@ -388,18 +404,28 @@ export function deleteCard(boardId: string, cardId: string): boolean { | ||
| 388 | 404 | const before = getCard(boardId, cardId); |
| 389 | 405 | if (!before) return false; |
| 390 | 406 | db.query("DELETE FROM card WHERE id = ?1").run(cardId); |
| 391 | logAction(boardId, { restoreCards: [before] }); | |
| 407 | logAction(boardId, `deleted "${elide(before.title)}"`, { restoreCards: [before] }); | |
| 392 | 408 | bumpRevision(boardId); |
| 393 | 409 | return true; |
| 394 | 410 | }).immediate(); |
| 395 | 411 | } |
| 396 | 412 | |
| 397 | export function undo(boardId: string): boolean { | |
| 398 | return db.transaction((): boolean => { | |
| 413 | /** The label of what Undo would reverse — one indexed read, nothing changed. */ | |
| 414 | export function peekUndo(boardId: string): string | null { | |
| 415 | const row = db | |
| 416 | .query<{ payload: string }, [string]>("SELECT payload FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT 1") | |
| 417 | .get(boardId); | |
| 418 | if (!row) return null; | |
| 419 | return (JSON.parse(row.payload) as Payload).label ?? "a change"; | |
| 420 | } | |
| 421 | ||
| 422 | /** Reverses the last change and returns its label, or null when there was nothing to undo. */ | |
| 423 | export function undo(boardId: string): string | null { | |
| 424 | return db.transaction((): string | null => { | |
| 399 | 425 | const row = db |
| 400 | 426 | .query<{ id: number; payload: string }, [string]>("SELECT id, payload FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT 1") |
| 401 | 427 | .get(boardId); |
| 402 | if (!row) return false; | |
| 428 | if (!row) return null; | |
| 403 | 429 | const payload = JSON.parse(row.payload) as Payload; |
| 404 | 430 | const restoreColumn = db.query( |
| 405 | 431 | "INSERT OR REPLACE INTO board_column (id, board_id, name, position) VALUES (?1, ?2, ?3, ?4)", |
| @@ -417,6 +443,6 @@ export function undo(boardId: string): boolean { | ||
| 417 | 443 | } |
| 418 | 444 | db.query("DELETE FROM action WHERE id = ?1").run(row.id); |
| 419 | 445 | bumpRevision(boardId); |
| 420 | return true; | |
| 446 | return payload.label ?? "a change"; | |
| 421 | 447 | }).immediate(); |
| 422 | 448 | } |
src/keys.ts +2 −2
| @@ -30,7 +30,7 @@ export type BoardStore = { | ||
| 30 | 30 | editCard(boardId: string, cardId: string, title: string): boolean; |
| 31 | 31 | deleteCard(boardId: string, cardId: string): boolean; |
| 32 | 32 | moveCard(boardId: string, cardId: string, columnId: string, index: number): boolean; |
| 33 | undo(boardId: string): boolean; | |
| 33 | undo(boardId: string): string | null; | |
| 34 | 34 | boardRevision(boardId: string): number; |
| 35 | 35 | boardsRevision(): number; |
| 36 | 36 | }; |
| @@ -305,7 +305,7 @@ function handleBoardKey(state: State, key: string, board: BoardStore): Outcome { | ||
| 305 | 305 | } else if (key === "f") { |
| 306 | 306 | next.prompt = { kind: "filter", label: "Filter (empty clears)", value: state.filter }; |
| 307 | 307 | } else if (key === "u") { |
| 308 | next.message = board.undo(state.boardId!) ? "" : "nothing to undo"; | |
| 308 | next.message = board.undo(state.boardId!) !== null ? "" : "nothing to undo"; | |
| 309 | 309 | } |
| 310 | 310 | return { state: clampCursors(next, board), quit: false }; |
| 311 | 311 | } |
src/limits.test.ts +3 −3
| @@ -37,12 +37,12 @@ test("the undo log keeps only the newest N actions per board, oldest dropped, ne | ||
| 37 | 37 | expect(rowCount).toBe(MAX_UNDO_DEPTH); |
| 38 | 38 | |
| 39 | 39 | // The newest change is still reachable... |
| 40 | expect(db.undo(board.id)).toBe(true); | |
| 40 | expect(db.undo(board.id)).toBe(`renamed the column "name-${total - 2}"`); | |
| 41 | 41 | expect(db.getColumn(board.id, column.id)!.name).toBe(`name-${total - 2}`); |
| 42 | 42 | |
| 43 | 43 | // ...but the log holds exactly N actions — one more undo than that runs out. |
| 44 | for (let i = 1; i < MAX_UNDO_DEPTH; i++) expect(db.undo(board.id)).toBe(true); | |
| 45 | expect(db.undo(board.id)).toBe(false); | |
| 44 | for (let i = 1; i < MAX_UNDO_DEPTH; i++) expect(db.undo(board.id)).not.toBeNull(); | |
| 45 | expect(db.undo(board.id)).toBeNull(); | |
| 46 | 46 | }); |
| 47 | 47 | |
| 48 | 48 | test("a token bucket refuses once its tokens run out", () => { |
src/routes.ts +1 −1
| @@ -317,7 +317,7 @@ async function dispatch(request: Request, url: URL, env: string, who: Who, ip: s | ||
| 317 | 317 | |
| 318 | 318 | if (method === "POST" && parts.length === 3 && parts[2] === "undo") { |
| 319 | 319 | if (!writeBucket.take(writeKey)) return writeRefused(request, who, boardId, writeKey); |
| 320 | return donePublish(request, who, boardId, db.undo(boardId), "undo"); | |
| 320 | return donePublish(request, who, boardId, db.undo(boardId) !== null, "undo"); | |
| 321 | 321 | } |
| 322 | 322 | |
| 323 | 323 | if (method === "POST" && parts.length === 3 && parts[2] === "columns") { |
new above
offline
Approved — anchoring the verdict here, on
peekUndo, because this whole pull is one function's worth of new surface.I read every changed line against #118 and the direction, ran the suite at this head, and used the preview as a person would.
What I checked
undo()wentboolean→string | null,peekUndo()is the single indexedSELECT ... ORDER BY id DESC LIMIT 1the issue specified, and the label rides in the existingpayloadJSON, so there is no schema change and no migration. Nothing reaches past that.restoreCards/restoreColumns, and nothing in this change renders a label — the comment onPayload.labelnames the escaping contract for the callers that will (escape()on the page, the control-character strip in the terminal), which is the right place for it. Titles reaching a label have already been throughcleanText, so no control byte can ride in from either door. Every query stays parameterised, andlogAction'sJSON.stringify({ label, ...payload })cannot be shadowed because the parameter is typedOmit<Payload, "label">. On the preview the floor is intact on/— CSP,nosniff,referrer-policy,vary: x-forge-identity, HSTS. The undo route still spends awriteBuckettoken keyed oncallerKey(who, ip)— the verified forge id, or the socket for a guest, never a header — before it touches the database, and its 404 /{ok:false}path is untouched.src/keys.ts'sukey needed no change, because a label is never the empty string; theBoardStoretype was updated alongside it.routes.tsconverts with an explicit!== nullat its one call site. Page and terminal behave exactly as before, which is right for a change that adds no capability yet — the README's undo section is still accurate, so nothing to add there either.bun testis green at8e54245— 201 pass, 0 fail, 1336 assertions, run here, not taken on trust. The new tests fail onmainby construction (they assert label strings whereundo()returnedtrue), and they cover all four things the issue listed: a label round-trip per mutation kind,peekUndonaming without consuming, a legacy row with no label undoing to"a change", and elision at 40. The edits todb.test.ts/limits.test.tstighten rather than weaken —toBe(true)became the exact label.POST /b/:id/undostill answers{"ok":true}to a JSON caller, and a card titledShip it <script>alert(1)</script>came back escaped in both the card and its edit input./answers in about 130 ms over the network. The two pictures matchmain, which is what a database-only change should look like.Nits for whoever picks up #43 — none blocking, none worth another round here
renameColumnlogs"renamed a column"andeditCardlogs"edited a card", whilemoveColumnanddeleteColumnname the thing they touched. For a rename the old name is the useful one (renamed the column "Doing") and it is already in hand asbefore.name; same foreditCardwithbefore.title. The row that names itself least is the one a person is most likely to be undoing by mistake.src/keys.tstests the result for truthiness whilesrc/routes.tstests!== null. Both are correct today only because no label is empty; the two call sites now disagree about what "nothing to undo" means. Settle on!== nullin both when #43 touches the undo path.peekUndohas no production caller until #43 renders it. That is what the issue asked for and it is tested, so it is landing ahead of its consumer rather than dead code — but it should acquire that caller in the next pull rather than sit.elideslices by UTF-16 code unit, so a title of 60 emoji can be cut mid-surrogate.cleanTextinsrc/text.tsalready slices the same way, so this follows the pattern the repo has rather than inventing one, andJSON.stringifyescapes the lone surrogate rather than corrupting the row. Cosmetic at worst.