sdtk-wiki-kit 0.1.0
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/README.md +262 -0
- package/assets/atlas/build_atlas.py +1110 -0
- package/assets/atlas/doc_atlas_viewer_template.html +3796 -0
- package/assets/atlas/vendor/mermaid.min.js +2029 -0
- package/assets/keys/sdtk-entitlement-public.pem +11 -0
- package/bin/sdtk-wiki.js +14 -0
- package/package.json +45 -0
- package/src/commands/ask.js +139 -0
- package/src/commands/atlas.js +339 -0
- package/src/commands/deferred.js +14 -0
- package/src/commands/help.js +67 -0
- package/src/commands/init.js +91 -0
- package/src/commands/lint.js +48 -0
- package/src/commands/wiki.js +251 -0
- package/src/index.js +65 -0
- package/src/lib/args.js +68 -0
- package/src/lib/browser-open.js +32 -0
- package/src/lib/errors.js +29 -0
- package/src/lib/wiki-ask.js +175 -0
- package/src/lib/wiki-compile.js +287 -0
- package/src/lib/wiki-config.js +180 -0
- package/src/lib/wiki-discover.js +271 -0
- package/src/lib/wiki-flags.js +89 -0
- package/src/lib/wiki-ingest.js +198 -0
- package/src/lib/wiki-lint.js +468 -0
- package/src/lib/wiki-paths.js +169 -0
- package/src/lib/wiki-premium-loader.js +364 -0
- package/src/lib/wiki-prune.js +334 -0
- package/src/lib/wiki-query-history.js +111 -0
- package/src/lib/wiki-runner.js +373 -0
- package/src/lib/wiki-workspace.js +144 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { CliError, ValidationError } = require("./errors");
|
|
6
|
+
const {
|
|
7
|
+
assertWikiWorkspaceWritePath,
|
|
8
|
+
getWikiGraphPath,
|
|
9
|
+
getWikiPagesPath,
|
|
10
|
+
getWikiProvenanceSourcesPath,
|
|
11
|
+
getWikiReportsPath,
|
|
12
|
+
getWikiWorkspacePath,
|
|
13
|
+
isPathInsideOrEqual,
|
|
14
|
+
resolveProjectPath,
|
|
15
|
+
} = require("./wiki-paths");
|
|
16
|
+
|
|
17
|
+
const REQUIRED_PAGE_FIELDS = [
|
|
18
|
+
"schema_version",
|
|
19
|
+
"product",
|
|
20
|
+
"managed_by",
|
|
21
|
+
"page_id",
|
|
22
|
+
"source_path",
|
|
23
|
+
"source_hash",
|
|
24
|
+
"title",
|
|
25
|
+
"family",
|
|
26
|
+
"role",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CATEGORY_DEFS = [
|
|
30
|
+
["schema", "Frontmatter and schema"],
|
|
31
|
+
["duplicates", "Duplicate page IDs"],
|
|
32
|
+
["brokenLinks", "Broken internal links"],
|
|
33
|
+
["orphans", "Orphan pages"],
|
|
34
|
+
["missingReciprocal", "Missing reciprocal related_pages"],
|
|
35
|
+
["provenance", "Provenance gaps"],
|
|
36
|
+
["downstream", "Downstream integration gaps"],
|
|
37
|
+
["thin", "Thin pages"],
|
|
38
|
+
["stale", "Stale pages"],
|
|
39
|
+
["markers", "TODO/Open Questions/Gaps"],
|
|
40
|
+
["contradictions", "Candidate contradictions"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function toPosix(value) {
|
|
44
|
+
return value.split(path.sep).join("/");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripQuotes(value) {
|
|
48
|
+
const text = String(value || "").trim();
|
|
49
|
+
if (
|
|
50
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
51
|
+
(text.startsWith("'") && text.endsWith("'"))
|
|
52
|
+
) {
|
|
53
|
+
return text.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseInlineList(value) {
|
|
59
|
+
const text = String(value || "").trim();
|
|
60
|
+
if (!text) return [];
|
|
61
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
62
|
+
const inner = text.slice(1, -1).trim();
|
|
63
|
+
if (!inner) return [];
|
|
64
|
+
return inner
|
|
65
|
+
.split(",")
|
|
66
|
+
.map((item) => stripQuotes(item.trim()))
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
return text
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((item) => stripQuotes(item.trim()))
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseFrontmatter(text) {
|
|
76
|
+
const source = String(text || "");
|
|
77
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return { fields: {}, body: source, hasFrontmatter: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const block = match[1].split(/\r?\n/);
|
|
83
|
+
const fields = {};
|
|
84
|
+
for (const line of block) {
|
|
85
|
+
const separator = line.indexOf(":");
|
|
86
|
+
if (separator === -1) continue;
|
|
87
|
+
const key = line.slice(0, separator).trim();
|
|
88
|
+
const rawValue = line.slice(separator + 1).trim();
|
|
89
|
+
fields[key] = key === "related_pages" ? parseInlineList(rawValue) : stripQuotes(rawValue);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
fields,
|
|
94
|
+
body: source.slice(match[0].length),
|
|
95
|
+
hasFrontmatter: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readJsonIfPresent(filePath, fallback = null) {
|
|
100
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return { __lintError: `Could not parse JSON at ${filePath}: ${error.message}` };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function listMarkdownPages(rootPath) {
|
|
109
|
+
if (!fs.existsSync(rootPath)) return [];
|
|
110
|
+
const files = [];
|
|
111
|
+
|
|
112
|
+
function walk(current) {
|
|
113
|
+
const entries = fs.readdirSync(current, { withFileTypes: true }).sort((a, b) =>
|
|
114
|
+
a.name.localeCompare(b.name)
|
|
115
|
+
);
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const absolute = path.join(current, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
walk(absolute);
|
|
120
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
121
|
+
if (entry.name === "_index.md") continue;
|
|
122
|
+
files.push(absolute);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
walk(rootPath);
|
|
128
|
+
return files;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractMarkdownLinks(body) {
|
|
132
|
+
const links = [];
|
|
133
|
+
const matcher = /\[[^\]]+\]\(([^)]+)\)/g;
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = matcher.exec(body || "")) !== null) {
|
|
136
|
+
const rawTarget = String(match[1] || "").trim();
|
|
137
|
+
if (!rawTarget) continue;
|
|
138
|
+
if (
|
|
139
|
+
rawTarget.startsWith("#") ||
|
|
140
|
+
/^(?:https?:|mailto:|tel:)/i.test(rawTarget)
|
|
141
|
+
) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
links.push(rawTarget);
|
|
145
|
+
}
|
|
146
|
+
return links;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createFindings() {
|
|
150
|
+
return Object.fromEntries(CATEGORY_DEFS.map(([key]) => [key, []]));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function appendFinding(findings, category, text) {
|
|
154
|
+
findings[category].push(text);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function graphDegreeMap(graphPayload) {
|
|
158
|
+
const degrees = new Map();
|
|
159
|
+
if (!graphPayload || graphPayload.__lintError) return degrees;
|
|
160
|
+
const edges = Array.isArray(graphPayload.edges) ? graphPayload.edges : [];
|
|
161
|
+
for (const edge of edges) {
|
|
162
|
+
const source = typeof edge.source === "string" ? edge.source : null;
|
|
163
|
+
const target = typeof edge.target === "string" ? edge.target : null;
|
|
164
|
+
if (source) degrees.set(source, (degrees.get(source) || 0) + 1);
|
|
165
|
+
if (target) degrees.set(target, (degrees.get(target) || 0) + 1);
|
|
166
|
+
}
|
|
167
|
+
return degrees;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readLintInputs(projectPath, findings) {
|
|
171
|
+
const pagesRoot = getWikiPagesPath(projectPath);
|
|
172
|
+
const pageFiles = listMarkdownPages(pagesRoot);
|
|
173
|
+
const provenance = readJsonIfPresent(getWikiProvenanceSourcesPath(projectPath), { sources: [] });
|
|
174
|
+
const graph = readJsonIfPresent(path.join(getWikiGraphPath(projectPath), "SDTK_DOC_GRAPH.json"), {
|
|
175
|
+
edges: [],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (provenance && provenance.__lintError) {
|
|
179
|
+
appendFinding(findings, "provenance", provenance.__lintError);
|
|
180
|
+
}
|
|
181
|
+
if (graph && graph.__lintError) {
|
|
182
|
+
appendFinding(findings, "downstream", graph.__lintError);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sources = provenance && Array.isArray(provenance.sources) ? provenance.sources : [];
|
|
186
|
+
return { graph, pageFiles, pagesRoot, sources };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function analyzePages(projectPath) {
|
|
190
|
+
const findings = createFindings();
|
|
191
|
+
const inputs = readLintInputs(projectPath, findings);
|
|
192
|
+
const pages = [];
|
|
193
|
+
const pagesById = new Map();
|
|
194
|
+
const inboundMarkdown = new Map();
|
|
195
|
+
const inboundRelated = new Map();
|
|
196
|
+
|
|
197
|
+
for (const filePath of inputs.pageFiles) {
|
|
198
|
+
const relPath = toPosix(path.relative(inputs.pagesRoot, filePath));
|
|
199
|
+
const parsed = parseFrontmatter(fs.readFileSync(filePath, "utf-8"));
|
|
200
|
+
const fields = parsed.fields;
|
|
201
|
+
const page = {
|
|
202
|
+
filePath,
|
|
203
|
+
relPath,
|
|
204
|
+
body: parsed.body,
|
|
205
|
+
fields,
|
|
206
|
+
pageId: fields.page_id || "",
|
|
207
|
+
sourcePath: fields.source_path || "",
|
|
208
|
+
sourceHash: fields.source_hash || "",
|
|
209
|
+
relatedPages: Array.isArray(fields.related_pages) ? fields.related_pages : [],
|
|
210
|
+
links: extractMarkdownLinks(parsed.body),
|
|
211
|
+
};
|
|
212
|
+
pages.push(page);
|
|
213
|
+
|
|
214
|
+
if (!parsed.hasFrontmatter) {
|
|
215
|
+
appendFinding(findings, "schema", `\`${relPath}\` is missing parseable frontmatter.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const missingFields = REQUIRED_PAGE_FIELDS.filter((field) => !fields[field]);
|
|
219
|
+
if (missingFields.length > 0) {
|
|
220
|
+
appendFinding(
|
|
221
|
+
findings,
|
|
222
|
+
"schema",
|
|
223
|
+
`\`${relPath}\` is missing required fields: ${missingFields.join(", ")}.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (fields.product && fields.product !== "SDTK-WIKI") {
|
|
227
|
+
appendFinding(findings, "schema", `\`${relPath}\` has unexpected product \`${fields.product}\`.`);
|
|
228
|
+
}
|
|
229
|
+
if (fields.managed_by && fields.managed_by !== "sdtk-wiki") {
|
|
230
|
+
appendFinding(findings, "schema", `\`${relPath}\` has unexpected managed_by \`${fields.managed_by}\`.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (page.pageId) {
|
|
234
|
+
if (!pagesById.has(page.pageId)) {
|
|
235
|
+
pagesById.set(page.pageId, []);
|
|
236
|
+
}
|
|
237
|
+
pagesById.get(page.pageId).push(page);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const [pageId, records] of pagesById.entries()) {
|
|
242
|
+
if (records.length > 1) {
|
|
243
|
+
appendFinding(
|
|
244
|
+
findings,
|
|
245
|
+
"duplicates",
|
|
246
|
+
`\`${pageId}\` appears in ${records.map((record) => `\`${record.relPath}\``).join(", ")}.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const pagesByRelPath = new Map(pages.map((page) => [toPosix(path.normalize(page.relPath)), page]));
|
|
252
|
+
for (const page of pages) {
|
|
253
|
+
for (const link of page.links) {
|
|
254
|
+
const rawPath = link.split("#")[0].trim();
|
|
255
|
+
if (!rawPath) continue;
|
|
256
|
+
const resolved = path.resolve(path.dirname(page.filePath), rawPath);
|
|
257
|
+
if (!isPathInsideOrEqual(resolved, inputs.pagesRoot) || !fs.existsSync(resolved)) {
|
|
258
|
+
appendFinding(findings, "brokenLinks", `\`${page.relPath}\` links to missing \`${link}\`.`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const targetRel = toPosix(path.relative(inputs.pagesRoot, resolved));
|
|
262
|
+
if (pagesByRelPath.has(targetRel)) {
|
|
263
|
+
inboundMarkdown.set(targetRel, (inboundMarkdown.get(targetRel) || 0) + 1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const targetId of page.relatedPages) {
|
|
268
|
+
const targetRecords = pagesById.get(targetId) || [];
|
|
269
|
+
if (targetRecords.length === 0) {
|
|
270
|
+
appendFinding(
|
|
271
|
+
findings,
|
|
272
|
+
"missingReciprocal",
|
|
273
|
+
`\`${page.relPath}\` declares related page \`${targetId}\`, but no target page exists.`
|
|
274
|
+
);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
for (const target of targetRecords) {
|
|
278
|
+
inboundRelated.set(target.pageId, (inboundRelated.get(target.pageId) || 0) + 1);
|
|
279
|
+
if (!target.relatedPages.includes(page.pageId)) {
|
|
280
|
+
appendFinding(
|
|
281
|
+
findings,
|
|
282
|
+
"missingReciprocal",
|
|
283
|
+
`\`${page.pageId}\` references \`${targetId}\`, but the target does not reference \`${page.pageId}\`.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const provenanceBySource = new Map(
|
|
291
|
+
inputs.sources
|
|
292
|
+
.filter((record) => record && typeof record.sourcePath === "string")
|
|
293
|
+
.map((record) => [record.sourcePath, record])
|
|
294
|
+
);
|
|
295
|
+
const degrees = graphDegreeMap(inputs.graph);
|
|
296
|
+
|
|
297
|
+
for (const page of pages) {
|
|
298
|
+
const sourceRecord = page.sourcePath ? provenanceBySource.get(page.sourcePath) : null;
|
|
299
|
+
if (page.sourcePath && !sourceRecord) {
|
|
300
|
+
appendFinding(
|
|
301
|
+
findings,
|
|
302
|
+
"provenance",
|
|
303
|
+
`\`${page.relPath}\` references source \`${page.sourcePath}\` missing from provenance sources.`
|
|
304
|
+
);
|
|
305
|
+
appendFinding(
|
|
306
|
+
findings,
|
|
307
|
+
"stale",
|
|
308
|
+
`\`${page.relPath}\` may be stale because source \`${page.sourcePath}\` is absent from provenance.`
|
|
309
|
+
);
|
|
310
|
+
} else if (
|
|
311
|
+
page.sourcePath &&
|
|
312
|
+
sourceRecord &&
|
|
313
|
+
typeof sourceRecord.sourceHash === "string" &&
|
|
314
|
+
page.sourceHash &&
|
|
315
|
+
sourceRecord.sourceHash !== page.sourceHash
|
|
316
|
+
) {
|
|
317
|
+
appendFinding(
|
|
318
|
+
findings,
|
|
319
|
+
"provenance",
|
|
320
|
+
`\`${page.relPath}\` source hash differs from provenance for \`${page.sourcePath}\`.`
|
|
321
|
+
);
|
|
322
|
+
appendFinding(
|
|
323
|
+
findings,
|
|
324
|
+
"stale",
|
|
325
|
+
`\`${page.relPath}\` may be stale because its source hash differs from provenance.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const inboundLinks = inboundMarkdown.get(page.relPath) || 0;
|
|
330
|
+
const relatedInboundCount = inboundRelated.get(page.pageId) || 0;
|
|
331
|
+
if (inboundLinks === 0 && relatedInboundCount === 0) {
|
|
332
|
+
appendFinding(findings, "orphans", `\`${page.relPath}\` has no inbound wiki links or reciprocal relationships.`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const graphDegree = page.sourcePath ? degrees.get(page.sourcePath) || 0 : 0;
|
|
336
|
+
if (
|
|
337
|
+
page.sourcePath &&
|
|
338
|
+
graphDegree === 0 &&
|
|
339
|
+
inboundLinks === 0 &&
|
|
340
|
+
relatedInboundCount === 0 &&
|
|
341
|
+
page.relatedPages.length === 0
|
|
342
|
+
) {
|
|
343
|
+
appendFinding(
|
|
344
|
+
findings,
|
|
345
|
+
"downstream",
|
|
346
|
+
`\`${page.relPath}\` has no downstream integration signal for source \`${page.sourcePath}\`.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const compactBody = page.body.replace(/\s+/g, " ").trim();
|
|
351
|
+
if (compactBody.length < 120) {
|
|
352
|
+
appendFinding(findings, "thin", `\`${page.relPath}\` is thin (${compactBody.length} normalized characters).`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const markerLabels = [];
|
|
356
|
+
if (/\bTODO\b/i.test(page.body)) markerLabels.push("TODO");
|
|
357
|
+
if (/open questions/i.test(page.body)) markerLabels.push("Open Questions");
|
|
358
|
+
if (/\bgaps?\b/i.test(page.body)) markerLabels.push("Gaps");
|
|
359
|
+
if (markerLabels.length > 0) {
|
|
360
|
+
appendFinding(findings, "markers", `\`${page.relPath}\` contains ${markerLabels.join(", ")} markers.`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
/\b(?:conflict|contradict|inconsistent)\b/i.test(page.body) ||
|
|
365
|
+
/\bmust\b[\s\S]{0,160}\bmust not\b/i.test(page.body) ||
|
|
366
|
+
/\bmust not\b[\s\S]{0,160}\bmust\b/i.test(page.body)
|
|
367
|
+
) {
|
|
368
|
+
appendFinding(
|
|
369
|
+
findings,
|
|
370
|
+
"contradictions",
|
|
371
|
+
`\`${page.relPath}\` contains heuristic contradiction language and should be reviewed manually.`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { findings, pageCount: pages.length };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function todayStamp() {
|
|
380
|
+
return new Date().toISOString().slice(0, 10);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function totalFindings(findings) {
|
|
384
|
+
return CATEGORY_DEFS.reduce((sum, [key]) => sum + findings[key].length, 0);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function renderReport({ projectPath, workspaceRoot, findings, pageCount }) {
|
|
388
|
+
const summaryRows = CATEGORY_DEFS.map(
|
|
389
|
+
([key, label]) => `| ${label} | ${findings[key].length} |`
|
|
390
|
+
);
|
|
391
|
+
const detailSections = CATEGORY_DEFS.flatMap(([key, label]) => {
|
|
392
|
+
const items = findings[key];
|
|
393
|
+
return [
|
|
394
|
+
`## ${label}`,
|
|
395
|
+
"",
|
|
396
|
+
...(items.length > 0 ? items.map((item) => `- ${item}`) : ["- None."]),
|
|
397
|
+
"",
|
|
398
|
+
];
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return [
|
|
402
|
+
"# SDTK-WIKI Lint Report",
|
|
403
|
+
"",
|
|
404
|
+
`Date: ${todayStamp()}`,
|
|
405
|
+
`Project root: \`${projectPath}\``,
|
|
406
|
+
`Workspace root: \`${workspaceRoot}\``,
|
|
407
|
+
`Pages scanned: ${pageCount}`,
|
|
408
|
+
`Total findings: ${totalFindings(findings)}`,
|
|
409
|
+
"",
|
|
410
|
+
"Report-only and non-destructive: lint writes this report and does not auto-modify wiki or source content.",
|
|
411
|
+
"Candidate contradictions are heuristic only and require manual review.",
|
|
412
|
+
"",
|
|
413
|
+
"## Summary",
|
|
414
|
+
"",
|
|
415
|
+
"| Category | Count |",
|
|
416
|
+
"|---|---:|",
|
|
417
|
+
...summaryRows,
|
|
418
|
+
"",
|
|
419
|
+
...detailSections,
|
|
420
|
+
].join("\n");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function runWikiLint(options = {}) {
|
|
424
|
+
const projectPath = resolveProjectPath(options.projectPath);
|
|
425
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
426
|
+
throw new ValidationError(`--project-path is not a valid directory: ${projectPath}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const workspaceRoot = getWikiWorkspacePath(projectPath);
|
|
430
|
+
if (!fs.existsSync(workspaceRoot) || !fs.statSync(workspaceRoot).isDirectory()) {
|
|
431
|
+
throw new ValidationError(
|
|
432
|
+
`No SDTK-WIKI workspace found at ${workspaceRoot}. Run "sdtk-wiki init" first.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const analysis = analyzePages(projectPath);
|
|
437
|
+
const reportsRoot = getWikiReportsPath(projectPath);
|
|
438
|
+
try {
|
|
439
|
+
assertWikiWorkspaceWritePath(reportsRoot, projectPath);
|
|
440
|
+
fs.mkdirSync(reportsRoot, { recursive: true });
|
|
441
|
+
const reportPath = path.join(reportsRoot, `lint-report-${todayStamp()}.md`);
|
|
442
|
+
assertWikiWorkspaceWritePath(reportPath, projectPath);
|
|
443
|
+
const report = renderReport({
|
|
444
|
+
projectPath,
|
|
445
|
+
workspaceRoot,
|
|
446
|
+
findings: analysis.findings,
|
|
447
|
+
pageCount: analysis.pageCount,
|
|
448
|
+
});
|
|
449
|
+
fs.writeFileSync(reportPath, report + "\n", "utf-8");
|
|
450
|
+
return {
|
|
451
|
+
reportPath,
|
|
452
|
+
totalFindings: totalFindings(analysis.findings),
|
|
453
|
+
findings: analysis.findings,
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error instanceof CliError) throw error;
|
|
457
|
+
throw new CliError(`Failed to write SDTK-WIKI lint report: ${error.message}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
CATEGORY_DEFS,
|
|
463
|
+
REQUIRED_PAGE_FIELDS,
|
|
464
|
+
analyzePages,
|
|
465
|
+
parseFrontmatter,
|
|
466
|
+
renderReport,
|
|
467
|
+
runWikiLint,
|
|
468
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const WIKI_WORKSPACE_RELATIVE = path.join(".sdtk", "wiki");
|
|
6
|
+
const WIKI_GRAPH_RELATIVE = path.join(".sdtk", "wiki", "graph");
|
|
7
|
+
const WIKI_MANIFEST_RELATIVE = path.join(".sdtk", "wiki", "manifest.json");
|
|
8
|
+
const WIKI_PAGES_RELATIVE = path.join(".sdtk", "wiki", "pages");
|
|
9
|
+
const WIKI_RAW_RELATIVE = path.join(".sdtk", "wiki", "raw");
|
|
10
|
+
const WIKI_RAW_DESCRIPTORS_RELATIVE = path.join(".sdtk", "wiki", "raw", "descriptors");
|
|
11
|
+
const WIKI_RAW_SOURCES_RELATIVE = path.join(".sdtk", "wiki", "raw", "sources.json");
|
|
12
|
+
const WIKI_PROVENANCE_RELATIVE = path.join(".sdtk", "wiki", "provenance");
|
|
13
|
+
const WIKI_PROVENANCE_INGEST_EVENTS_RELATIVE = path.join(".sdtk", "wiki", "provenance", "ingest-events.json");
|
|
14
|
+
const WIKI_PROVENANCE_SOURCES_RELATIVE = path.join(".sdtk", "wiki", "provenance", "sources.json");
|
|
15
|
+
const WIKI_QUERIES_RELATIVE = path.join(".sdtk", "wiki", "queries");
|
|
16
|
+
const WIKI_REPORTS_RELATIVE = path.join(".sdtk", "wiki", "reports");
|
|
17
|
+
const WIKI_LOGS_RELATIVE = path.join(".sdtk", "wiki", "logs");
|
|
18
|
+
const LEGACY_ATLAS_RELATIVE = path.join(".sdtk", "atlas");
|
|
19
|
+
|
|
20
|
+
function resolveProjectPath(projectPath) {
|
|
21
|
+
return path.resolve(projectPath || process.cwd());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeComparablePath(targetPath) {
|
|
25
|
+
const resolved = path.resolve(targetPath);
|
|
26
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPathInsideOrEqual(targetPath, rootPath) {
|
|
30
|
+
const comparableTarget = normalizeComparablePath(targetPath);
|
|
31
|
+
const comparableRoot = normalizeComparablePath(rootPath);
|
|
32
|
+
return (
|
|
33
|
+
comparableTarget === comparableRoot ||
|
|
34
|
+
comparableTarget.startsWith(comparableRoot + path.sep)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertPathInsideOrEqual(
|
|
39
|
+
targetPath,
|
|
40
|
+
rootPath,
|
|
41
|
+
message = "Refusing to access a path outside the allowed root"
|
|
42
|
+
) {
|
|
43
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
44
|
+
if (!isPathInsideOrEqual(resolvedTarget, rootPath)) {
|
|
45
|
+
throw new Error(`${message}: ${resolvedTarget}`);
|
|
46
|
+
}
|
|
47
|
+
return resolvedTarget;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getWikiWorkspacePath(projectPath) {
|
|
51
|
+
return path.join(resolveProjectPath(projectPath), WIKI_WORKSPACE_RELATIVE);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getWikiGraphPath(projectPath) {
|
|
55
|
+
return path.join(resolveProjectPath(projectPath), WIKI_GRAPH_RELATIVE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getWikiManifestPath(projectPath) {
|
|
59
|
+
return path.join(resolveProjectPath(projectPath), WIKI_MANIFEST_RELATIVE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getWikiPagesPath(projectPath) {
|
|
63
|
+
return path.join(resolveProjectPath(projectPath), WIKI_PAGES_RELATIVE);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getWikiRawPath(projectPath) {
|
|
67
|
+
return path.join(resolveProjectPath(projectPath), WIKI_RAW_RELATIVE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getWikiRawDescriptorsPath(projectPath) {
|
|
71
|
+
return path.join(resolveProjectPath(projectPath), WIKI_RAW_DESCRIPTORS_RELATIVE);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getWikiRawSourcesPath(projectPath) {
|
|
75
|
+
return path.join(resolveProjectPath(projectPath), WIKI_RAW_SOURCES_RELATIVE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getWikiProvenancePath(projectPath) {
|
|
79
|
+
return path.join(resolveProjectPath(projectPath), WIKI_PROVENANCE_RELATIVE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getWikiProvenanceIngestEventsPath(projectPath) {
|
|
83
|
+
return path.join(resolveProjectPath(projectPath), WIKI_PROVENANCE_INGEST_EVENTS_RELATIVE);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getWikiProvenanceSourcesPath(projectPath) {
|
|
87
|
+
return path.join(resolveProjectPath(projectPath), WIKI_PROVENANCE_SOURCES_RELATIVE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getWikiQueriesPath(projectPath) {
|
|
91
|
+
return path.join(resolveProjectPath(projectPath), WIKI_QUERIES_RELATIVE);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getWikiReportsPath(projectPath) {
|
|
95
|
+
return path.join(resolveProjectPath(projectPath), WIKI_REPORTS_RELATIVE);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getWikiLogsPath(projectPath) {
|
|
99
|
+
return path.join(resolveProjectPath(projectPath), WIKI_LOGS_RELATIVE);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getLegacyAtlasPath(projectPath) {
|
|
103
|
+
return path.join(resolveProjectPath(projectPath), LEGACY_ATLAS_RELATIVE);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function assertWikiWorkspaceWritePath(targetPath, projectPath) {
|
|
107
|
+
return assertPathInsideOrEqual(
|
|
108
|
+
targetPath,
|
|
109
|
+
getWikiWorkspacePath(projectPath),
|
|
110
|
+
"Refusing to write outside project-local .sdtk/wiki workspace"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function describeWikiPaths(projectPath) {
|
|
115
|
+
return {
|
|
116
|
+
projectPath: resolveProjectPath(projectPath),
|
|
117
|
+
wikiWorkspacePath: getWikiWorkspacePath(projectPath),
|
|
118
|
+
wikiGraphPath: getWikiGraphPath(projectPath),
|
|
119
|
+
wikiManifestPath: getWikiManifestPath(projectPath),
|
|
120
|
+
wikiPagesPath: getWikiPagesPath(projectPath),
|
|
121
|
+
wikiRawPath: getWikiRawPath(projectPath),
|
|
122
|
+
wikiRawDescriptorsPath: getWikiRawDescriptorsPath(projectPath),
|
|
123
|
+
wikiRawSourcesPath: getWikiRawSourcesPath(projectPath),
|
|
124
|
+
wikiProvenancePath: getWikiProvenancePath(projectPath),
|
|
125
|
+
wikiProvenanceIngestEventsPath: getWikiProvenanceIngestEventsPath(projectPath),
|
|
126
|
+
wikiProvenanceSourcesPath: getWikiProvenanceSourcesPath(projectPath),
|
|
127
|
+
wikiQueriesPath: getWikiQueriesPath(projectPath),
|
|
128
|
+
wikiReportsPath: getWikiReportsPath(projectPath),
|
|
129
|
+
wikiLogsPath: getWikiLogsPath(projectPath),
|
|
130
|
+
legacyAtlasPath: getLegacyAtlasPath(projectPath),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
LEGACY_ATLAS_RELATIVE,
|
|
136
|
+
WIKI_GRAPH_RELATIVE,
|
|
137
|
+
WIKI_LOGS_RELATIVE,
|
|
138
|
+
WIKI_MANIFEST_RELATIVE,
|
|
139
|
+
WIKI_PAGES_RELATIVE,
|
|
140
|
+
WIKI_RAW_DESCRIPTORS_RELATIVE,
|
|
141
|
+
WIKI_RAW_RELATIVE,
|
|
142
|
+
WIKI_RAW_SOURCES_RELATIVE,
|
|
143
|
+
WIKI_PROVENANCE_INGEST_EVENTS_RELATIVE,
|
|
144
|
+
WIKI_PROVENANCE_RELATIVE,
|
|
145
|
+
WIKI_PROVENANCE_SOURCES_RELATIVE,
|
|
146
|
+
WIKI_QUERIES_RELATIVE,
|
|
147
|
+
WIKI_REPORTS_RELATIVE,
|
|
148
|
+
WIKI_WORKSPACE_RELATIVE,
|
|
149
|
+
assertPathInsideOrEqual,
|
|
150
|
+
assertWikiWorkspaceWritePath,
|
|
151
|
+
describeWikiPaths,
|
|
152
|
+
getLegacyAtlasPath,
|
|
153
|
+
getWikiGraphPath,
|
|
154
|
+
getWikiLogsPath,
|
|
155
|
+
getWikiManifestPath,
|
|
156
|
+
getWikiPagesPath,
|
|
157
|
+
getWikiProvenanceIngestEventsPath,
|
|
158
|
+
getWikiProvenancePath,
|
|
159
|
+
getWikiProvenanceSourcesPath,
|
|
160
|
+
getWikiQueriesPath,
|
|
161
|
+
getWikiRawDescriptorsPath,
|
|
162
|
+
getWikiRawPath,
|
|
163
|
+
getWikiRawSourcesPath,
|
|
164
|
+
getWikiReportsPath,
|
|
165
|
+
getWikiWorkspacePath,
|
|
166
|
+
isPathInsideOrEqual,
|
|
167
|
+
normalizeComparablePath,
|
|
168
|
+
resolveProjectPath,
|
|
169
|
+
};
|