Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01M5CF3ZFBNBAQSW6VPQ18DV2H
anchor: function apply
created: 2026-10-20T10:53:36Z
norm: '1'
sig: ff9533133fda3460
body_hash: 5f79de6b89df7e1a
raw_hash: 9aa0de0f92a88d1b
vouched: 2026-10-20T12:58:16Z
lines: 369-461
---

Validate the known title node by Lexical identity as well as the leading H1: title normalization can move a displaced title back to first position and restore its H1 tag after CRDT validation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5CP75J7N7BTES0G2NA6R6AD
anchor: function apply
created: 2026-10-20T12:57:41Z
norm: '1'
sig: ff9533133fda3460
body_hash: 5f79de6b89df7e1a
raw_hash: 9aa0de0f92a88d1b
lines: 369-476
---

When a client deletes the known title, normalization inserts a blank H1. An existing body H1 with the same Lexical identity and unchanged text stays body prose, even if its text fails title validation; a new or changed heading must still pass the title guard.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5CF3ZERVF08NRE6A50VG0N2
anchor: function validDocumentTitleChange
created: 2026-10-20T10:53:36Z
norm: '1'
sig: 001da7675d90c836
body_hash: 1cd05a48bdc3e78d
raw_hash: 810f7d9b440c364c
lines: 41-48
---

Validate raw proposed title text before whitespace normalization or length bounding. Permit only an exactly unchanged legacy corrupt title so durable documents can still be opened and explicitly repaired.
32 changes: 32 additions & 0 deletions apps/server/src/agent/heading-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,38 @@ test("draft_heading rejects lengths, CR titles, blank lines, and non-paragraph g
expect(context.plan.chat.jobOutput).toBeUndefined();
});

test("draft_heading rejects corrupt title text without changing the previous title or goal", async () => {
let context = await opened("# Resurfacing old notes\n");
headingJob(context.plan);
context.plan.plannerTitle = "Resurfacing old notes";
let before = room.project(context.plan.document);
let stored = await context.storage.collaboration.load(context.channel.id, context.now);
let errors = spyOn(console, "error").mockImplementation(() => {});
try {
for (
let title of [
"Resurfacing old notes �00\u0017a9 \u0016e29 runtime=default",
"Resurfacing old notes \u000000\u0017a9 \u0016e29 runtime=default",
"Notes\u0000runtime=default",
"Notes\u0085runtime=default",
"Notes\ud800",
"Notes\udc00",
]
) {
let result = await headingTool(context).handler({ ...INPUT, title }, {} as never);
expect(String(result)).toStartWith("Error:");
expect(room.project(context.plan.document)).toBe(before);
expect(context.plan.revision).toBe(0);
expect(context.plan.document.seq).toBe(0);
expect(context.plan.chat.jobOutput).toBeUndefined();
}
} finally {
errors.mockRestore();
}
expect(context.broadcasts.filter(frame => frame.kind === "plan:update")).toHaveLength(0);
expect(await context.storage.collaboration.load(context.channel.id, context.now)).toEqual(stored);
});

test("failed heading persistence leaves no live edit, publication, or job output", async () => {
let context = await opened(CARD);
headingJob(context.plan);
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/agent/heading-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
documentTitleText,
MAX_TITLE_LENGTH,
normalizeDocumentTitle,
validDocumentTitleText,
withDocumentTitle,
} from "@chopin/dialect";
import { parse } from "@chopin/dialect/parse";
Expand Down Expand Up @@ -41,6 +42,9 @@ function input(raw: unknown): Input {
) {
throw new Error(`title must be at most ${MAX_TITLE_LENGTH} Unicode characters on one line.`);
}
if (!validDocumentTitleText(value.title)) {
throw new Error("title must not contain control characters or damaged Unicode text.");
}
let title = normalizeDocumentTitle(value.title);
let goal: string | undefined;
if (Object.hasOwn(value, "goal")) {
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/channels/title.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { MAX_TITLE_LENGTH, normalizeDocumentTitle } from "@chopin/dialect";
import { MAX_TITLE_LENGTH, normalizeDocumentTitle, validDocumentTitleText } from "@chopin/dialect";
export { MAX_TITLE_LENGTH };

/** Explicit titles share the same normalization as authored headings. */
export function normalizedTitle(value: unknown): string | undefined {
return typeof value === "string" ? normalizeDocumentTitle(value) : undefined;
return typeof value === "string" && validDocumentTitleText(value)
? normalizeDocumentTitle(value)
: undefined;
}
Loading
Loading