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