@zosmaai/pi-llm-wiki 0.10.7 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.de.md +35 -4
- package/README.es.md +260 -170
- package/README.fr.md +35 -4
- package/README.hi.md +35 -4
- package/README.ja.md +35 -4
- package/README.ko.md +35 -4
- package/README.md +38 -3
- package/README.pt.md +35 -4
- package/README.ru.md +35 -4
- package/README.zh.md +260 -170
- package/assets/demo.gif +0 -0
- package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
- package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
- package/dist/extensions/llm-wiki/lib/inject.js +65 -0
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
- package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
- package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
- package/dist/extensions/llm-wiki/lib/observation.js +283 -0
- package/dist/extensions/llm-wiki/lib/recall.js +875 -0
- package/dist/extensions/llm-wiki/lib/retro.js +158 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
- package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
- package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
- package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
- package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
- package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
- package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
- package/dist/extensions/llm-wiki/lib/utils.js +347 -0
- package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
- package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
- package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
- package/dist/mcp/exec.js +121 -0
- package/dist/mcp/index.js +229 -0
- package/dist/mcp/operations.js +130 -0
- package/dist/package.json +1 -0
- package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
- package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
- package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
- package/extensions/llm-wiki/index.ts +22 -36
- package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
- package/extensions/llm-wiki/lib/embeddings.ts +9 -3
- package/extensions/llm-wiki/lib/guardrails.ts +174 -29
- package/extensions/llm-wiki/lib/indexing.ts +2 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
- package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
- package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
- package/extensions/llm-wiki/lib/metadata.ts +531 -116
- package/extensions/llm-wiki/lib/observation.ts +37 -43
- package/extensions/llm-wiki/lib/recall.ts +61 -33
- package/extensions/llm-wiki/lib/retro.ts +65 -41
- package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
- package/extensions/llm-wiki/lib/source-packet.ts +44 -31
- package/extensions/llm-wiki/lib/tools.ts +406 -348
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +121 -130
- package/extensions/llm-wiki/lib/vault-format.ts +363 -0
- package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
- package/mcp/exec.ts +122 -0
- package/mcp/index.ts +60 -250
- package/mcp/operations.ts +176 -0
- package/package.json +8 -2
- package/scripts/migrate-llm-wiki.js +801 -0
- package/skills/llm-wiki/SKILL.md +8 -6
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { type Document, isAlias, isMap, isScalar, isSeq, parseAllDocuments, stringify } from "yaml";
|
|
3
|
+
|
|
4
|
+
export const FRONTMATTER_MAX_BYTES = 128 * 1024;
|
|
5
|
+
export const FRONTMATTER_MAX_DEPTH = 32;
|
|
6
|
+
|
|
7
|
+
export type DiagnosticSeverity = "warning" | "error";
|
|
8
|
+
export type DiagnosticCode =
|
|
9
|
+
| "config_invalid_knowledge_format"
|
|
10
|
+
| "frontmatter_missing"
|
|
11
|
+
| "frontmatter_parse_error"
|
|
12
|
+
| "frontmatter_duplicate_key"
|
|
13
|
+
| "frontmatter_alias_forbidden"
|
|
14
|
+
| "frontmatter_custom_tag_forbidden"
|
|
15
|
+
| "frontmatter_multiple_documents"
|
|
16
|
+
| "frontmatter_limit_bytes"
|
|
17
|
+
| "frontmatter_limit_depth"
|
|
18
|
+
| "concept_missing_type"
|
|
19
|
+
| "concept_identity_collision"
|
|
20
|
+
| "concept_reserved_name"
|
|
21
|
+
| "okf_version_mismatch"
|
|
22
|
+
| "link_path_escape"
|
|
23
|
+
| "link_unresolved"
|
|
24
|
+
| "event_invalid_json"
|
|
25
|
+
| "event_invalid_timestamp"
|
|
26
|
+
| "event_missing_kind";
|
|
27
|
+
|
|
28
|
+
export interface KnowledgeDiagnostic {
|
|
29
|
+
severity: DiagnosticSeverity;
|
|
30
|
+
code: DiagnosticCode;
|
|
31
|
+
path: string;
|
|
32
|
+
message: string;
|
|
33
|
+
line?: number;
|
|
34
|
+
column?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type KnowledgeValue =
|
|
38
|
+
| null
|
|
39
|
+
| boolean
|
|
40
|
+
| number
|
|
41
|
+
| string
|
|
42
|
+
| KnowledgeValue[]
|
|
43
|
+
| { [key: string]: KnowledgeValue };
|
|
44
|
+
|
|
45
|
+
export type KnowledgeCreationFields = {
|
|
46
|
+
type: string;
|
|
47
|
+
sources?: never;
|
|
48
|
+
} & Omit<Record<string, KnowledgeValue>, "type" | "sources">;
|
|
49
|
+
|
|
50
|
+
export type KnowledgeSources =
|
|
51
|
+
| { kind: "absent" }
|
|
52
|
+
| { kind: "canonical"; value: Array<Record<string, KnowledgeValue>> }
|
|
53
|
+
| { kind: "legacy-scalar"; value: string }
|
|
54
|
+
| { kind: "legacy-list"; value: string[] }
|
|
55
|
+
| { kind: "unknown-shape"; value: KnowledgeValue };
|
|
56
|
+
|
|
57
|
+
export interface KnowledgeFrontmatter {
|
|
58
|
+
type: string;
|
|
59
|
+
title?: KnowledgeValue;
|
|
60
|
+
description?: KnowledgeValue;
|
|
61
|
+
resource?: KnowledgeValue;
|
|
62
|
+
tags?: KnowledgeValue;
|
|
63
|
+
generated?: KnowledgeValue;
|
|
64
|
+
verified?: KnowledgeValue;
|
|
65
|
+
status?: KnowledgeValue;
|
|
66
|
+
stale_after?: KnowledgeValue;
|
|
67
|
+
category?: KnowledgeValue;
|
|
68
|
+
domain?: KnowledgeValue;
|
|
69
|
+
aliases?: KnowledgeValue;
|
|
70
|
+
recall_triggers?: KnowledgeValue;
|
|
71
|
+
created?: KnowledgeValue;
|
|
72
|
+
updated?: KnowledgeValue;
|
|
73
|
+
summary?: KnowledgeValue;
|
|
74
|
+
raw_path?: KnowledgeValue;
|
|
75
|
+
source_id?: KnowledgeValue;
|
|
76
|
+
[key: string]: KnowledgeValue | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface KnowledgeDocument {
|
|
80
|
+
id: string;
|
|
81
|
+
path: string;
|
|
82
|
+
frontmatter: KnowledgeFrontmatter;
|
|
83
|
+
sources: KnowledgeSources;
|
|
84
|
+
extensions: Record<string, KnowledgeValue>;
|
|
85
|
+
body: string;
|
|
86
|
+
compatibility: {
|
|
87
|
+
legacyFields: string[];
|
|
88
|
+
hasLegacyWikilinks: boolean;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ParseKnowledgeResult =
|
|
93
|
+
| { ok: true; document: KnowledgeDocument; diagnostics: KnowledgeDiagnostic[] }
|
|
94
|
+
| { ok: false; diagnostics: KnowledgeDiagnostic[] };
|
|
95
|
+
|
|
96
|
+
export type ParseFrontmatterResult =
|
|
97
|
+
| {
|
|
98
|
+
ok: true;
|
|
99
|
+
mapping: Record<string, KnowledgeValue>;
|
|
100
|
+
body: string;
|
|
101
|
+
diagnostics: KnowledgeDiagnostic[];
|
|
102
|
+
}
|
|
103
|
+
| { ok: false; diagnostics: KnowledgeDiagnostic[] };
|
|
104
|
+
|
|
105
|
+
export interface KnowledgePatchFields {
|
|
106
|
+
type?: string;
|
|
107
|
+
title?: KnowledgeValue;
|
|
108
|
+
description?: KnowledgeValue;
|
|
109
|
+
resource?: KnowledgeValue;
|
|
110
|
+
tags?: KnowledgeValue;
|
|
111
|
+
generated?: KnowledgeValue;
|
|
112
|
+
verified?: KnowledgeValue;
|
|
113
|
+
status?: KnowledgeValue;
|
|
114
|
+
stale_after?: KnowledgeValue;
|
|
115
|
+
category?: KnowledgeValue;
|
|
116
|
+
domain?: KnowledgeValue;
|
|
117
|
+
aliases?: KnowledgeValue;
|
|
118
|
+
recall_triggers?: KnowledgeValue;
|
|
119
|
+
created?: KnowledgeValue;
|
|
120
|
+
updated?: KnowledgeValue;
|
|
121
|
+
summary?: KnowledgeValue;
|
|
122
|
+
raw_path?: KnowledgeValue;
|
|
123
|
+
source_id?: KnowledgeValue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface KnowledgePatch {
|
|
127
|
+
fields?: KnowledgePatchFields;
|
|
128
|
+
body?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const STANDARD_FIELDS = new Set([
|
|
132
|
+
"type",
|
|
133
|
+
"title",
|
|
134
|
+
"description",
|
|
135
|
+
"resource",
|
|
136
|
+
"tags",
|
|
137
|
+
"sources",
|
|
138
|
+
"generated",
|
|
139
|
+
"verified",
|
|
140
|
+
"status",
|
|
141
|
+
"stale_after",
|
|
142
|
+
"category",
|
|
143
|
+
"domain",
|
|
144
|
+
"aliases",
|
|
145
|
+
"recall_triggers",
|
|
146
|
+
"created",
|
|
147
|
+
"updated",
|
|
148
|
+
"summary",
|
|
149
|
+
"raw_path",
|
|
150
|
+
"source_id",
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const LEGACY_COMPAT_FIELDS = new Set(["created", "updated", "summary", "raw_path", "source_id"]);
|
|
154
|
+
|
|
155
|
+
function diag(
|
|
156
|
+
severity: DiagnosticSeverity,
|
|
157
|
+
code: DiagnosticCode,
|
|
158
|
+
path: string,
|
|
159
|
+
message: string,
|
|
160
|
+
line?: number,
|
|
161
|
+
column?: number,
|
|
162
|
+
): KnowledgeDiagnostic {
|
|
163
|
+
return { severity, code, path, message, line, column };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function classifySources(raw: KnowledgeValue | undefined): KnowledgeSources {
|
|
167
|
+
if (raw === undefined) return { kind: "absent" };
|
|
168
|
+
if (raw === null) return { kind: "unknown-shape", value: null };
|
|
169
|
+
if (typeof raw === "string") return { kind: "legacy-scalar", value: raw };
|
|
170
|
+
if (Array.isArray(raw)) {
|
|
171
|
+
if (raw.every((v) => typeof v === "string")) {
|
|
172
|
+
return { kind: "legacy-list", value: raw };
|
|
173
|
+
}
|
|
174
|
+
if (raw.every((v) => v && typeof v === "object" && !Array.isArray(v))) {
|
|
175
|
+
return { kind: "canonical", value: raw as Array<Record<string, KnowledgeValue>> };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { kind: "unknown-shape", value: raw };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hasAlias(node: unknown): boolean {
|
|
182
|
+
if (isAlias(node)) return true;
|
|
183
|
+
if (isMap(node)) {
|
|
184
|
+
return node.items.some((item) => hasAlias(item.key) || hasAlias(item.value));
|
|
185
|
+
}
|
|
186
|
+
if (isSeq(node)) {
|
|
187
|
+
return node.items.some((item) => hasAlias(item));
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function hasCustomTag(node: unknown): boolean {
|
|
193
|
+
if (node && typeof node === "object" && "tag" in node && typeof node.tag === "string") {
|
|
194
|
+
if (node.tag.startsWith("!")) return true;
|
|
195
|
+
}
|
|
196
|
+
if (isMap(node)) {
|
|
197
|
+
return node.items.some((item) => hasCustomTag(item.key) || hasCustomTag(item.value));
|
|
198
|
+
}
|
|
199
|
+
if (isSeq(node)) {
|
|
200
|
+
return node.items.some((item) => hasCustomTag(item));
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function maxDepth(node: unknown, current: number): number {
|
|
206
|
+
if (isMap(node)) {
|
|
207
|
+
let deepest = current + 1;
|
|
208
|
+
for (const item of node.items) {
|
|
209
|
+
deepest = Math.max(
|
|
210
|
+
deepest,
|
|
211
|
+
maxDepth(item.key, current + 1),
|
|
212
|
+
maxDepth(item.value, current + 1),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return deepest;
|
|
216
|
+
}
|
|
217
|
+
if (isSeq(node)) {
|
|
218
|
+
let deepest = current + 1;
|
|
219
|
+
for (const item of node.items) {
|
|
220
|
+
deepest = Math.max(deepest, maxDepth(item, current + 1));
|
|
221
|
+
}
|
|
222
|
+
return deepest;
|
|
223
|
+
}
|
|
224
|
+
return current;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function toKnowledgeValue(node: unknown): KnowledgeValue {
|
|
228
|
+
if (node === null) return null;
|
|
229
|
+
if (isScalar(node)) {
|
|
230
|
+
const v = node.value;
|
|
231
|
+
if (v === null) return null;
|
|
232
|
+
if (typeof v === "boolean" || typeof v === "number" || typeof v === "string") return v;
|
|
233
|
+
return String(v);
|
|
234
|
+
}
|
|
235
|
+
if (isSeq(node)) {
|
|
236
|
+
return node.items.map(toKnowledgeValue);
|
|
237
|
+
}
|
|
238
|
+
if (isMap(node)) {
|
|
239
|
+
const obj: Record<string, KnowledgeValue> = Object.create(null);
|
|
240
|
+
for (const item of node.items) {
|
|
241
|
+
const key = String(isScalar(item.key) ? item.key.value : item.key);
|
|
242
|
+
obj[key] = toKnowledgeValue(item.value);
|
|
243
|
+
}
|
|
244
|
+
return obj;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function detectLegacyWikilinks(body: string): boolean {
|
|
250
|
+
return /\[\[[^\]]+\]\]/.test(body);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseFrontmatterBlock(
|
|
254
|
+
content: string,
|
|
255
|
+
path: string,
|
|
256
|
+
requireType: boolean,
|
|
257
|
+
): ParseFrontmatterResult | ParseKnowledgeResult {
|
|
258
|
+
const diagnostics: KnowledgeDiagnostic[] = [];
|
|
259
|
+
|
|
260
|
+
// Normalize line endings
|
|
261
|
+
const normalized = content.replace(/\r\n?/g, "\n");
|
|
262
|
+
|
|
263
|
+
// Require opening --- on line 1
|
|
264
|
+
if (!normalized.startsWith("---\n")) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
diagnostics: [
|
|
268
|
+
diag("error", "frontmatter_missing", path, "Missing frontmatter opening delimiter"),
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find closing ---
|
|
274
|
+
// Per spec: when a candidate immediately follows a YAML explicit-end line ...
|
|
275
|
+
// and another delimiter exists later, treat that candidate as an internal
|
|
276
|
+
// second-document marker and continue to the later closing fence.
|
|
277
|
+
let closingIndex = -1;
|
|
278
|
+
let i = 4; // skip opening ---\n
|
|
279
|
+
const candidates: Array<{ index: number; followsExplicitEnd: boolean }> = [];
|
|
280
|
+
let lastLineWasExplicitEnd = false;
|
|
281
|
+
while (i < normalized.length) {
|
|
282
|
+
const newlinePos = normalized.indexOf("\n", i);
|
|
283
|
+
const lineEnd = newlinePos === -1 ? normalized.length : newlinePos;
|
|
284
|
+
const line = normalized.slice(i, lineEnd);
|
|
285
|
+
if (line === "---") {
|
|
286
|
+
candidates.push({ index: i, followsExplicitEnd: lastLineWasExplicitEnd });
|
|
287
|
+
lastLineWasExplicitEnd = false;
|
|
288
|
+
} else if (line === "...") {
|
|
289
|
+
lastLineWasExplicitEnd = true;
|
|
290
|
+
} else {
|
|
291
|
+
lastLineWasExplicitEnd = false;
|
|
292
|
+
}
|
|
293
|
+
i = lineEnd + 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Find the proper closing fence
|
|
297
|
+
if (candidates.length === 1) {
|
|
298
|
+
closingIndex = candidates[0].index;
|
|
299
|
+
} else if (candidates.length > 1) {
|
|
300
|
+
// If first candidate follows explicit end and there's another later,
|
|
301
|
+
// skip the first (it's a second-document marker)
|
|
302
|
+
if (candidates[0].followsExplicitEnd && candidates.length >= 2) {
|
|
303
|
+
closingIndex = candidates[1].index;
|
|
304
|
+
} else {
|
|
305
|
+
closingIndex = candidates[0].index;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (closingIndex === -1) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
diagnostics: [
|
|
313
|
+
diag("error", "frontmatter_parse_error", path, "Missing frontmatter closing delimiter"),
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const yamlText = normalized.slice(4, closingIndex);
|
|
319
|
+
|
|
320
|
+
// Check byte limit
|
|
321
|
+
const byteLength = Buffer.byteLength(yamlText, "utf8");
|
|
322
|
+
if (byteLength > FRONTMATTER_MAX_BYTES) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
diagnostics: [
|
|
326
|
+
diag(
|
|
327
|
+
"error",
|
|
328
|
+
"frontmatter_limit_bytes",
|
|
329
|
+
path,
|
|
330
|
+
`Frontmatter exceeds ${FRONTMATTER_MAX_BYTES} bytes`,
|
|
331
|
+
),
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Parse with yaml library - use lenient mode first, then validate
|
|
337
|
+
let docs: Document[];
|
|
338
|
+
try {
|
|
339
|
+
docs = parseAllDocuments(yamlText, {
|
|
340
|
+
schema: "core",
|
|
341
|
+
merge: false,
|
|
342
|
+
uniqueKeys: true,
|
|
343
|
+
});
|
|
344
|
+
} catch (e: unknown) {
|
|
345
|
+
const err = e as Error & { pos?: number; line?: number; col?: number };
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
diagnostics: [
|
|
349
|
+
diag(
|
|
350
|
+
"error",
|
|
351
|
+
"frontmatter_parse_error",
|
|
352
|
+
path,
|
|
353
|
+
`YAML parse error: ${err.message}`,
|
|
354
|
+
err.line ?? undefined,
|
|
355
|
+
err.col ?? undefined,
|
|
356
|
+
),
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// YAML parser errors must be surfaced before conversion. `yaml` records
|
|
362
|
+
// malformed syntax and nested duplicate keys on the Document instead of
|
|
363
|
+
// throwing from parseAllDocuments.
|
|
364
|
+
const yamlErrors = docs.flatMap((document) => document.errors);
|
|
365
|
+
if (yamlErrors.length > 0) {
|
|
366
|
+
const duplicate = yamlErrors.find((error) => error.code === "DUPLICATE_KEY");
|
|
367
|
+
const error = duplicate ?? yamlErrors[0];
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
diagnostics: [
|
|
371
|
+
diag(
|
|
372
|
+
"error",
|
|
373
|
+
duplicate ? "frontmatter_duplicate_key" : "frontmatter_parse_error",
|
|
374
|
+
path,
|
|
375
|
+
`YAML parse error: ${error.message}`,
|
|
376
|
+
),
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check for multiple documents
|
|
382
|
+
if (docs.length !== 1) {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
diagnostics: [
|
|
386
|
+
diag(
|
|
387
|
+
"error",
|
|
388
|
+
"frontmatter_multiple_documents",
|
|
389
|
+
path,
|
|
390
|
+
"Multiple YAML documents in frontmatter",
|
|
391
|
+
),
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const doc = docs[0];
|
|
397
|
+
const contents = doc.contents;
|
|
398
|
+
|
|
399
|
+
// Must be a mapping
|
|
400
|
+
if (!isMap(contents)) {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
diagnostics: [
|
|
404
|
+
diag("error", "frontmatter_parse_error", path, "Frontmatter must be a YAML mapping"),
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check for aliases
|
|
410
|
+
if (hasAlias(contents)) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
diagnostics: [
|
|
414
|
+
diag("error", "frontmatter_alias_forbidden", path, "YAML aliases are not allowed"),
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check for custom tags
|
|
420
|
+
if (hasCustomTag(contents)) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
diagnostics: [
|
|
424
|
+
diag("error", "frontmatter_custom_tag_forbidden", path, "Custom YAML tags are not allowed"),
|
|
425
|
+
],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check for duplicate keys
|
|
430
|
+
const seenKeys = new Set<string>();
|
|
431
|
+
for (const item of contents.items) {
|
|
432
|
+
const key = String(isScalar(item.key) ? item.key.value : item.key);
|
|
433
|
+
if (seenKeys.has(key)) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
diagnostics: [
|
|
437
|
+
diag(
|
|
438
|
+
"error",
|
|
439
|
+
"frontmatter_duplicate_key",
|
|
440
|
+
path,
|
|
441
|
+
`Duplicate key: ${key}`,
|
|
442
|
+
undefined,
|
|
443
|
+
undefined,
|
|
444
|
+
),
|
|
445
|
+
],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
seenKeys.add(key);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check depth
|
|
452
|
+
const depth = maxDepth(contents, 0);
|
|
453
|
+
if (depth > FRONTMATTER_MAX_DEPTH) {
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
diagnostics: [
|
|
457
|
+
diag(
|
|
458
|
+
"error",
|
|
459
|
+
"frontmatter_limit_depth",
|
|
460
|
+
path,
|
|
461
|
+
`Frontmatter nesting exceeds ${FRONTMATTER_MAX_DEPTH}`,
|
|
462
|
+
),
|
|
463
|
+
],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const mapping = toKnowledgeValue(contents) as Record<string, KnowledgeValue>;
|
|
468
|
+
|
|
469
|
+
// Build body: remove closing fence and one optional blank separator line
|
|
470
|
+
let body = normalized.slice(closingIndex + 4); // skip closing ---\n
|
|
471
|
+
if (body.startsWith("\n")) {
|
|
472
|
+
// Remove exactly one leading blank separator line; additional leading blanks belong to body
|
|
473
|
+
body = body.slice(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!requireType) {
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
mapping,
|
|
480
|
+
body,
|
|
481
|
+
diagnostics,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Require type field
|
|
486
|
+
const rawType = mapping.type;
|
|
487
|
+
if (typeof rawType !== "string" || rawType.trim() === "") {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
diagnostics: [diag("error", "concept_missing_type", path, "Missing or empty type field")],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Classify sources
|
|
495
|
+
const sources = classifySources(mapping.sources);
|
|
496
|
+
|
|
497
|
+
// Split standard fields from extensions
|
|
498
|
+
const frontmatter: KnowledgeFrontmatter = { type: rawType };
|
|
499
|
+
const extensions: Record<string, KnowledgeValue> = Object.create(null);
|
|
500
|
+
const legacyFields: string[] = [];
|
|
501
|
+
|
|
502
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
503
|
+
if (key === "sources") continue; // handled separately
|
|
504
|
+
if (STANDARD_FIELDS.has(key)) {
|
|
505
|
+
(frontmatter as Record<string, unknown>)[key] = value;
|
|
506
|
+
if (LEGACY_COMPAT_FIELDS.has(key)) {
|
|
507
|
+
legacyFields.push(key);
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
extensions[key] = value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Record legacy source shape
|
|
515
|
+
if (sources.kind === "legacy-scalar" || sources.kind === "legacy-list") {
|
|
516
|
+
legacyFields.push("sources");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const hasLegacyWikilinks = detectLegacyWikilinks(body);
|
|
520
|
+
if (hasLegacyWikilinks) {
|
|
521
|
+
legacyFields.push("wikilinks");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
ok: true,
|
|
526
|
+
document: {
|
|
527
|
+
id: path.replace(/\.md$/, ""),
|
|
528
|
+
path,
|
|
529
|
+
frontmatter,
|
|
530
|
+
sources,
|
|
531
|
+
extensions,
|
|
532
|
+
body,
|
|
533
|
+
compatibility: {
|
|
534
|
+
legacyFields,
|
|
535
|
+
hasLegacyWikilinks,
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
diagnostics,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function parseMarkdownFrontmatter(content: string, path: string): ParseFrontmatterResult {
|
|
543
|
+
const result = parseFrontmatterBlock(content, path, false);
|
|
544
|
+
if ("ok" in result && result.ok && "mapping" in result) {
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
return result as ParseFrontmatterResult;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function parseKnowledgeDocument(content: string, path: string): ParseKnowledgeResult {
|
|
551
|
+
const result = parseFrontmatterBlock(content, path, true);
|
|
552
|
+
return result as ParseKnowledgeResult;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function serializeKnowledgeDocument(document: KnowledgeDocument): string {
|
|
556
|
+
// Rebuild mapping from frontmatter, sources, and extensions
|
|
557
|
+
const mapping: Record<string, KnowledgeValue> = Object.create(null);
|
|
558
|
+
|
|
559
|
+
// Standard frontmatter fields (excluding sources)
|
|
560
|
+
for (const key of STANDARD_FIELDS) {
|
|
561
|
+
if (key === "sources") continue;
|
|
562
|
+
const value = document.frontmatter[key as keyof KnowledgeFrontmatter];
|
|
563
|
+
if (value !== undefined) {
|
|
564
|
+
mapping[key] = value;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Sources
|
|
569
|
+
if (document.sources.kind !== "absent") {
|
|
570
|
+
mapping.sources = document.sources.value;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Extensions
|
|
574
|
+
for (const [key, value] of Object.entries(document.extensions)) {
|
|
575
|
+
mapping[key] = value;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const yaml = stringify(mapping, {
|
|
579
|
+
aliasDuplicateObjects: false,
|
|
580
|
+
lineWidth: 0,
|
|
581
|
+
})
|
|
582
|
+
.replace(/\r\n?/g, "\n")
|
|
583
|
+
.replace(/\n*$/, "\n");
|
|
584
|
+
|
|
585
|
+
const body = document.body.replace(/\r\n?/g, "\n").replace(/\n*$/, "");
|
|
586
|
+
return body ? `---\n${yaml}---\n\n${body}\n` : `---\n${yaml}---\n`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function createKnowledgeDocument<T extends KnowledgeCreationFields>(
|
|
590
|
+
path: string,
|
|
591
|
+
fields: T & NoInfer<KnowledgeCreationFields>,
|
|
592
|
+
body: string,
|
|
593
|
+
sources?: Array<Record<string, KnowledgeValue>>,
|
|
594
|
+
): KnowledgeDocument {
|
|
595
|
+
if (Object.hasOwn(fields, "sources")) {
|
|
596
|
+
throw new Error("Pass canonical sources as the fourth argument");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const frontmatter: KnowledgeFrontmatter = { type: fields.type };
|
|
600
|
+
const extensions: Record<string, KnowledgeValue> = Object.create(null);
|
|
601
|
+
|
|
602
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
603
|
+
if (key === "type") continue;
|
|
604
|
+
if (STANDARD_FIELDS.has(key)) {
|
|
605
|
+
(frontmatter as Record<string, unknown>)[key] = value;
|
|
606
|
+
} else {
|
|
607
|
+
extensions[key] = value;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const sourcesUnion = sources
|
|
612
|
+
? { kind: "canonical" as const, value: sources }
|
|
613
|
+
: { kind: "absent" as const };
|
|
614
|
+
|
|
615
|
+
const normalizedBody = body.replace(/\r\n?/g, "\n").replace(/\n*$/, "");
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
id: path.replace(/\.md$/, ""),
|
|
619
|
+
path,
|
|
620
|
+
frontmatter,
|
|
621
|
+
sources: sourcesUnion,
|
|
622
|
+
extensions,
|
|
623
|
+
body: normalizedBody,
|
|
624
|
+
compatibility: {
|
|
625
|
+
legacyFields: [],
|
|
626
|
+
hasLegacyWikilinks: detectLegacyWikilinks(normalizedBody),
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function patchKnowledgeDocument(
|
|
632
|
+
document: KnowledgeDocument,
|
|
633
|
+
patch: KnowledgePatch,
|
|
634
|
+
): KnowledgeDocument {
|
|
635
|
+
const newFrontmatter = { ...document.frontmatter };
|
|
636
|
+
if (patch.fields) {
|
|
637
|
+
for (const [key, value] of Object.entries(patch.fields)) {
|
|
638
|
+
if (value !== undefined) {
|
|
639
|
+
(newFrontmatter as Record<string, unknown>)[key] = value;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const newBody = patch.body ?? document.body;
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
...document,
|
|
648
|
+
frontmatter: newFrontmatter,
|
|
649
|
+
body: newBody,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function readKnowledgeDocumentFile(path: string, id: string): ParseKnowledgeResult {
|
|
654
|
+
const content = readFileSync(path, "utf8");
|
|
655
|
+
return parseKnowledgeDocument(content, id);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function writeKnowledgeDocumentFile(path: string, document: KnowledgeDocument): void {
|
|
659
|
+
const content = serializeKnowledgeDocument(document);
|
|
660
|
+
writeFileSync(path, content, "utf8");
|
|
661
|
+
}
|