@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,331 @@
1
+ // history.mjs — the operation log at <wiki>/.llmwiki/op-log.yaml.
2
+ //
3
+ // Append-only. One record per top-level operation (Build, Extend, Rebuild,
4
+ // Fix, Join, Rollback, Migrate). The log lives alongside the private git
5
+ // repo but is not a commit — it's a simple YAML file the skill owns and
6
+ // treats as an audit trail. Later phases (history subcommand, decision log
7
+ // lookups, Fix's AUTO class) read this file to reason about prior ops.
8
+ //
9
+ // We intentionally hand-roll the YAML serialization. The log shape is
10
+ // fixed and simple, so a full YAML dependency would be overkill and could
11
+ // reintroduce non-determinism.
12
+
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ readFileSync,
17
+ renameSync,
18
+ writeFileSync,
19
+ } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+
22
+ export function opLogPath(wikiRoot) {
23
+ return join(wikiRoot, ".llmwiki", "op-log.yaml");
24
+ }
25
+
26
+ // The schema each record must satisfy. Extra keys are allowed and
27
+ // preserved verbatim on round-trip (in case a later phase wants to stash
28
+ // per-op metadata without an interface change).
29
+ const REQUIRED_FIELDS = [
30
+ "op_id",
31
+ "operation",
32
+ "layout_mode",
33
+ "started",
34
+ "finished",
35
+ "base_commit",
36
+ "final_commit",
37
+ "summary",
38
+ ];
39
+
40
+ function validateEntry(entry) {
41
+ if (!entry || typeof entry !== "object") {
42
+ throw new Error("op-log entry must be an object");
43
+ }
44
+ for (const f of REQUIRED_FIELDS) {
45
+ if (!(f in entry)) {
46
+ throw new Error(`op-log entry missing required field "${f}"`);
47
+ }
48
+ }
49
+ }
50
+
51
+ // Hand-rolled minimal YAML emitter. Supports only the record shape we
52
+ // write here — strings, numbers, booleans, and arrays of primitives.
53
+ // Nested objects are NOT supported: the parser below cannot round-trip
54
+ // them and this file deliberately keeps both sides of the codec in
55
+ // lockstep. If a future phase needs nested metadata, extend both the
56
+ // emitter AND the parser AND add a round-trip property test.
57
+ //
58
+ // Strings are aggressively quoted whenever they could be misread as a
59
+ // YAML scalar (reserved punctuation, boolean-ish literals, or anything
60
+ // that parses as a number). The companion `parseValue` below recognises
61
+ // the same quoting rule.
62
+ function needsQuoting(value) {
63
+ if (value === "") return true;
64
+ if (/[:#{}\[\],&*!|>'"%@`\n\r\t]/.test(value)) return true;
65
+ if (/^[- ?]/.test(value)) return true;
66
+ // Number-ish, boolean-ish, null-ish literals MUST be quoted so they
67
+ // round-trip as strings through parseValue.
68
+ if (/^-?\d+$/.test(value)) return true;
69
+ if (value === "true" || value === "false" || value === "null") return true;
70
+ return false;
71
+ }
72
+
73
+ // Escape a string for the quoted-scalar form. Handles backslash, double
74
+ // quote, newline, carriage return, and tab — which is every hazard the
75
+ // single-line YAML form can produce. Other control characters (U+0000..
76
+ // U+001F except \n \r \t) are rejected loudly because we have no
77
+ // round-trip-safe escape for them and silently dropping them would
78
+ // corrupt the audit trail.
79
+ function escapeQuoted(value) {
80
+ for (let i = 0; i < value.length; i++) {
81
+ const c = value.charCodeAt(i);
82
+ if (c < 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) {
83
+ throw new Error(
84
+ `op-log emitter: control character U+${c.toString(16).padStart(4, "0")} ` +
85
+ "is not round-trip-safe; strip it before logging",
86
+ );
87
+ }
88
+ }
89
+ let out = '"';
90
+ for (let i = 0; i < value.length; i++) {
91
+ const ch = value[i];
92
+ switch (ch) {
93
+ case "\\":
94
+ out += "\\\\";
95
+ break;
96
+ case '"':
97
+ out += '\\"';
98
+ break;
99
+ case "\n":
100
+ out += "\\n";
101
+ break;
102
+ case "\r":
103
+ out += "\\r";
104
+ break;
105
+ case "\t":
106
+ out += "\\t";
107
+ break;
108
+ default:
109
+ out += ch;
110
+ }
111
+ }
112
+ out += '"';
113
+ return out;
114
+ }
115
+
116
+ function emitScalar(value) {
117
+ if (value === null || value === undefined) return "null";
118
+ if (typeof value === "boolean" || typeof value === "number") {
119
+ return String(value);
120
+ }
121
+ if (typeof value === "string") {
122
+ if (needsQuoting(value)) return escapeQuoted(value);
123
+ return value;
124
+ }
125
+ throw new Error(`op-log emitter: unsupported scalar type ${typeof value}`);
126
+ }
127
+
128
+ function emitEntry(entry) {
129
+ const lines = [];
130
+ lines.push("- op_id: " + emitScalar(entry.op_id));
131
+ const ordered = [
132
+ "operation",
133
+ "layout_mode",
134
+ "started",
135
+ "finished",
136
+ "base_commit",
137
+ "final_commit",
138
+ "summary",
139
+ ];
140
+ for (const k of ordered) {
141
+ lines.push(` ${k}: ${emitScalar(entry[k])}`);
142
+ }
143
+ // Extra keys preserved in sorted order for determinism.
144
+ const known = new Set(["op_id", ...ordered]);
145
+ const extras = Object.keys(entry)
146
+ .filter((k) => !known.has(k))
147
+ .sort();
148
+ for (const k of extras) {
149
+ const v = entry[k];
150
+ if (Array.isArray(v)) {
151
+ lines.push(` ${k}:`);
152
+ for (const item of v) {
153
+ lines.push(` - ${emitScalar(item)}`);
154
+ }
155
+ } else if (v && typeof v === "object") {
156
+ // Nested objects are intentionally not supported — see the big
157
+ // comment on `needsQuoting` above. Callers must flatten first.
158
+ throw new Error(
159
+ `op-log emitter: nested object extras not supported for key "${k}". ` +
160
+ "Flatten the object or serialise it as a single string value.",
161
+ );
162
+ } else {
163
+ lines.push(` ${k}: ${emitScalar(v)}`);
164
+ }
165
+ }
166
+ return lines.join("\n");
167
+ }
168
+
169
+ // Append an entry. Creates the file (and the .llmwiki/ directory) if
170
+ // needed. Atomic: writes to a sibling temp file then renames, so a SIGKILL
171
+ // mid-append leaves either the old file intact or the new file complete,
172
+ // never a truncated in-between state. Append-only audit logs deserve the
173
+ // paranoia because we cannot recover from corruption once it's written.
174
+ export function appendOpLog(wikiRoot, entry) {
175
+ validateEntry(entry);
176
+ const path = opLogPath(wikiRoot);
177
+ mkdirSync(dirname(path), { recursive: true });
178
+ const block = emitEntry(entry) + "\n";
179
+ let payload;
180
+ if (!existsSync(path)) {
181
+ payload =
182
+ "# skill-llm-wiki operation log — append-only audit trail\n" + block;
183
+ } else {
184
+ const existing = readFileSync(path, "utf8");
185
+ const prefix = existing.endsWith("\n") ? existing : existing + "\n";
186
+ payload = prefix + block;
187
+ }
188
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
189
+ writeFileSync(tmp, payload, "utf8");
190
+ renameSync(tmp, path);
191
+ }
192
+
193
+ // Parse the log file into an array of entry objects. Hand-rolled parser
194
+ // that accepts what emitEntry emits; anything else is a hard error. The
195
+ // contract is "this file is owned by the skill" — if it looks wrong, we
196
+ // want to know, not paper over it.
197
+ //
198
+ // A new entry begins at any line matching `- <key>: <value>` (the list-item
199
+ // dash). This is independent of which key appears first, so extending the
200
+ // emitter with new ordered fields does NOT break the parser.
201
+ const ENTRY_START_RE = /^- (\w+):\s*(.*)$/;
202
+ const INDENTED_KEY_RE = /^ (\w+):\s*(.*)$/;
203
+ const INDENTED_LIST_RE = /^ - (.*)$/;
204
+
205
+ // Defence-in-depth against __proto__ / constructor key poisoning in the
206
+ // parsed op-log. The op-log file is produced by the skill itself, so
207
+ // this is belt-and-braces for a future attacker who might slip a
208
+ // crafted entry in via a manual edit or a corrupted sync.
209
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
210
+ function assertSafeKey(key, lineIndex) {
211
+ if (FORBIDDEN_KEYS.has(key)) {
212
+ throw new Error(
213
+ `op-log: forbidden key "${key}" at line ${lineIndex + 1}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ export function readOpLog(wikiRoot) {
219
+ const path = opLogPath(wikiRoot);
220
+ if (!existsSync(path)) return [];
221
+ const raw = readFileSync(path, "utf8");
222
+ const lines = raw.split(/\r?\n/);
223
+ const entries = [];
224
+ let current = null;
225
+ let currentListKey = null;
226
+ for (let i = 0; i < lines.length; i++) {
227
+ const line = lines[i];
228
+ if (line === "" || line.startsWith("#")) continue;
229
+ const entryMatch = ENTRY_START_RE.exec(line);
230
+ if (entryMatch) {
231
+ if (current) entries.push(current);
232
+ current = {};
233
+ const [, key, rest] = entryMatch;
234
+ assertSafeKey(key, i);
235
+ if (rest === "") {
236
+ current[key] = [];
237
+ currentListKey = key;
238
+ } else {
239
+ current[key] = parseValue(rest);
240
+ currentListKey = null;
241
+ }
242
+ continue;
243
+ }
244
+ if (!current) {
245
+ throw new Error(`op-log: stray line at ${i + 1}: ${line}`);
246
+ }
247
+ const listMatch = INDENTED_LIST_RE.exec(line);
248
+ if (listMatch && currentListKey) {
249
+ if (!Array.isArray(current[currentListKey])) {
250
+ current[currentListKey] = [];
251
+ }
252
+ current[currentListKey].push(parseValue(listMatch[1]));
253
+ continue;
254
+ }
255
+ const m = INDENTED_KEY_RE.exec(line);
256
+ if (!m) {
257
+ throw new Error(`op-log: unrecognised line at ${i + 1}: ${line}`);
258
+ }
259
+ const key = m[1];
260
+ const rest = m[2];
261
+ assertSafeKey(key, i);
262
+ if (rest === "") {
263
+ // Parent of an array (next lines start ` - `). Nested-object
264
+ // extras are explicitly unsupported on the emit side.
265
+ current[key] = [];
266
+ currentListKey = key;
267
+ } else {
268
+ current[key] = parseValue(rest);
269
+ currentListKey = null;
270
+ }
271
+ }
272
+ if (current) entries.push(current);
273
+ return entries;
274
+ }
275
+
276
+ // Char-by-char unescape so the parser consumes exactly one escape
277
+ // sequence at a time and cannot be confused by `\\n` (backslash followed
278
+ // by the letter n) versus `\n` (actual newline). Mirrors escapeQuoted.
279
+ function unescapeQuoted(body) {
280
+ let out = "";
281
+ for (let i = 0; i < body.length; i++) {
282
+ if (body[i] === "\\" && i + 1 < body.length) {
283
+ const next = body[i + 1];
284
+ switch (next) {
285
+ case "\\":
286
+ out += "\\";
287
+ break;
288
+ case '"':
289
+ out += '"';
290
+ break;
291
+ case "n":
292
+ out += "\n";
293
+ break;
294
+ case "r":
295
+ out += "\r";
296
+ break;
297
+ case "t":
298
+ out += "\t";
299
+ break;
300
+ default:
301
+ // Unknown escape sequence — pass through the following char
302
+ // verbatim. This is lenient by design so the parser survives
303
+ // format extensions without hard failure.
304
+ out += next;
305
+ }
306
+ i++;
307
+ } else {
308
+ out += body[i];
309
+ }
310
+ }
311
+ return out;
312
+ }
313
+
314
+ function parseValue(raw) {
315
+ if (raw === "null" || raw === "") return null;
316
+ if (raw === "true") return true;
317
+ if (raw === "false") return false;
318
+ if (/^-?\d+$/.test(raw)) return Number(raw);
319
+ if (raw.startsWith('"') && raw.endsWith('"')) {
320
+ return unescapeQuoted(raw.slice(1, -1));
321
+ }
322
+ return raw;
323
+ }
324
+
325
+ // Find an entry by op_id or by rollback-style "pre-<op-id>" shorthand.
326
+ // Returns null when no match is found.
327
+ export function findOpByRef(wikiRoot, ref) {
328
+ const entries = readOpLog(wikiRoot);
329
+ const opId = ref.startsWith("pre-") ? ref.slice(4) : ref;
330
+ return entries.find((e) => e.op_id === opId) ?? null;
331
+ }