Skip to content

Commit deb835f

Browse files
ankur-archclaude
andcommitted
docs: collision-safe placeholder restoration in markdown protectors
Sequential per-token replace could corrupt output when protected code literally contained sentinel-looking text. Both protectors now use PUA-character delimiters lengthened deterministically until absent from the input, with a single-pass regex restore, and the guard gains a protector round-trip check with adversarial inputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 70e6715 commit deb835f

2 files changed

Lines changed: 112 additions & 10 deletions

File tree

apps/docs/scripts/lint-agent-ready.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { fileURLToPath } from "node:url";
2121
const { source } = await import("@/lib/source");
2222
const llms = await import("@/lib/llms");
2323
const { getLLMText } = await import("@/lib/get-llm-text");
24+
const { protectFencedCodeBlocks, protectInlineCode } = await import("@/lib/llm-markdown");
2425
const { withDocsBasePath } = await import("@/lib/urls");
2526
const { agentSkillMarkdown } = await import("@/lib/agent-skill");
2627
const { mcpDiscoveryDocument } = await import("@/lib/mcp-discovery");
@@ -368,6 +369,74 @@ if (mcpErrors.length > 0) {
368369
);
369370
}
370371

372+
// ── Check 10: placeholder protectors are collision-safe ──────────────────────
373+
// Regression coverage for the protect/restore pipeline shared by llm-markdown.ts
374+
// and get-llm-text.ts. Adversarial inputs embed text that LOOKS like the internal
375+
// placeholders inside inline code and fenced blocks, plus a normal body link. A
376+
// correct implementation restores every input byte-for-byte and rewrites ONLY the
377+
// body links that sit outside code. This guards against the old sequential-replace
378+
// corruption (a protected span reintroducing a later token; the fenced restore
379+
// consuming a sentinel reintroduced by an inline span).
380+
{
381+
// Mirror absolutizeInBodyLinks exactly: fences first, then inline spans, rewrite
382+
// the body links in between, then restore inline then fences.
383+
const runPipeline = (markdown: string) => {
384+
const fences = protectFencedCodeBlocks(markdown);
385+
const inline = protectInlineCode(fences.markdown);
386+
const rewritten = inline.markdown.replace(/\]\((\/[^)\s]*)\)/g, (full, target: string) => {
387+
if (target.startsWith("//")) return full;
388+
return `](https://www.prisma.io${target})`;
389+
});
390+
return fences.restore(inline.restore(rewritten));
391+
};
392+
393+
const fence = "```";
394+
const cases: { name: string; input: string; expected: string }[] = [
395+
{
396+
name: "inline code containing old-format placeholder text is preserved",
397+
input: "Use `__LLM_INLINE_CODE_1__` and `__LLM_FENCED_CODE_BLOCK_0__` literally here.",
398+
expected: "Use `__LLM_INLINE_CODE_1__` and `__LLM_FENCED_CODE_BLOCK_0__` literally here.",
399+
},
400+
{
401+
name: "fenced block containing inline-sentinel-looking text + body link is preserved",
402+
input: `${fence}\n__LLM_INLINE_CODE_0__ and [x](/orm/a)\n${fence}`,
403+
expected: `${fence}\n__LLM_INLINE_CODE_0__ and [x](/orm/a)\n${fence}`,
404+
},
405+
{
406+
name: "link inside inline code is NOT rewritten",
407+
input: "See `[label](/orm/foo)` inline.",
408+
expected: "See `[label](/orm/foo)` inline.",
409+
},
410+
{
411+
name: "body link outside code IS rewritten",
412+
input: "See [label](/orm/foo) here.",
413+
expected: "See [label](https://www.prisma.io/orm/foo) here.",
414+
},
415+
{
416+
name: "mixed: body link rewrites while placeholder-looking code stays verbatim",
417+
input: `Body [a](/x) then \`__LLM_FENCED_CODE_BLOCK_0__\` then\n${fence}\ncode [b](/y)\n${fence}\nend.`,
418+
expected: `Body [a](https://www.prisma.io/x) then \`__LLM_FENCED_CODE_BLOCK_0__\` then\n${fence}\ncode [b](/y)\n${fence}\nend.`,
419+
},
420+
];
421+
422+
const protectorFailures = cases.flatMap((testCase) => {
423+
const actual = runPipeline(testCase.input);
424+
if (actual === testCase.expected) return [];
425+
return [
426+
`${testCase.name}\n expected: ${JSON.stringify(testCase.expected)}\n actual: ${JSON.stringify(actual)}`,
427+
];
428+
});
429+
430+
if (protectorFailures.length > 0) {
431+
fail(
432+
"Protector round-trip",
433+
`${protectorFailures.length} of ${cases.length} case(s) failed:\n ${protectorFailures.join("\n ")}`,
434+
);
435+
} else {
436+
pass("Protector round-trip", `all ${cases.length} adversarial protect/restore cases pass`);
437+
}
438+
}
439+
371440
// ── Report ───────────────────────────────────────────────────────────────────
372441
const icon: Record<Status, string> = { pass: "✓", warn: "⚠", fail: "✗" };
373442
console.log("\nAgent-readiness checks\n");

