@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.de.md +35 -4
  3. package/README.es.md +260 -170
  4. package/README.fr.md +35 -4
  5. package/README.hi.md +35 -4
  6. package/README.ja.md +35 -4
  7. package/README.ko.md +35 -4
  8. package/README.md +38 -3
  9. package/README.pt.md +35 -4
  10. package/README.ru.md +35 -4
  11. package/README.zh.md +260 -170
  12. package/assets/demo.gif +0 -0
  13. package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
  14. package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
  15. package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
  16. package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
  17. package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
  18. package/dist/extensions/llm-wiki/lib/inject.js +65 -0
  19. package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
  20. package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
  21. package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
  22. package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
  23. package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
  24. package/dist/extensions/llm-wiki/lib/observation.js +283 -0
  25. package/dist/extensions/llm-wiki/lib/recall.js +875 -0
  26. package/dist/extensions/llm-wiki/lib/retro.js +158 -0
  27. package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
  28. package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
  29. package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
  30. package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
  31. package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
  32. package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
  33. package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
  34. package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
  35. package/dist/extensions/llm-wiki/lib/utils.js +347 -0
  36. package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
  37. package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
  38. package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
  39. package/dist/mcp/exec.js +121 -0
  40. package/dist/mcp/index.js +229 -0
  41. package/dist/mcp/operations.js +130 -0
  42. package/dist/package.json +1 -0
  43. package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
  44. package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
  45. package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
  46. package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
  47. package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
  48. package/extensions/llm-wiki/index.ts +22 -36
  49. package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
  50. package/extensions/llm-wiki/lib/embeddings.ts +9 -3
  51. package/extensions/llm-wiki/lib/guardrails.ts +174 -29
  52. package/extensions/llm-wiki/lib/indexing.ts +2 -1
  53. package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
  54. package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
  55. package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
  56. package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
  57. package/extensions/llm-wiki/lib/metadata.ts +531 -116
  58. package/extensions/llm-wiki/lib/observation.ts +37 -43
  59. package/extensions/llm-wiki/lib/recall.ts +61 -33
  60. package/extensions/llm-wiki/lib/retro.ts +65 -41
  61. package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
  62. package/extensions/llm-wiki/lib/source-packet.ts +44 -31
  63. package/extensions/llm-wiki/lib/tools.ts +406 -348
  64. package/extensions/llm-wiki/lib/trajectory.ts +15 -1
  65. package/extensions/llm-wiki/lib/utils.ts +121 -130
  66. package/extensions/llm-wiki/lib/vault-format.ts +363 -0
  67. package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
  68. package/mcp/exec.ts +122 -0
  69. package/mcp/index.ts +60 -250
  70. package/mcp/operations.ts +176 -0
  71. package/package.json +8 -2
  72. package/scripts/migrate-llm-wiki.js +801 -0
  73. 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
+ }