@zosmaai/pi-llm-wiki 0.10.9 → 0.11.1

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