@agent-wiki/mcp-server 0.3.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/README.md +251 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +181 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +474 -0
- package/dist/server.js.map +1 -0
- package/dist/wiki.d.ts +218 -0
- package/dist/wiki.d.ts.map +1 -0
- package/dist/wiki.js +1378 -0
- package/dist/wiki.js.map +1 -0
- package/package.json +53 -0
package/dist/wiki.js
ADDED
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Wiki engine — pure data layer, zero LLM dependency.
|
|
3
|
+
*
|
|
4
|
+
* Architecture (Karpathy LLM Wiki pattern):
|
|
5
|
+
*
|
|
6
|
+
* raw/ — Immutable source documents. Write-once, never modified.
|
|
7
|
+
* Each file has a .meta.yaml sidecar with provenance.
|
|
8
|
+
*
|
|
9
|
+
* wiki/ — Mutable Markdown layer. Three kinds of files:
|
|
10
|
+
* 1. System: index.md, log.md, timeline.md (auto-maintained)
|
|
11
|
+
* 2. Entity pages: concept-*, person-*, artifact-*, etc.
|
|
12
|
+
* 3. Synthesis pages: synthesis-* (distilled from multiple pages)
|
|
13
|
+
*
|
|
14
|
+
* schemas/ — Entity templates (person, concept, event, etc.)
|
|
15
|
+
*
|
|
16
|
+
* Key principles:
|
|
17
|
+
* - Raw files are IMMUTABLE — the source of truth
|
|
18
|
+
* - Wiki pages are MUTABLE — compiled knowledge, continuously refined
|
|
19
|
+
* - Self-checking: lint detects contradictions, broken links, stale claims
|
|
20
|
+
* - Knowledge compounds: every write improves the whole
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, statSync, copyFileSync, createWriteStream } from "node:fs";
|
|
23
|
+
import { pipeline } from "node:stream/promises";
|
|
24
|
+
import { Readable } from "node:stream";
|
|
25
|
+
import { join, relative, resolve, basename, extname, dirname } from "node:path";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
import matter from "gray-matter";
|
|
28
|
+
import yaml from "js-yaml";
|
|
29
|
+
// System pages that lint should treat specially
|
|
30
|
+
const SYSTEM_PAGES = new Set(["index.md", "log.md", "timeline.md"]);
|
|
31
|
+
// ── Wiki Class ────────────────────────────────────────────────────
|
|
32
|
+
export class Wiki {
|
|
33
|
+
config;
|
|
34
|
+
/**
|
|
35
|
+
* @param root — path to config root (where .agent-wiki.yaml lives)
|
|
36
|
+
* @param workspace — override workspace directory (all data: wiki/, raw/, schemas/).
|
|
37
|
+
* If not set, falls back to: AGENT_WIKI_WORKSPACE env → config file → root.
|
|
38
|
+
*/
|
|
39
|
+
constructor(root, workspace) {
|
|
40
|
+
const resolvedRoot = resolve(root ?? ".");
|
|
41
|
+
this.config = Wiki.loadConfig(resolvedRoot, workspace);
|
|
42
|
+
}
|
|
43
|
+
// ── Init ──────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Initialize a new knowledge base.
|
|
46
|
+
* @param path — config root (where .agent-wiki.yaml is created)
|
|
47
|
+
* @param workspace — optional separate workspace directory for all data.
|
|
48
|
+
* If set, wiki/, raw/, schemas/ go there instead of path.
|
|
49
|
+
*/
|
|
50
|
+
static init(path, workspace) {
|
|
51
|
+
const configRoot = resolve(path);
|
|
52
|
+
const wsRoot = workspace ? resolve(workspace) : configRoot;
|
|
53
|
+
const wikiDir = join(wsRoot, "wiki");
|
|
54
|
+
const rawDir = join(wsRoot, "raw");
|
|
55
|
+
const schemasDir = join(wsRoot, "schemas");
|
|
56
|
+
for (const dir of [configRoot, wsRoot, wikiDir, rawDir, schemasDir]) {
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
const nowShort = now.replace("T", " ").slice(0, 16) + " UTC";
|
|
61
|
+
// index.md
|
|
62
|
+
writeFileSync(join(wikiDir, "index.md"), `---
|
|
63
|
+
title: Knowledge Base Index
|
|
64
|
+
type: index
|
|
65
|
+
created: "${now}"
|
|
66
|
+
updated: "${now}"
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
# Knowledge Base Index
|
|
70
|
+
|
|
71
|
+
## Categories
|
|
72
|
+
|
|
73
|
+
_No pages yet. Use your agent to add knowledge._
|
|
74
|
+
|
|
75
|
+
## Recent Updates
|
|
76
|
+
|
|
77
|
+
_No updates yet._
|
|
78
|
+
`);
|
|
79
|
+
// log.md
|
|
80
|
+
writeFileSync(join(wikiDir, "log.md"), `---
|
|
81
|
+
title: Operation Log
|
|
82
|
+
type: log
|
|
83
|
+
created: "${now}"
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
# Operation Log
|
|
87
|
+
|
|
88
|
+
| Time | Operation | Page | Summary |
|
|
89
|
+
|------|-----------|------|---------|
|
|
90
|
+
| ${nowShort} | init | — | Knowledge base initialized |
|
|
91
|
+
`);
|
|
92
|
+
// timeline.md
|
|
93
|
+
writeFileSync(join(wikiDir, "timeline.md"), `---
|
|
94
|
+
title: Knowledge Timeline
|
|
95
|
+
type: timeline
|
|
96
|
+
created: "${now}"
|
|
97
|
+
updated: "${now}"
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
# Knowledge Timeline
|
|
101
|
+
|
|
102
|
+
_Chronological view of all knowledge in this wiki._
|
|
103
|
+
|
|
104
|
+
## ${now.slice(0, 10)}
|
|
105
|
+
|
|
106
|
+
- **init** — Knowledge base created
|
|
107
|
+
`);
|
|
108
|
+
// default config — include workspace if separate from config root
|
|
109
|
+
const configData = {
|
|
110
|
+
version: "2",
|
|
111
|
+
wiki: {
|
|
112
|
+
...(workspace ? { workspace: wsRoot } : {}),
|
|
113
|
+
path: "wiki/",
|
|
114
|
+
raw_path: "raw/",
|
|
115
|
+
schemas_path: "schemas/",
|
|
116
|
+
},
|
|
117
|
+
lint: {
|
|
118
|
+
check_orphans: true,
|
|
119
|
+
check_stale_days: 30,
|
|
120
|
+
check_missing_sources: true,
|
|
121
|
+
check_contradictions: true,
|
|
122
|
+
check_integrity: true,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
writeFileSync(join(configRoot, ".agent-wiki.yaml"), yaml.dump(configData, { lineWidth: 100 }));
|
|
126
|
+
// default schemas
|
|
127
|
+
writeDefaultSchemas(schemasDir);
|
|
128
|
+
// .gitignore in workspace (if separate, also add one there)
|
|
129
|
+
writeFileSync(join(configRoot, ".gitignore"), "node_modules/\ndist/\n.env\n");
|
|
130
|
+
if (workspace && wsRoot !== configRoot) {
|
|
131
|
+
writeFileSync(join(wsRoot, ".gitignore"), "# Agent Wiki workspace data\n");
|
|
132
|
+
}
|
|
133
|
+
return new Wiki(configRoot, workspace);
|
|
134
|
+
}
|
|
135
|
+
// ── Config ────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Load config from .agent-wiki.yaml.
|
|
138
|
+
*
|
|
139
|
+
* Workspace resolution priority:
|
|
140
|
+
* 1. Explicit `workspaceOverride` parameter (from CLI --workspace)
|
|
141
|
+
* 2. `AGENT_WIKI_WORKSPACE` environment variable
|
|
142
|
+
* 3. `workspace` field in .agent-wiki.yaml (absolute, or relative to config file)
|
|
143
|
+
* 4. Fall back to config root itself
|
|
144
|
+
*
|
|
145
|
+
* All data dirs (wiki/, raw/, schemas/) resolve relative to workspace.
|
|
146
|
+
*/
|
|
147
|
+
static loadConfig(root, workspaceOverride) {
|
|
148
|
+
let raw = {};
|
|
149
|
+
const configPath = join(root, ".agent-wiki.yaml");
|
|
150
|
+
const homeConfigPath = join(process.env.HOME ?? "~", ".agent-wiki.yaml");
|
|
151
|
+
if (existsSync(configPath)) {
|
|
152
|
+
raw = yaml.load(readFileSync(configPath, "utf-8")) ?? {};
|
|
153
|
+
}
|
|
154
|
+
else if (existsSync(homeConfigPath)) {
|
|
155
|
+
raw = yaml.load(readFileSync(homeConfigPath, "utf-8")) ?? {};
|
|
156
|
+
}
|
|
157
|
+
const wikiData = (raw.wiki ?? {});
|
|
158
|
+
const lintData = (raw.lint ?? {});
|
|
159
|
+
// Resolve workspace directory (priority: override > env > config > root)
|
|
160
|
+
let workspace;
|
|
161
|
+
if (workspaceOverride) {
|
|
162
|
+
workspace = resolve(workspaceOverride);
|
|
163
|
+
}
|
|
164
|
+
else if (process.env.AGENT_WIKI_WORKSPACE) {
|
|
165
|
+
workspace = resolve(process.env.AGENT_WIKI_WORKSPACE);
|
|
166
|
+
}
|
|
167
|
+
else if (wikiData.workspace) {
|
|
168
|
+
// Relative paths in config resolve against the config file's directory
|
|
169
|
+
workspace = resolve(root, wikiData.workspace);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
workspace = root;
|
|
173
|
+
}
|
|
174
|
+
// Ensure workspace exists
|
|
175
|
+
mkdirSync(workspace, { recursive: true });
|
|
176
|
+
return {
|
|
177
|
+
configRoot: root,
|
|
178
|
+
workspace,
|
|
179
|
+
wikiDir: join(workspace, wikiData.path ?? "wiki"),
|
|
180
|
+
rawDir: join(workspace, wikiData.raw_path ?? "raw"),
|
|
181
|
+
schemasDir: join(workspace, wikiData.schemas_path ?? "schemas"),
|
|
182
|
+
lint: {
|
|
183
|
+
checkOrphans: lintData.check_orphans ?? true,
|
|
184
|
+
checkStaleDays: lintData.check_stale_days ?? 30,
|
|
185
|
+
checkMissingSources: lintData.check_missing_sources ?? true,
|
|
186
|
+
checkContradictions: lintData.check_contradictions ?? true,
|
|
187
|
+
checkIntegrity: lintData.check_integrity ?? true,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════
|
|
192
|
+
// RAW LAYER — Immutable source documents
|
|
193
|
+
// ═══════════════════════════════════════════════════════════════
|
|
194
|
+
/** Register a raw document. Copies file to raw/ with metadata sidecar.
|
|
195
|
+
* If content is provided as string, writes it directly.
|
|
196
|
+
* If sourcePath is an existing file, copies it.
|
|
197
|
+
* Raw files are IMMUTABLE — re-adding the same path is an error. */
|
|
198
|
+
rawAdd(filename, opts) {
|
|
199
|
+
const rawPath = join(this.config.rawDir, filename);
|
|
200
|
+
const metaPath = rawPath + ".meta.yaml";
|
|
201
|
+
// Immutability guard — never overwrite existing raw files
|
|
202
|
+
if (existsSync(rawPath)) {
|
|
203
|
+
throw new Error(`Raw file already exists: ${filename}. Raw files are immutable.`);
|
|
204
|
+
}
|
|
205
|
+
mkdirSync(dirname(rawPath), { recursive: true });
|
|
206
|
+
// Write content
|
|
207
|
+
if (opts.content !== undefined) {
|
|
208
|
+
writeFileSync(rawPath, opts.content);
|
|
209
|
+
}
|
|
210
|
+
else if (opts.sourcePath && existsSync(opts.sourcePath)) {
|
|
211
|
+
copyFileSync(opts.sourcePath, rawPath);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
throw new Error("Either content or a valid sourcePath is required");
|
|
215
|
+
}
|
|
216
|
+
// Compute hash and size
|
|
217
|
+
const buf = readFileSync(rawPath);
|
|
218
|
+
const sha256 = createHash("sha256").update(buf).digest("hex");
|
|
219
|
+
const size = buf.length;
|
|
220
|
+
const now = new Date().toISOString();
|
|
221
|
+
const doc = {
|
|
222
|
+
path: filename,
|
|
223
|
+
sourceUrl: opts.sourceUrl,
|
|
224
|
+
downloadedAt: now,
|
|
225
|
+
sha256,
|
|
226
|
+
size,
|
|
227
|
+
mimeType: opts.mimeType ?? guessMime(filename),
|
|
228
|
+
description: opts.description,
|
|
229
|
+
tags: opts.tags,
|
|
230
|
+
};
|
|
231
|
+
// Write metadata sidecar
|
|
232
|
+
writeFileSync(metaPath, yaml.dump(doc, { lineWidth: 100 }));
|
|
233
|
+
this.log("raw-add", filename, `Added raw: ${filename} (${formatBytes(size)}, sha256:${sha256.slice(0, 12)}...)`);
|
|
234
|
+
return doc;
|
|
235
|
+
}
|
|
236
|
+
/** List all raw documents with metadata. */
|
|
237
|
+
rawList() {
|
|
238
|
+
if (!existsSync(this.config.rawDir))
|
|
239
|
+
return [];
|
|
240
|
+
const docs = [];
|
|
241
|
+
for (const file of listAllFiles(this.config.rawDir, this.config.rawDir)) {
|
|
242
|
+
if (file.endsWith(".meta.yaml"))
|
|
243
|
+
continue; // skip sidecars
|
|
244
|
+
const metaPath = join(this.config.rawDir, file) + ".meta.yaml";
|
|
245
|
+
if (existsSync(metaPath)) {
|
|
246
|
+
const meta = yaml.load(readFileSync(metaPath, "utf-8"));
|
|
247
|
+
docs.push(meta);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// raw file without metadata — create minimal entry
|
|
251
|
+
const fullPath = join(this.config.rawDir, file);
|
|
252
|
+
const buf = readFileSync(fullPath);
|
|
253
|
+
docs.push({
|
|
254
|
+
path: file,
|
|
255
|
+
downloadedAt: statSync(fullPath).mtime.toISOString(),
|
|
256
|
+
sha256: createHash("sha256").update(buf).digest("hex"),
|
|
257
|
+
size: buf.length,
|
|
258
|
+
mimeType: guessMime(file),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return docs;
|
|
263
|
+
}
|
|
264
|
+
/** Read a raw document's content. */
|
|
265
|
+
rawRead(filename) {
|
|
266
|
+
const fullPath = join(this.config.rawDir, filename);
|
|
267
|
+
if (!existsSync(fullPath))
|
|
268
|
+
return null;
|
|
269
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
270
|
+
const metaPath = fullPath + ".meta.yaml";
|
|
271
|
+
const meta = existsSync(metaPath)
|
|
272
|
+
? yaml.load(readFileSync(metaPath, "utf-8"))
|
|
273
|
+
: null;
|
|
274
|
+
return { content, meta };
|
|
275
|
+
}
|
|
276
|
+
/** Verify integrity of all raw files against their stored hashes. */
|
|
277
|
+
rawVerify() {
|
|
278
|
+
const results = [];
|
|
279
|
+
if (!existsSync(this.config.rawDir))
|
|
280
|
+
return results;
|
|
281
|
+
for (const file of listAllFiles(this.config.rawDir, this.config.rawDir)) {
|
|
282
|
+
if (file.endsWith(".meta.yaml"))
|
|
283
|
+
continue;
|
|
284
|
+
const fullPath = join(this.config.rawDir, file);
|
|
285
|
+
const metaPath = fullPath + ".meta.yaml";
|
|
286
|
+
if (!existsSync(metaPath)) {
|
|
287
|
+
results.push({ path: file, status: "missing-meta" });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const meta = yaml.load(readFileSync(metaPath, "utf-8"));
|
|
291
|
+
const buf = readFileSync(fullPath);
|
|
292
|
+
const actualHash = createHash("sha256").update(buf).digest("hex");
|
|
293
|
+
results.push({
|
|
294
|
+
path: file,
|
|
295
|
+
status: actualHash === meta.sha256 ? "ok" : "corrupted",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return results;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Fetch a file from a URL and save it to raw/.
|
|
302
|
+
* Supports arXiv smart resolution: arxiv.org/abs/XXXX → arxiv.org/pdf/XXXX.pdf
|
|
303
|
+
* Returns the RawDocument metadata.
|
|
304
|
+
*/
|
|
305
|
+
async rawFetch(url, opts = {}) {
|
|
306
|
+
// ── arXiv smart URL resolution ──
|
|
307
|
+
let resolvedUrl = url;
|
|
308
|
+
let inferredFilename = opts.filename;
|
|
309
|
+
const arxivAbsMatch = url.match(/arxiv\.org\/abs\/(\d+\.\d+)(v\d+)?/);
|
|
310
|
+
const arxivPdfMatch = url.match(/arxiv\.org\/pdf\/(\d+\.\d+)(v\d+)?/);
|
|
311
|
+
if (arxivAbsMatch) {
|
|
312
|
+
const id = arxivAbsMatch[1] + (arxivAbsMatch[2] ?? "");
|
|
313
|
+
resolvedUrl = `https://arxiv.org/pdf/${id}.pdf`;
|
|
314
|
+
if (!inferredFilename)
|
|
315
|
+
inferredFilename = `arxiv-${id.replace(/\./g, "-")}.pdf`;
|
|
316
|
+
}
|
|
317
|
+
else if (arxivPdfMatch && !inferredFilename) {
|
|
318
|
+
const id = arxivPdfMatch[1] + (arxivPdfMatch[2] ?? "");
|
|
319
|
+
inferredFilename = `arxiv-${id.replace(/\./g, "-")}.pdf`;
|
|
320
|
+
}
|
|
321
|
+
// ── Infer filename from URL if not provided ──
|
|
322
|
+
if (!inferredFilename) {
|
|
323
|
+
const urlObj = new URL(resolvedUrl);
|
|
324
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
325
|
+
const lastPart = pathParts[pathParts.length - 1] ?? "download";
|
|
326
|
+
// Clean up query params and fragments
|
|
327
|
+
inferredFilename = lastPart.split("?")[0].split("#")[0];
|
|
328
|
+
// If no extension, try to add one based on content-type later
|
|
329
|
+
if (!inferredFilename.includes(".")) {
|
|
330
|
+
inferredFilename += ".bin";
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ── Immutability guard ──
|
|
334
|
+
const rawPath = join(this.config.rawDir, inferredFilename);
|
|
335
|
+
if (existsSync(rawPath)) {
|
|
336
|
+
throw new Error(`Raw file already exists: ${inferredFilename}. Raw files are immutable.`);
|
|
337
|
+
}
|
|
338
|
+
mkdirSync(dirname(rawPath), { recursive: true });
|
|
339
|
+
// ── Download ──
|
|
340
|
+
const response = await fetch(resolvedUrl, {
|
|
341
|
+
headers: {
|
|
342
|
+
"User-Agent": "agent-wiki/0.3.0",
|
|
343
|
+
},
|
|
344
|
+
redirect: "follow",
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(`Download failed: HTTP ${response.status} ${response.statusText} — ${resolvedUrl}`);
|
|
348
|
+
}
|
|
349
|
+
// Update filename extension based on content-type if it was generic
|
|
350
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
351
|
+
if (inferredFilename.endsWith(".bin")) {
|
|
352
|
+
const extMap = {
|
|
353
|
+
"application/pdf": ".pdf",
|
|
354
|
+
"text/html": ".html",
|
|
355
|
+
"text/plain": ".txt",
|
|
356
|
+
"application/json": ".json",
|
|
357
|
+
"text/markdown": ".md",
|
|
358
|
+
"image/png": ".png",
|
|
359
|
+
"image/jpeg": ".jpg",
|
|
360
|
+
"application/xml": ".xml",
|
|
361
|
+
"text/xml": ".xml",
|
|
362
|
+
};
|
|
363
|
+
for (const [mime, ext] of Object.entries(extMap)) {
|
|
364
|
+
if (contentType.includes(mime)) {
|
|
365
|
+
inferredFilename = inferredFilename.replace(/\.bin$/, ext);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Re-check with potentially updated filename
|
|
371
|
+
const finalPath = join(this.config.rawDir, inferredFilename);
|
|
372
|
+
if (finalPath !== rawPath && existsSync(finalPath)) {
|
|
373
|
+
throw new Error(`Raw file already exists: ${inferredFilename}. Raw files are immutable.`);
|
|
374
|
+
}
|
|
375
|
+
// Stream to file
|
|
376
|
+
const body = response.body;
|
|
377
|
+
if (!body)
|
|
378
|
+
throw new Error("Empty response body");
|
|
379
|
+
const nodeStream = Readable.fromWeb(body);
|
|
380
|
+
const fileStream = createWriteStream(finalPath);
|
|
381
|
+
await pipeline(nodeStream, fileStream);
|
|
382
|
+
// ── Compute hash and create metadata ──
|
|
383
|
+
const buf = readFileSync(finalPath);
|
|
384
|
+
const sha256 = createHash("sha256").update(buf).digest("hex");
|
|
385
|
+
const now = new Date().toISOString();
|
|
386
|
+
const mime = contentType.split(";")[0]?.trim() || guessMime(inferredFilename);
|
|
387
|
+
const doc = {
|
|
388
|
+
path: inferredFilename,
|
|
389
|
+
sourceUrl: url, // original URL, not resolved
|
|
390
|
+
downloadedAt: now,
|
|
391
|
+
sha256,
|
|
392
|
+
size: buf.length,
|
|
393
|
+
mimeType: mime,
|
|
394
|
+
description: opts.description,
|
|
395
|
+
tags: opts.tags,
|
|
396
|
+
};
|
|
397
|
+
// Write metadata sidecar
|
|
398
|
+
writeFileSync(finalPath + ".meta.yaml", yaml.dump(doc, { lineWidth: 100 }));
|
|
399
|
+
this.log("raw-fetch", inferredFilename, `Downloaded from ${url} (${formatBytes(buf.length)}, ${mime})`);
|
|
400
|
+
return doc;
|
|
401
|
+
}
|
|
402
|
+
// ═══════════════════════════════════════════════════════════════
|
|
403
|
+
// WIKI LAYER — Mutable compiled knowledge
|
|
404
|
+
// ═══════════════════════════════════════════════════════════════
|
|
405
|
+
// ── CRUD ──────────────────────────────────────────────────────
|
|
406
|
+
/** Read a wiki page. Returns null if not found. */
|
|
407
|
+
read(pagePath) {
|
|
408
|
+
const fullPath = join(this.config.wikiDir, pagePath);
|
|
409
|
+
if (!existsSync(fullPath)) {
|
|
410
|
+
const withMd = fullPath.endsWith(".md") ? fullPath : fullPath + ".md";
|
|
411
|
+
if (!existsSync(withMd))
|
|
412
|
+
return null;
|
|
413
|
+
return this.parsePage(relative(this.config.wikiDir, withMd), readFileSync(withMd, "utf-8"));
|
|
414
|
+
}
|
|
415
|
+
return this.parsePage(pagePath, readFileSync(fullPath, "utf-8"));
|
|
416
|
+
}
|
|
417
|
+
/** Write (create or update) a wiki page. Content must include frontmatter.
|
|
418
|
+
* Automatically injects/updates created and updated timestamps. */
|
|
419
|
+
write(pagePath, content, source) {
|
|
420
|
+
const fullPath = join(this.config.wikiDir, pagePath);
|
|
421
|
+
const dir = dirname(fullPath);
|
|
422
|
+
mkdirSync(dir, { recursive: true });
|
|
423
|
+
const now = new Date().toISOString();
|
|
424
|
+
// Parse incoming content to inject timestamps
|
|
425
|
+
const parsed = matter(content);
|
|
426
|
+
if (!parsed.data.created) {
|
|
427
|
+
// Check if page already exists — preserve original created time
|
|
428
|
+
if (existsSync(fullPath)) {
|
|
429
|
+
const existing = matter(readFileSync(fullPath, "utf-8"));
|
|
430
|
+
parsed.data.created = existing.data.created ?? now;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
parsed.data.created = now;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
parsed.data.updated = now;
|
|
437
|
+
// Reconstruct content with updated frontmatter
|
|
438
|
+
const finalContent = matter.stringify(parsed.content, parsed.data);
|
|
439
|
+
writeFileSync(fullPath, finalContent.trimEnd() + "\n");
|
|
440
|
+
this.log("write", pagePath, `Wrote ${pagePath}${source ? ` (${source})` : ""}`);
|
|
441
|
+
}
|
|
442
|
+
/** Delete a wiki page. Returns true if it existed. */
|
|
443
|
+
delete(pagePath) {
|
|
444
|
+
// Guard: never delete system pages
|
|
445
|
+
if (SYSTEM_PAGES.has(pagePath)) {
|
|
446
|
+
throw new Error(`Cannot delete system page: ${pagePath}`);
|
|
447
|
+
}
|
|
448
|
+
const fullPath = join(this.config.wikiDir, pagePath);
|
|
449
|
+
if (!existsSync(fullPath))
|
|
450
|
+
return false;
|
|
451
|
+
unlinkSync(fullPath);
|
|
452
|
+
this.log("delete", pagePath, `Deleted ${pagePath}`);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
/** List all wiki pages, optionally filtered by type or tag. */
|
|
456
|
+
list(filterType, filterTag) {
|
|
457
|
+
const pages = this.listAllPages();
|
|
458
|
+
if (!filterType && !filterTag)
|
|
459
|
+
return pages;
|
|
460
|
+
return pages.filter((p) => {
|
|
461
|
+
const page = this.read(p);
|
|
462
|
+
if (!page)
|
|
463
|
+
return false;
|
|
464
|
+
if (filterType && page.type !== filterType)
|
|
465
|
+
return false;
|
|
466
|
+
if (filterTag && !page.tags.includes(filterTag))
|
|
467
|
+
return false;
|
|
468
|
+
return true;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
// ── Search ────────────────────────────────────────────────────
|
|
472
|
+
/** Keyword search across all wiki pages. Returns paths sorted by relevance. */
|
|
473
|
+
search(query, limit = 10) {
|
|
474
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
475
|
+
if (terms.length === 0)
|
|
476
|
+
return [];
|
|
477
|
+
const pages = this.listAllPages();
|
|
478
|
+
const results = [];
|
|
479
|
+
for (const pagePath of pages) {
|
|
480
|
+
const page = this.read(pagePath);
|
|
481
|
+
if (!page)
|
|
482
|
+
continue;
|
|
483
|
+
const text = (page.title + " " + page.tags.join(" ") + " " + page.content).toLowerCase();
|
|
484
|
+
let score = 0;
|
|
485
|
+
for (const term of terms) {
|
|
486
|
+
let idx = 0;
|
|
487
|
+
while ((idx = text.indexOf(term, idx)) !== -1) {
|
|
488
|
+
score++;
|
|
489
|
+
idx += term.length;
|
|
490
|
+
}
|
|
491
|
+
if (page.title.toLowerCase().includes(term))
|
|
492
|
+
score += 5;
|
|
493
|
+
if (page.tags.some((t) => String(t).toLowerCase().includes(term)))
|
|
494
|
+
score += 3;
|
|
495
|
+
// Boost synthesis pages slightly — they represent distilled knowledge
|
|
496
|
+
if (page.type === "synthesis")
|
|
497
|
+
score += 1;
|
|
498
|
+
}
|
|
499
|
+
if (score > 0) {
|
|
500
|
+
const firstIdx = text.indexOf(terms[0]);
|
|
501
|
+
const start = Math.max(0, firstIdx - 50);
|
|
502
|
+
const end = Math.min(text.length, firstIdx + 100);
|
|
503
|
+
const snippet = (start > 0 ? "..." : "") + text.slice(start, end).trim() + (end < text.length ? "..." : "");
|
|
504
|
+
results.push({ path: pagePath, score, snippet });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
results.sort((a, b) => b.score - a.score);
|
|
508
|
+
return results.slice(0, limit);
|
|
509
|
+
}
|
|
510
|
+
// ── Lint — Self-checking & error detection ────────────────────
|
|
511
|
+
/** Run comprehensive health checks. Pure rules, no LLM.
|
|
512
|
+
* Detects: contradictions, orphans, broken links, missing sources,
|
|
513
|
+
* stale content, structural issues, integrity problems. */
|
|
514
|
+
lint() {
|
|
515
|
+
const pages = this.listAllPages();
|
|
516
|
+
const report = {
|
|
517
|
+
pagesChecked: pages.length,
|
|
518
|
+
rawChecked: 0,
|
|
519
|
+
issues: [],
|
|
520
|
+
contradictions: [],
|
|
521
|
+
};
|
|
522
|
+
// Build a map of all pages for cross-referencing
|
|
523
|
+
const pageMap = new Map();
|
|
524
|
+
for (const pagePath of pages) {
|
|
525
|
+
const page = this.read(pagePath);
|
|
526
|
+
if (page)
|
|
527
|
+
pageMap.set(pagePath, page);
|
|
528
|
+
}
|
|
529
|
+
for (const [pagePath, page] of pageMap) {
|
|
530
|
+
// ── Missing frontmatter ──
|
|
531
|
+
if (Object.keys(page.frontmatter).length === 0) {
|
|
532
|
+
report.issues.push({
|
|
533
|
+
severity: "warning",
|
|
534
|
+
page: pagePath,
|
|
535
|
+
message: "Missing YAML frontmatter",
|
|
536
|
+
suggestion: "Add frontmatter with title, type, tags, and sources",
|
|
537
|
+
autoFixable: true,
|
|
538
|
+
category: "structure",
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
// ── Missing title ──
|
|
542
|
+
if (!page.title || page.title === basename(pagePath, extname(pagePath))) {
|
|
543
|
+
report.issues.push({
|
|
544
|
+
severity: "warning",
|
|
545
|
+
page: pagePath,
|
|
546
|
+
message: "Missing or auto-generated title",
|
|
547
|
+
suggestion: "Add a meaningful title in frontmatter",
|
|
548
|
+
autoFixable: false,
|
|
549
|
+
category: "structure",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
// ── Orphan pages ──
|
|
553
|
+
if (this.config.lint.checkOrphans && !SYSTEM_PAGES.has(pagePath)) {
|
|
554
|
+
const slug = basename(pagePath, extname(pagePath));
|
|
555
|
+
const hasIncoming = [...pageMap].some(([other, otherPage]) => {
|
|
556
|
+
if (other === pagePath)
|
|
557
|
+
return false;
|
|
558
|
+
return otherPage.links.includes(slug);
|
|
559
|
+
});
|
|
560
|
+
if (!hasIncoming) {
|
|
561
|
+
report.issues.push({
|
|
562
|
+
severity: "warning",
|
|
563
|
+
page: pagePath,
|
|
564
|
+
message: "Orphan page — no other pages link here",
|
|
565
|
+
suggestion: `Add [[${slug}]] to related pages or index.md`,
|
|
566
|
+
autoFixable: true,
|
|
567
|
+
category: "orphan",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// ── Broken links ──
|
|
572
|
+
for (const link of page.links) {
|
|
573
|
+
const linkPath = link.endsWith(".md") ? link : link + ".md";
|
|
574
|
+
if (!pages.includes(linkPath) && !pages.includes(link)) {
|
|
575
|
+
report.issues.push({
|
|
576
|
+
severity: "error",
|
|
577
|
+
page: pagePath,
|
|
578
|
+
message: `Broken link: [[${link}]]`,
|
|
579
|
+
suggestion: `Create ${link}.md or fix the link`,
|
|
580
|
+
autoFixable: false,
|
|
581
|
+
category: "broken-link",
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// ── Missing sources (non-system pages) ──
|
|
586
|
+
if (this.config.lint.checkMissingSources && !SYSTEM_PAGES.has(pagePath)) {
|
|
587
|
+
if (page.sources.length === 0 && page.type && page.type !== "index" && page.type !== "log" && page.type !== "timeline") {
|
|
588
|
+
report.issues.push({
|
|
589
|
+
severity: "info",
|
|
590
|
+
page: pagePath,
|
|
591
|
+
message: "No sources listed — claims are not traceable to raw documents",
|
|
592
|
+
suggestion: "Add sources to frontmatter linking to raw/ files or URLs",
|
|
593
|
+
autoFixable: false,
|
|
594
|
+
category: "missing-source",
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// ── Synthesis page integrity ──
|
|
599
|
+
if (page.type === "synthesis" && page.derivedFrom) {
|
|
600
|
+
for (const src of page.derivedFrom) {
|
|
601
|
+
const srcPath = src.endsWith(".md") ? src : src + ".md";
|
|
602
|
+
if (!pages.includes(srcPath) && !pages.includes(src)) {
|
|
603
|
+
report.issues.push({
|
|
604
|
+
severity: "error",
|
|
605
|
+
page: pagePath,
|
|
606
|
+
message: `Synthesis source missing: ${src}`,
|
|
607
|
+
suggestion: `The page this synthesis derives from no longer exists. Review and update.`,
|
|
608
|
+
autoFixable: false,
|
|
609
|
+
category: "integrity",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// ── Stale content ──
|
|
615
|
+
if (this.config.lint.checkStaleDays > 0) {
|
|
616
|
+
try {
|
|
617
|
+
const fullPath = join(this.config.wikiDir, pagePath);
|
|
618
|
+
const stat = statSync(fullPath);
|
|
619
|
+
const ageDays = Math.floor((Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24));
|
|
620
|
+
if (ageDays > this.config.lint.checkStaleDays) {
|
|
621
|
+
report.issues.push({
|
|
622
|
+
severity: "info",
|
|
623
|
+
page: pagePath,
|
|
624
|
+
message: `Stale content — last modified ${ageDays} days ago`,
|
|
625
|
+
suggestion: "Review and update if needed",
|
|
626
|
+
autoFixable: false,
|
|
627
|
+
category: "stale",
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// stat failed, skip
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// ── Cross-page contradiction detection ──
|
|
637
|
+
if (this.config.lint.checkContradictions) {
|
|
638
|
+
const contradictions = this.detectContradictions(pageMap);
|
|
639
|
+
report.contradictions = contradictions;
|
|
640
|
+
for (const c of contradictions) {
|
|
641
|
+
report.issues.push({
|
|
642
|
+
severity: c.severity,
|
|
643
|
+
page: c.pageA,
|
|
644
|
+
message: `Contradiction with [[${basename(c.pageB, ".md")}]]: ${c.claim}`,
|
|
645
|
+
suggestion: `"${c.excerptA}" vs "${c.excerptB}" — review and resolve`,
|
|
646
|
+
autoFixable: false,
|
|
647
|
+
category: "contradiction",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// ── Raw file integrity ──
|
|
652
|
+
if (this.config.lint.checkIntegrity) {
|
|
653
|
+
const rawResults = this.rawVerify();
|
|
654
|
+
report.rawChecked = rawResults.length;
|
|
655
|
+
for (const r of rawResults) {
|
|
656
|
+
if (r.status === "corrupted") {
|
|
657
|
+
report.issues.push({
|
|
658
|
+
severity: "error",
|
|
659
|
+
page: `raw/${r.path}`,
|
|
660
|
+
message: "Raw file corrupted — SHA-256 mismatch",
|
|
661
|
+
suggestion: "Re-download the original source",
|
|
662
|
+
autoFixable: false,
|
|
663
|
+
category: "integrity",
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
else if (r.status === "missing-meta") {
|
|
667
|
+
report.issues.push({
|
|
668
|
+
severity: "warning",
|
|
669
|
+
page: `raw/${r.path}`,
|
|
670
|
+
message: "Raw file has no metadata sidecar (.meta.yaml)",
|
|
671
|
+
suggestion: "Use raw_add to properly register this file",
|
|
672
|
+
autoFixable: true,
|
|
673
|
+
category: "integrity",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
this.log("lint", "—", `Checked ${report.pagesChecked} pages + ${report.rawChecked} raw files, found ${report.issues.length} issues (${report.contradictions.length} contradictions)`);
|
|
679
|
+
return report;
|
|
680
|
+
}
|
|
681
|
+
// ── Contradiction detection ───────────────────────────────────
|
|
682
|
+
/** Detect contradictions between pages.
|
|
683
|
+
* Looks for numeric claims, date claims, and factual statements
|
|
684
|
+
* that conflict across pages about the same entity/topic. */
|
|
685
|
+
detectContradictions(pageMap) {
|
|
686
|
+
const contradictions = [];
|
|
687
|
+
// Extract claims from pages — look for patterns like "X is Y", dates, numbers
|
|
688
|
+
const claims = new Map();
|
|
689
|
+
for (const [pagePath, page] of pageMap) {
|
|
690
|
+
if (SYSTEM_PAGES.has(pagePath))
|
|
691
|
+
continue;
|
|
692
|
+
// Extract date claims: "published in YYYY", "released YYYY", "founded YYYY"
|
|
693
|
+
const datePatterns = page.content.matchAll(/(?:published|released|founded|created|introduced|launched|announced|born|died)\s+(?:in\s+)?(\d{4})/gi);
|
|
694
|
+
for (const m of datePatterns) {
|
|
695
|
+
const key = m[0].replace(/\d{4}/, "YEAR").toLowerCase().trim();
|
|
696
|
+
const entry = { page: pagePath, excerpt: m[0], value: m[1] };
|
|
697
|
+
if (!claims.has(key))
|
|
698
|
+
claims.set(key, []);
|
|
699
|
+
claims.get(key).push(entry);
|
|
700
|
+
}
|
|
701
|
+
// Extract numeric claims: "achieved XX% mAP", "XX FPS", "XX parameters"
|
|
702
|
+
const numericPatterns = page.content.matchAll(/(\d+\.?\d*)\s*(%|fps|ms|map|ap|parameters|params|layers|million|billion|m\b|b\b|k\b)/gi);
|
|
703
|
+
for (const m of numericPatterns) {
|
|
704
|
+
// Context: 30 chars before and after
|
|
705
|
+
const idx = page.content.indexOf(m[0]);
|
|
706
|
+
const ctxStart = Math.max(0, idx - 30);
|
|
707
|
+
const ctxEnd = Math.min(page.content.length, idx + m[0].length + 30);
|
|
708
|
+
const context = page.content.slice(ctxStart, ctxEnd).replace(/\n/g, " ").trim();
|
|
709
|
+
// Key = normalized context without the number
|
|
710
|
+
const key = context.replace(/\d+\.?\d*/g, "N").toLowerCase().slice(0, 60);
|
|
711
|
+
const entry = { page: pagePath, excerpt: context, value: m[1] };
|
|
712
|
+
if (!claims.has(key))
|
|
713
|
+
claims.set(key, []);
|
|
714
|
+
claims.get(key).push(entry);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Compare claims from different pages
|
|
718
|
+
for (const [claimKey, entries] of claims) {
|
|
719
|
+
if (entries.length < 2)
|
|
720
|
+
continue;
|
|
721
|
+
// Group by page
|
|
722
|
+
const byPage = new Map();
|
|
723
|
+
for (const e of entries) {
|
|
724
|
+
if (!byPage.has(e.page))
|
|
725
|
+
byPage.set(e.page, e);
|
|
726
|
+
}
|
|
727
|
+
const uniquePages = [...byPage.values()];
|
|
728
|
+
if (uniquePages.length < 2)
|
|
729
|
+
continue;
|
|
730
|
+
// Check if values differ
|
|
731
|
+
for (let i = 0; i < uniquePages.length; i++) {
|
|
732
|
+
for (let j = i + 1; j < uniquePages.length; j++) {
|
|
733
|
+
const a = uniquePages[i];
|
|
734
|
+
const b = uniquePages[j];
|
|
735
|
+
if (a.value !== b.value) {
|
|
736
|
+
contradictions.push({
|
|
737
|
+
claim: claimKey.replace(/\bn\b/gi, "?"),
|
|
738
|
+
pageA: a.page,
|
|
739
|
+
excerptA: a.excerpt,
|
|
740
|
+
pageB: b.page,
|
|
741
|
+
excerptB: b.excerpt,
|
|
742
|
+
severity: Math.abs(parseFloat(a.value) - parseFloat(b.value)) > 10 ? "error" : "warning",
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return contradictions;
|
|
749
|
+
}
|
|
750
|
+
// ── Synthesis — Knowledge distillation ────────────────────────
|
|
751
|
+
/** Get context for synthesis: reads multiple pages and returns
|
|
752
|
+
* their content for the agent to distill into a new page. */
|
|
753
|
+
synthesizeContext(pagePaths) {
|
|
754
|
+
const pages = [];
|
|
755
|
+
const allTags = new Set();
|
|
756
|
+
const allLinks = new Set();
|
|
757
|
+
for (const p of pagePaths) {
|
|
758
|
+
const page = this.read(p) ?? this.read(p + ".md");
|
|
759
|
+
if (page) {
|
|
760
|
+
pages.push({ path: page.path, title: page.title, content: page.content });
|
|
761
|
+
page.tags.forEach((t) => allTags.add(t));
|
|
762
|
+
page.links.forEach((l) => allLinks.add(l));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Generate suggestions for the synthesis
|
|
766
|
+
const suggestions = [];
|
|
767
|
+
if (pages.length >= 2) {
|
|
768
|
+
suggestions.push(`Combine insights from ${pages.map((p) => p.title).join(", ")}`);
|
|
769
|
+
suggestions.push("Look for common themes, contradictions, and gaps");
|
|
770
|
+
suggestions.push("Create cross-references using [[page-name]] syntax");
|
|
771
|
+
}
|
|
772
|
+
if (allTags.size > 0) {
|
|
773
|
+
suggestions.push(`Suggested tags: ${[...allTags].join(", ")}`);
|
|
774
|
+
}
|
|
775
|
+
return { pages, suggestions };
|
|
776
|
+
}
|
|
777
|
+
// ── Auto-Classification ───────────────────────────────────────
|
|
778
|
+
/** Auto-classify content into entity type and suggested tags.
|
|
779
|
+
* Pure heuristic — zero LLM dependency. Analyzes title, body,
|
|
780
|
+
* and structure to determine the best type and relevant tags.
|
|
781
|
+
* If frontmatter already has a type, respects it. */
|
|
782
|
+
classify(content) {
|
|
783
|
+
const parsed = matter(content);
|
|
784
|
+
const body = parsed.content.toLowerCase();
|
|
785
|
+
const title = (parsed.data.title ?? "").toLowerCase();
|
|
786
|
+
const combined = title + " " + body;
|
|
787
|
+
// If frontmatter already specifies type, respect it but still suggest tags
|
|
788
|
+
if (parsed.data.type && typeof parsed.data.type === "string" && parsed.data.type !== "note") {
|
|
789
|
+
const existingTags = Array.isArray(parsed.data.tags) ? parsed.data.tags.map(String) : [];
|
|
790
|
+
const suggestedTags = existingTags.length > 0 ? existingTags : this.extractTags(combined);
|
|
791
|
+
return { type: parsed.data.type, tags: suggestedTags, confidence: 1.0 };
|
|
792
|
+
}
|
|
793
|
+
// Score each entity type based on keyword signals
|
|
794
|
+
const scores = {
|
|
795
|
+
person: 0, concept: 0, event: 0, artifact: 0,
|
|
796
|
+
comparison: 0, summary: 0, "how-to": 0, synthesis: 0, note: 0,
|
|
797
|
+
};
|
|
798
|
+
// Person signals
|
|
799
|
+
for (const w of ["born", "career", "biography", "researcher", "professor", "author",
|
|
800
|
+
"founder", "role:", "affiliat", "人物", "创始人", "研究员"]) {
|
|
801
|
+
if (combined.includes(w))
|
|
802
|
+
scores.person += 2;
|
|
803
|
+
}
|
|
804
|
+
// Concept signals
|
|
805
|
+
for (const w of ["definition", "theory", "concept", "principle", "paradigm",
|
|
806
|
+
"what is", "核心思想", "定义", "概念", "理论", "原理"]) {
|
|
807
|
+
if (combined.includes(w))
|
|
808
|
+
scores.concept += 2;
|
|
809
|
+
}
|
|
810
|
+
// Event signals
|
|
811
|
+
for (const w of ["happened", "occurred", "conference", "launched", "announced",
|
|
812
|
+
"event", "发布会", "事件", "会议"]) {
|
|
813
|
+
if (combined.includes(w))
|
|
814
|
+
scores.event += 2;
|
|
815
|
+
}
|
|
816
|
+
// Artifact signals (papers, tools, models)
|
|
817
|
+
for (const w of ["paper", "论文", "tool", "library", "framework", "model",
|
|
818
|
+
"version", "release", "arxiv", "github", "引用", "doi"]) {
|
|
819
|
+
if (combined.includes(w))
|
|
820
|
+
scores.artifact += 2;
|
|
821
|
+
}
|
|
822
|
+
// Comparison signals
|
|
823
|
+
for (const w of ["vs", "versus", "compared", "comparison", "benchmark",
|
|
824
|
+
"对比", "比较", "横评"]) {
|
|
825
|
+
if (combined.includes(w))
|
|
826
|
+
scores.comparison += 2;
|
|
827
|
+
}
|
|
828
|
+
// Many table rows strongly suggest comparison
|
|
829
|
+
if ((body.match(/\|/g) ?? []).length > 15)
|
|
830
|
+
scores.comparison += 3;
|
|
831
|
+
// Summary signals
|
|
832
|
+
for (const w of ["summary", "overview", "timeline", "history", "evolution",
|
|
833
|
+
"演进", "总结", "概述", "版本", "时间线", "回顾"]) {
|
|
834
|
+
if (combined.includes(w))
|
|
835
|
+
scores.summary += 2;
|
|
836
|
+
}
|
|
837
|
+
// How-to signals
|
|
838
|
+
for (const w of ["step", "guide", "tutorial", "how to", "procedure",
|
|
839
|
+
"install", "setup", "步骤", "指南", "教程", "安装"]) {
|
|
840
|
+
if (combined.includes(w))
|
|
841
|
+
scores["how-to"] += 2;
|
|
842
|
+
}
|
|
843
|
+
// Synthesis signals
|
|
844
|
+
for (const w of ["synthesis", "derived from", "combining", "integrat",
|
|
845
|
+
"综合", "提炼", "整合"]) {
|
|
846
|
+
if (combined.includes(w))
|
|
847
|
+
scores.synthesis += 2;
|
|
848
|
+
}
|
|
849
|
+
// Pick the highest-scoring type
|
|
850
|
+
let bestType = "note";
|
|
851
|
+
let bestScore = 0;
|
|
852
|
+
for (const [type, score] of Object.entries(scores)) {
|
|
853
|
+
if (score > bestScore) {
|
|
854
|
+
bestScore = score;
|
|
855
|
+
bestType = type;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const confidence = bestScore > 0 ? Math.min(bestScore / 10, 1.0) : 0.3;
|
|
859
|
+
const tags = this.extractTags(combined);
|
|
860
|
+
return { type: bestType, tags, confidence };
|
|
861
|
+
}
|
|
862
|
+
/** Auto-classify and inject type/tags into content if missing.
|
|
863
|
+
* Returns the enriched content string. */
|
|
864
|
+
autoClassifyContent(content) {
|
|
865
|
+
const parsed = matter(content);
|
|
866
|
+
// Only auto-classify if type is missing or is generic "note"
|
|
867
|
+
if (parsed.data.type && parsed.data.type !== "note")
|
|
868
|
+
return content;
|
|
869
|
+
const classification = this.classify(content);
|
|
870
|
+
if (!parsed.data.type || parsed.data.type === "note") {
|
|
871
|
+
parsed.data.type = classification.type;
|
|
872
|
+
}
|
|
873
|
+
// Merge tags: keep existing + add new suggestions (deduplicated)
|
|
874
|
+
const existingTags = Array.isArray(parsed.data.tags) ? parsed.data.tags.map(String) : [];
|
|
875
|
+
const merged = [...new Set([...existingTags, ...classification.tags])];
|
|
876
|
+
if (merged.length > 0)
|
|
877
|
+
parsed.data.tags = merged;
|
|
878
|
+
return matter.stringify(parsed.content, parsed.data);
|
|
879
|
+
}
|
|
880
|
+
/** Extract relevant tags from text using keyword matching. */
|
|
881
|
+
extractTags(text) {
|
|
882
|
+
const knownTags = {
|
|
883
|
+
"yolo": "yolo", "object detection": "object-detection", "目标检测": "object-detection",
|
|
884
|
+
"computer vision": "computer-vision", "计算机视觉": "computer-vision",
|
|
885
|
+
"deep learning": "deep-learning", "深度学习": "deep-learning",
|
|
886
|
+
"machine learning": "machine-learning", "机器学习": "machine-learning",
|
|
887
|
+
"transformer": "transformer", "attention": "attention-mechanism",
|
|
888
|
+
"cnn": "cnn", "卷积": "cnn", "real-time": "real-time", "实时": "real-time",
|
|
889
|
+
"python": "python", "pytorch": "pytorch", "tensorflow": "tensorflow",
|
|
890
|
+
"arxiv": "academic", "论文": "academic", "paper": "academic",
|
|
891
|
+
"benchmark": "benchmark", "基准": "benchmark",
|
|
892
|
+
"neural network": "neural-network", "神经网络": "neural-network",
|
|
893
|
+
"nlp": "nlp", "自然语言": "nlp", "language model": "llm", "大模型": "llm",
|
|
894
|
+
"gan": "gan", "diffusion": "diffusion", "stable diffusion": "stable-diffusion",
|
|
895
|
+
"reinforcement learning": "reinforcement-learning", "强化学习": "reinforcement-learning",
|
|
896
|
+
"autonomous driving": "autonomous-driving", "自动驾驶": "autonomous-driving",
|
|
897
|
+
"segmentation": "segmentation", "分割": "segmentation",
|
|
898
|
+
"detection": "detection", "检测": "detection",
|
|
899
|
+
"classification": "classification", "分类": "classification",
|
|
900
|
+
"training": "training", "inference": "inference", "推理": "inference",
|
|
901
|
+
"edge deploy": "edge-deployment", "边缘部署": "edge-deployment",
|
|
902
|
+
"anchor": "anchor", "backbone": "backbone", "fpn": "feature-pyramid",
|
|
903
|
+
"docker": "docker", "kubernetes": "kubernetes",
|
|
904
|
+
"api": "api", "rest": "rest-api", "mcp": "mcp",
|
|
905
|
+
};
|
|
906
|
+
const tags = new Set();
|
|
907
|
+
const lower = text.toLowerCase();
|
|
908
|
+
for (const [keyword, tag] of Object.entries(knownTags)) {
|
|
909
|
+
if (lower.includes(keyword))
|
|
910
|
+
tags.add(tag);
|
|
911
|
+
}
|
|
912
|
+
return [...tags];
|
|
913
|
+
}
|
|
914
|
+
// ── Schemas ───────────────────────────────────────────────────
|
|
915
|
+
/** List available entity type schemas. */
|
|
916
|
+
schemas() {
|
|
917
|
+
const dir = this.config.schemasDir;
|
|
918
|
+
if (!existsSync(dir))
|
|
919
|
+
return [];
|
|
920
|
+
const result = [];
|
|
921
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith(".md"))) {
|
|
922
|
+
const content = readFileSync(join(dir, file), "utf-8");
|
|
923
|
+
const parsed = matter(content);
|
|
924
|
+
result.push({
|
|
925
|
+
name: basename(file, ".md"),
|
|
926
|
+
description: parsed.data.description ?? "",
|
|
927
|
+
template: content,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return result;
|
|
931
|
+
}
|
|
932
|
+
// ── Log ───────────────────────────────────────────────────────
|
|
933
|
+
/** Get operation log entries. */
|
|
934
|
+
getLog(limit = 20) {
|
|
935
|
+
const logPath = join(this.config.wikiDir, "log.md");
|
|
936
|
+
if (!existsSync(logPath))
|
|
937
|
+
return [];
|
|
938
|
+
const content = readFileSync(logPath, "utf-8");
|
|
939
|
+
const lines = content.split("\n").filter((l) => l.startsWith("|") && !l.startsWith("| Time") && !l.startsWith("|---"));
|
|
940
|
+
const entries = [];
|
|
941
|
+
for (const line of lines) {
|
|
942
|
+
const cols = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
943
|
+
if (cols.length >= 4) {
|
|
944
|
+
entries.push({ time: cols[0], operation: cols[1], page: cols[2], summary: cols[3] });
|
|
945
|
+
}
|
|
946
|
+
else if (cols.length >= 3) {
|
|
947
|
+
entries.push({ time: cols[0], operation: cols[1], page: "—", summary: cols[2] });
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return entries.slice(-limit);
|
|
951
|
+
}
|
|
952
|
+
// ── Index rebuild ─────────────────────────────────────────────
|
|
953
|
+
/** Rebuild index.md from all pages. Groups by type with page counts. */
|
|
954
|
+
rebuildIndex() {
|
|
955
|
+
const pages = this.listAllPages().filter((p) => !SYSTEM_PAGES.has(p));
|
|
956
|
+
const categories = {};
|
|
957
|
+
let rawCount = 0;
|
|
958
|
+
try {
|
|
959
|
+
rawCount = this.rawList().length;
|
|
960
|
+
}
|
|
961
|
+
catch { /* no raw dir */ }
|
|
962
|
+
for (const pagePath of pages) {
|
|
963
|
+
const page = this.read(pagePath);
|
|
964
|
+
if (!page)
|
|
965
|
+
continue;
|
|
966
|
+
const type = page.type ?? "uncategorized";
|
|
967
|
+
if (!categories[type])
|
|
968
|
+
categories[type] = [];
|
|
969
|
+
const slug = basename(pagePath, extname(pagePath));
|
|
970
|
+
const updated = page.updated ? ` _(${page.updated.slice(0, 10)})_` : "";
|
|
971
|
+
categories[type].push(`- [[${slug}]] — ${page.title}${updated}`);
|
|
972
|
+
}
|
|
973
|
+
const now = new Date().toISOString();
|
|
974
|
+
let lines = [
|
|
975
|
+
"---",
|
|
976
|
+
"title: Knowledge Base Index",
|
|
977
|
+
"type: index",
|
|
978
|
+
`created: "${this.read("index.md")?.created ?? now}"`,
|
|
979
|
+
`updated: "${now}"`,
|
|
980
|
+
"---",
|
|
981
|
+
"",
|
|
982
|
+
"# Knowledge Base Index",
|
|
983
|
+
"",
|
|
984
|
+
`**${pages.length} pages** across **${Object.keys(categories).length} categories** | **${rawCount} raw sources**`,
|
|
985
|
+
"",
|
|
986
|
+
];
|
|
987
|
+
const sortedTypes = Object.keys(categories).sort();
|
|
988
|
+
if (sortedTypes.length > 0) {
|
|
989
|
+
for (const type of sortedTypes) {
|
|
990
|
+
const label = type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
991
|
+
lines.push(`## ${label} (${categories[type].length})`);
|
|
992
|
+
lines.push("");
|
|
993
|
+
lines.push(...categories[type]);
|
|
994
|
+
lines.push("");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
lines.push("_No pages yet._");
|
|
999
|
+
lines.push("");
|
|
1000
|
+
}
|
|
1001
|
+
lines.push("---", "", `_Last rebuilt: ${now.replace("T", " ").slice(0, 16)} UTC_`, "");
|
|
1002
|
+
writeFileSync(join(this.config.wikiDir, "index.md"), lines.join("\n"));
|
|
1003
|
+
this.log("rebuild-index", "index.md", `Rebuilt index with ${pages.length} pages`);
|
|
1004
|
+
}
|
|
1005
|
+
// ── Timeline ──────────────────────────────────────────────────
|
|
1006
|
+
/** Rebuild timeline.md — chronological view of all knowledge. */
|
|
1007
|
+
rebuildTimeline() {
|
|
1008
|
+
const pages = this.listAllPages().filter((p) => !SYSTEM_PAGES.has(p));
|
|
1009
|
+
const entries = [];
|
|
1010
|
+
for (const pagePath of pages) {
|
|
1011
|
+
const page = this.read(pagePath);
|
|
1012
|
+
if (!page)
|
|
1013
|
+
continue;
|
|
1014
|
+
const date = page.created ?? page.updated ?? "unknown";
|
|
1015
|
+
entries.push({
|
|
1016
|
+
date: date.slice(0, 10),
|
|
1017
|
+
page: basename(pagePath, extname(pagePath)),
|
|
1018
|
+
title: page.title,
|
|
1019
|
+
type: page.type ?? "note",
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
entries.sort((a, b) => b.date.localeCompare(a.date));
|
|
1023
|
+
const now = new Date().toISOString();
|
|
1024
|
+
let lines = [
|
|
1025
|
+
"---",
|
|
1026
|
+
"title: Knowledge Timeline",
|
|
1027
|
+
"type: timeline",
|
|
1028
|
+
`updated: "${now}"`,
|
|
1029
|
+
"---",
|
|
1030
|
+
"",
|
|
1031
|
+
"# Knowledge Timeline",
|
|
1032
|
+
"",
|
|
1033
|
+
`_${entries.length} entries — last rebuilt: ${now.replace("T", " ").slice(0, 16)} UTC_`,
|
|
1034
|
+
"",
|
|
1035
|
+
];
|
|
1036
|
+
// Group by date
|
|
1037
|
+
let currentDate = "";
|
|
1038
|
+
for (const e of entries) {
|
|
1039
|
+
if (e.date !== currentDate) {
|
|
1040
|
+
currentDate = e.date;
|
|
1041
|
+
lines.push(`## ${currentDate}`, "");
|
|
1042
|
+
}
|
|
1043
|
+
lines.push(`- **[${e.type}]** [[${e.page}]] — ${e.title}`);
|
|
1044
|
+
}
|
|
1045
|
+
lines.push("");
|
|
1046
|
+
writeFileSync(join(this.config.wikiDir, "timeline.md"), lines.join("\n"));
|
|
1047
|
+
this.log("rebuild-timeline", "timeline.md", `Rebuilt timeline with ${entries.length} entries`);
|
|
1048
|
+
}
|
|
1049
|
+
// ── Internal helpers ──────────────────────────────────────────
|
|
1050
|
+
listAllPages() {
|
|
1051
|
+
const dir = this.config.wikiDir;
|
|
1052
|
+
if (!existsSync(dir))
|
|
1053
|
+
return [];
|
|
1054
|
+
return listMdFiles(dir, dir);
|
|
1055
|
+
}
|
|
1056
|
+
parsePage(pagePath, raw) {
|
|
1057
|
+
const parsed = matter(raw);
|
|
1058
|
+
const fm = parsed.data;
|
|
1059
|
+
const body = parsed.content.trim();
|
|
1060
|
+
const linkMatches = body.matchAll(/\[\[([^\]]+)\]\]/g);
|
|
1061
|
+
const links = [...linkMatches].map((m) => m[1]);
|
|
1062
|
+
return {
|
|
1063
|
+
path: pagePath,
|
|
1064
|
+
title: fm.title ?? basename(pagePath, extname(pagePath)),
|
|
1065
|
+
type: fm.type,
|
|
1066
|
+
tags: Array.isArray(fm.tags) ? fm.tags.map(String) : [],
|
|
1067
|
+
sources: Array.isArray(fm.sources) ? fm.sources.map(String) : [],
|
|
1068
|
+
content: body,
|
|
1069
|
+
frontmatter: fm,
|
|
1070
|
+
links,
|
|
1071
|
+
created: fm.created,
|
|
1072
|
+
updated: fm.updated,
|
|
1073
|
+
derivedFrom: Array.isArray(fm.derived_from) ? fm.derived_from.map(String) : undefined,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
log(operation, page, summary) {
|
|
1077
|
+
const logPath = join(this.config.wikiDir, "log.md");
|
|
1078
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 16) + " UTC";
|
|
1079
|
+
const entry = `| ${now} | ${operation} | ${page} | ${summary} |\n`;
|
|
1080
|
+
if (existsSync(logPath)) {
|
|
1081
|
+
const content = readFileSync(logPath, "utf-8");
|
|
1082
|
+
writeFileSync(logPath, content + entry);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const header = "---\ntitle: Operation Log\ntype: log\n---\n\n" +
|
|
1086
|
+
"# Operation Log\n\n" +
|
|
1087
|
+
"| Time | Operation | Page | Summary |\n" +
|
|
1088
|
+
"|------|-----------|------|--------|\n" +
|
|
1089
|
+
entry;
|
|
1090
|
+
writeFileSync(logPath, header);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// ── File helpers ──────────────────────────────────────────────────
|
|
1095
|
+
function listMdFiles(dir, root) {
|
|
1096
|
+
const result = [];
|
|
1097
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1098
|
+
if (entry.name.startsWith("."))
|
|
1099
|
+
continue;
|
|
1100
|
+
const full = join(dir, entry.name);
|
|
1101
|
+
if (entry.isDirectory()) {
|
|
1102
|
+
result.push(...listMdFiles(full, root));
|
|
1103
|
+
}
|
|
1104
|
+
else if (entry.name.endsWith(".md")) {
|
|
1105
|
+
result.push(relative(root, full));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return result.sort();
|
|
1109
|
+
}
|
|
1110
|
+
function listAllFiles(dir, root) {
|
|
1111
|
+
const result = [];
|
|
1112
|
+
if (!existsSync(dir))
|
|
1113
|
+
return result;
|
|
1114
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1115
|
+
if (entry.name.startsWith("."))
|
|
1116
|
+
continue;
|
|
1117
|
+
const full = join(dir, entry.name);
|
|
1118
|
+
if (entry.isDirectory()) {
|
|
1119
|
+
result.push(...listAllFiles(full, root));
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
result.push(relative(root, full));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return result.sort();
|
|
1126
|
+
}
|
|
1127
|
+
function guessMime(filename) {
|
|
1128
|
+
const ext = extname(filename).toLowerCase();
|
|
1129
|
+
const map = {
|
|
1130
|
+
".md": "text/markdown",
|
|
1131
|
+
".txt": "text/plain",
|
|
1132
|
+
".pdf": "application/pdf",
|
|
1133
|
+
".html": "text/html",
|
|
1134
|
+
".json": "application/json",
|
|
1135
|
+
".yaml": "text/yaml",
|
|
1136
|
+
".yml": "text/yaml",
|
|
1137
|
+
".csv": "text/csv",
|
|
1138
|
+
".xml": "text/xml",
|
|
1139
|
+
".png": "image/png",
|
|
1140
|
+
".jpg": "image/jpeg",
|
|
1141
|
+
".jpeg": "image/jpeg",
|
|
1142
|
+
".gif": "image/gif",
|
|
1143
|
+
".svg": "image/svg+xml",
|
|
1144
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1145
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1146
|
+
};
|
|
1147
|
+
return map[ext] ?? "application/octet-stream";
|
|
1148
|
+
}
|
|
1149
|
+
function formatBytes(bytes) {
|
|
1150
|
+
if (bytes < 1024)
|
|
1151
|
+
return `${bytes} B`;
|
|
1152
|
+
if (bytes < 1024 * 1024)
|
|
1153
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1154
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1155
|
+
}
|
|
1156
|
+
function writeDefaultSchemas(dir) {
|
|
1157
|
+
const schemas = {
|
|
1158
|
+
"person.md": `---
|
|
1159
|
+
template: person
|
|
1160
|
+
description: Profile of a person
|
|
1161
|
+
---
|
|
1162
|
+
|
|
1163
|
+
# {{title}}
|
|
1164
|
+
|
|
1165
|
+
**Role:** [TODO]
|
|
1166
|
+
**Affiliations:** [TODO]
|
|
1167
|
+
|
|
1168
|
+
## Key Contributions
|
|
1169
|
+
|
|
1170
|
+
- [TODO]
|
|
1171
|
+
|
|
1172
|
+
## Relationships
|
|
1173
|
+
|
|
1174
|
+
- [TODO]
|
|
1175
|
+
|
|
1176
|
+
## Sources
|
|
1177
|
+
|
|
1178
|
+
- [TODO]
|
|
1179
|
+
`,
|
|
1180
|
+
"concept.md": `---
|
|
1181
|
+
template: concept
|
|
1182
|
+
description: An idea, theory, or abstract concept
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
# {{title}}
|
|
1186
|
+
|
|
1187
|
+
## Definition
|
|
1188
|
+
|
|
1189
|
+
[TODO]
|
|
1190
|
+
|
|
1191
|
+
## Properties
|
|
1192
|
+
|
|
1193
|
+
- [TODO]
|
|
1194
|
+
|
|
1195
|
+
## Relationships
|
|
1196
|
+
|
|
1197
|
+
- [TODO]
|
|
1198
|
+
|
|
1199
|
+
## Examples
|
|
1200
|
+
|
|
1201
|
+
- [TODO]
|
|
1202
|
+
|
|
1203
|
+
## Sources
|
|
1204
|
+
|
|
1205
|
+
- [TODO]
|
|
1206
|
+
`,
|
|
1207
|
+
"event.md": `---
|
|
1208
|
+
template: event
|
|
1209
|
+
description: Something that happened at a specific time
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
# {{title}}
|
|
1213
|
+
|
|
1214
|
+
**Date:** [TODO]
|
|
1215
|
+
**Location:** [TODO]
|
|
1216
|
+
|
|
1217
|
+
## Participants
|
|
1218
|
+
|
|
1219
|
+
- [TODO]
|
|
1220
|
+
|
|
1221
|
+
## What Happened
|
|
1222
|
+
|
|
1223
|
+
[TODO]
|
|
1224
|
+
|
|
1225
|
+
## Outcomes & Impact
|
|
1226
|
+
|
|
1227
|
+
[TODO]
|
|
1228
|
+
|
|
1229
|
+
## Sources
|
|
1230
|
+
|
|
1231
|
+
- [TODO]
|
|
1232
|
+
`,
|
|
1233
|
+
"artifact.md": `---
|
|
1234
|
+
template: artifact
|
|
1235
|
+
description: A tool, paper, product, or created thing
|
|
1236
|
+
---
|
|
1237
|
+
|
|
1238
|
+
# {{title}}
|
|
1239
|
+
|
|
1240
|
+
**Type:** [TODO]
|
|
1241
|
+
**Creator:** [TODO]
|
|
1242
|
+
**Date:** [TODO]
|
|
1243
|
+
|
|
1244
|
+
## Purpose
|
|
1245
|
+
|
|
1246
|
+
[TODO]
|
|
1247
|
+
|
|
1248
|
+
## Key Features
|
|
1249
|
+
|
|
1250
|
+
- [TODO]
|
|
1251
|
+
|
|
1252
|
+
## Sources
|
|
1253
|
+
|
|
1254
|
+
- [TODO]
|
|
1255
|
+
`,
|
|
1256
|
+
"comparison.md": `---
|
|
1257
|
+
template: comparison
|
|
1258
|
+
description: Side-by-side analysis of two or more items
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
# {{title}}
|
|
1262
|
+
|
|
1263
|
+
## Items Compared
|
|
1264
|
+
|
|
1265
|
+
| Dimension | Item A | Item B |
|
|
1266
|
+
|-----------|--------|--------|
|
|
1267
|
+
| [TODO] | [TODO] | [TODO] |
|
|
1268
|
+
|
|
1269
|
+
## Analysis
|
|
1270
|
+
|
|
1271
|
+
[TODO]
|
|
1272
|
+
|
|
1273
|
+
## Verdict
|
|
1274
|
+
|
|
1275
|
+
[TODO]
|
|
1276
|
+
|
|
1277
|
+
## Sources
|
|
1278
|
+
|
|
1279
|
+
- [TODO]
|
|
1280
|
+
`,
|
|
1281
|
+
"summary.md": `---
|
|
1282
|
+
template: summary
|
|
1283
|
+
description: Summary of a source document
|
|
1284
|
+
---
|
|
1285
|
+
|
|
1286
|
+
# {{title}}
|
|
1287
|
+
|
|
1288
|
+
**Source:** [TODO]
|
|
1289
|
+
**Date:** [TODO]
|
|
1290
|
+
|
|
1291
|
+
## Key Points
|
|
1292
|
+
|
|
1293
|
+
1. [TODO]
|
|
1294
|
+
|
|
1295
|
+
## Detailed Summary
|
|
1296
|
+
|
|
1297
|
+
[TODO]
|
|
1298
|
+
|
|
1299
|
+
## Sources
|
|
1300
|
+
|
|
1301
|
+
- [TODO]
|
|
1302
|
+
`,
|
|
1303
|
+
"how-to.md": `---
|
|
1304
|
+
template: how-to
|
|
1305
|
+
description: A procedure or guide
|
|
1306
|
+
---
|
|
1307
|
+
|
|
1308
|
+
# {{title}}
|
|
1309
|
+
|
|
1310
|
+
## Goal
|
|
1311
|
+
|
|
1312
|
+
[TODO]
|
|
1313
|
+
|
|
1314
|
+
## Prerequisites
|
|
1315
|
+
|
|
1316
|
+
- [TODO]
|
|
1317
|
+
|
|
1318
|
+
## Steps
|
|
1319
|
+
|
|
1320
|
+
1. [TODO]
|
|
1321
|
+
|
|
1322
|
+
## Pitfalls
|
|
1323
|
+
|
|
1324
|
+
- [TODO]
|
|
1325
|
+
|
|
1326
|
+
## Sources
|
|
1327
|
+
|
|
1328
|
+
- [TODO]
|
|
1329
|
+
`,
|
|
1330
|
+
"note.md": `---
|
|
1331
|
+
template: note
|
|
1332
|
+
description: Freeform knowledge — anything that does not fit other templates
|
|
1333
|
+
---
|
|
1334
|
+
|
|
1335
|
+
# {{title}}
|
|
1336
|
+
|
|
1337
|
+
{{content}}
|
|
1338
|
+
|
|
1339
|
+
## Sources
|
|
1340
|
+
|
|
1341
|
+
- [TODO]
|
|
1342
|
+
`,
|
|
1343
|
+
"synthesis.md": `---
|
|
1344
|
+
template: synthesis
|
|
1345
|
+
description: Distilled knowledge combining insights from multiple pages
|
|
1346
|
+
---
|
|
1347
|
+
|
|
1348
|
+
# {{title}}
|
|
1349
|
+
|
|
1350
|
+
**Derived from:** [TODO: list source pages with [[links]]]
|
|
1351
|
+
**Date:** [TODO]
|
|
1352
|
+
|
|
1353
|
+
## Key Insights
|
|
1354
|
+
|
|
1355
|
+
[TODO: What emerges from combining these sources?]
|
|
1356
|
+
|
|
1357
|
+
## Connections
|
|
1358
|
+
|
|
1359
|
+
[TODO: How do these sources relate to each other?]
|
|
1360
|
+
|
|
1361
|
+
## Contradictions & Open Questions
|
|
1362
|
+
|
|
1363
|
+
[TODO: Where do sources disagree? What remains unclear?]
|
|
1364
|
+
|
|
1365
|
+
## Synthesis
|
|
1366
|
+
|
|
1367
|
+
[TODO: The integrated understanding]
|
|
1368
|
+
|
|
1369
|
+
## Sources
|
|
1370
|
+
|
|
1371
|
+
- [TODO]
|
|
1372
|
+
`,
|
|
1373
|
+
};
|
|
1374
|
+
for (const [filename, content] of Object.entries(schemas)) {
|
|
1375
|
+
writeFileSync(join(dir, filename), content);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
//# sourceMappingURL=wiki.js.map
|