Skip to content

Commit 9544b48

Browse files
kronosapiensclaude
andauthored
fix: reject LLM output that modifies links in defrag (#485)
fix: reject LLM output that modifies links in defrag passes The sentence and terminology passes tell the LLM not to modify links, but it did anyway (e.g. inventing /client/sdk/typescript). Added a programmatic guard that compares link URLs before and after, rejecting any output where links were added, removed, or changed. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 152d5cb commit 9544b48

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

scripts/defrag-sentence.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
collectDocFiles,
2222
loadTextFile,
2323
callClaude,
24+
checkLinksPreserved,
2425
} from "./lib/defrag-utils.mjs";
2526

2627
const DRY_RUN = process.argv.includes("--dry-run");
@@ -107,6 +108,15 @@ async function main() {
107108
continue;
108109
}
109110

111+
const linkCheck = checkLinksPreserved(original, normalized);
112+
if (!linkCheck.ok) {
113+
console.warn(` REJECTED: LLM modified links in ${file.rel}`);
114+
if (linkCheck.added.length) console.warn(` Added: ${linkCheck.added.join(", ")}`);
115+
if (linkCheck.removed.length) console.warn(` Removed: ${linkCheck.removed.join(", ")}`);
116+
filesErrored++;
117+
continue;
118+
}
119+
110120
if (!DRY_RUN) {
111121
writeFileSync(file.path, normalized, "utf-8");
112122
}

scripts/defrag-terminology.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
collectDocFiles,
2424
loadTextFile,
2525
callClaude,
26+
checkLinksPreserved,
2627
} from "./lib/defrag-utils.mjs";
2728
import { join } from "path";
2829

@@ -102,6 +103,15 @@ async function main() {
102103
continue;
103104
}
104105

106+
const linkCheck = checkLinksPreserved(original, normalized);
107+
if (!linkCheck.ok) {
108+
console.warn(` REJECTED: LLM modified links in ${file.rel}`);
109+
if (linkCheck.added.length) console.warn(` Added: ${linkCheck.added.join(", ")}`);
110+
if (linkCheck.removed.length) console.warn(` Removed: ${linkCheck.removed.join(", ")}`);
111+
filesErrored++;
112+
continue;
113+
}
114+
105115
if (!DRY_RUN) {
106116
writeFileSync(file.path, normalized, "utf-8");
107117
}

scripts/lib/defrag-utils.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,50 @@ export function getValidRoutes() {
5151
return files.map((f) => "/" + f.rel.replace(/\.(md|mdx)$/, "").replace(/\/index$/, ""));
5252
}
5353

54+
/**
55+
* Extract all markdown link URLs from content (ignoring code blocks).
56+
*/
57+
export function extractLinkUrls(content) {
58+
const urls = [];
59+
let inCodeBlock = false;
60+
for (const line of content.split("\n")) {
61+
if (line.trimStart().startsWith("```")) {
62+
inCodeBlock = !inCodeBlock;
63+
continue;
64+
}
65+
if (inCodeBlock) continue;
66+
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
67+
let match;
68+
while ((match = linkRegex.exec(line)) !== null) {
69+
urls.push(match[2]);
70+
}
71+
}
72+
return urls;
73+
}
74+
75+
/**
76+
* Check that the LLM output preserved all original links and added none.
77+
* Returns { ok, added, removed } where added/removed are URL arrays.
78+
*/
79+
export function checkLinksPreserved(original, corrected) {
80+
const originalUrls = extractLinkUrls(original);
81+
const correctedUrls = extractLinkUrls(corrected);
82+
const origCounts = new Map();
83+
for (const u of originalUrls) origCounts.set(u, (origCounts.get(u) || 0) + 1);
84+
const corrCounts = new Map();
85+
for (const u of correctedUrls) corrCounts.set(u, (corrCounts.get(u) || 0) + 1);
86+
87+
const added = [];
88+
const removed = [];
89+
const allUrls = new Set([...origCounts.keys(), ...corrCounts.keys()]);
90+
for (const u of allUrls) {
91+
const diff = (corrCounts.get(u) || 0) - (origCounts.get(u) || 0);
92+
if (diff > 0) for (let i = 0; i < diff; i++) added.push(u);
93+
if (diff < 0) for (let i = 0; i < -diff; i++) removed.push(u);
94+
}
95+
return { ok: added.length === 0 && removed.length === 0, added, removed };
96+
}
97+
5498
/**
5599
* Check if any single diff hunk exceeds the size limit.
56100
*/

0 commit comments

Comments
 (0)