@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
|
@@ -1,36 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import type { KnowledgeDiagnostic, KnowledgeDocument } from "./knowledge-document.js";
|
|
14
|
+
import { buildResolvedBacklinks } from "./knowledge-links.js";
|
|
15
|
+
import { type VaultPaths, isPathWithin, readJson } from "./utils.js";
|
|
16
|
+
import {
|
|
17
|
+
assertWritableVault,
|
|
18
|
+
compareCodePoint,
|
|
19
|
+
discoverKnowledgeDocuments,
|
|
20
|
+
inspectVaultFormat,
|
|
21
|
+
} from "./vault-format.js";
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Metadata generation for the LLM Wiki.
|
|
15
25
|
*
|
|
16
|
-
* Rebuilds registry.json, backlinks.json, index.md, log.md
|
|
17
|
-
*
|
|
26
|
+
* Rebuilds registry.json, backlinks.json, index.md, log.md deterministically
|
|
27
|
+
* from the current state of wiki/ and raw/. In OKF mode, also generates
|
|
28
|
+
* wiki/index.md and wiki/log.md projections.
|
|
29
|
+
*
|
|
30
|
+
* Fail-closed: computes all outputs in memory before writing any file.
|
|
31
|
+
* Blocking diagnostics prevent any writes.
|
|
18
32
|
*/
|
|
19
33
|
|
|
20
34
|
export interface RegistryEntry {
|
|
21
|
-
type:
|
|
22
|
-
| "source"
|
|
23
|
-
| "entity"
|
|
24
|
-
| "concept"
|
|
25
|
-
| "synthesis"
|
|
26
|
-
| "analysis"
|
|
27
|
-
| "requirement"
|
|
28
|
-
| "trajectory"
|
|
29
|
-
| "skill"
|
|
30
|
-
| "case";
|
|
35
|
+
type: string;
|
|
31
36
|
title: string;
|
|
32
|
-
created
|
|
33
|
-
updated
|
|
37
|
+
created?: string;
|
|
38
|
+
updated?: string;
|
|
34
39
|
[key: string]: unknown;
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -50,86 +55,147 @@ export interface WikiEvent {
|
|
|
50
55
|
[key: string]: unknown;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
export interface ProjectionResult {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
diagnostics: KnowledgeDiagnostic[];
|
|
61
|
+
registry?: Registry;
|
|
62
|
+
backlinks?: Backlinks;
|
|
63
|
+
}
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
/** Rebuild the complete metadata layer with fail-closed semantics. */
|
|
66
|
+
export function rebuildMetadata(paths: VaultPaths): ProjectionResult {
|
|
67
|
+
// Step 1: Validate vault format and mode
|
|
68
|
+
const vaultState = inspectVaultFormat(paths);
|
|
69
|
+
if (vaultState.blocking) {
|
|
70
|
+
return { ok: false, diagnostics: vaultState.diagnostics };
|
|
71
|
+
}
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
// Step 2: Discover all knowledge documents
|
|
74
|
+
const discovery = discoverKnowledgeDocuments(paths);
|
|
75
|
+
if (discovery.blocking) {
|
|
76
|
+
return { ok: false, diagnostics: discovery.diagnostics };
|
|
77
|
+
}
|
|
63
78
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
const documents = discovery.documents;
|
|
80
|
+
const allDiagnostics: KnowledgeDiagnostic[] = [
|
|
81
|
+
...vaultState.diagnostics,
|
|
82
|
+
...discovery.diagnostics,
|
|
83
|
+
];
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const pages: Record<string, RegistryEntry> = {};
|
|
85
|
+
// Step 3: Build registry from documents + raw fallbacks
|
|
86
|
+
const registry = buildRegistry(paths, documents);
|
|
71
87
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const type = String(frontmatter.type || "page") as RegistryEntry["type"];
|
|
76
|
-
const title = String(frontmatter.title || page.relative.split("/").pop() || "Untitled");
|
|
88
|
+
// Step 4: Build backlinks from discovered documents
|
|
89
|
+
const knownIds = new Set(documents.map((d) => d.id));
|
|
90
|
+
const backlinks = buildBacklinks(documents, knownIds, allDiagnostics);
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
title,
|
|
81
|
-
created: String(frontmatter.created || fmtDate()),
|
|
82
|
-
updated: String(frontmatter.updated || frontmatter.created || fmtDate()),
|
|
83
|
-
...frontmatter,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
92
|
+
const eventLogResult = buildOkfLog(readText(join(paths.meta, "events.jsonl")));
|
|
93
|
+
allDiagnostics.push(...eventLogResult.diagnostics);
|
|
86
94
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
// Step 5: Build meta/index.md
|
|
96
|
+
const metaIndex = buildIndexMarkdown(registry);
|
|
97
|
+
|
|
98
|
+
// Step 6: Build meta/log.md (existing rich format)
|
|
99
|
+
const metaLog = buildLogMarkdown(paths);
|
|
100
|
+
|
|
101
|
+
// Step 7: Build OKF projections if in okf-0.2 mode
|
|
102
|
+
const okfIndexes: Map<string, string> | null =
|
|
103
|
+
vaultState.knowledgeFormat === "okf-0.2"
|
|
104
|
+
? buildDirectoryIndexes(documents, readJson(join(paths.dotWiki, "config.json"), {}))
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
const okfLog: string | null =
|
|
108
|
+
vaultState.knowledgeFormat === "okf-0.2" ? eventLogResult.markdown : null;
|
|
109
|
+
|
|
110
|
+
// Step 8: Atomic write all projections
|
|
111
|
+
mkdirSync(paths.meta, { recursive: true });
|
|
112
|
+
|
|
113
|
+
const registryJson = `${JSON.stringify(registry, null, 2)}\n`;
|
|
114
|
+
const backlinksJson = `${JSON.stringify(backlinks, null, 2)}\n`;
|
|
115
|
+
|
|
116
|
+
atomicWriteFile(join(paths.meta, "registry.json"), registryJson);
|
|
117
|
+
atomicWriteFile(join(paths.meta, "backlinks.json"), backlinksJson);
|
|
118
|
+
atomicWriteFile(join(paths.meta, "index.md"), metaIndex);
|
|
119
|
+
atomicWriteFile(join(paths.meta, "log.md"), metaLog);
|
|
120
|
+
|
|
121
|
+
// Step 9: Write OKF projections if applicable
|
|
122
|
+
if (okfIndexes && okfIndexes.size > 0) {
|
|
123
|
+
mkdirSync(paths.wiki, { recursive: true });
|
|
124
|
+
for (const [indexPath, content] of okfIndexes) {
|
|
125
|
+
atomicWriteFile(join(paths.wiki, indexPath), content);
|
|
106
126
|
}
|
|
127
|
+
// Prune obsolete generated indexes
|
|
128
|
+
pruneObsoleteIndexes(paths, okfIndexes);
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
if (okfLog !== null) {
|
|
132
|
+
mkdirSync(paths.wiki, { recursive: true });
|
|
133
|
+
atomicWriteFile(join(paths.wiki, "log.md"), okfLog);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
diagnostics: allDiagnostics,
|
|
139
|
+
registry,
|
|
140
|
+
backlinks,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Lightweight rebuild is the same as full rebuild. */
|
|
145
|
+
export const rebuildMetadataLight = rebuildMetadata;
|
|
116
146
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
/** Build registry from discovered documents and raw fallbacks. */
|
|
148
|
+
function buildRegistry(paths: VaultPaths, documents: KnowledgeDocument[]): Registry {
|
|
149
|
+
const pages: Record<string, RegistryEntry> = {};
|
|
120
150
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
151
|
+
// Add discovered documents
|
|
152
|
+
for (const doc of documents) {
|
|
153
|
+
const title = getSemanticTitle(doc);
|
|
154
|
+
const entry: RegistryEntry = {
|
|
155
|
+
type: doc.frontmatter.type,
|
|
156
|
+
title,
|
|
157
|
+
};
|
|
158
|
+
// Copy known frontmatter fields (excluding type which is already set)
|
|
159
|
+
for (const key of [
|
|
160
|
+
"description",
|
|
161
|
+
"tags",
|
|
162
|
+
"category",
|
|
163
|
+
"domain",
|
|
164
|
+
"aliases",
|
|
165
|
+
"recall_triggers",
|
|
166
|
+
"status",
|
|
167
|
+
"stale_after",
|
|
168
|
+
"resource",
|
|
169
|
+
"generated",
|
|
170
|
+
"verified",
|
|
171
|
+
"summary",
|
|
172
|
+
"raw_path",
|
|
173
|
+
"source_id",
|
|
174
|
+
] as const) {
|
|
175
|
+
if (doc.frontmatter[key] !== undefined) {
|
|
176
|
+
entry[key] = doc.frontmatter[key];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Copy extension fields
|
|
180
|
+
for (const [key, value] of Object.entries(doc.extensions)) {
|
|
181
|
+
if (!(key in entry)) {
|
|
182
|
+
entry[key] = value;
|
|
129
183
|
}
|
|
130
184
|
}
|
|
185
|
+
// Only include created/updated if they are non-empty strings
|
|
186
|
+
if (typeof doc.frontmatter.created === "string" && doc.frontmatter.created.trim()) {
|
|
187
|
+
entry.created = doc.frontmatter.created;
|
|
188
|
+
}
|
|
189
|
+
if (typeof doc.frontmatter.updated === "string" && doc.frontmatter.updated.trim()) {
|
|
190
|
+
entry.updated = doc.frontmatter.updated;
|
|
191
|
+
}
|
|
192
|
+
pages[doc.id] = entry;
|
|
131
193
|
}
|
|
132
194
|
|
|
195
|
+
// Raw source/trajectory fallback entries (registry-only compatibility)
|
|
196
|
+
addRawFallbacks(paths, pages, "sources", "source");
|
|
197
|
+
addRawFallbacks(paths, pages, "trajectories", "trajectory");
|
|
198
|
+
|
|
133
199
|
return {
|
|
134
200
|
version: "1.0",
|
|
135
201
|
last_updated: new Date().toISOString(),
|
|
@@ -137,30 +203,77 @@ export function buildRegistry(paths: VaultPaths): Registry {
|
|
|
137
203
|
};
|
|
138
204
|
}
|
|
139
205
|
|
|
140
|
-
|
|
141
|
-
|
|
206
|
+
function addRawFallbacks(
|
|
207
|
+
paths: VaultPaths,
|
|
208
|
+
pages: Record<string, RegistryEntry>,
|
|
209
|
+
dirName: "sources" | "trajectories",
|
|
210
|
+
type: string,
|
|
211
|
+
): void {
|
|
212
|
+
const rawDir = dirName === "sources" ? paths.rawSources : paths.rawTrajectories;
|
|
213
|
+
if (!existsSync(rawDir)) return;
|
|
214
|
+
|
|
215
|
+
for (const entry of readdirSync(rawDir)) {
|
|
216
|
+
const manifestPath = join(rawDir, entry, "manifest.json");
|
|
217
|
+
if (!existsSync(manifestPath)) continue;
|
|
218
|
+
|
|
219
|
+
const manifest = readJson<Record<string, unknown>>(manifestPath, {});
|
|
220
|
+
const id = String(manifest.id || entry);
|
|
221
|
+
const pageKey = `${dirName}/${id}`;
|
|
222
|
+
|
|
223
|
+
// Don't overwrite a parsed document
|
|
224
|
+
if (pages[pageKey]) continue;
|
|
225
|
+
|
|
226
|
+
const captured = String(manifest.captured || "");
|
|
227
|
+
pages[pageKey] = {
|
|
228
|
+
type,
|
|
229
|
+
title: String(manifest.title || id),
|
|
230
|
+
...(captured ? { created: captured, updated: captured } : {}),
|
|
231
|
+
...manifest,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getSemanticTitle(doc: KnowledgeDocument): string {
|
|
237
|
+
if (typeof doc.frontmatter.title === "string" && doc.frontmatter.title.trim()) {
|
|
238
|
+
return doc.frontmatter.title.trim();
|
|
239
|
+
}
|
|
240
|
+
return doc.id.split("/").pop() || "Untitled";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Build backlinks from discovered documents using shared link resolution. */
|
|
244
|
+
function buildBacklinks(
|
|
245
|
+
documents: KnowledgeDocument[],
|
|
246
|
+
knownIds: Set<string>,
|
|
247
|
+
diagnostics: KnowledgeDiagnostic[],
|
|
248
|
+
): Backlinks {
|
|
142
249
|
const inbound: Backlinks = {};
|
|
143
250
|
|
|
144
|
-
// Initialize
|
|
145
|
-
for (const
|
|
146
|
-
inbound[id] = [];
|
|
251
|
+
// Initialize parsed concept IDs with empty arrays
|
|
252
|
+
for (const doc of documents) {
|
|
253
|
+
inbound[doc.id] = [];
|
|
147
254
|
}
|
|
148
255
|
|
|
149
|
-
//
|
|
150
|
-
for (const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
256
|
+
// Resolve links for each document
|
|
257
|
+
for (const doc of documents) {
|
|
258
|
+
const result = buildResolvedBacklinks(doc.id, doc.body, knownIds);
|
|
259
|
+
diagnostics.push(...result.diagnostics);
|
|
260
|
+
for (const target of result.targets) {
|
|
261
|
+
if (inbound[target] && !inbound[target].includes(doc.id)) {
|
|
262
|
+
inbound[target].push(doc.id);
|
|
155
263
|
}
|
|
156
264
|
}
|
|
157
265
|
}
|
|
158
266
|
|
|
267
|
+
// Sort targets for determinism
|
|
268
|
+
for (const [id, targets] of Object.entries(inbound)) {
|
|
269
|
+
inbound[id] = [...targets].sort(compareCodePoint);
|
|
270
|
+
}
|
|
271
|
+
|
|
159
272
|
return inbound;
|
|
160
273
|
}
|
|
161
274
|
|
|
162
|
-
/** Build index
|
|
163
|
-
|
|
275
|
+
/** Build meta/index.md in existing rich format. */
|
|
276
|
+
function buildIndexMarkdown(registry: Registry): string {
|
|
164
277
|
const byType: Record<string, Array<{ id: string; entry: RegistryEntry }>> = {};
|
|
165
278
|
|
|
166
279
|
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
@@ -178,7 +291,7 @@ export function buildIndexMarkdown(registry: Registry): string {
|
|
|
178
291
|
const label = `${type.charAt(0).toUpperCase() + type.slice(1)}s`;
|
|
179
292
|
sections.push(`## ${label}\n`);
|
|
180
293
|
for (const { id, entry } of items.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
181
|
-
sections.push(`- [[${id}]] — ${entry.title} *(created: ${entry.created})*`);
|
|
294
|
+
sections.push(`- [[${id}]] — ${entry.title} *(created: ${entry.created || "unknown"})*`);
|
|
182
295
|
}
|
|
183
296
|
sections.push("");
|
|
184
297
|
}
|
|
@@ -189,17 +302,20 @@ export function buildIndexMarkdown(registry: Registry): string {
|
|
|
189
302
|
return `${sections.join("\n")}\n`;
|
|
190
303
|
}
|
|
191
304
|
|
|
192
|
-
/** Build log
|
|
193
|
-
|
|
305
|
+
/** Build meta/log.md in existing rich format. */
|
|
306
|
+
function buildLogMarkdown(paths: VaultPaths): string {
|
|
194
307
|
const eventsPath = join(paths.meta, "events.jsonl");
|
|
195
308
|
const events: WikiEvent[] = [];
|
|
196
309
|
|
|
197
310
|
if (existsSync(eventsPath)) {
|
|
198
|
-
const raw =
|
|
311
|
+
const raw = readText(eventsPath).trim();
|
|
199
312
|
for (const line of raw.split("\n")) {
|
|
200
313
|
if (!line.trim()) continue;
|
|
201
314
|
try {
|
|
202
|
-
|
|
315
|
+
const candidate: unknown = JSON.parse(line);
|
|
316
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
317
|
+
events.push(candidate as WikiEvent);
|
|
318
|
+
}
|
|
203
319
|
} catch {
|
|
204
320
|
// skip malformed
|
|
205
321
|
}
|
|
@@ -231,20 +347,319 @@ export function buildLogMarkdown(paths: VaultPaths): string {
|
|
|
231
347
|
|
|
232
348
|
/** Append an event to events.jsonl. */
|
|
233
349
|
export function appendEvent(paths: VaultPaths, event: Omit<WikiEvent, "timestamp">): void {
|
|
350
|
+
assertWritableVault(paths);
|
|
351
|
+
const { timestamp: _ignored, kind: rawKind, ...details } = event as WikiEvent;
|
|
352
|
+
const kind = typeof rawKind === "string" ? rawKind.trim() : "";
|
|
353
|
+
if (!kind) throw new Error("Event kind must be a non-empty string");
|
|
234
354
|
mkdirSync(paths.meta, { recursive: true });
|
|
235
355
|
const eventsPath = join(paths.meta, "events.jsonl");
|
|
236
|
-
const line = JSON.stringify({ timestamp: new Date().toISOString(),
|
|
356
|
+
const line = JSON.stringify({ ...details, timestamp: new Date().toISOString(), kind });
|
|
237
357
|
writeFileSync(eventsPath, `${line}\n`, { flag: "a", encoding: "utf-8" });
|
|
238
358
|
}
|
|
239
359
|
|
|
240
|
-
/**
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
360
|
+
/** Atomic write: temp file + rename. */
|
|
361
|
+
function atomicWriteFile(path: string, content: string): void {
|
|
362
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
363
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
364
|
+
writeFileSync(temporary, content, "utf8");
|
|
365
|
+
renameSync(temporary, path);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Read text file or return empty string. */
|
|
369
|
+
function readText(path: string): string {
|
|
370
|
+
try {
|
|
371
|
+
if (!existsSync(path)) return "";
|
|
372
|
+
return readFileSync(path, "utf8");
|
|
373
|
+
} catch {
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Prune obsolete generated indexes in OKF mode. */
|
|
379
|
+
function pruneObsoleteIndexes(paths: VaultPaths, currentIndexes: Map<string, string>): void {
|
|
380
|
+
const currentPaths = new Set(currentIndexes.keys());
|
|
381
|
+
|
|
382
|
+
function walkDir(dir: string, relative: string): string[] {
|
|
383
|
+
const results: string[] = [];
|
|
384
|
+
if (!existsSync(dir)) return results;
|
|
385
|
+
for (const entry of readdirSync(dir)) {
|
|
386
|
+
const fullPath = join(dir, entry);
|
|
387
|
+
let stat: ReturnType<typeof lstatSync>;
|
|
388
|
+
try {
|
|
389
|
+
stat = lstatSync(fullPath);
|
|
390
|
+
} catch {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (stat.isSymbolicLink()) continue;
|
|
394
|
+
const relPath = relative ? `${relative}/${entry}` : entry;
|
|
395
|
+
if (entry.toLowerCase() === "index.md") {
|
|
396
|
+
results.push(relPath);
|
|
397
|
+
} else if (stat.isDirectory()) {
|
|
398
|
+
results.push(...walkDir(fullPath, relPath));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return results;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const allIndexes = walkDir(paths.wiki, "");
|
|
405
|
+
for (const indexPath of allIndexes) {
|
|
406
|
+
// Never prune root index.md
|
|
407
|
+
if (indexPath === "index.md") continue;
|
|
408
|
+
if (!currentPaths.has(indexPath)) {
|
|
409
|
+
const fullPath = join(paths.wiki, indexPath);
|
|
410
|
+
try {
|
|
411
|
+
if (lstatSync(fullPath).isSymbolicLink() || !isPathWithin(paths.wiki, fullPath)) continue;
|
|
412
|
+
rmSync(fullPath);
|
|
413
|
+
// Try to remove parent dir if empty
|
|
414
|
+
const parentDir = dirname(fullPath);
|
|
415
|
+
if (
|
|
416
|
+
!lstatSync(parentDir).isSymbolicLink() &&
|
|
417
|
+
isPathWithin(paths.wiki, parentDir) &&
|
|
418
|
+
existsSync(parentDir) &&
|
|
419
|
+
readdirSync(parentDir).length === 0
|
|
420
|
+
) {
|
|
421
|
+
rmSync(parentDir);
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
// Ignore errors during pruning
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ===== OKF Projection Renderers =====
|
|
431
|
+
|
|
432
|
+
function escapeLabel(value: string): string {
|
|
433
|
+
return value.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/\]/g, "\\]");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function encodeRelativePath(value: string): string {
|
|
437
|
+
return value.split("/").map(encodeURIComponent).join("/");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function compactDescription(value: unknown): string | undefined {
|
|
441
|
+
if (typeof value !== "string") return undefined;
|
|
442
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
443
|
+
return compact || undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function buildDirectoryIndexes(
|
|
447
|
+
documents: KnowledgeDocument[],
|
|
448
|
+
config: { name?: unknown },
|
|
449
|
+
): Map<string, string> {
|
|
450
|
+
const indexes = new Map<string, string>();
|
|
451
|
+
|
|
452
|
+
// Build directory tree from concept documents only
|
|
453
|
+
const directories = new Map<string, { dirs: Set<string>; concepts: KnowledgeDocument[] }>();
|
|
454
|
+
|
|
455
|
+
for (const doc of documents) {
|
|
456
|
+
const parts = doc.id.split("/");
|
|
457
|
+
|
|
458
|
+
// Walk the path, tracking parent-child directory relationships
|
|
459
|
+
for (let i = 0; i < parts.length; i++) {
|
|
460
|
+
const parentPath = i === 0 ? "" : parts.slice(0, i).join("/");
|
|
461
|
+
if (!directories.has(parentPath)) {
|
|
462
|
+
directories.set(parentPath, { dirs: new Set(), concepts: [] });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (i === parts.length - 1) {
|
|
466
|
+
// Last part is the concept file
|
|
467
|
+
directories.get(parentPath)!.concepts.push(doc);
|
|
468
|
+
} else {
|
|
469
|
+
// This is a subdirectory
|
|
470
|
+
directories.get(parentPath)!.dirs.add(parts[i]);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Always emit root index
|
|
476
|
+
const vaultName =
|
|
477
|
+
typeof config.name === "string" && config.name.trim() ? config.name.trim() : "Wiki";
|
|
478
|
+
if (!directories.has("")) {
|
|
479
|
+
directories.set("", { dirs: new Set(), concepts: [] });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Render each index
|
|
483
|
+
for (const [dirPath, { dirs, concepts }] of directories) {
|
|
484
|
+
const indexPath = dirPath ? `${dirPath}/index.md` : "index.md";
|
|
485
|
+
const lines: string[] = [];
|
|
486
|
+
|
|
487
|
+
if (dirPath === "") {
|
|
488
|
+
lines.push("---");
|
|
489
|
+
lines.push('okf_version: "0.2"');
|
|
490
|
+
lines.push("---");
|
|
491
|
+
lines.push("");
|
|
492
|
+
lines.push(`# ${escapeLabel(vaultName)}`);
|
|
493
|
+
} else {
|
|
494
|
+
const dirName = dirPath.split("/").pop()!;
|
|
495
|
+
lines.push(`# ${escapeLabel(dirName)}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// List directories first
|
|
499
|
+
if (dirs.size > 0) {
|
|
500
|
+
lines.push("");
|
|
501
|
+
lines.push("## Directories");
|
|
502
|
+
lines.push("");
|
|
503
|
+
for (const subDir of [...dirs].sort(compareCodePoint)) {
|
|
504
|
+
const encoded = `${encodeRelativePath(subDir)}/`;
|
|
505
|
+
lines.push(`- [${escapeLabel(subDir)}/](${encoded})`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// List concepts
|
|
510
|
+
if (concepts.length > 0) {
|
|
511
|
+
lines.push("");
|
|
512
|
+
lines.push("## Concepts");
|
|
513
|
+
lines.push("");
|
|
514
|
+
const sorted = [...concepts].sort((a, b) => {
|
|
515
|
+
const aRel = dirPath ? a.id.slice(dirPath.length + 1) : a.id;
|
|
516
|
+
const bRel = dirPath ? b.id.slice(dirPath.length + 1) : b.id;
|
|
517
|
+
return compareCodePoint(aRel, bRel);
|
|
518
|
+
});
|
|
519
|
+
for (const doc of sorted) {
|
|
520
|
+
const relId = dirPath ? doc.id.slice(dirPath.length + 1) : doc.id;
|
|
521
|
+
const title =
|
|
522
|
+
typeof doc.frontmatter.title === "string" && doc.frontmatter.title.trim()
|
|
523
|
+
? doc.frontmatter.title.trim()
|
|
524
|
+
: relId.split("/").pop()!;
|
|
525
|
+
const desc = compactDescription(doc.frontmatter.description);
|
|
526
|
+
const encoded = encodeRelativePath(`${relId}.md`);
|
|
527
|
+
const descPart = desc ? ` — ${desc}` : "";
|
|
528
|
+
lines.push(`- [${escapeLabel(title)}](${encoded})${descPart}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
indexes.set(indexPath, `${lines.join("\n")}\n`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return indexes;
|
|
536
|
+
}
|
|
247
537
|
|
|
248
|
-
|
|
249
|
-
|
|
538
|
+
export interface OkfLogResult {
|
|
539
|
+
markdown: string;
|
|
540
|
+
diagnostics: KnowledgeDiagnostic[];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
544
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
545
|
+
if (value && typeof value === "object") {
|
|
546
|
+
return Object.fromEntries(
|
|
547
|
+
Object.entries(value as Record<string, unknown>)
|
|
548
|
+
.sort(([a], [b]) => compareCodePoint(a, b))
|
|
549
|
+
.map(([key, child]) => [key, canonicalJsonValue(child)]),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return value;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function okfDiag(
|
|
556
|
+
severity: "warning" | "error",
|
|
557
|
+
code: KnowledgeDiagnostic["code"],
|
|
558
|
+
path: string,
|
|
559
|
+
message: string,
|
|
560
|
+
): KnowledgeDiagnostic {
|
|
561
|
+
return { severity, code, path, message };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function buildOkfLog(eventsJsonl: string, path = "meta/events.jsonl"): OkfLogResult {
|
|
565
|
+
const diagnostics: KnowledgeDiagnostic[] = [];
|
|
566
|
+
const events: Array<{
|
|
567
|
+
seq: number;
|
|
568
|
+
timestamp: string;
|
|
569
|
+
epoch: number;
|
|
570
|
+
date: string;
|
|
571
|
+
kind: string;
|
|
572
|
+
details: string;
|
|
573
|
+
}> = [];
|
|
574
|
+
|
|
575
|
+
const lines = eventsJsonl.split("\n");
|
|
576
|
+
for (let i = 0; i < lines.length; i++) {
|
|
577
|
+
const line = lines[i];
|
|
578
|
+
if (!line.trim()) continue;
|
|
579
|
+
|
|
580
|
+
let parsed: Record<string, unknown>;
|
|
581
|
+
try {
|
|
582
|
+
const candidate: unknown = JSON.parse(line);
|
|
583
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
584
|
+
throw new Error("event must be a JSON object");
|
|
585
|
+
}
|
|
586
|
+
parsed = candidate as Record<string, unknown>;
|
|
587
|
+
} catch {
|
|
588
|
+
diagnostics.push(
|
|
589
|
+
okfDiag("warning", "event_invalid_json", path, `Invalid JSON at line ${i + 1}`),
|
|
590
|
+
);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const rawTs = parsed.timestamp;
|
|
595
|
+
if (typeof rawTs !== "string" || Number.isNaN(Date.parse(rawTs))) {
|
|
596
|
+
diagnostics.push(
|
|
597
|
+
okfDiag("warning", "event_invalid_timestamp", path, `Invalid timestamp at line ${i + 1}`),
|
|
598
|
+
);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const rawKind = parsed.kind;
|
|
603
|
+
if (typeof rawKind !== "string" || !rawKind.trim()) {
|
|
604
|
+
diagnostics.push(
|
|
605
|
+
okfDiag("warning", "event_missing_kind", path, `Missing kind at line ${i + 1}`),
|
|
606
|
+
);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const ts = new Date(rawTs);
|
|
611
|
+
const date = ts.toISOString().split("T")[0];
|
|
612
|
+
const kind = rawKind.trim();
|
|
613
|
+
|
|
614
|
+
const detailEntries: [string, unknown][] = Object.entries(parsed).filter(
|
|
615
|
+
([k]) => k !== "timestamp" && k !== "kind",
|
|
616
|
+
);
|
|
617
|
+
const details =
|
|
618
|
+
detailEntries.length > 0
|
|
619
|
+
? JSON.stringify(canonicalJsonValue(Object.fromEntries(detailEntries)))
|
|
620
|
+
: "";
|
|
621
|
+
|
|
622
|
+
events.push({ seq: i, timestamp: rawTs, epoch: ts.getTime(), date, kind, details });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Group by date
|
|
626
|
+
const byDate = new Map<string, typeof events>();
|
|
627
|
+
for (const ev of events) {
|
|
628
|
+
if (!byDate.has(ev.date)) byDate.set(ev.date, []);
|
|
629
|
+
byDate.get(ev.date)!.push(ev);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const sortedDates = [...byDate.keys()].sort((a, b) => b.localeCompare(a));
|
|
633
|
+
|
|
634
|
+
const outLines: string[] = ["# Wiki Update Log"];
|
|
635
|
+
|
|
636
|
+
for (const date of sortedDates) {
|
|
637
|
+
const dayEvents = byDate.get(date)!;
|
|
638
|
+
dayEvents.sort((a, b) => b.epoch - a.epoch || b.seq - a.seq);
|
|
639
|
+
|
|
640
|
+
outLines.push("");
|
|
641
|
+
outLines.push(`## ${date}`);
|
|
642
|
+
outLines.push("");
|
|
643
|
+
|
|
644
|
+
for (const ev of dayEvents) {
|
|
645
|
+
const escapedKind = ev.kind
|
|
646
|
+
.replace(/\\/g, "\\\\")
|
|
647
|
+
.replace(/\*/g, "\\*")
|
|
648
|
+
.replace(/_/g, "\\_")
|
|
649
|
+
.replace(/\[/g, "\\[")
|
|
650
|
+
.replace(/\]/g, "\\]")
|
|
651
|
+
.replace(/\s+/g, " ");
|
|
652
|
+
|
|
653
|
+
if (ev.details) {
|
|
654
|
+
outLines.push(`- **${escapedKind}**: ${ev.details}`);
|
|
655
|
+
} else {
|
|
656
|
+
outLines.push(`- **${escapedKind}**`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
markdown: `${outLines.join("\n")}\n`,
|
|
663
|
+
diagnostics,
|
|
664
|
+
};
|
|
250
665
|
}
|