@ctxr/skill-llm-wiki 1.0.1

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/LICENSE +21 -0
  3. package/README.md +484 -0
  4. package/SKILL.md +252 -0
  5. package/guide/basics/concepts.md +74 -0
  6. package/guide/basics/index.md +45 -0
  7. package/guide/basics/schema.md +140 -0
  8. package/guide/cli.md +256 -0
  9. package/guide/correctness/index.md +45 -0
  10. package/guide/correctness/invariants.md +89 -0
  11. package/guide/correctness/safety.md +96 -0
  12. package/guide/history/diff.md +110 -0
  13. package/guide/history/hidden-git.md +130 -0
  14. package/guide/history/index.md +52 -0
  15. package/guide/history/remote-sync.md +113 -0
  16. package/guide/index.md +134 -0
  17. package/guide/isolation/coexistence.md +134 -0
  18. package/guide/isolation/index.md +44 -0
  19. package/guide/isolation/scale.md +251 -0
  20. package/guide/layout/in-place-mode.md +97 -0
  21. package/guide/layout/index.md +53 -0
  22. package/guide/layout/layout-contract.md +131 -0
  23. package/guide/layout/layout-modes.md +115 -0
  24. package/guide/operations/index.md +76 -0
  25. package/guide/operations/ingest/build.md +75 -0
  26. package/guide/operations/ingest/extend.md +61 -0
  27. package/guide/operations/ingest/index.md +54 -0
  28. package/guide/operations/ingest/join.md +65 -0
  29. package/guide/operations/maintain/fix.md +66 -0
  30. package/guide/operations/maintain/index.md +47 -0
  31. package/guide/operations/maintain/rebuild.md +86 -0
  32. package/guide/operations/validate.md +48 -0
  33. package/guide/substrate/index.md +47 -0
  34. package/guide/substrate/operators.md +96 -0
  35. package/guide/substrate/tiered-ai.md +363 -0
  36. package/guide/ux/index.md +44 -0
  37. package/guide/ux/preflight.md +150 -0
  38. package/guide/ux/user-intent.md +135 -0
  39. package/package.json +55 -0
  40. package/scripts/cli.mjs +893 -0
  41. package/scripts/commands/remote.mjs +93 -0
  42. package/scripts/commands/review.mjs +253 -0
  43. package/scripts/commands/sync.mjs +84 -0
  44. package/scripts/lib/chunk.mjs +421 -0
  45. package/scripts/lib/cluster-detect.mjs +516 -0
  46. package/scripts/lib/decision-log.mjs +343 -0
  47. package/scripts/lib/draft.mjs +158 -0
  48. package/scripts/lib/embeddings.mjs +366 -0
  49. package/scripts/lib/frontmatter.mjs +497 -0
  50. package/scripts/lib/git-commands.mjs +155 -0
  51. package/scripts/lib/git.mjs +486 -0
  52. package/scripts/lib/gitignore.mjs +62 -0
  53. package/scripts/lib/history.mjs +331 -0
  54. package/scripts/lib/indices.mjs +510 -0
  55. package/scripts/lib/ingest.mjs +258 -0
  56. package/scripts/lib/intent.mjs +713 -0
  57. package/scripts/lib/interactive.mjs +99 -0
  58. package/scripts/lib/migrate.mjs +126 -0
  59. package/scripts/lib/nest-applier.mjs +260 -0
  60. package/scripts/lib/operators.mjs +1365 -0
  61. package/scripts/lib/orchestrator.mjs +718 -0
  62. package/scripts/lib/paths.mjs +197 -0
  63. package/scripts/lib/preflight.mjs +213 -0
  64. package/scripts/lib/provenance.mjs +672 -0
  65. package/scripts/lib/quality-metric.mjs +269 -0
  66. package/scripts/lib/query-fixture.mjs +71 -0
  67. package/scripts/lib/rollback.mjs +95 -0
  68. package/scripts/lib/shape-check.mjs +172 -0
  69. package/scripts/lib/similarity-cache.mjs +126 -0
  70. package/scripts/lib/similarity.mjs +230 -0
  71. package/scripts/lib/snapshot.mjs +54 -0
  72. package/scripts/lib/source-frontmatter.mjs +85 -0
  73. package/scripts/lib/tier2-protocol.mjs +470 -0
  74. package/scripts/lib/tiered.mjs +453 -0
  75. package/scripts/lib/validate.mjs +362 -0