apps/docs/src/lib/llm-markdown.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -374,12 +374,50 @@ function replaceComponentBlocks(
374374
return result;
375375
}
376376

377+
// Base sentinel delimiter built from Unicode Private Use Area characters, which
378+
// cannot legitimately appear in MDX source. Real placeholder text a page might
379+
// discuss (e.g. `__LLM_INLINE_CODE_1__`) can never look like one of these
380+
// tokens, so protected content is never mistaken for a placeholder.
381+
const SENTINEL_DELIMITER = "\uE000\uE001";
382+
383+
function escapeRegExp(value: string) {
384+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
385+
}
386+
387+
/**
388+
* Builds a collision-free placeholder scheme for one protector.
389+
*
390+
* The delimiter starts from PUA characters that cannot appear in MDX source and
391+
* is lengthened deterministically until it is guaranteed absent from `input`, so
392+
* a token can never collide with real content — including content that literally
393+
* spells out one of these placeholders. `label` keeps the two protectors' tokens
394+
* distinct so restoring one protector never touches the other's placeholders
395+
* (cross-protector safety). `restore` performs a single left-to-right pass and so
396+
* never rescans already-substituted content, which removes the corruption that a
397+
* sequential per-token replace loop could introduce.
398+
*/
399+
function createProtector(input: string, label: string) {
400+
let delimiter = SENTINEL_DELIMITER;
401+
while (input.includes(delimiter)) delimiter += "\uE000";
402+
403+
const boundary = escapeRegExp(delimiter);
404+
const pattern = new RegExp(`${boundary}${label}_(\\d+)${boundary}`, "g");
405+
406+
return {
407+
token: (index: number) => `${delimiter}${label}_${index}${delimiter}`,
408+
restore(value: string, blocks: readonly string[]) {
409+
return value.replace(pattern, (match, index: string) => blocks[Number(index)] ?? match);
410+
},
411+
};
412+
}
413+
377414
export function protectFencedCodeBlocks(markdown: string) {
378415
const blocks: string[] = [];
416+
const protector = createProtector(markdown, "LLM_FENCED_CODE_BLOCK");
379417
const protectedMarkdown = markdown.replace(
380418
/^([ \t]*)([`~]{3,})[^\n]*\n[\s\S]*?^\1\2\s*$/gm,
381419
(match) => {
382-
const token = `__LLM_FENCED_CODE_BLOCK_${blocks.length}__`;
420+
const token = protector.token(blocks.length);
383421
blocks.push(match);
384422
return token;
385423
},
@@ -388,10 +426,7 @@ export function protectFencedCodeBlocks(markdown: string) {
388426
return {
389427
markdown: protectedMarkdown,
390428
restore(value: string) {
391-
return blocks.reduce(
392-
(text, block, index) => text.replace(`__LLM_FENCED_CODE_BLOCK_${index}__`, block),
393-
value,
394-
);
429+
return protector.restore(value, blocks);
395430
},
396431
};
397432
}
@@ -405,19 +440,17 @@ export function protectFencedCodeBlocks(markdown: string) {
405440
*/
406441
export function protectInlineCode(markdown: string) {
407442
const spans: string[] = [];
443+
const protector = createProtector(markdown, "LLM_INLINE_CODE");
408444
const protectedMarkdown = markdown.replace(/(`+)[^\n]+?\1/g, (match) => {
409-
const token = `__LLM_INLINE_CODE_${spans.length}__`;
445+
const token = protector.token(spans.length);
410446
spans.push(match);
411447
return token;
412448
});
413449

414450
return {
415451
markdown: protectedMarkdown,
416452
restore(value: string) {
417-
return spans.reduce(
418-
(text, span, index) => text.replace(`__LLM_INLINE_CODE_${index}__`, span),
419-
value,
420-
);
453+
return protector.restore(value, spans);
421454
},
422455
};
423456
}

0 commit comments

Comments
 (0)