@kontextmind/kxm 0.7.188 → 0.7.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/README.md +1 -0
- package/docs/contributing/ci-and-release.md +1 -1
- package/docs/contributing/development.md +2 -2
- package/docs/contributing/writing-docs.md +9 -2
- package/docs/evals/planner-bakeoff-2026-10/judge/argon.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/journal.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/reviewer.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/weekly.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/wiki.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/writer2.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/argon.txt +30 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/journal.txt +22 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/reviewer.txt +28 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/shared.txt +20 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/weekly.txt +23 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/wiki.txt +27 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/writer2.txt +40 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/argon-fable.txt +68 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/argon-sonnet.txt +69 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/journal-fable.txt +60 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/journal-sonnet.txt +79 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/reviewer-fable.txt +74 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/reviewer-sonnet.txt +95 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/weekly-fable.txt +63 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/weekly-sonnet.txt +66 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/wiki-fable.txt +72 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/wiki-sonnet.txt +79 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/writer2-fable.txt +73 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/writer2-sonnet.txt +79 -0
- package/docs/evals/planner-bakeoff-2026-10/score.py +304 -0
- package/docs/evals/planner-bakeoff-2026-10.md +164 -0
- package/package.json +2 -2
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/scripts/check-plan-links.d.mts +4 -0
- package/scripts/check-plan-links.mjs +234 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Relative links and heading anchors under plans/.
|
|
4
|
+
// Docs lint runs this. It does not call the network.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
|
|
12
|
+
export function githubSlug(value) {
|
|
13
|
+
return value.toLowerCase().replace(/[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu, "").replace(/ /g, "-");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function headingText(raw) {
|
|
17
|
+
let text = raw.trim().replace(/\s+#+\s*$/, "");
|
|
18
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
19
|
+
text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
20
|
+
text = text.replace(/<([^>\s]+)>/g, "$1");
|
|
21
|
+
text = text.replace(/`([^`]*)`/g, "$1");
|
|
22
|
+
text = text.replace(/<\/?[^>]+>/g, "");
|
|
23
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
24
|
+
text = text.replace(/"/g, '"').replace(/'/g, "'");
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function headingIds(markdown) {
|
|
29
|
+
const lines = markdown.split("\n");
|
|
30
|
+
const ids = new Set();
|
|
31
|
+
const occurrences = Object.create(null);
|
|
32
|
+
const own = Object.hasOwnProperty;
|
|
33
|
+
let index = 0;
|
|
34
|
+
let fence = null;
|
|
35
|
+
if (lines[0]?.trim() === "---") {
|
|
36
|
+
index = 1;
|
|
37
|
+
while (index < lines.length && lines[index].trim() !== "---") index += 1;
|
|
38
|
+
index += 1;
|
|
39
|
+
}
|
|
40
|
+
function addSlug(text) {
|
|
41
|
+
const original = githubSlug(headingText(text));
|
|
42
|
+
let result = original;
|
|
43
|
+
while (own.call(occurrences, result)) {
|
|
44
|
+
occurrences[original] = (occurrences[original] || 0) + 1;
|
|
45
|
+
result = `${original}-${occurrences[original]}`;
|
|
46
|
+
}
|
|
47
|
+
occurrences[result] = 0;
|
|
48
|
+
ids.add(result);
|
|
49
|
+
}
|
|
50
|
+
for (; index < lines.length; index += 1) {
|
|
51
|
+
const line = lines[index];
|
|
52
|
+
const opened = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
53
|
+
if (opened && !fence) {
|
|
54
|
+
fence = opened[1];
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (fence && new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) {
|
|
58
|
+
fence = null;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (fence) continue;
|
|
62
|
+
const atx = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
63
|
+
if (atx) addSlug(atx[2]);
|
|
64
|
+
for (const match of line.matchAll(/\sid=["']([^"']+)["']/g)) ids.add(match[1]);
|
|
65
|
+
for (const match of line.matchAll(/\sname=["']([^"']+)["']/g)) ids.add(match[1]);
|
|
66
|
+
}
|
|
67
|
+
return ids;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function visibleLines(markdown) {
|
|
71
|
+
const lines = markdown.split("\n");
|
|
72
|
+
const visible = [];
|
|
73
|
+
let fence = null;
|
|
74
|
+
let comment = false;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (!fence && !comment) {
|
|
77
|
+
const opened = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
78
|
+
if (opened) {
|
|
79
|
+
fence = opened[1];
|
|
80
|
+
visible.push("");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (fence) {
|
|
85
|
+
if (new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) fence = null;
|
|
86
|
+
visible.push("");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let rest = line;
|
|
90
|
+
let kept = "";
|
|
91
|
+
while (rest.length > 0) {
|
|
92
|
+
if (comment) {
|
|
93
|
+
const end = rest.indexOf("-->");
|
|
94
|
+
if (end < 0) {
|
|
95
|
+
rest = "";
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
comment = false;
|
|
99
|
+
rest = rest.slice(end + 3);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const start = rest.indexOf("<!--");
|
|
103
|
+
if (start < 0) {
|
|
104
|
+
kept += rest;
|
|
105
|
+
rest = "";
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
kept += rest.slice(0, start);
|
|
109
|
+
rest = rest.slice(start + 4);
|
|
110
|
+
comment = true;
|
|
111
|
+
}
|
|
112
|
+
visible.push(kept);
|
|
113
|
+
}
|
|
114
|
+
return visible;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function destinationOf(raw) {
|
|
118
|
+
let dest = raw.trim();
|
|
119
|
+
if (dest.startsWith("<") && dest.endsWith(">")) dest = dest.slice(1, -1).trim();
|
|
120
|
+
dest = dest.replace(/\s+"[^"]*"$/, "").replace(/\s+'[^']*'$/, "");
|
|
121
|
+
return dest;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function problemsForDestination(file, lineNumber, dest) {
|
|
125
|
+
if (!dest || /^[a-z][a-z0-9+.-]*:/i.test(dest)) return [];
|
|
126
|
+
if (dest.startsWith("//")) return [];
|
|
127
|
+
if (dest.startsWith("/")) {
|
|
128
|
+
return [`${file}:${lineNumber}: absolute path is not a repository link: ${dest}`];
|
|
129
|
+
}
|
|
130
|
+
const hash = dest.indexOf("#");
|
|
131
|
+
let pathPart = hash >= 0 ? dest.slice(0, hash) : dest;
|
|
132
|
+
const frag = hash >= 0 ? dest.slice(hash + 1) : "";
|
|
133
|
+
pathPart = pathPart.split("?")[0];
|
|
134
|
+
let decodedPath = pathPart;
|
|
135
|
+
let decodedFrag = frag;
|
|
136
|
+
try { decodedPath = decodeURIComponent(pathPart); } catch { /* keep raw */ }
|
|
137
|
+
try { decodedFrag = decodeURIComponent(frag); } catch { /* keep raw */ }
|
|
138
|
+
if (!decodedPath) {
|
|
139
|
+
if (!decodedFrag) return [];
|
|
140
|
+
const ids = headingIds(readFileSync(file, "utf8"));
|
|
141
|
+
if (!ids.has(decodedFrag)) return [`${file}:${lineNumber}: missing anchor #${decodedFrag}`];
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const target = resolve(dirname(file), decodedPath);
|
|
145
|
+
if (!existsSync(target)) return [`${file}:${lineNumber}: missing ${dest}`];
|
|
146
|
+
if (decodedFrag && target.endsWith(".md")) {
|
|
147
|
+
const ids = headingIds(readFileSync(target, "utf8"));
|
|
148
|
+
if (!ids.has(decodedFrag)) return [`${file}:${lineNumber}: missing anchor ${dest}`];
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function relatedProblems(file, markdown) {
|
|
154
|
+
if (!markdown.startsWith("---\n")) return [];
|
|
155
|
+
const end = markdown.indexOf("\n---", 4);
|
|
156
|
+
if (end < 0) return [];
|
|
157
|
+
const front = markdown.slice(4, end).split("\n");
|
|
158
|
+
const problems = [];
|
|
159
|
+
let inRelated = false;
|
|
160
|
+
for (let index = 0; index < front.length; index += 1) {
|
|
161
|
+
const line = front[index];
|
|
162
|
+
const inline = line.match(/^related:\s*\[(.*)\]\s*$/);
|
|
163
|
+
if (inline) {
|
|
164
|
+
for (const item of inline[1].split(",")) {
|
|
165
|
+
const rel = item.trim().replace(/^["']|["']$/g, "");
|
|
166
|
+
if (!rel || /^[a-z][a-z0-9+.-]*:/i.test(rel)) continue;
|
|
167
|
+
if (!existsSync(resolve(dirname(file), rel))) {
|
|
168
|
+
problems.push(`${file}:${index + 2}: missing related ${rel}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (/^related:\s*$/.test(line)) {
|
|
174
|
+
inRelated = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (inRelated && /^\S/.test(line)) inRelated = false;
|
|
178
|
+
if (!inRelated) continue;
|
|
179
|
+
const item = line.match(/^\s+-\s+(.+?)\s*$/);
|
|
180
|
+
if (!item) continue;
|
|
181
|
+
const rel = item[1].replace(/^["']|["']$/g, "");
|
|
182
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(rel)) continue;
|
|
183
|
+
if (!existsSync(resolve(dirname(file), rel))) {
|
|
184
|
+
problems.push(`${file}:${index + 2}: missing related ${rel}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return problems;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const linkRe = /(!?\[[^\]\n]*\])\(([^)\n]+)\)/g;
|
|
191
|
+
// Footnote bodies (`[^id]: prose`) are not link destinations. The URL inside
|
|
192
|
+
// the body is an ordinary inline link.
|
|
193
|
+
const refRe = /^ {0,3}\[([^\]^\n][^\]]*)\]:\s+(\S+)/;
|
|
194
|
+
|
|
195
|
+
export function checkFile(file) {
|
|
196
|
+
const markdown = readFileSync(file, "utf8");
|
|
197
|
+
const problems = relatedProblems(file, markdown);
|
|
198
|
+
const lines = visibleLines(markdown);
|
|
199
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
200
|
+
const line = lines[index];
|
|
201
|
+
for (const match of line.matchAll(linkRe)) {
|
|
202
|
+
problems.push(...problemsForDestination(file, index + 1, destinationOf(match[2])));
|
|
203
|
+
}
|
|
204
|
+
const ref = line.match(refRe);
|
|
205
|
+
if (ref) problems.push(...problemsForDestination(file, index + 1, destinationOf(ref[2])));
|
|
206
|
+
}
|
|
207
|
+
return problems;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function walk(dir, out = []) {
|
|
211
|
+
for (const name of readdirSync(dir)) {
|
|
212
|
+
const path = join(dir, name);
|
|
213
|
+
if (statSync(path).isDirectory()) walk(path, out);
|
|
214
|
+
else if (name.endsWith(".md")) out.push(path);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function checkPlans(root = repoRoot) {
|
|
220
|
+
const dir = join(root, "plans");
|
|
221
|
+
return walk(dir).flatMap((file) => checkFile(file));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function main() {
|
|
225
|
+
const problems = checkPlans();
|
|
226
|
+
for (const problem of problems) console.error(problem);
|
|
227
|
+
if (problems.length > 0) {
|
|
228
|
+
console.error(`check-plan-links: ${problems.length} broken`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
234
|
+
if (invoked) main();
|