@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.
- package/CHANGELOG.md +134 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/SKILL.md +252 -0
- package/guide/basics/concepts.md +74 -0
- package/guide/basics/index.md +45 -0
- package/guide/basics/schema.md +140 -0
- package/guide/cli.md +256 -0
- package/guide/correctness/index.md +45 -0
- package/guide/correctness/invariants.md +89 -0
- package/guide/correctness/safety.md +96 -0
- package/guide/history/diff.md +110 -0
- package/guide/history/hidden-git.md +130 -0
- package/guide/history/index.md +52 -0
- package/guide/history/remote-sync.md +113 -0
- package/guide/index.md +134 -0
- package/guide/isolation/coexistence.md +134 -0
- package/guide/isolation/index.md +44 -0
- package/guide/isolation/scale.md +251 -0
- package/guide/layout/in-place-mode.md +97 -0
- package/guide/layout/index.md +53 -0
- package/guide/layout/layout-contract.md +131 -0
- package/guide/layout/layout-modes.md +115 -0
- package/guide/operations/index.md +76 -0
- package/guide/operations/ingest/build.md +75 -0
- package/guide/operations/ingest/extend.md +61 -0
- package/guide/operations/ingest/index.md +54 -0
- package/guide/operations/ingest/join.md +65 -0
- package/guide/operations/maintain/fix.md +66 -0
- package/guide/operations/maintain/index.md +47 -0
- package/guide/operations/maintain/rebuild.md +86 -0
- package/guide/operations/validate.md +48 -0
- package/guide/substrate/index.md +47 -0
- package/guide/substrate/operators.md +96 -0
- package/guide/substrate/tiered-ai.md +363 -0
- package/guide/ux/index.md +44 -0
- package/guide/ux/preflight.md +150 -0
- package/guide/ux/user-intent.md +135 -0
- package/package.json +55 -0
- package/scripts/cli.mjs +893 -0
- package/scripts/commands/remote.mjs +93 -0
- package/scripts/commands/review.mjs +253 -0
- package/scripts/commands/sync.mjs +84 -0
- package/scripts/lib/chunk.mjs +421 -0
- package/scripts/lib/cluster-detect.mjs +516 -0
- package/scripts/lib/decision-log.mjs +343 -0
- package/scripts/lib/draft.mjs +158 -0
- package/scripts/lib/embeddings.mjs +366 -0
- package/scripts/lib/frontmatter.mjs +497 -0
- package/scripts/lib/git-commands.mjs +155 -0
- package/scripts/lib/git.mjs +486 -0
- package/scripts/lib/gitignore.mjs +62 -0
- package/scripts/lib/history.mjs +331 -0
- package/scripts/lib/indices.mjs +510 -0
- package/scripts/lib/ingest.mjs +258 -0
- package/scripts/lib/intent.mjs +713 -0
- package/scripts/lib/interactive.mjs +99 -0
- package/scripts/lib/migrate.mjs +126 -0
- package/scripts/lib/nest-applier.mjs +260 -0
- package/scripts/lib/operators.mjs +1365 -0
- package/scripts/lib/orchestrator.mjs +718 -0
- package/scripts/lib/paths.mjs +197 -0
- package/scripts/lib/preflight.mjs +213 -0
- package/scripts/lib/provenance.mjs +672 -0
- package/scripts/lib/quality-metric.mjs +269 -0
- package/scripts/lib/query-fixture.mjs +71 -0
- package/scripts/lib/rollback.mjs +95 -0
- package/scripts/lib/shape-check.mjs +172 -0
- package/scripts/lib/similarity-cache.mjs +126 -0
- package/scripts/lib/similarity.mjs +230 -0
- package/scripts/lib/snapshot.mjs +54 -0
- package/scripts/lib/source-frontmatter.mjs +85 -0
- package/scripts/lib/tier2-protocol.mjs +470 -0
- package/scripts/lib/tiered.mjs +453 -0
- 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
|
+
}
|