@@ -0,0 +1,362 @@
1
+ // Validator: runs the hard invariants from the methodology against a wiki.
2
+ // Reports findings as structured objects so tools can consume them.
3
+
4
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
5
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
6
+ import { parseFrontmatter } from "./frontmatter.mjs";
7
+ import { readIndex } from "./indices.mjs";
8
+ import { isWikiRoot } from "./paths.mjs";
9
+ import { gitFsck, gitRefExists, gitRevParse, gitRun } from "./git.mjs";
10
+ import { provenancePath, readProvenance, verifyCoverage } from "./provenance.mjs";
11
+ import { readOpLog } from "./history.mjs";
12
+
13
+ export function validateWiki(wikiRoot) {
14
+ const findings = [];
15
+ const push = (severity, code, target, message) =>
16
+ findings.push({ severity, code, target, message });
17
+
18
+ if (!isWikiRoot(wikiRoot)) {
19
+ push("error", "WIKI-01", wikiRoot, "path is not a valid wiki root (no index.md or wrong naming)");
20
+ return findings;
21
+ }
22
+
23
+ // GIT-01 — guarded: only fires when the private git repo exists.
24
+ // When it fires, git fsck must pass with no non-dangling errors AND
25
+ // the most recent operation's pre-op tag must be reachable from HEAD.
26
+ runGit01(wikiRoot, push);
27
+
28
+ // LOSS-01 — guarded: only fires when .llmwiki/provenance.yaml exists.
29
+ // Every source byte must be accounted for via target sources[] or
30
+ // discarded_ranges[], with no gaps or overlaps.
31
+ runLoss01(wikiRoot, push);
32
+
33
+ const allEntries = collectAll(wikiRoot, push);
34
+
35
+ // Index maps for cross-checks
36
+ const byId = new Map();
37
+ const aliasTo = new Map();
38
+ for (const e of allEntries) {
39
+ if (byId.has(e.data.id)) {
40
+ push("error", "DUP-ID", e.absolute, `duplicate id "${e.data.id}" (also in ${byId.get(e.data.id).absolute})`);
41
+ } else {
42
+ byId.set(e.data.id, e);
43
+ }
44
+ for (const a of e.data.aliases ?? []) {
45
+ if (byId.has(a)) {
46
+ push("error", "ALIAS-COLLIDES-ID", e.absolute, `alias "${a}" collides with a live id`);
47
+ }
48
+ aliasTo.set(a, e);
49
+ }
50
+ }
51
+
52
+ for (const e of allEntries) {
53
+ const data = e.data;
54
+
55
+ // #1 Required frontmatter fields
56
+ const required = ["id", "type", "depth_role", "focus"];
57
+ for (const f of required) {
58
+ if (!(f in data)) push("error", "MISSING-FIELD", e.absolute, `required field "${f}" missing`);
59
+ }
60
+
61
+ // #2 id matches filename/directory
62
+ if (data.type === "index") {
63
+ if (data.id !== basename(dirname(e.absolute))) {
64
+ push("error", "ID-MISMATCH-DIR", e.absolute, `index id "${data.id}" must match directory name "${basename(dirname(e.absolute))}"`);
65
+ }
66
+ } else {
67
+ const expected = basename(e.absolute, ".md");
68
+ if (data.id !== expected) {
69
+ push("error", "ID-MISMATCH-FILE", e.absolute, `id "${data.id}" must match filename "${expected}"`);
70
+ }
71
+ }
72
+
73
+ // #3 depth_role matches tree position
74
+ const depth = depthOf(e.absolute, wikiRoot, data.type === "index");
75
+ const expectedRole = depth === 0 ? "category" : depth === 1 ? "category" : "subcategory";
76
+ if (data.type === "index") {
77
+ // Tolerate either category or subcategory at depth ≥1
78
+ if (depth === 0 && data.depth_role !== "category") {
79
+ push("error", "DEPTH-ROLE", e.absolute, `root index must have depth_role: category`);
80
+ }
81
+ } else if (data.depth_role !== "leaf") {
82
+ push("error", "DEPTH-ROLE", e.absolute, `leaf entry must have depth_role: leaf`);
83
+ }
84
+
85
+ // #8 parents[] required and non-empty (except root)
86
+ const isRoot = data.type === "index" && dirname(e.absolute) === wikiRoot;
87
+ if (!isRoot) {
88
+ if (!Array.isArray(data.parents) || data.parents.length === 0) {
89
+ push("error", "PARENTS-REQUIRED", e.absolute, `non-root entry must declare parents[]`);
90
+ }
91
+ }
92
+
93
+ // #11 leaf size cap
94
+ if (data.type === "primary" || data.type === "overlay") {
95
+ const lineCount = readFileSync(e.absolute, "utf8").split("\n").length;
96
+ const cap = data.type === "overlay" ? 200 : 500;
97
+ if (lineCount > cap) {
98
+ push("warning", "SIZE-CAP", e.absolute, `${data.type} entry exceeds ${cap}-line cap (${lineCount})`);
99
+ }
100
+ }
101
+
102
+ // #12 parent file contract: index.md body must not contain leaf-content signatures
103
+ if (data.type === "index") {
104
+ const { body } = parseFrontmatter(readFileSync(e.absolute, "utf8"), e.absolute);
105
+ const authored = extractAuthoredZone(body);
106
+ if (authored && /^\s*- \[ \]/m.test(authored)) {
107
+ push("error", "PARENT-CONTRACT", e.absolute, `index body contains checklist items — content must live in a leaf`);
108
+ }
109
+ if (authored && /^\s*```/m.test(authored)) {
110
+ push("error", "PARENT-CONTRACT", e.absolute, `index body contains code fences — content must live in a leaf`);
111
+ }
112
+ const budget = 2048;
113
+ if (authored && authored.length > budget) {
114
+ push("error", "PARENT-CONTRACT", e.absolute, `index authored zone ${authored.length} bytes exceeds ${budget}-byte budget`);
115
+ }
116
+ }
117
+
118
+ // #6 links[].id resolves
119
+ for (const link of data.links ?? []) {
120
+ if (!link || typeof link !== "object") continue;
121
+ const target = link.id;
122
+ if (!target) continue;
123
+ if (!byId.has(target) && !aliasTo.has(target)) {
124
+ push("error", "DANGLING-LINK", e.absolute, `links[].id "${target}" does not resolve`);
125
+ }
126
+ }
127
+
128
+ // #5 overlay targets resolve
129
+ if (data.type === "overlay") {
130
+ for (const target of data.overlay_targets ?? []) {
131
+ if (!byId.has(target) && !aliasTo.has(target)) {
132
+ push("error", "DANGLING-OVERLAY", e.absolute, `overlay_targets "${target}" does not resolve`);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ return findings;
139
+ }
140
+
141
+ function collectAll(wikiRoot, push) {
142
+ const out = [];
143
+ const stack = [wikiRoot];
144
+ while (stack.length > 0) {
145
+ const dir = stack.pop();
146
+ let entries;
147
+ try {
148
+ entries = readdirSync(dir, { withFileTypes: true });
149
+ } catch {
150
+ continue;
151
+ }
152
+ // index.md
153
+ const indexPath = join(dir, "index.md");
154
+ if (existsSync(indexPath)) {
155
+ try {
156
+ const { data } = parseFrontmatter(readFileSync(indexPath, "utf8"), indexPath);
157
+ out.push({ absolute: indexPath, data });
158
+ } catch (err) {
159
+ push("error", "PARSE", indexPath, err.message);
160
+ }
161
+ }
162
+ for (const e of entries) {
163
+ if (e.name.startsWith(".")) continue;
164
+ const full = join(dir, e.name);
165
+ if (e.isDirectory()) {
166
+ stack.push(full);
167
+ } else if (e.isFile() && e.name.endsWith(".md") && e.name !== "index.md") {
168
+ try {
169
+ const { data } = parseFrontmatter(readFileSync(full, "utf8"), full);
170
+ if (data && data.id) out.push({ absolute: full, data });
171
+ } catch (err) {
172
+ push("error", "PARSE", full, err.message);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return out;
178
+ }
179
+
180
+ function depthOf(absPath, wikiRoot, isIndex) {
181
+ const base = isIndex ? dirname(absPath) : dirname(absPath);
182
+ const rel = relative(wikiRoot, base);
183
+ if (rel === "" || rel === ".") return 0;
184
+ return rel.split("/").filter(Boolean).length;
185
+ }
186
+
187
+ function extractAuthoredZone(body) {
188
+ const start = body.indexOf("<!-- BEGIN AUTHORED ORIENTATION -->");
189
+ const end = body.indexOf("<!-- END AUTHORED ORIENTATION -->");
190
+ if (start === -1 || end === -1) return "";
191
+ return body.slice(start + "<!-- BEGIN AUTHORED ORIENTATION -->".length, end);
192
+ }
193
+
194
+ // GIT-01 — private git repo integrity.
195
+ //
196
+ // Guarded: only runs when `.llmwiki/git/HEAD` exists. When it runs, it
197
+ // requires `git fsck --no-dangling --no-reflogs` to exit cleanly AND
198
+ // the pre-op tag of the most recent logged operation to be reachable
199
+ // from HEAD (git ancestor check). Unreachable pre-op tags indicate
200
+ // either a tampered tree or a bug in the orchestrator's rollback path.
201
+ function runGit01(wikiRoot, push) {
202
+ if (!existsSync(join(wikiRoot, ".llmwiki", "git", "HEAD"))) return;
203
+ const fsck = gitFsck(wikiRoot);
204
+ if (!fsck.ok) {
205
+ push(
206
+ "error",
207
+ "GIT-01",
208
+ wikiRoot,
209
+ `git fsck failed: ${(fsck.stderr || fsck.stdout || "").trim()}`,
210
+ );
211
+ return;
212
+ }
213
+ // Find the most recent op's pre-op tag from the op-log. Empty log is
214
+ // a legitimate "freshly initialised wiki, no ops yet" — skip ancestor
215
+ // check in that case.
216
+ let opLog = [];
217
+ try {
218
+ opLog = readOpLog(wikiRoot);
219
+ } catch (err) {
220
+ push(
221
+ "error",
222
+ "GIT-01",
223
+ join(wikiRoot, ".llmwiki", "op-log.yaml"),
224
+ `unreadable op-log: ${err.message}`,
225
+ );
226
+ return;
227
+ }
228
+ if (opLog.length === 0) return;
229
+ const latest = opLog[opLog.length - 1];
230
+ const preTag = `pre-op/${latest.op_id}`;
231
+ if (!gitRefExists(wikiRoot, preTag)) {
232
+ push(
233
+ "error",
234
+ "GIT-01",
235
+ wikiRoot,
236
+ `pre-op tag ${preTag} for latest logged op not found in the private repo`,
237
+ );
238
+ return;
239
+ }
240
+ const headSha = gitRevParse(wikiRoot, "HEAD");
241
+ const preSha = gitRevParse(wikiRoot, preTag);
242
+ if (!headSha || !preSha) {
243
+ push(
244
+ "error",
245
+ "GIT-01",
246
+ wikiRoot,
247
+ `unable to resolve HEAD or ${preTag}`,
248
+ );
249
+ return;
250
+ }
251
+ // Ancestor check: pre-op/<latest> must be reachable from HEAD. Use
252
+ // `git merge-base --is-ancestor`, which exits 0 when preSha is an
253
+ // ancestor of headSha and exit 1 when it is not — O(log N) walk
254
+ // without materialising the full ancestry. Equality short-circuits
255
+ // for the common post-rollback case where HEAD === pre-op.
256
+ if (headSha === preSha) return;
257
+ try {
258
+ const r = gitRun(wikiRoot, [
259
+ "merge-base",
260
+ "--is-ancestor",
261
+ preSha,
262
+ headSha,
263
+ ]);
264
+ if (r.status === 0) return;
265
+ if (r.status === 1) {
266
+ push(
267
+ "error",
268
+ "GIT-01",
269
+ wikiRoot,
270
+ `pre-op/${latest.op_id} (${preSha.slice(0, 12)}) is not an ancestor of HEAD (${headSha.slice(0, 12)})`,
271
+ );
272
+ return;
273
+ }
274
+ push(
275
+ "error",
276
+ "GIT-01",
277
+ wikiRoot,
278
+ `merge-base --is-ancestor exited ${r.status}: ${(r.stderr || "").trim()}`,
279
+ );
280
+ } catch (err) {
281
+ push("error", "GIT-01", wikiRoot, `ancestor check failed: ${err.message}`);
282
+ }
283
+ }
284
+
285
+ // LOSS-01 — provenance coverage of every source byte.
286
+ //
287
+ // Guarded: only runs when `.llmwiki/provenance.yaml` exists. Walks the
288
+ // manifest's target entries, computes the reverse source → ranges
289
+ // index, and asserts that every byte is covered by either a target's
290
+ // preserved/split/merged/transformed range or an explicit discarded
291
+ // range. Source sizes come from the manifest's `source_size` field
292
+ // (authoritative at ingest time) so the check does NOT depend on the
293
+ // source file still being available at validation time.
294
+ function runLoss01(wikiRoot, push) {
295
+ if (!existsSync(provenancePath(wikiRoot))) return;
296
+ let doc;
297
+ try {
298
+ doc = readProvenance(wikiRoot);
299
+ } catch (err) {
300
+ push(
301
+ "error",
302
+ "LOSS-01",
303
+ provenancePath(wikiRoot),
304
+ `unreadable provenance manifest: ${err.message}`,
305
+ );
306
+ return;
307
+ }
308
+ // Build an in-memory lookup from the source_size fields recorded by
309
+ // recordSource at ingest time.
310
+ const sizeIndex = new Map();
311
+ for (const entry of Object.values(doc.targets)) {
312
+ for (const s of entry.sources || []) {
313
+ if (typeof s.source_size === "number") {
314
+ sizeIndex.set(s.source_path, s.source_size);
315
+ }
316
+ }
317
+ }
318
+ const result = verifyCoverage(wikiRoot, (path) => {
319
+ if (sizeIndex.has(path)) return sizeIndex.get(path);
320
+ return null;
321
+ });
322
+ if (result.ok) return;
323
+ for (const u of result.uncovered) {
324
+ const rangeDesc = u.byte_range
325
+ ? ` bytes ${u.byte_range[0]}..${u.byte_range[1]}`
326
+ : "";
327
+ push(
328
+ "error",
329
+ "LOSS-01",
330
+ join(wikiRoot, u.source_path || "<unknown-source>"),
331
+ `${u.source_path ?? "<unknown>"}${rangeDesc}: ${u.reason}`,
332
+ );
333
+ }
334
+ for (const o of result.overlaps) {
335
+ push(
336
+ "error",
337
+ "LOSS-01",
338
+ join(wikiRoot, o.source_path),
339
+ `${o.source_path} bytes ${o.byte_range[0]}..${o.byte_range[1]} claimed by ${o.target} AND another target`,
340
+ );
341
+ }
342
+ for (const ob of result.out_of_bounds || []) {
343
+ push(
344
+ "error",
345
+ "LOSS-01",
346
+ join(wikiRoot, ob.source_path),
347
+ `${ob.source_path} bytes ${ob.byte_range[0]}..${ob.byte_range[1]} exceed source_size ${ob.source_size} (target ${ob.target})`,
348
+ );
349
+ }
350
+ }
351
+
352
+ export function summariseFindings(findings) {
353
+ const byCode = new Map();
354
+ let errors = 0;
355
+ let warnings = 0;
356
+ for (const f of findings) {
357
+ if (f.severity === "error") errors++;
358
+ else if (f.severity === "warning") warnings++;
359
+ byCode.set(f.code, (byCode.get(f.code) ?? 0) + 1);
360
+ }
361
+ return { errors, warnings, byCode };
362
+ }