@zosmaai/pi-llm-wiki 0.10.7 → 0.11.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/CHANGELOG.md +4 -0
- package/README.de.md +35 -4
- package/README.es.md +260 -170
- package/README.fr.md +35 -4
- package/README.hi.md +35 -4
- package/README.ja.md +35 -4
- package/README.ko.md +35 -4
- package/README.md +38 -3
- package/README.pt.md +35 -4
- package/README.ru.md +35 -4
- package/README.zh.md +260 -170
- package/assets/demo.gif +0 -0
- package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
- package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
- package/dist/extensions/llm-wiki/lib/inject.js +65 -0
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
- package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
- package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
- package/dist/extensions/llm-wiki/lib/observation.js +283 -0
- package/dist/extensions/llm-wiki/lib/recall.js +875 -0
- package/dist/extensions/llm-wiki/lib/retro.js +158 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
- package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
- package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
- package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
- package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
- package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
- package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
- package/dist/extensions/llm-wiki/lib/utils.js +347 -0
- package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
- package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
- package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
- package/dist/mcp/exec.js +121 -0
- package/dist/mcp/index.js +229 -0
- package/dist/mcp/operations.js +130 -0
- package/dist/package.json +1 -0
- package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
- package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
- package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
- package/extensions/llm-wiki/index.ts +22 -36
- package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
- package/extensions/llm-wiki/lib/embeddings.ts +9 -3
- package/extensions/llm-wiki/lib/guardrails.ts +174 -29
- package/extensions/llm-wiki/lib/indexing.ts +2 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
- package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
- package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
- package/extensions/llm-wiki/lib/metadata.ts +531 -116
- package/extensions/llm-wiki/lib/observation.ts +37 -43
- package/extensions/llm-wiki/lib/recall.ts +61 -33
- package/extensions/llm-wiki/lib/retro.ts +65 -41
- package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
- package/extensions/llm-wiki/lib/source-packet.ts +44 -31
- package/extensions/llm-wiki/lib/tools.ts +406 -348
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +121 -130
- package/extensions/llm-wiki/lib/vault-format.ts +363 -0
- package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
- package/mcp/exec.ts +122 -0
- package/mcp/index.ts +60 -250
- package/mcp/operations.ts +176 -0
- package/package.json +8 -2
- package/scripts/migrate-llm-wiki.js +801 -0
- package/skills/llm-wiki/SKILL.md +8 -6
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants, chmodSync, closeSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, utimesSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { createKnowledgeDocument, parseKnowledgeDocument, parseMarkdownFrontmatter, serializeKnowledgeDocument, } from "./knowledge-document.js";
|
|
5
|
+
import { readJson } from "./utils.js";
|
|
6
|
+
import { discoverKnowledgeDocuments, inspectVaultFormat } from "./vault-format.js";
|
|
7
|
+
const RECOVERABLE = new Set([
|
|
8
|
+
"frontmatter_missing",
|
|
9
|
+
"frontmatter_parse_error",
|
|
10
|
+
"concept_missing_type",
|
|
11
|
+
]);
|
|
12
|
+
const TRANSACTION_PREFIX = "legacy-repair-";
|
|
13
|
+
const JOURNAL_NAME = "journal.json";
|
|
14
|
+
function sha256(content) {
|
|
15
|
+
return createHash("sha256").update(content).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
function inferredType(path, registryType) {
|
|
18
|
+
if (typeof registryType === "string" && registryType.trim())
|
|
19
|
+
return registryType;
|
|
20
|
+
const folder = path.split("/")[0];
|
|
21
|
+
return ({
|
|
22
|
+
sources: "source",
|
|
23
|
+
entities: "entity",
|
|
24
|
+
concepts: "concept",
|
|
25
|
+
syntheses: "synthesis",
|
|
26
|
+
analyses: "analysis",
|
|
27
|
+
requirements: "requirement",
|
|
28
|
+
skills: "skill",
|
|
29
|
+
cases: "case",
|
|
30
|
+
}[folder] ?? "concept");
|
|
31
|
+
}
|
|
32
|
+
function inferredTitle(path, content, registryTitle) {
|
|
33
|
+
if (typeof registryTitle === "string" && registryTitle.trim())
|
|
34
|
+
return registryTitle;
|
|
35
|
+
const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
36
|
+
if (heading)
|
|
37
|
+
return heading;
|
|
38
|
+
return basename(path, ".md").replace(/[-_]+/g, " ").trim() || "Recovered page";
|
|
39
|
+
}
|
|
40
|
+
function splitBrokenFrontmatter(content) {
|
|
41
|
+
const normalized = content.replace(/\r\n?/g, "\n");
|
|
42
|
+
if (!normalized.startsWith("---\n"))
|
|
43
|
+
return { raw: "", body: normalized };
|
|
44
|
+
const closing = normalized.indexOf("\n---\n", 4);
|
|
45
|
+
if (closing === -1) {
|
|
46
|
+
throw new Error("Cannot safely repair frontmatter without an unambiguous closing delimiter");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
raw: normalized.slice(4, closing),
|
|
50
|
+
body: normalized.slice(closing + 5).replace(/^\n/, ""),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function repairedContent(path, content, diagnostics, type, title) {
|
|
54
|
+
if (diagnostics.includes("concept_missing_type")) {
|
|
55
|
+
const parsed = parseMarkdownFrontmatter(content, path);
|
|
56
|
+
if (parsed.ok) {
|
|
57
|
+
const candidate = content.replace(/^---\r?\n/, `---\ntype: ${JSON.stringify(type)}\n`);
|
|
58
|
+
if (parseKnowledgeDocument(candidate, path).ok)
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const broken = splitBrokenFrontmatter(content);
|
|
63
|
+
return serializeKnowledgeDocument(createKnowledgeDocument(path, {
|
|
64
|
+
type,
|
|
65
|
+
title,
|
|
66
|
+
legacy_repair: true,
|
|
67
|
+
...(broken.raw ? { legacy_frontmatter: broken.raw } : {}),
|
|
68
|
+
}, broken.body));
|
|
69
|
+
}
|
|
70
|
+
function containedPath(root, path, physicalRoot = root) {
|
|
71
|
+
const target = resolve(root, path);
|
|
72
|
+
const relation = relative(root, target);
|
|
73
|
+
if (relation === "" ||
|
|
74
|
+
relation === ".." ||
|
|
75
|
+
relation.startsWith(`..${sep}`) ||
|
|
76
|
+
isAbsolute(relation)) {
|
|
77
|
+
throw new Error(`Unsafe legacy repair path: ${path}`);
|
|
78
|
+
}
|
|
79
|
+
let current = resolve(physicalRoot);
|
|
80
|
+
const physicalRelation = relative(current, target);
|
|
81
|
+
if (physicalRelation === ".." ||
|
|
82
|
+
physicalRelation.startsWith(`..${sep}`) ||
|
|
83
|
+
isAbsolute(physicalRelation)) {
|
|
84
|
+
throw new Error(`Unsafe legacy repair path: ${path}`);
|
|
85
|
+
}
|
|
86
|
+
for (const part of ["", ...physicalRelation.split(sep)]) {
|
|
87
|
+
if (part)
|
|
88
|
+
current = join(current, part);
|
|
89
|
+
if (!existsSync(current))
|
|
90
|
+
break;
|
|
91
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
92
|
+
throw new Error(`Legacy repair refuses symlinked path: ${current}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return target;
|
|
96
|
+
}
|
|
97
|
+
function syncDirectory(path) {
|
|
98
|
+
let handle;
|
|
99
|
+
try {
|
|
100
|
+
handle = openSync(path, "r");
|
|
101
|
+
fsyncSync(handle);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (process.platform !== "win32")
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (handle !== undefined)
|
|
109
|
+
closeSync(handle);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function ensurePrivateParent(path) {
|
|
113
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
114
|
+
}
|
|
115
|
+
function durableWriteNew(path, content) {
|
|
116
|
+
ensurePrivateParent(path);
|
|
117
|
+
const handle = openSync(path, "wx", 0o600);
|
|
118
|
+
try {
|
|
119
|
+
writeFileSync(handle, content);
|
|
120
|
+
fsyncSync(handle);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
closeSync(handle);
|
|
124
|
+
}
|
|
125
|
+
syncDirectory(dirname(path));
|
|
126
|
+
}
|
|
127
|
+
function atomicWriteJson(path, value) {
|
|
128
|
+
ensurePrivateParent(path);
|
|
129
|
+
const temporary = `${path}.${process.pid}-${randomUUID()}.tmp`;
|
|
130
|
+
durableWriteNew(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
131
|
+
renameSync(temporary, path);
|
|
132
|
+
syncDirectory(dirname(path));
|
|
133
|
+
}
|
|
134
|
+
function prepareReplacement(path, content, metadata) {
|
|
135
|
+
const mode = metadata.mode & 0o7777;
|
|
136
|
+
let created = false;
|
|
137
|
+
try {
|
|
138
|
+
const existing = readFileSync(path);
|
|
139
|
+
if (sha256(existing) !== sha256(content)) {
|
|
140
|
+
throw new Error(`Legacy repair temporary file changed: ${path}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error.code !== "ENOENT")
|
|
145
|
+
throw error;
|
|
146
|
+
const handle = openSync(path, "wx", mode);
|
|
147
|
+
try {
|
|
148
|
+
writeFileSync(handle, content);
|
|
149
|
+
fsyncSync(handle);
|
|
150
|
+
created = true;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
closeSync(handle);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!created) {
|
|
157
|
+
chmodSync(path, mode);
|
|
158
|
+
utimesSync(path, new Date(metadata.atime_ms), new Date(metadata.mtime_ms));
|
|
159
|
+
}
|
|
160
|
+
chmodSync(path, mode);
|
|
161
|
+
utimesSync(path, new Date(metadata.atime_ms), new Date(metadata.mtime_ms));
|
|
162
|
+
syncDirectory(dirname(path));
|
|
163
|
+
}
|
|
164
|
+
function readRegularFile(path) {
|
|
165
|
+
const flags = process.platform === "win32" ? "r" : constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
166
|
+
const handle = openSync(path, flags);
|
|
167
|
+
try {
|
|
168
|
+
const stat = fstatSync(handle);
|
|
169
|
+
if (!stat.isFile() || stat.nlink !== 1) {
|
|
170
|
+
throw new Error(`Legacy repair requires a regular, non-hard-linked file: ${path}`);
|
|
171
|
+
}
|
|
172
|
+
return { stat, content: readFileSync(handle) };
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
closeSync(handle);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function installReplacement(paths, pagePath, repaired, entry, transactionDir) {
|
|
179
|
+
const preparedPath = `${pagePath}.legacy-repair-${entry.after_sha256.slice(0, 16)}`;
|
|
180
|
+
const displacedPath = containedPath(join(transactionDir, "displaced"), entry.path, paths.root);
|
|
181
|
+
containedPath(paths.wiki, entry.path, paths.root);
|
|
182
|
+
containedPath(paths.wiki, relative(paths.wiki, preparedPath), paths.root);
|
|
183
|
+
ensurePrivateParent(displacedPath);
|
|
184
|
+
prepareReplacement(preparedPath, repaired, entry);
|
|
185
|
+
if (existsSync(pagePath) && !existsSync(displacedPath)) {
|
|
186
|
+
renameSync(pagePath, displacedPath);
|
|
187
|
+
syncDirectory(dirname(pagePath));
|
|
188
|
+
syncDirectory(dirname(displacedPath));
|
|
189
|
+
}
|
|
190
|
+
if (!existsSync(displacedPath) || sha256(readFileSync(displacedPath)) !== entry.before_sha256) {
|
|
191
|
+
if (!existsSync(pagePath) && existsSync(displacedPath)) {
|
|
192
|
+
renameSync(displacedPath, pagePath);
|
|
193
|
+
syncDirectory(dirname(pagePath));
|
|
194
|
+
syncDirectory(dirname(displacedPath));
|
|
195
|
+
}
|
|
196
|
+
if (existsSync(preparedPath))
|
|
197
|
+
unlinkSync(preparedPath);
|
|
198
|
+
throw new Error(`Legacy page changed during repair: ${entry.path}`);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
linkSync(preparedPath, pagePath);
|
|
202
|
+
syncDirectory(dirname(pagePath));
|
|
203
|
+
unlinkSync(preparedPath);
|
|
204
|
+
syncDirectory(dirname(preparedPath));
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (existsSync(preparedPath))
|
|
208
|
+
unlinkSync(preparedPath);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function processAlive(pid) {
|
|
213
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
214
|
+
return false;
|
|
215
|
+
try {
|
|
216
|
+
process.kill(pid, 0);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return error.code === "EPERM";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function acquireLock(paths) {
|
|
224
|
+
const path = join(paths.dotWiki, ".legacy-repair.lock");
|
|
225
|
+
const operationId = randomUUID();
|
|
226
|
+
const content = `${JSON.stringify({ operation_id: operationId, pid: process.pid, started: new Date().toISOString() })}\n`;
|
|
227
|
+
try {
|
|
228
|
+
durableWriteNew(path, content);
|
|
229
|
+
return { path, operationId };
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code !== "EEXIST")
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
let observed;
|
|
236
|
+
let current;
|
|
237
|
+
try {
|
|
238
|
+
observed = readFileSync(path, "utf8");
|
|
239
|
+
current = JSON.parse(observed);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw new Error(`Legacy repair lock is unreadable: ${path}`);
|
|
243
|
+
}
|
|
244
|
+
if (processAlive(current.pid))
|
|
245
|
+
throw new Error(`Legacy repair is already running: ${path}`);
|
|
246
|
+
const takeoverPath = `${path}.takeover`;
|
|
247
|
+
const takeoverId = randomUUID();
|
|
248
|
+
try {
|
|
249
|
+
durableWriteNew(takeoverPath, `${takeoverId}\n`);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error.code === "EEXIST") {
|
|
253
|
+
throw new Error(`Legacy repair lock takeover is already running: ${takeoverPath}`);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
if (readFileSync(path, "utf8") !== observed || processAlive(current.pid)) {
|
|
259
|
+
throw new Error(`Legacy repair lock changed during takeover: ${path}`);
|
|
260
|
+
}
|
|
261
|
+
unlinkSync(path);
|
|
262
|
+
syncDirectory(dirname(path));
|
|
263
|
+
durableWriteNew(path, content);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (existsSync(takeoverPath) && readFileSync(takeoverPath, "utf8") === `${takeoverId}\n`) {
|
|
267
|
+
unlinkSync(takeoverPath);
|
|
268
|
+
syncDirectory(dirname(takeoverPath));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { path, operationId };
|
|
272
|
+
}
|
|
273
|
+
function releaseLock(lock) {
|
|
274
|
+
if (!existsSync(lock.path))
|
|
275
|
+
return;
|
|
276
|
+
const current = JSON.parse(readFileSync(lock.path, "utf8"));
|
|
277
|
+
if (current.operation_id !== lock.operationId)
|
|
278
|
+
return;
|
|
279
|
+
unlinkSync(lock.path);
|
|
280
|
+
syncDirectory(dirname(lock.path));
|
|
281
|
+
}
|
|
282
|
+
function inProgressJournal(paths) {
|
|
283
|
+
if (!existsSync(paths.outputs))
|
|
284
|
+
return undefined;
|
|
285
|
+
const journals = readdirSync(paths.outputs)
|
|
286
|
+
.filter((entry) => entry.startsWith(TRANSACTION_PREFIX))
|
|
287
|
+
.map((entry) => join(paths.outputs, entry, JOURNAL_NAME))
|
|
288
|
+
.filter(existsSync);
|
|
289
|
+
if (journals.length > 1)
|
|
290
|
+
throw new Error("Multiple incomplete legacy repair journals found");
|
|
291
|
+
return journals[0];
|
|
292
|
+
}
|
|
293
|
+
function readJournal(path, paths) {
|
|
294
|
+
const journal = JSON.parse(readFileSync(path, "utf8"));
|
|
295
|
+
if (journal.version !== 1 ||
|
|
296
|
+
journal.root !== paths.root ||
|
|
297
|
+
typeof journal.operation_id !== "string" ||
|
|
298
|
+
!Array.isArray(journal.entries) ||
|
|
299
|
+
!Array.isArray(journal.completed)) {
|
|
300
|
+
throw new Error(`Invalid legacy repair journal: ${path}`);
|
|
301
|
+
}
|
|
302
|
+
return journal;
|
|
303
|
+
}
|
|
304
|
+
function applyJournal(paths, journalPath, journal, diagnostics, afterCheckpoint, beforeCommit) {
|
|
305
|
+
const transactionDir = dirname(journalPath);
|
|
306
|
+
for (const entry of journal.entries) {
|
|
307
|
+
const pagePath = containedPath(paths.wiki, entry.path, paths.root);
|
|
308
|
+
const backupPath = containedPath(join(transactionDir, "wiki"), entry.path, paths.root);
|
|
309
|
+
const repairedPath = containedPath(join(transactionDir, "repaired"), entry.path, paths.root);
|
|
310
|
+
const backup = readFileSync(backupPath);
|
|
311
|
+
const repaired = readFileSync(repairedPath);
|
|
312
|
+
if (sha256(backup) !== entry.before_sha256 || sha256(repaired) !== entry.after_sha256) {
|
|
313
|
+
throw new Error(`Legacy repair transaction verification failed: ${entry.path}`);
|
|
314
|
+
}
|
|
315
|
+
const currentHash = existsSync(pagePath) ? sha256(readFileSync(pagePath)) : undefined;
|
|
316
|
+
if (currentHash === entry.before_sha256 || currentHash === undefined) {
|
|
317
|
+
beforeCommit?.(entry.path);
|
|
318
|
+
installReplacement(paths, pagePath, repaired, entry, transactionDir);
|
|
319
|
+
}
|
|
320
|
+
else if (currentHash !== entry.after_sha256) {
|
|
321
|
+
throw new Error(`Legacy page changed during repair: ${entry.path}`);
|
|
322
|
+
}
|
|
323
|
+
const preparedPath = `${pagePath}.legacy-repair-${entry.after_sha256.slice(0, 16)}`;
|
|
324
|
+
if (existsSync(preparedPath)) {
|
|
325
|
+
if (sha256(readFileSync(preparedPath)) !== entry.after_sha256) {
|
|
326
|
+
throw new Error(`Legacy repair temporary file changed: ${preparedPath}`);
|
|
327
|
+
}
|
|
328
|
+
unlinkSync(preparedPath);
|
|
329
|
+
syncDirectory(dirname(preparedPath));
|
|
330
|
+
}
|
|
331
|
+
if (!journal.completed.includes(entry.path)) {
|
|
332
|
+
journal.completed.push(entry.path);
|
|
333
|
+
atomicWriteJson(journalPath, journal);
|
|
334
|
+
afterCheckpoint?.(entry.path);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const manifestPath = join(transactionDir, "manifest.json");
|
|
338
|
+
atomicWriteJson(manifestPath, {
|
|
339
|
+
version: 1,
|
|
340
|
+
repaired_at: new Date().toISOString(),
|
|
341
|
+
entries: journal.entries.map(({ mode: _mode, atime_ms: _atime, mtime_ms: _mtime, ...entry }) => ({
|
|
342
|
+
...entry,
|
|
343
|
+
displaced: relative(paths.root, containedPath(join(transactionDir, "displaced"), entry.path, paths.root)).replace(/\\/g, "/"),
|
|
344
|
+
})),
|
|
345
|
+
});
|
|
346
|
+
unlinkSync(journalPath);
|
|
347
|
+
syncDirectory(transactionDir);
|
|
348
|
+
return {
|
|
349
|
+
repaired: journal.entries.length,
|
|
350
|
+
backupDir: transactionDir,
|
|
351
|
+
manifestPath,
|
|
352
|
+
entries: journal.entries,
|
|
353
|
+
diagnostics,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function createJournal(paths, diagnostics, operationId, now) {
|
|
357
|
+
const byPath = new Map();
|
|
358
|
+
for (const diagnostic of diagnostics) {
|
|
359
|
+
if (!RECOVERABLE.has(diagnostic.code))
|
|
360
|
+
continue;
|
|
361
|
+
const current = byPath.get(diagnostic.path) ?? [];
|
|
362
|
+
current.push(diagnostic);
|
|
363
|
+
byPath.set(diagnostic.path, current);
|
|
364
|
+
}
|
|
365
|
+
if (byPath.size === 0)
|
|
366
|
+
return undefined;
|
|
367
|
+
mkdirSync(paths.outputs, { recursive: true });
|
|
368
|
+
const transactionDir = join(paths.outputs, `${TRANSACTION_PREFIX}${now.toISOString().replace(/[:.]/g, "-")}-${operationId}`);
|
|
369
|
+
mkdirSync(transactionDir, { mode: 0o700 });
|
|
370
|
+
syncDirectory(paths.outputs);
|
|
371
|
+
const registry = readJson(join(paths.meta, "registry.json"), {});
|
|
372
|
+
const entries = [];
|
|
373
|
+
for (const [path, pathDiagnostics] of [...byPath].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
|
|
374
|
+
const pagePath = containedPath(paths.wiki, path, paths.root);
|
|
375
|
+
const { stat, content: original } = readRegularFile(pagePath);
|
|
376
|
+
const content = original.toString("utf8");
|
|
377
|
+
if (!Buffer.from(content).equals(original))
|
|
378
|
+
throw new Error(`Legacy page is not UTF-8: ${path}`);
|
|
379
|
+
const id = path.replace(/\.md$/i, "");
|
|
380
|
+
const registryEntry = registry.pages?.[id];
|
|
381
|
+
const type = inferredType(path, registryEntry?.type);
|
|
382
|
+
const title = inferredTitle(path, content, registryEntry?.title);
|
|
383
|
+
const codes = [...new Set(pathDiagnostics.map((diagnostic) => diagnostic.code))];
|
|
384
|
+
const repaired = Buffer.from(repairedContent(path, content, codes, type, title));
|
|
385
|
+
if (!parseKnowledgeDocument(repaired.toString("utf8"), path).ok) {
|
|
386
|
+
throw new Error(`Repair did not produce a valid page: ${path}`);
|
|
387
|
+
}
|
|
388
|
+
const backupPath = containedPath(join(transactionDir, "wiki"), path, paths.root);
|
|
389
|
+
const repairedPath = containedPath(join(transactionDir, "repaired"), path, paths.root);
|
|
390
|
+
durableWriteNew(backupPath, original);
|
|
391
|
+
durableWriteNew(repairedPath, repaired);
|
|
392
|
+
entries.push({
|
|
393
|
+
path,
|
|
394
|
+
backup: relative(paths.root, backupPath).replace(/\\/g, "/"),
|
|
395
|
+
before_sha256: sha256(original),
|
|
396
|
+
after_sha256: sha256(repaired),
|
|
397
|
+
diagnostics: codes,
|
|
398
|
+
mode: Number(stat.mode),
|
|
399
|
+
atime_ms: Number(stat.atimeMs),
|
|
400
|
+
mtime_ms: Number(stat.mtimeMs),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const journal = {
|
|
404
|
+
version: 1,
|
|
405
|
+
operation_id: operationId,
|
|
406
|
+
root: paths.root,
|
|
407
|
+
started: now.toISOString(),
|
|
408
|
+
entries,
|
|
409
|
+
completed: [],
|
|
410
|
+
};
|
|
411
|
+
const path = join(transactionDir, JOURNAL_NAME);
|
|
412
|
+
atomicWriteJson(path, journal);
|
|
413
|
+
return { path, journal };
|
|
414
|
+
}
|
|
415
|
+
/** Repair malformed legacy pages with durable backups and crash-resumable checkpoints. */
|
|
416
|
+
export function repairLegacyKnowledgeDocuments(paths, now = new Date(), afterCheckpoint, beforeCommit) {
|
|
417
|
+
containedPath(paths.root, relative(paths.root, paths.dotWiki), paths.root);
|
|
418
|
+
containedPath(paths.root, relative(paths.root, paths.wiki), paths.root);
|
|
419
|
+
containedPath(paths.root, relative(paths.root, paths.outputs), paths.root);
|
|
420
|
+
const initialState = inspectVaultFormat(paths);
|
|
421
|
+
if (initialState.knowledgeFormat !== "legacy") {
|
|
422
|
+
return { repaired: 0, entries: [], diagnostics: initialState.diagnostics };
|
|
423
|
+
}
|
|
424
|
+
const lock = acquireLock(paths);
|
|
425
|
+
try {
|
|
426
|
+
const state = inspectVaultFormat(paths);
|
|
427
|
+
const discovery = discoverKnowledgeDocuments(paths);
|
|
428
|
+
if (state.knowledgeFormat !== "legacy") {
|
|
429
|
+
return { repaired: 0, entries: [], diagnostics: discovery.diagnostics };
|
|
430
|
+
}
|
|
431
|
+
const existing = inProgressJournal(paths);
|
|
432
|
+
if (existing) {
|
|
433
|
+
return applyJournal(paths, existing, readJournal(existing, paths), discovery.diagnostics, afterCheckpoint, beforeCommit);
|
|
434
|
+
}
|
|
435
|
+
const created = createJournal(paths, discovery.diagnostics, lock.operationId, now);
|
|
436
|
+
if (!created)
|
|
437
|
+
return { repaired: 0, entries: [], diagnostics: discovery.diagnostics };
|
|
438
|
+
return applyJournal(paths, created.path, created.journal, discovery.diagnostics, afterCheckpoint, beforeCommit);
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
releaseLock(lock);
|
|
442
|
+
}
|
|
443
|
+
}
|