@tangle-network/agent-knowledge 1.12.1 → 2.0.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.
@@ -0,0 +1,2119 @@
1
+ import {
2
+ searchKnowledge
3
+ } from "./chunk-DQ3PDMDP.js";
4
+ import {
5
+ sha256,
6
+ slugify,
7
+ stableId
8
+ } from "./chunk-YMKHCTS2.js";
9
+
10
+ // src/adapters.ts
11
+ var textSourceAdapter = {
12
+ id: "text",
13
+ canLoad: (input) => Boolean(input.text) || /\.(md|txt|json|csv)$/i.test(input.uri),
14
+ load: (input) => ({
15
+ title: input.uri.split("/").pop(),
16
+ mediaType: mediaTypeFor(input.uri),
17
+ text: decodeText(input),
18
+ anchors: anchorsForText(input.uri, decodeText(input)),
19
+ metadata: input.metadata
20
+ })
21
+ };
22
+ function mediaTypeFor(uri) {
23
+ const lower = uri.toLowerCase();
24
+ if (lower.endsWith(".md")) return "text/markdown";
25
+ if (lower.endsWith(".txt")) return "text/plain";
26
+ if (lower.endsWith(".json")) return "application/json";
27
+ if (lower.endsWith(".csv")) return "text/csv";
28
+ if (lower.endsWith(".pdf")) return "application/pdf";
29
+ return "application/octet-stream";
30
+ }
31
+ function decodeText(input) {
32
+ return input.text ?? (input.bytes ? new TextDecoder().decode(input.bytes).slice(0, 2e5) : void 0);
33
+ }
34
+ function anchorsForText(uri, text) {
35
+ if (!text) return [];
36
+ const lines = text.split("\n");
37
+ const anchors = [
38
+ { id: "all", sourceId: "", label: "Full source", lineStart: 1, lineEnd: lines.length }
39
+ ];
40
+ for (let i = 0; i < lines.length; i += 50) {
41
+ anchors.push({
42
+ id: `l${i + 1}`,
43
+ sourceId: "",
44
+ label: `${uri}:${i + 1}`,
45
+ lineStart: i + 1,
46
+ lineEnd: Math.min(lines.length, i + 50)
47
+ });
48
+ }
49
+ return anchors;
50
+ }
51
+
52
+ // src/wikilinks.ts
53
+ var WIKILINK_REGEX = /\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]/g;
54
+ function extractWikilinks(content) {
55
+ const links = [];
56
+ const regex = new RegExp(WIKILINK_REGEX.source, "g");
57
+ let match;
58
+ match = regex.exec(content);
59
+ while (match !== null) {
60
+ links.push(match[1].trim());
61
+ match = regex.exec(content);
62
+ }
63
+ return [...new Set(links)];
64
+ }
65
+ function normalizeLinkTarget(target) {
66
+ return target.trim().replace(/\.md$/i, "").toLowerCase().replace(/\s+/g, "-");
67
+ }
68
+
69
+ // src/graph.ts
70
+ function buildKnowledgeGraph(pages) {
71
+ const byId = /* @__PURE__ */ new Map();
72
+ const bySlug = /* @__PURE__ */ new Map();
73
+ for (const page of pages) {
74
+ byId.set(page.id, page);
75
+ bySlug.set(normalizeLinkTarget(page.id), page);
76
+ bySlug.set(normalizeLinkTarget(page.title), page);
77
+ bySlug.set(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")), page);
78
+ }
79
+ const incoming = /* @__PURE__ */ new Map();
80
+ const outgoing = /* @__PURE__ */ new Map();
81
+ const edgesByKey = /* @__PURE__ */ new Map();
82
+ for (const page of pages) {
83
+ outgoing.set(page.id, 0);
84
+ incoming.set(page.id, 0);
85
+ }
86
+ for (const page of pages) {
87
+ for (const raw of page.outLinks) {
88
+ const target = bySlug.get(normalizeLinkTarget(raw));
89
+ if (!target || target.id === page.id) continue;
90
+ const key = `${page.id}->${target.id}`;
91
+ const edge = edgesByKey.get(key);
92
+ if (edge) edge.weight += 1;
93
+ else
94
+ edgesByKey.set(key, {
95
+ source: page.id,
96
+ target: target.id,
97
+ weight: 1,
98
+ reasons: ["wikilink"]
99
+ });
100
+ outgoing.set(page.id, (outgoing.get(page.id) ?? 0) + 1);
101
+ incoming.set(target.id, (incoming.get(target.id) ?? 0) + 1);
102
+ }
103
+ }
104
+ addSourceOverlapEdges(pages, edgesByKey);
105
+ const nodes = pages.map((page) => ({
106
+ id: page.id,
107
+ title: page.title,
108
+ path: page.path,
109
+ tags: page.tags,
110
+ sourceIds: page.sourceIds,
111
+ outDegree: outgoing.get(page.id) ?? 0,
112
+ inDegree: incoming.get(page.id) ?? 0
113
+ }));
114
+ return { nodes, edges: [...edgesByKey.values()].sort((a, b) => b.weight - a.weight) };
115
+ }
116
+ function addSourceOverlapEdges(pages, edges) {
117
+ for (let i = 0; i < pages.length; i++) {
118
+ for (let j = i + 1; j < pages.length; j++) {
119
+ const a = pages[i];
120
+ const b = pages[j];
121
+ const overlap = a.sourceIds.filter((source) => b.sourceIds.includes(source));
122
+ if (overlap.length === 0) continue;
123
+ const key = `${a.id}->${b.id}`;
124
+ const edge = edges.get(key);
125
+ if (edge) {
126
+ edge.weight += overlap.length * 0.5;
127
+ edge.reasons.push("shared-source");
128
+ } else {
129
+ edges.set(key, {
130
+ source: a.id,
131
+ target: b.id,
132
+ weight: overlap.length * 0.5,
133
+ reasons: ["shared-source"]
134
+ });
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ // src/mutation-lock.ts
141
+ import { AsyncLocalStorage } from "async_hooks";
142
+ import { mkdir as mkdir2, readFile } from "fs/promises";
143
+ import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
144
+ import { check, lock } from "proper-lockfile";
145
+
146
+ // src/durable-fs.ts
147
+ import { randomUUID } from "crypto";
148
+ import { constants } from "fs";
149
+ import {
150
+ lstat,
151
+ mkdir,
152
+ open,
153
+ readdir,
154
+ realpath,
155
+ rename,
156
+ rm
157
+ } from "fs/promises";
158
+ import { basename, dirname, isAbsolute, resolve } from "path";
159
+ var DIRECTORY_FLAGS = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
160
+ var CREATE_FILE_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW;
161
+ var READ_FILE_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW;
162
+ async function writeFileDurable(path, data, options = {}) {
163
+ const directory = dirname(path);
164
+ await mkdir(directory, { recursive: true });
165
+ await withDirectoryHandle(directory, async (handle, anchoredDirectory) => {
166
+ const target = resolve(anchoredDirectory, basename(path));
167
+ const existingMode = await regularFileModeOrUndefined(target);
168
+ const requestedMode = options.mode ?? existingMode;
169
+ const temporary = resolve(anchoredDirectory, `.${basename(path)}.${randomUUID()}.tmp`);
170
+ let temporaryHandle;
171
+ try {
172
+ temporaryHandle = await open(temporary, CREATE_FILE_FLAGS, requestedMode ?? 438);
173
+ await temporaryHandle.writeFile(
174
+ data,
175
+ options.encoding === void 0 ? void 0 : { encoding: options.encoding }
176
+ );
177
+ if (requestedMode !== void 0) await temporaryHandle.chmod(requestedMode);
178
+ await temporaryHandle.sync();
179
+ await temporaryHandle.close();
180
+ temporaryHandle = void 0;
181
+ await rename(temporary, target);
182
+ await syncDirectoryHandle(handle);
183
+ } finally {
184
+ await temporaryHandle?.close().catch(() => void 0);
185
+ await rm(temporary, { force: true }).catch(() => void 0);
186
+ }
187
+ });
188
+ }
189
+ async function writeJsonDurable(path, value) {
190
+ await writeFileDurable(path, `${JSON.stringify(value, null, 2)}
191
+ `, { encoding: "utf8" });
192
+ }
193
+ async function writeFileDurableWithinRoot(root, relativePath, data, options = {}) {
194
+ await withSafeDescendant(root, relativePath, (path) => writeFileDurable(path, data, options));
195
+ }
196
+ async function writeJsonDurableWithinRoot(root, relativePath, value) {
197
+ await writeFileDurableWithinRoot(root, relativePath, `${JSON.stringify(value, null, 2)}
198
+ `, {
199
+ encoding: "utf8"
200
+ });
201
+ }
202
+ async function renameDurable(source, target) {
203
+ const sourceDir = dirname(source);
204
+ const targetDir = dirname(target);
205
+ await withDirectoryHandle(sourceDir, async (sourceHandle, anchoredSourceDir) => {
206
+ await withDirectoryHandle(targetDir, async (targetHandle, anchoredTargetDir) => {
207
+ await rename(
208
+ resolve(anchoredSourceDir, basename(source)),
209
+ resolve(anchoredTargetDir, basename(target))
210
+ );
211
+ await syncDirectoryHandle(sourceHandle);
212
+ if (targetDir !== sourceDir) await syncDirectoryHandle(targetHandle);
213
+ });
214
+ });
215
+ }
216
+ async function removeDurable(path) {
217
+ await withDirectoryHandle(dirname(path), async (handle, anchoredDirectory) => {
218
+ await rm(resolve(anchoredDirectory, basename(path)), { force: true });
219
+ await syncDirectoryHandle(handle);
220
+ });
221
+ }
222
+ async function syncDirectory(path) {
223
+ await withDirectoryHandle(path, syncDirectoryHandle);
224
+ }
225
+ async function withSafeDescendant(root, relativePath, use) {
226
+ const normalized = normalizeRelativePath(relativePath);
227
+ const parts = normalized.split("/");
228
+ const filename = parts.pop();
229
+ const directory = await openSafeDirectoryTree(root, parts.join("/"), true);
230
+ try {
231
+ const target = resolve(anchoredDirectoryPath(directory.handle, directory.path), filename);
232
+ await regularFileModeOrUndefined(target);
233
+ return await use(target);
234
+ } finally {
235
+ await directory.handle.close();
236
+ }
237
+ }
238
+ async function withSafeDirectory(root, relativePath, create, use) {
239
+ const directory = await openSafeDirectoryTree(root, relativePath, create);
240
+ try {
241
+ return await use(anchoredDirectoryPath(directory.handle, directory.path));
242
+ } finally {
243
+ await directory.handle.close();
244
+ }
245
+ }
246
+ async function readRegularFileNoFollow(path) {
247
+ let handle;
248
+ try {
249
+ handle = await open(path, READ_FILE_FLAGS);
250
+ const entry = await handle.stat();
251
+ if (!entry.isFile()) throw new Error(`knowledge path is not a regular file: ${path}`);
252
+ return { bytes: await handle.readFile(), mode: entry.mode & 511 };
253
+ } catch (error) {
254
+ const code = error?.code;
255
+ if (code === "ELOOP" || code === "ENOTDIR") {
256
+ throw new Error(`knowledge path is not a regular file: ${path}`, { cause: error });
257
+ }
258
+ throw error;
259
+ } finally {
260
+ await handle?.close().catch(() => void 0);
261
+ }
262
+ }
263
+ async function readRegularFileWithinRoot(root, relativePath) {
264
+ const normalized = normalizeRelativePath(relativePath);
265
+ const parts = normalized.split("/");
266
+ const filename = parts.pop();
267
+ const directory = await openSafeDirectoryTree(root, parts.join("/"), false);
268
+ try {
269
+ return await readRegularFileNoFollow(
270
+ resolve(anchoredDirectoryPath(directory.handle, directory.path), filename)
271
+ );
272
+ } finally {
273
+ await directory.handle.close();
274
+ }
275
+ }
276
+ async function listRegularFilesWithinRoot(root, relativeDirectory) {
277
+ const normalized = normalizeRelativePath(relativeDirectory);
278
+ return withSafeDirectory(
279
+ root,
280
+ normalized,
281
+ false,
282
+ (directory) => listRegularFilesFromOpenDirectory(directory, normalized)
283
+ );
284
+ }
285
+ function isMissingFile(error) {
286
+ return error?.code === "ENOENT";
287
+ }
288
+ async function withDirectoryHandle(path, use) {
289
+ const handle = await openDirectory(path);
290
+ try {
291
+ return await use(handle, anchoredDirectoryPath(handle, path));
292
+ } finally {
293
+ await handle.close();
294
+ }
295
+ }
296
+ async function listRegularFilesFromOpenDirectory(directory, relativeDirectory) {
297
+ const out = [];
298
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
299
+ const relativePath = `${relativeDirectory}/${entry.name}`;
300
+ if (entry.isDirectory()) {
301
+ out.push(
302
+ ...await withSafeDirectory(
303
+ directory,
304
+ entry.name,
305
+ false,
306
+ (child) => listRegularFilesFromOpenDirectory(child, relativePath)
307
+ )
308
+ );
309
+ } else if (entry.isFile()) {
310
+ out.push({
311
+ path: relativePath,
312
+ ...await readRegularFileNoFollow(resolve(directory, entry.name))
313
+ });
314
+ } else {
315
+ throw new Error(`knowledge tree contains an unsupported filesystem entry: ${relativePath}`);
316
+ }
317
+ }
318
+ return out;
319
+ }
320
+ async function openSafeDirectoryTree(root, relativePath, create) {
321
+ if (create) await mkdir(root, { recursive: true });
322
+ const resolvedRoot = isKernelAnchoredPath(root) ? root : await realpath(root);
323
+ const normalized = relativePath === "" ? "" : normalizeRelativePath(relativePath);
324
+ let currentPath = resolvedRoot;
325
+ let currentHandle = await openDirectory(resolvedRoot);
326
+ try {
327
+ for (const part of normalized.split("/").filter(Boolean)) {
328
+ const anchoredParent = anchoredDirectoryPath(currentHandle, currentPath);
329
+ const childPath = resolve(anchoredParent, part);
330
+ let childHandle;
331
+ try {
332
+ childHandle = await openDirectory(childPath);
333
+ } catch (error) {
334
+ if (!create || !isMissingFile(error)) throw unsafeDirectoryError(childPath, error);
335
+ let created = false;
336
+ try {
337
+ await mkdir(childPath);
338
+ created = true;
339
+ } catch (mkdirError) {
340
+ if (mkdirError?.code !== "EEXIST") throw mkdirError;
341
+ }
342
+ childHandle = await openDirectory(childPath).catch((openError) => {
343
+ throw unsafeDirectoryError(childPath, openError);
344
+ });
345
+ if (created) await syncDirectoryHandle(currentHandle);
346
+ }
347
+ await currentHandle.close();
348
+ currentHandle = childHandle;
349
+ currentPath = resolve(currentPath, part);
350
+ }
351
+ return { handle: currentHandle, path: currentPath };
352
+ } catch (error) {
353
+ await currentHandle.close().catch(() => void 0);
354
+ throw error;
355
+ }
356
+ }
357
+ async function openDirectory(path) {
358
+ const isKernelDescriptor = /^\/proc\/self\/fd\/\d+$/.test(path);
359
+ if (!isKernelDescriptor) {
360
+ const entry2 = await lstat(path);
361
+ if (entry2.isSymbolicLink() || !entry2.isDirectory()) {
362
+ throw new Error(`knowledge path has an unsafe directory: ${path}`);
363
+ }
364
+ }
365
+ const flags = isKernelDescriptor ? constants.O_RDONLY | constants.O_DIRECTORY : DIRECTORY_FLAGS;
366
+ const handle = await open(path, flags);
367
+ const entry = await handle.stat();
368
+ if (!entry.isDirectory()) {
369
+ await handle.close();
370
+ throw new Error(`knowledge path has an unsafe directory: ${path}`);
371
+ }
372
+ return handle;
373
+ }
374
+ function isKernelAnchoredPath(path) {
375
+ return process.platform === "linux" && /^\/proc\/self\/fd\/\d+(?:\/|$)/.test(path);
376
+ }
377
+ function anchoredDirectoryPath(handle, fallback) {
378
+ return process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : fallback;
379
+ }
380
+ async function regularFileModeOrUndefined(path) {
381
+ try {
382
+ const entry = await lstat(path);
383
+ if (entry.isSymbolicLink() || !entry.isFile()) {
384
+ throw new Error(`knowledge path is not a regular file: ${path}`);
385
+ }
386
+ return entry.mode & 511;
387
+ } catch (error) {
388
+ if (isMissingFile(error)) return void 0;
389
+ throw error;
390
+ }
391
+ }
392
+ async function syncDirectoryHandle(handle) {
393
+ if (process.platform === "win32") return;
394
+ await handle.sync();
395
+ }
396
+ function unsafeDirectoryError(path, cause) {
397
+ const code = cause?.code;
398
+ if (code === "ELOOP" || code === "ENOTDIR") {
399
+ return new Error(`knowledge path has an unsafe directory: ${path}`, { cause });
400
+ }
401
+ return cause instanceof Error ? cause : new Error(`could not open knowledge directory: ${path}`);
402
+ }
403
+ function normalizeRelativePath(path) {
404
+ if (path.length === 0 || isAbsolute(path)) {
405
+ throw new Error(`knowledge path must be a non-empty relative path: ${path}`);
406
+ }
407
+ const normalized = path.replace(/\\/g, "/");
408
+ if (normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
409
+ throw new Error(`knowledge path contains an unsafe segment: ${path}`);
410
+ }
411
+ return normalized;
412
+ }
413
+
414
+ // src/file-transaction.ts
415
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
416
+ import { mkdtemp, readdir as readdir2, rm as rm2 } from "fs/promises";
417
+ import { join, relative, resolve as resolve2, sep } from "path";
418
+ import { contentHash } from "@tangle-network/agent-eval";
419
+ import { z } from "zod";
420
+ var digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
421
+ var fileModeSchema = z.number().int().min(0).max(511);
422
+ var DEFAULT_FILE_MODE = 420;
423
+ var transactionEntrySchema = z.object({
424
+ index: z.number().int().nonnegative(),
425
+ path: z.string().min(1),
426
+ beforeHash: digestSchema.nullable(),
427
+ afterHash: digestSchema.nullable(),
428
+ beforeMode: fileModeSchema.optional(),
429
+ afterMode: fileModeSchema.optional()
430
+ }).strict();
431
+ var transactionSchema = z.object({
432
+ schemaVersion: z.literal(1),
433
+ kind: z.literal("knowledge-file-transaction"),
434
+ transactionId: z.string().uuid(),
435
+ purpose: z.string().min(1),
436
+ createdAt: z.string().min(1),
437
+ entries: z.array(transactionEntrySchema).min(1)
438
+ }).strict();
439
+ var transactionDirectionSchema = z.object({
440
+ schemaVersion: z.literal(1),
441
+ transactionId: z.string().uuid(),
442
+ direction: z.literal("rollback")
443
+ }).strict();
444
+ function knowledgeFileTransactionPlanHash(entries) {
445
+ const normalized = entries.map((entry) => normalizePlanEntry(entry)).sort((left, right) => left.path.localeCompare(right.path));
446
+ if (new Set(normalized.map((entry) => entry.path)).size !== normalized.length) {
447
+ throw new Error("knowledge transaction plan repeats a path");
448
+ }
449
+ return contentHash(normalized);
450
+ }
451
+ async function prepareKnowledgeFileTransaction(input) {
452
+ if (input.mutations.length === 0) throw new Error("knowledge file transaction has no mutations");
453
+ return withTransactionRoot(input.root, input.transactionRoot, true, async (transactionRoot) => {
454
+ const activeTransactions = await activeTransactionDirectoryNames(transactionRoot);
455
+ if (activeTransactions.length > 0) {
456
+ throw new Error(
457
+ `knowledge file transaction is already active: ${activeTransactions.join(", ")}`
458
+ );
459
+ }
460
+ const paths = /* @__PURE__ */ new Set();
461
+ const prepared = await Promise.all(
462
+ input.mutations.map(async (mutation, index) => {
463
+ const path = assertKnowledgeMutationPath(mutation.path);
464
+ if (paths.has(path)) throw new Error(`knowledge file transaction repeats path: ${path}`);
465
+ if (mutation.content === null && mutation.mode !== void 0) {
466
+ throw new Error(`deleted knowledge file cannot declare a mode: ${path}`);
467
+ }
468
+ paths.add(path);
469
+ const before = await withSafeDescendant(input.root, path, readRegularFile);
470
+ const after = mutation.content === null ? null : Buffer.isBuffer(mutation.content) ? mutation.content : Buffer.from(mutation.content, "utf8");
471
+ const afterMode = after ? fileModeSchema.parse(mutation.mode ?? before?.mode ?? DEFAULT_FILE_MODE) : void 0;
472
+ if (!input.includeUnchanged && sameBytes(before?.bytes ?? null, after) && before?.mode === afterMode) {
473
+ return null;
474
+ }
475
+ return {
476
+ entry: {
477
+ index,
478
+ path,
479
+ beforeHash: before ? hashBytes(before.bytes) : null,
480
+ afterHash: after ? hashBytes(after) : null,
481
+ ...before ? { beforeMode: before.mode } : {},
482
+ ...afterMode === void 0 ? {} : { afterMode }
483
+ },
484
+ before: before?.bytes ?? null,
485
+ after
486
+ };
487
+ })
488
+ );
489
+ const changed = prepared.filter((entry) => entry !== null);
490
+ if (changed.length === 0) return null;
491
+ const preparationDir = await mkdtemp(join(transactionRoot, "prepare-"));
492
+ let activated = false;
493
+ try {
494
+ for (const item of changed) {
495
+ if (item.before) {
496
+ await writeFileDurable(
497
+ snapshotPath(preparationDir, "before", item.entry.index),
498
+ item.before
499
+ );
500
+ }
501
+ if (item.after) {
502
+ await writeFileDurable(
503
+ snapshotPath(preparationDir, "after", item.entry.index),
504
+ item.after
505
+ );
506
+ }
507
+ }
508
+ const transaction = transactionSchema.parse({
509
+ schemaVersion: 1,
510
+ kind: "knowledge-file-transaction",
511
+ transactionId: randomUUID2(),
512
+ purpose: input.purpose,
513
+ createdAt: (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
514
+ entries: changed.map((item) => item.entry)
515
+ });
516
+ await writeJsonDurable(join(preparationDir, "transaction.json"), transaction);
517
+ const activeDir = join(
518
+ transactionRoot,
519
+ activeTransactionDirectoryName(transaction.transactionId)
520
+ );
521
+ await renameDurable(preparationDir, activeDir);
522
+ activated = true;
523
+ const competingTransactions = await activeTransactionDirectoryNames(transactionRoot);
524
+ if (competingTransactions.length > 1) {
525
+ await rm2(activeDir, { recursive: true, force: true });
526
+ await syncDirectory(transactionRoot);
527
+ throw new Error("multiple knowledge file transactions became active concurrently");
528
+ }
529
+ return transaction;
530
+ } finally {
531
+ if (!activated) await rm2(preparationDir, { recursive: true, force: true });
532
+ }
533
+ });
534
+ }
535
+ async function commitKnowledgeFileMutations(input) {
536
+ if (await loadKnowledgeFileTransaction({
537
+ root: input.root,
538
+ transactionRoot: input.transactionRoot
539
+ })) {
540
+ throw new Error("recover the pending knowledge transaction before planning another write");
541
+ }
542
+ const transaction = await prepareKnowledgeFileTransaction({
543
+ root: input.root,
544
+ transactionRoot: input.transactionRoot,
545
+ purpose: input.purpose,
546
+ mutations: input.mutations,
547
+ now: input.now
548
+ });
549
+ if (!transaction) return false;
550
+ input.assertOwned?.();
551
+ await applyKnowledgeFileTransaction({
552
+ root: input.root,
553
+ transactionRoot: input.transactionRoot,
554
+ transaction,
555
+ beforeCommit: input.assertOwned
556
+ });
557
+ await finishKnowledgeFileTransaction({
558
+ root: input.root,
559
+ transactionRoot: input.transactionRoot,
560
+ transaction,
561
+ assertOwned: input.assertOwned
562
+ });
563
+ return true;
564
+ }
565
+ async function recoverKnowledgeFileTransaction(input) {
566
+ const pending = await inspectKnowledgeFileTransaction({
567
+ root: input.root,
568
+ transactionRoot: input.transactionRoot
569
+ });
570
+ if (!pending) return false;
571
+ const { transaction, direction } = pending;
572
+ if (transaction.purpose !== input.expectedPurpose) {
573
+ throw new Error(`knowledge transaction '${transaction.purpose}' requires its owner to resume`);
574
+ }
575
+ input.validate?.(transaction);
576
+ input.assertOwned?.();
577
+ if (input.direction === "apply" && direction === "rollback") {
578
+ throw new Error(`knowledge transaction '${transaction.transactionId}' is already rolling back`);
579
+ }
580
+ const requestedDirection = input.direction ?? direction;
581
+ if (requestedDirection === "rollback") {
582
+ await rollbackKnowledgeFileTransaction({
583
+ root: input.root,
584
+ transactionRoot: input.transactionRoot,
585
+ transaction,
586
+ beforeCommit: input.assertOwned
587
+ });
588
+ } else {
589
+ await applyKnowledgeFileTransaction({
590
+ root: input.root,
591
+ transactionRoot: input.transactionRoot,
592
+ transaction,
593
+ beforeCommit: input.assertOwned
594
+ });
595
+ }
596
+ await finishKnowledgeFileTransaction({
597
+ root: input.root,
598
+ transactionRoot: input.transactionRoot,
599
+ transaction,
600
+ assertOwned: input.assertOwned
601
+ });
602
+ return true;
603
+ }
604
+ async function loadKnowledgeFileTransaction(input) {
605
+ return (await inspectKnowledgeFileTransaction(input))?.transaction ?? null;
606
+ }
607
+ async function inspectKnowledgeFileTransaction(input) {
608
+ return loadKnowledgeFileTransactionState(input);
609
+ }
610
+ async function applyKnowledgeFileTransaction(input) {
611
+ const transaction = transactionSchema.parse(input.transaction);
612
+ assertTransactionEntries(transaction);
613
+ await withTransactionDirectory(
614
+ input.root,
615
+ input.transactionRoot,
616
+ transaction,
617
+ async (transactionDir) => {
618
+ await assertActiveTransaction(transactionDir, transaction);
619
+ if (await readTransactionDirection(transactionDir, transaction) === "rollback") {
620
+ throw new Error(`knowledge transaction '${transaction.transactionId}' is rolling back`);
621
+ }
622
+ for (const entry of transaction.entries) {
623
+ await withSafeDescendant(input.root, entry.path, async (target) => {
624
+ const current = await readRegularFile(target);
625
+ if (fileMatches(current, entry.afterHash, entry.afterMode)) return;
626
+ if (!fileMatches(current, entry.beforeHash, entry.beforeMode)) {
627
+ throw new Error(`knowledge file changed outside transaction: ${entry.path}`);
628
+ }
629
+ await input.beforeCommit?.(entry);
630
+ if (entry.afterHash === null) {
631
+ await removeDurable(target);
632
+ return;
633
+ }
634
+ const after = (await readRegularFileNoFollow(snapshotPath(transactionDir, "after", entry.index))).bytes;
635
+ if (hashBytes(after) !== entry.afterHash) {
636
+ throw new Error(`knowledge transaction snapshot changed: ${entry.path}`);
637
+ }
638
+ await writeFileDurable(target, after, { mode: entry.afterMode });
639
+ });
640
+ }
641
+ await assertKnowledgeFileTransactionApplied(input.root, transaction);
642
+ }
643
+ );
644
+ }
645
+ async function assertKnowledgeFileTransactionApplied(root, transaction) {
646
+ for (const entry of transaction.entries) {
647
+ await withSafeDescendant(root, entry.path, async (target) => {
648
+ const current = await readRegularFile(target);
649
+ if (!fileMatches(current, entry.afterHash, entry.afterMode)) {
650
+ throw new Error(`knowledge file transaction did not commit: ${entry.path}`);
651
+ }
652
+ });
653
+ }
654
+ }
655
+ async function finishKnowledgeFileTransaction(input) {
656
+ const transaction = transactionSchema.parse(input.transaction);
657
+ assertTransactionEntries(transaction);
658
+ await withTransactionRoot(input.root, input.transactionRoot, false, async (transactionRoot) => {
659
+ const activeName = activeTransactionDirectoryName(transaction.transactionId);
660
+ await withTransactionDirectory(
661
+ input.root,
662
+ input.transactionRoot,
663
+ transaction,
664
+ async (transactionDir) => {
665
+ await assertActiveTransaction(transactionDir, transaction);
666
+ input.assertOwned?.();
667
+ const direction = await readTransactionDirection(transactionDir, transaction);
668
+ await assertKnowledgeFileTransactionTerminal(input.root, transaction, direction);
669
+ await withSafeDirectory(transactionRoot, activeName, false, async (currentDir) => {
670
+ await assertActiveTransaction(currentDir, transaction);
671
+ const currentDirection = await readTransactionDirection(currentDir, transaction);
672
+ if (currentDirection !== direction) {
673
+ throw new Error(
674
+ `knowledge transaction '${transaction.transactionId}' changed direction`
675
+ );
676
+ }
677
+ await assertKnowledgeFileTransactionTerminal(input.root, transaction, currentDirection);
678
+ });
679
+ }
680
+ );
681
+ await rm2(join(transactionRoot, activeName), { recursive: true, force: false });
682
+ await syncDirectory(transactionRoot);
683
+ });
684
+ }
685
+ async function rollbackKnowledgeFileTransaction(input) {
686
+ await withTransactionDirectory(
687
+ input.root,
688
+ input.transactionRoot,
689
+ input.transaction,
690
+ async (transactionDir) => {
691
+ const transaction = transactionSchema.parse(input.transaction);
692
+ assertTransactionEntries(transaction);
693
+ await assertActiveTransaction(transactionDir, transaction);
694
+ await input.beforeCommit?.(transaction.entries.at(-1));
695
+ const direction = await readTransactionDirection(transactionDir, transaction);
696
+ if (direction !== "rollback") {
697
+ await writeJsonDurable(join(transactionDir, "direction.json"), {
698
+ schemaVersion: 1,
699
+ transactionId: transaction.transactionId,
700
+ direction: "rollback"
701
+ });
702
+ }
703
+ for (const entry of [...transaction.entries].reverse()) {
704
+ await withSafeDescendant(input.root, entry.path, async (target) => {
705
+ const current = await readRegularFile(target);
706
+ if (fileMatches(current, entry.beforeHash, entry.beforeMode)) return;
707
+ if (!fileMatches(current, entry.afterHash, entry.afterMode)) {
708
+ throw new Error(`knowledge file changed outside rollback: ${entry.path}`);
709
+ }
710
+ await input.beforeCommit?.(entry);
711
+ if (entry.beforeHash === null) {
712
+ await removeDurable(target);
713
+ return;
714
+ }
715
+ const before = (await readRegularFileNoFollow(snapshotPath(transactionDir, "before", entry.index))).bytes;
716
+ if (hashBytes(before) !== entry.beforeHash) {
717
+ throw new Error(`knowledge rollback snapshot changed: ${entry.path}`);
718
+ }
719
+ await writeFileDurable(target, before, { mode: entry.beforeMode });
720
+ });
721
+ }
722
+ await input.beforeCommit?.(transaction.entries[0]);
723
+ for (const entry of transaction.entries) {
724
+ await withSafeDescendant(input.root, entry.path, async (target) => {
725
+ const current = await readRegularFile(target);
726
+ if (!fileMatches(current, entry.beforeHash, entry.beforeMode)) {
727
+ throw new Error(`knowledge file transaction did not roll back: ${entry.path}`);
728
+ }
729
+ });
730
+ }
731
+ }
732
+ );
733
+ }
734
+ async function loadKnowledgeFileTransactionState(input) {
735
+ try {
736
+ return await withTransactionRoot(input.root, input.transactionRoot, false, async (root) => {
737
+ const activeTransactions = await activeTransactionDirectoryNames(root);
738
+ if (activeTransactions.length === 0) return null;
739
+ if (activeTransactions.length > 1) {
740
+ throw new Error("multiple knowledge file transactions are active");
741
+ }
742
+ return withSafeDirectory(root, activeTransactions[0], false, async (transactionDir) => {
743
+ const transaction = await readActiveTransaction(transactionDir);
744
+ return {
745
+ transaction,
746
+ direction: await readTransactionDirection(transactionDir, transaction)
747
+ };
748
+ });
749
+ });
750
+ } catch (error) {
751
+ if (isMissingFile(error)) return null;
752
+ throw error;
753
+ }
754
+ }
755
+ async function readActiveTransaction(transactionDir) {
756
+ const transaction = transactionSchema.parse(
757
+ JSON.parse(
758
+ (await readRegularFileNoFollow(join(transactionDir, "transaction.json"))).bytes.toString(
759
+ "utf8"
760
+ )
761
+ )
762
+ );
763
+ assertTransactionEntries(transaction);
764
+ return transaction;
765
+ }
766
+ async function assertActiveTransaction(transactionDir, expected) {
767
+ const active = await readActiveTransaction(transactionDir);
768
+ if (active.transactionId !== expected.transactionId || contentHash(active) !== contentHash(transactionSchema.parse(expected))) {
769
+ throw new Error(`active knowledge transaction does not match '${expected.transactionId}'`);
770
+ }
771
+ }
772
+ async function readTransactionDirection(transactionDir, transaction) {
773
+ try {
774
+ const direction = transactionDirectionSchema.parse(
775
+ JSON.parse(
776
+ (await readRegularFileNoFollow(join(transactionDir, "direction.json"))).bytes.toString(
777
+ "utf8"
778
+ )
779
+ )
780
+ );
781
+ if (direction.transactionId !== transaction.transactionId) {
782
+ throw new Error("knowledge transaction direction belongs to another transaction");
783
+ }
784
+ return direction.direction;
785
+ } catch (error) {
786
+ if (isMissingFile(error)) return "apply";
787
+ throw error;
788
+ }
789
+ }
790
+ function snapshotPath(root, side, index) {
791
+ return join(root, side, `${index}.bin`);
792
+ }
793
+ async function withTransactionDirectory(root, transactionRoot, transaction, use) {
794
+ const parsed = transactionSchema.parse(transaction);
795
+ try {
796
+ return await withTransactionRoot(
797
+ root,
798
+ transactionRoot,
799
+ false,
800
+ (safeRoot) => withSafeDirectory(safeRoot, activeTransactionDirectoryName(parsed.transactionId), false, use)
801
+ );
802
+ } catch (error) {
803
+ if (isMissingFile(error)) {
804
+ throw new Error(`active knowledge transaction does not match '${parsed.transactionId}'`, {
805
+ cause: error
806
+ });
807
+ }
808
+ throw error;
809
+ }
810
+ }
811
+ async function activeTransactionDirectoryNames(transactionRoot) {
812
+ const active = [];
813
+ for (const entry of await readdir2(transactionRoot, { withFileTypes: true })) {
814
+ if (entry.name === "active") {
815
+ throw new Error("knowledge transaction store contains an unsupported active journal");
816
+ }
817
+ if (!entry.name.startsWith("active-")) continue;
818
+ if (!entry.isDirectory()) {
819
+ throw new Error(`knowledge transaction journal is not a directory: ${entry.name}`);
820
+ }
821
+ activeTransactionDirectoryName(entry.name.slice("active-".length));
822
+ active.push(entry.name);
823
+ }
824
+ return active.sort();
825
+ }
826
+ function activeTransactionDirectoryName(transactionId) {
827
+ return `active-${z.string().uuid().parse(transactionId)}`;
828
+ }
829
+ async function assertKnowledgeFileTransactionTerminal(root, transaction, direction) {
830
+ if (direction === "apply") {
831
+ await assertKnowledgeFileTransactionApplied(root, transaction);
832
+ return;
833
+ }
834
+ for (const entry of transaction.entries) {
835
+ await withSafeDescendant(root, entry.path, async (target) => {
836
+ const current = await readRegularFile(target);
837
+ if (!fileMatches(current, entry.beforeHash, entry.beforeMode)) {
838
+ throw new Error(`knowledge file transaction did not roll back: ${entry.path}`);
839
+ }
840
+ });
841
+ }
842
+ }
843
+ async function withTransactionRoot(root, transactionRoot, create, use) {
844
+ if (isKernelAnchoredPath(transactionRoot)) {
845
+ const match = transactionRoot.match(/^(\/proc\/self\/fd\/\d+)(?:\/(.*))?$/);
846
+ if (!match?.[1]) throw new Error("knowledge transaction directory has an invalid anchor");
847
+ return withSafeDirectory(match[1], match[2] ?? "", create, use);
848
+ }
849
+ const resolvedRoot = resolve2(root);
850
+ const resolvedTransactionRoot = resolve2(transactionRoot);
851
+ if (!resolvedTransactionRoot.startsWith(`${resolvedRoot}${sep}`)) {
852
+ throw new Error("knowledge transaction directory escaped its root");
853
+ }
854
+ return withSafeDirectory(
855
+ root,
856
+ relative(resolvedRoot, resolvedTransactionRoot).replace(/\\/g, "/"),
857
+ create,
858
+ use
859
+ );
860
+ }
861
+ function assertTransactionEntries(transaction) {
862
+ const indexes = /* @__PURE__ */ new Set();
863
+ const paths = /* @__PURE__ */ new Set();
864
+ for (const entry of transaction.entries) {
865
+ const normalized = assertKnowledgeMutationPath(entry.path);
866
+ if (entry.path !== normalized || indexes.has(entry.index) || paths.has(entry.path)) {
867
+ throw new Error("knowledge file transaction has duplicate or unsafe entries");
868
+ }
869
+ assertHashModePair(entry.path, "before", entry.beforeHash, entry.beforeMode);
870
+ assertHashModePair(entry.path, "after", entry.afterHash, entry.afterMode);
871
+ indexes.add(entry.index);
872
+ paths.add(entry.path);
873
+ }
874
+ }
875
+ function normalizePlanEntry(entry) {
876
+ const path = assertKnowledgeMutationPath(entry.path);
877
+ assertHashModePair(path, "before", entry.beforeHash, entry.beforeMode);
878
+ assertHashModePair(path, "after", entry.afterHash, entry.afterMode);
879
+ return {
880
+ path,
881
+ beforeHash: entry.beforeHash,
882
+ afterHash: entry.afterHash,
883
+ ...entry.beforeMode === void 0 ? {} : { beforeMode: fileModeSchema.parse(entry.beforeMode) },
884
+ ...entry.afterMode === void 0 ? {} : { afterMode: fileModeSchema.parse(entry.afterMode) }
885
+ };
886
+ }
887
+ function assertHashModePair(path, side, hash, mode) {
888
+ if (hash === null !== (mode === void 0)) {
889
+ throw new Error(`knowledge transaction ${side} hash and mode disagree: ${path}`);
890
+ }
891
+ if (mode !== void 0) fileModeSchema.parse(mode);
892
+ }
893
+ function assertKnowledgeMutationPath(path) {
894
+ const normalized = normalizeTransactionPath(path);
895
+ if (normalized === ".agent-knowledge/sources.json" || normalized.startsWith("knowledge/") || normalized.startsWith("raw/")) {
896
+ return normalized;
897
+ }
898
+ throw new Error(`knowledge transaction contains an unsupported path: ${path}`);
899
+ }
900
+ function normalizeTransactionPath(path) {
901
+ const normalized = path.replace(/\\/g, "/");
902
+ if (normalized.length === 0 || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
903
+ throw new Error(`knowledge file transaction has an unsafe path: ${path}`);
904
+ }
905
+ return normalized;
906
+ }
907
+ async function readRegularFile(path) {
908
+ try {
909
+ return await readRegularFileNoFollow(path);
910
+ } catch (error) {
911
+ if (isMissingFile(error)) return null;
912
+ throw error;
913
+ }
914
+ }
915
+ function hashBytes(bytes) {
916
+ return createHash("sha256").update(bytes).digest("hex");
917
+ }
918
+ function sameBytes(left, right) {
919
+ if (left === null || right === null) return left === right;
920
+ return left.equals(right);
921
+ }
922
+ function fileMatches(file, hash, mode) {
923
+ return (file ? hashBytes(file.bytes) : null) === hash && file?.mode === mode;
924
+ }
925
+
926
+ // src/mutation-lock.ts
927
+ var DEFAULT_STALE_MS = 15 * 60 * 1e3;
928
+ var DEFAULT_READ_RETRIES = 100;
929
+ var DEFAULT_READ_WAIT_MS = 25;
930
+ var activeRoots = new AsyncLocalStorage();
931
+ var activeReadRoots = new AsyncLocalStorage();
932
+ var KnowledgeLockLostError = class extends Error {
933
+ constructor(message, options) {
934
+ super(message, options);
935
+ this.name = "KnowledgeLockLostError";
936
+ }
937
+ };
938
+ async function withKnowledgeMutation(root, mutate, options = {}) {
939
+ const resolvedRoot = resolve3(root);
940
+ const active = activeRoots.getStore();
941
+ const existing = active?.get(resolvedRoot);
942
+ if (existing?.active) {
943
+ existing.lock.assertOwned();
944
+ const result = await mutate(existing.lock);
945
+ existing.lock.assertOwned();
946
+ return result;
947
+ }
948
+ return withSafeDirectory(resolvedRoot, ".agent-knowledge", true, async (cacheDir) => {
949
+ const acquired = await acquireDurableFileLock(resolvedRoot, {
950
+ lockfilePath: join2(cacheDir, "mutation.lock.durable"),
951
+ staleMs: options.staleMs,
952
+ retries: options.retries ?? { retries: 100, factor: 1.1, minTimeout: 10, maxTimeout: 200, randomize: true }
953
+ });
954
+ const mutationLock = {
955
+ transactionRoot: join2(cacheDir, "file-transactions"),
956
+ assertOwned: acquired.assertOwned
957
+ };
958
+ const scope = { active: true, lock: mutationLock };
959
+ try {
960
+ const pending = await loadKnowledgeFileTransaction({
961
+ root: resolvedRoot,
962
+ transactionRoot: mutationLock.transactionRoot
963
+ });
964
+ if (pending) {
965
+ const resume = options.resumeTransaction;
966
+ if (!resume || pending.purpose !== resume.purpose) {
967
+ throw new Error(`knowledge transaction '${pending.purpose}' requires its owner to resume`);
968
+ }
969
+ }
970
+ const locks = new Map(active);
971
+ locks.set(resolvedRoot, scope);
972
+ return await activeRoots.run(locks, async () => {
973
+ const epoch = await beginMutationEpoch(cacheDir);
974
+ let completed = false;
975
+ try {
976
+ if (pending) {
977
+ const resume = options.resumeTransaction;
978
+ if (!resume)
979
+ throw new Error(
980
+ `knowledge transaction '${pending.purpose}' requires its owner to resume`
981
+ );
982
+ await recoverKnowledgeFileTransaction({
983
+ root: resolvedRoot,
984
+ transactionRoot: mutationLock.transactionRoot,
985
+ expectedPurpose: resume.purpose,
986
+ direction: resume.direction,
987
+ validate: resume.validate,
988
+ assertOwned: acquired.assertOwned
989
+ });
990
+ }
991
+ mutationLock.assertOwned();
992
+ const result = await mutate(mutationLock);
993
+ mutationLock.assertOwned();
994
+ completed = true;
995
+ return result;
996
+ } finally {
997
+ mutationLock.assertOwned();
998
+ const stillPending = await loadKnowledgeFileTransaction({
999
+ root: resolvedRoot,
1000
+ transactionRoot: mutationLock.transactionRoot
1001
+ });
1002
+ if (completed || !stillPending) await finishMutationEpoch(cacheDir, epoch);
1003
+ }
1004
+ });
1005
+ } finally {
1006
+ scope.active = false;
1007
+ await acquired.release();
1008
+ }
1009
+ });
1010
+ }
1011
+ async function inspectPendingKnowledgeMutation(root) {
1012
+ const resolvedRoot = resolve3(root);
1013
+ const pending = await inspectKnowledgeFileTransaction({
1014
+ root: resolvedRoot,
1015
+ transactionRoot: join2(resolvedRoot, ".agent-knowledge", "file-transactions")
1016
+ });
1017
+ if (!pending) return null;
1018
+ const { transaction, direction } = pending;
1019
+ return {
1020
+ transactionId: transaction.transactionId,
1021
+ purpose: transaction.purpose,
1022
+ createdAt: transaction.createdAt,
1023
+ direction,
1024
+ paths: transaction.entries.map((entry) => entry.path)
1025
+ };
1026
+ }
1027
+ async function recoverPendingKnowledgeMutation(root, options) {
1028
+ const pending = await inspectPendingKnowledgeMutation(root);
1029
+ if (!pending) throw new Error("knowledge base has no pending mutation");
1030
+ let matched = false;
1031
+ await withKnowledgeMutation(
1032
+ root,
1033
+ () => {
1034
+ if (!matched) throw new Error("pending knowledge mutation changed before recovery");
1035
+ },
1036
+ {
1037
+ resumeTransaction: {
1038
+ purpose: pending.purpose,
1039
+ direction: options.action,
1040
+ validate(transaction) {
1041
+ if (transaction.transactionId !== options.transactionId) {
1042
+ throw new Error(`pending knowledge mutation does not match '${options.transactionId}'`);
1043
+ }
1044
+ matched = true;
1045
+ }
1046
+ }
1047
+ }
1048
+ );
1049
+ }
1050
+ async function withKnowledgeRead(root, read, options = {}) {
1051
+ const resolvedRoot = resolve3(root);
1052
+ if (activeRoots.getStore()?.get(resolvedRoot)?.active) return await read();
1053
+ if (activeReadRoots.getStore()?.has(resolvedRoot)) return await read();
1054
+ const readRoots = new Set(activeReadRoots.getStore());
1055
+ readRoots.add(resolvedRoot);
1056
+ return activeReadRoots.run(readRoots, async () => {
1057
+ const retries = options.retries ?? DEFAULT_READ_RETRIES;
1058
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
1059
+ const before = await readMutationEpoch(resolvedRoot);
1060
+ if (isOdd(before)) {
1061
+ await waitForActiveMutationEpoch(resolvedRoot, before, options);
1062
+ continue;
1063
+ }
1064
+ const result = await read();
1065
+ const after = await readMutationEpoch(resolvedRoot);
1066
+ if (before === after && !isOdd(after)) return result;
1067
+ if (isOdd(after)) await waitForActiveMutationEpoch(resolvedRoot, after, options);
1068
+ }
1069
+ throw new Error("knowledge read could not observe a stable mutation epoch");
1070
+ });
1071
+ }
1072
+ async function acquireDurableFileLock(target, options) {
1073
+ const stale = Math.max(5e3, options.staleMs ?? DEFAULT_STALE_MS);
1074
+ const update = Math.max(1e3, Math.floor(stale / 3));
1075
+ let compromised;
1076
+ let released = false;
1077
+ await mkdir2(dirname2(options.lockfilePath), { recursive: true });
1078
+ const release = await lock(target, {
1079
+ lockfilePath: options.lockfilePath,
1080
+ realpath: false,
1081
+ stale,
1082
+ update,
1083
+ retries: options.retries,
1084
+ onCompromised(error) {
1085
+ compromised = error;
1086
+ }
1087
+ });
1088
+ return {
1089
+ assertOwned() {
1090
+ if (released) throw new KnowledgeLockLostError("knowledge filesystem lock is no longer owned");
1091
+ if (compromised)
1092
+ throw new KnowledgeLockLostError("knowledge filesystem lock was compromised", {
1093
+ cause: compromised
1094
+ });
1095
+ },
1096
+ async release() {
1097
+ if (released) return;
1098
+ released = true;
1099
+ let releaseError;
1100
+ try {
1101
+ await release();
1102
+ } catch (error) {
1103
+ releaseError = error;
1104
+ }
1105
+ if (releaseError && compromised) {
1106
+ throw new AggregateError(
1107
+ [releaseError, compromised],
1108
+ "knowledge filesystem lock was compromised and could not be released"
1109
+ );
1110
+ }
1111
+ if (releaseError) throw releaseError;
1112
+ if (compromised) {
1113
+ throw new KnowledgeLockLostError("knowledge filesystem lock was compromised", {
1114
+ cause: compromised
1115
+ });
1116
+ }
1117
+ }
1118
+ };
1119
+ }
1120
+ async function beginMutationEpoch(cacheDir) {
1121
+ const current = await readMutationEpochFromCache(cacheDir);
1122
+ const odd = isOdd(current) ? current : current + 1;
1123
+ await writeMutationEpoch(cacheDir, odd);
1124
+ return odd;
1125
+ }
1126
+ async function finishMutationEpoch(cacheDir, odd) {
1127
+ await writeMutationEpoch(cacheDir, isOdd(odd) ? odd + 1 : odd);
1128
+ }
1129
+ async function readMutationEpoch(root) {
1130
+ try {
1131
+ return await withSafeDirectory(root, ".agent-knowledge", false, readMutationEpochFromCache);
1132
+ } catch (error) {
1133
+ if (isMissingFile(error)) return 0;
1134
+ throw error;
1135
+ }
1136
+ }
1137
+ async function readMutationEpochFromCache(cacheDir) {
1138
+ try {
1139
+ const text = await withSafeDescendant(
1140
+ cacheDir,
1141
+ "mutation-epoch.json",
1142
+ (path) => readFile(path, "utf8")
1143
+ );
1144
+ const parsed = JSON.parse(text);
1145
+ const epoch = parsed.epoch;
1146
+ if (typeof epoch !== "number" || !Number.isSafeInteger(epoch) || epoch < 0) {
1147
+ throw new Error("knowledge mutation epoch is invalid");
1148
+ }
1149
+ return epoch;
1150
+ } catch (error) {
1151
+ if (isMissingFile(error)) return 0;
1152
+ throw error;
1153
+ }
1154
+ }
1155
+ async function writeMutationEpoch(cacheDir, epoch) {
1156
+ await writeJsonDurableWithinRoot(cacheDir, "mutation-epoch.json", {
1157
+ epoch,
1158
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1159
+ });
1160
+ }
1161
+ async function waitForActiveMutationEpoch(root, epoch, options) {
1162
+ if (!await hasActiveMutationLock(root, options)) {
1163
+ throw new Error(`knowledge mutation epoch ${epoch} is odd with no active writer`);
1164
+ }
1165
+ await new Promise((resolve4) => setTimeout(resolve4, options.waitMs ?? DEFAULT_READ_WAIT_MS));
1166
+ }
1167
+ async function hasActiveMutationLock(root, options) {
1168
+ try {
1169
+ return await withSafeDirectory(
1170
+ root,
1171
+ ".agent-knowledge",
1172
+ false,
1173
+ (cacheDir) => check(root, {
1174
+ lockfilePath: join2(cacheDir, "mutation.lock.durable"),
1175
+ realpath: false,
1176
+ stale: Math.max(5e3, options.staleMs ?? DEFAULT_STALE_MS)
1177
+ })
1178
+ );
1179
+ } catch (error) {
1180
+ if (isMissingFile(error)) return false;
1181
+ throw error;
1182
+ }
1183
+ }
1184
+ function isOdd(value) {
1185
+ return value % 2 === 1;
1186
+ }
1187
+
1188
+ // src/schemas.ts
1189
+ import { z as z2 } from "zod";
1190
+ var SourceAnchorSchema = z2.object({
1191
+ id: z2.string().min(1),
1192
+ sourceId: z2.string().min(1),
1193
+ label: z2.string().optional(),
1194
+ page: z2.number().int().positive().optional(),
1195
+ lineStart: z2.number().int().positive().optional(),
1196
+ lineEnd: z2.number().int().positive().optional(),
1197
+ charStart: z2.number().int().nonnegative().optional(),
1198
+ charEnd: z2.number().int().nonnegative().optional(),
1199
+ timestampMs: z2.number().nonnegative().optional(),
1200
+ metadata: z2.record(z2.string(), z2.unknown()).optional()
1201
+ });
1202
+ var SourceRecordSchema = z2.object({
1203
+ id: z2.string().min(1),
1204
+ uri: z2.string().min(1),
1205
+ title: z2.string().optional(),
1206
+ mediaType: z2.string().optional(),
1207
+ contentHash: z2.string().min(16),
1208
+ text: z2.string().optional(),
1209
+ anchors: z2.array(SourceAnchorSchema).optional(),
1210
+ validUntil: z2.iso.datetime().optional(),
1211
+ lastVerifiedAt: z2.iso.datetime().optional(),
1212
+ metadata: z2.record(z2.string(), z2.unknown()).optional(),
1213
+ createdAt: z2.string().min(1)
1214
+ });
1215
+ var KnowledgePageSchema = z2.object({
1216
+ id: z2.string().min(1),
1217
+ path: z2.string().min(1),
1218
+ title: z2.string().min(1),
1219
+ text: z2.string(),
1220
+ frontmatter: z2.record(z2.string(), z2.unknown()),
1221
+ sourceIds: z2.array(z2.string()),
1222
+ tags: z2.array(z2.string()),
1223
+ outLinks: z2.array(z2.string())
1224
+ });
1225
+ var KnowledgeGraphNodeSchema = z2.object({
1226
+ id: z2.string(),
1227
+ title: z2.string(),
1228
+ path: z2.string(),
1229
+ tags: z2.array(z2.string()),
1230
+ sourceIds: z2.array(z2.string()),
1231
+ outDegree: z2.number().int().nonnegative(),
1232
+ inDegree: z2.number().int().nonnegative()
1233
+ });
1234
+ var KnowledgeGraphEdgeSchema = z2.object({
1235
+ source: z2.string(),
1236
+ target: z2.string(),
1237
+ weight: z2.number(),
1238
+ reasons: z2.array(z2.string())
1239
+ });
1240
+ var KnowledgeIndexSchema = z2.object({
1241
+ root: z2.string(),
1242
+ generatedAt: z2.string(),
1243
+ sources: z2.array(SourceRecordSchema),
1244
+ pages: z2.array(KnowledgePageSchema),
1245
+ graph: z2.object({
1246
+ nodes: z2.array(KnowledgeGraphNodeSchema),
1247
+ edges: z2.array(KnowledgeGraphEdgeSchema)
1248
+ })
1249
+ });
1250
+ var KnowledgeEventSchema = z2.object({
1251
+ id: z2.string().min(1),
1252
+ type: z2.enum([
1253
+ "source.added",
1254
+ "proposal.applied",
1255
+ "index.built",
1256
+ "lint.run",
1257
+ "optimization.run",
1258
+ "release.promoted",
1259
+ "release.rejected"
1260
+ ]),
1261
+ createdAt: z2.string().min(1),
1262
+ actor: z2.string().optional(),
1263
+ target: z2.string().optional(),
1264
+ metadata: z2.record(z2.string(), z2.unknown()).optional()
1265
+ });
1266
+ var KnowledgeBaseCandidateSchema = z2.object({
1267
+ id: z2.string().min(1),
1268
+ units: z2.array(
1269
+ z2.object({
1270
+ id: z2.string().min(1),
1271
+ title: z2.string().min(1),
1272
+ text: z2.string(),
1273
+ claims: z2.array(
1274
+ z2.object({
1275
+ id: z2.string().min(1),
1276
+ text: z2.string().min(1),
1277
+ refs: z2.array(
1278
+ z2.object({
1279
+ sourceId: z2.string().min(1),
1280
+ anchorId: z2.string().optional(),
1281
+ quote: z2.string().optional()
1282
+ })
1283
+ ),
1284
+ confidence: z2.number().min(0).max(1).optional(),
1285
+ status: z2.enum(["draft", "active", "superseded", "rejected"]).optional(),
1286
+ metadata: z2.record(z2.string(), z2.unknown()).optional()
1287
+ })
1288
+ ).optional(),
1289
+ relations: z2.array(
1290
+ z2.object({
1291
+ sourceId: z2.string(),
1292
+ targetId: z2.string(),
1293
+ predicate: z2.string(),
1294
+ weight: z2.number().optional(),
1295
+ metadata: z2.record(z2.string(), z2.unknown()).optional()
1296
+ })
1297
+ ).optional(),
1298
+ sourceIds: z2.array(z2.string()).optional(),
1299
+ tags: z2.array(z2.string()).optional(),
1300
+ metadata: z2.record(z2.string(), z2.unknown()).optional(),
1301
+ updatedAt: z2.string().optional()
1302
+ })
1303
+ ),
1304
+ retrievalPolicy: z2.string().optional(),
1305
+ synthesisPolicy: z2.string().optional(),
1306
+ questionPolicy: z2.string().optional(),
1307
+ updatePolicy: z2.string().optional(),
1308
+ metadata: z2.record(z2.string(), z2.unknown()).optional()
1309
+ });
1310
+
1311
+ // src/frontmatter.ts
1312
+ function parseFrontmatter(content) {
1313
+ const normalized = content.replace(/\r\n/g, "\n");
1314
+ if (!normalized.startsWith("---\n")) return { frontmatter: {}, body: normalized };
1315
+ const end = normalized.indexOf("\n---", 4);
1316
+ if (end < 0) return { frontmatter: {}, body: normalized };
1317
+ const raw = normalized.slice(4, end);
1318
+ const after = normalized.slice(end).replace(/^\n---\s*\n?/, "");
1319
+ return { frontmatter: parseSimpleYaml(raw), body: after };
1320
+ }
1321
+ function formatFrontmatter(frontmatter, body) {
1322
+ const lines = Object.entries(frontmatter).filter(([, value]) => value !== void 0).flatMap(([key, value]) => formatYamlField(key, value));
1323
+ if (lines.length === 0) return body;
1324
+ return `---
1325
+ ${lines.join("\n")}
1326
+ ---
1327
+ ${body.replace(/^\n+/, "")}`;
1328
+ }
1329
+ function parseSimpleYaml(raw) {
1330
+ const out = {};
1331
+ const lines = raw.split("\n");
1332
+ for (let i = 0; i < lines.length; i++) {
1333
+ const line = lines[i];
1334
+ const scalar = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
1335
+ if (!scalar) continue;
1336
+ const key = scalar[1];
1337
+ const rest = scalar[2].trim();
1338
+ if (rest === "") {
1339
+ const items = [];
1340
+ while (i + 1 < lines.length) {
1341
+ const item = /^\s*-\s*(.+?)\s*$/.exec(lines[i + 1]);
1342
+ if (!item) break;
1343
+ items.push(unquote(item[1]));
1344
+ i++;
1345
+ }
1346
+ out[key] = items;
1347
+ continue;
1348
+ }
1349
+ if (rest.startsWith("[") && rest.endsWith("]")) {
1350
+ out[key] = rest.slice(1, -1).split(",").map((part) => unquote(part.trim())).filter(Boolean);
1351
+ } else if (rest === "true" || rest === "false") {
1352
+ out[key] = rest === "true";
1353
+ } else if (/^-?\d+(?:\.\d+)?$/.test(rest)) {
1354
+ out[key] = Number(rest);
1355
+ } else {
1356
+ out[key] = unquote(rest);
1357
+ }
1358
+ }
1359
+ return out;
1360
+ }
1361
+ function formatYamlField(key, value) {
1362
+ if (Array.isArray(value)) {
1363
+ return [`${key}:`, ...value.map((item) => ` - ${String(item)}`)];
1364
+ }
1365
+ if (typeof value === "string") return [`${key}: ${value}`];
1366
+ if (typeof value === "number" || typeof value === "boolean") return [`${key}: ${String(value)}`];
1367
+ return [`${key}: ${JSON.stringify(value)}`];
1368
+ }
1369
+ function unquote(value) {
1370
+ return value.replace(/^['"]|['"]$/g, "");
1371
+ }
1372
+
1373
+ // src/store.ts
1374
+ import { join as join3 } from "path";
1375
+ function layoutFor(root) {
1376
+ return {
1377
+ root,
1378
+ knowledgeDir: join3(root, "knowledge"),
1379
+ rawSourcesDir: join3(root, "raw", "sources"),
1380
+ sourceRegistryPath: join3(root, ".agent-knowledge", "sources.json"),
1381
+ indexPath: join3(root, "knowledge", "index.md"),
1382
+ logPath: join3(root, "knowledge", "log.md"),
1383
+ cacheDir: join3(root, ".agent-knowledge")
1384
+ };
1385
+ }
1386
+ var SCAFFOLD_PAGE_BASENAMES = ["index.md", "log.md"];
1387
+ function isScaffoldPath(path) {
1388
+ const normalized = path.replace(/\\/g, "/");
1389
+ for (const basename3 of SCAFFOLD_PAGE_BASENAMES) {
1390
+ if (normalized === `knowledge/${basename3}`) return true;
1391
+ if (normalized.endsWith(`/${basename3}`)) return true;
1392
+ }
1393
+ return false;
1394
+ }
1395
+ async function initKnowledgeBase(root) {
1396
+ const layout = layoutFor(root);
1397
+ if (await knowledgeBaseInitialized(layout)) return layout;
1398
+ return withKnowledgeMutation(root, () => initKnowledgeBaseUnlocked(root));
1399
+ }
1400
+ async function initKnowledgeBaseUnlocked(root) {
1401
+ const layout = layoutFor(root);
1402
+ await withSafeDirectory(root, "knowledge", true, () => void 0);
1403
+ await withSafeDirectory(root, "raw/sources", true, () => void 0);
1404
+ await withSafeDirectory(root, ".agent-knowledge", true, () => void 0);
1405
+ await writeIfMissing(root, "knowledge/index.md", "# Knowledge Index\n\n");
1406
+ await writeIfMissing(root, "knowledge/log.md", "# Knowledge Log\n\n");
1407
+ await writeIfMissing(
1408
+ root,
1409
+ ".agent-knowledge/sources.json",
1410
+ '{\n "generatedAt": "1970-01-01T00:00:00.000Z",\n "sources": []\n}\n'
1411
+ );
1412
+ return layout;
1413
+ }
1414
+ async function loadKnowledgePages(root) {
1415
+ return withKnowledgeRead(root, () => loadKnowledgePagesUnlocked(root));
1416
+ }
1417
+ async function loadKnowledgePagesUnlocked(root) {
1418
+ let files;
1419
+ try {
1420
+ files = await listRegularFilesWithinRoot(root, "knowledge");
1421
+ } catch (error) {
1422
+ if (isMissingFile(error)) return [];
1423
+ throw error;
1424
+ }
1425
+ const pages = [];
1426
+ for (const file of files) {
1427
+ const rel = file.path;
1428
+ if (!rel.endsWith(".md")) continue;
1429
+ if (isScaffoldPath(rel)) continue;
1430
+ const content = file.bytes.toString("utf8");
1431
+ const { frontmatter, body } = parseFrontmatter(content);
1432
+ const title = stringField(frontmatter.title) ?? firstHeading(body) ?? rel.split("/").pop().replace(/\.md$/, "");
1433
+ const sourceIds = arrayField(frontmatter.sources);
1434
+ const tags = arrayField(frontmatter.tags);
1435
+ pages.push({
1436
+ id: stringField(frontmatter.id) ?? slugify(rel.replace(/^knowledge\//, "").replace(/\.md$/, "")),
1437
+ path: rel,
1438
+ title,
1439
+ text: body,
1440
+ frontmatter,
1441
+ sourceIds,
1442
+ tags,
1443
+ outLinks: extractWikilinks(body).map(normalizeLinkTarget)
1444
+ });
1445
+ }
1446
+ pages.sort((a, b) => a.path.localeCompare(b.path));
1447
+ return pages;
1448
+ }
1449
+ async function writeJson(path, value) {
1450
+ await writeJsonDurable(path, value);
1451
+ }
1452
+ async function writeIfMissing(root, relativePath, content) {
1453
+ try {
1454
+ await readRegularFileWithinRoot(root, relativePath);
1455
+ } catch (error) {
1456
+ if (!isMissingFile(error)) throw error;
1457
+ await writeFileDurableWithinRoot(root, relativePath, content, { encoding: "utf8" });
1458
+ }
1459
+ }
1460
+ async function knowledgeBaseInitialized(layout) {
1461
+ try {
1462
+ await withSafeDirectory(layout.root, "knowledge", false, () => void 0);
1463
+ await withSafeDirectory(layout.root, "raw/sources", false, () => void 0);
1464
+ await withSafeDirectory(layout.root, ".agent-knowledge", false, () => void 0);
1465
+ await readRegularFileWithinRoot(layout.root, "knowledge/index.md");
1466
+ await readRegularFileWithinRoot(layout.root, "knowledge/log.md");
1467
+ await readRegularFileWithinRoot(layout.root, ".agent-knowledge/sources.json");
1468
+ return true;
1469
+ } catch (error) {
1470
+ if (isMissingFile(error)) return false;
1471
+ throw error;
1472
+ }
1473
+ }
1474
+ function stringField(value) {
1475
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
1476
+ }
1477
+ function arrayField(value) {
1478
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1479
+ }
1480
+ function firstHeading(body) {
1481
+ return /^#\s+(.+)$/m.exec(body)?.[1]?.trim();
1482
+ }
1483
+
1484
+ // src/sources.ts
1485
+ import { lstat as lstat2, readdir as readdir3, readFile as readFile2 } from "fs/promises";
1486
+ import { basename as basename2, join as join4, relative as relative2 } from "path";
1487
+ import { z as z3 } from "zod";
1488
+ var sourceRegistrySchema = z3.object({
1489
+ generatedAt: z3.string().min(1),
1490
+ sources: z3.array(SourceRecordSchema.passthrough())
1491
+ }).strict();
1492
+ async function loadSourceRegistry(root) {
1493
+ return withKnowledgeRead(root, () => loadSourceRegistryUnlocked(root));
1494
+ }
1495
+ async function loadSourceRegistryUnlocked(root) {
1496
+ try {
1497
+ const file = await readRegularFileWithinRoot(root, ".agent-knowledge/sources.json");
1498
+ return sourceRegistrySchema.parse(JSON.parse(file.bytes.toString("utf8")));
1499
+ } catch (error) {
1500
+ if (error?.code === "ENOENT") {
1501
+ return { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), sources: [] };
1502
+ }
1503
+ throw error;
1504
+ }
1505
+ }
1506
+ async function writeSourceRegistry(root, registry) {
1507
+ await withKnowledgeMutation(root, async () => {
1508
+ await writeJsonDurableWithinRoot(
1509
+ root,
1510
+ ".agent-knowledge/sources.json",
1511
+ sourceRegistrySchema.parse(registry)
1512
+ );
1513
+ });
1514
+ }
1515
+ async function addSourcePath(root, sourcePath, options = {}) {
1516
+ const sourceStat = await lstat2(sourcePath);
1517
+ if (sourceStat.isDirectory()) {
1518
+ const files = await listSourceFiles(sourcePath);
1519
+ const prepared = await Promise.all(
1520
+ files.map((file) => preparePathSource(root, file.path, file.mode, options))
1521
+ );
1522
+ return commitSourceBatch(root, prepared, options);
1523
+ }
1524
+ if (!sourceStat.isFile()) throw new Error(`unsupported source path: ${sourcePath}`);
1525
+ return commitSourceBatch(
1526
+ root,
1527
+ [await preparePathSource(root, sourcePath, sourceStat.mode & 511, options)],
1528
+ options
1529
+ );
1530
+ }
1531
+ async function preparePathSource(root, sourcePath, mode, options) {
1532
+ const bytes = await readFile2(sourcePath);
1533
+ const contentHash3 = sha256(bytes.toString("base64"));
1534
+ const fileName = basename2(sourcePath);
1535
+ const adapters = options.adapters ?? [textSourceAdapter];
1536
+ const adapter = adapters.find((candidate) => candidate.canLoad({ uri: sourcePath, bytes }));
1537
+ const loaded = adapter ? await adapter.load({ uri: sourcePath, bytes }) : {};
1538
+ const id = stableId("src", `${contentHash3}:${fileName}`);
1539
+ const targetRel = rawSourcePath(fileName, contentHash3, ext(fileName));
1540
+ const copyIntoRaw = options.copyIntoRaw ?? true;
1541
+ return {
1542
+ record: {
1543
+ id,
1544
+ uri: copyIntoRaw ? targetRel : sourcePath,
1545
+ title: loaded.title ?? fileName,
1546
+ mediaType: loaded.mediaType ?? mediaTypeFor2(fileName),
1547
+ contentHash: contentHash3,
1548
+ text: loaded.text ?? textPreview(fileName, bytes),
1549
+ anchors: (loaded.anchors ?? []).map((anchor) => ({ ...anchor, sourceId: id })),
1550
+ createdAt: (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
1551
+ metadata: {
1552
+ ...loaded.metadata ?? {},
1553
+ originalPath: sourcePath,
1554
+ sizeBytes: bytes.length,
1555
+ projectRelativePath: relative2(root, sourcePath).replace(/\\/g, "/")
1556
+ }
1557
+ },
1558
+ mutation: copyIntoRaw ? { path: targetRel, content: bytes, mode } : null,
1559
+ purposeParts: [sourcePath, contentHash3, copyIntoRaw, mode]
1560
+ };
1561
+ }
1562
+ async function addSourceText(root, input, options = {}) {
1563
+ const [record] = await commitSourceBatch(root, [await prepareTextSource(input, options)], options);
1564
+ return record;
1565
+ }
1566
+ async function prepareTextSource(input, options) {
1567
+ const text = input.text;
1568
+ const contentHash3 = sha256(text);
1569
+ const fileName = basename2(input.uri) || `${slugify(input.title ?? input.uri)}.txt`;
1570
+ const adapterInput = { uri: input.uri, text, metadata: input.metadata };
1571
+ const adapter = (options.adapters ?? [textSourceAdapter]).find(
1572
+ (candidate) => candidate.canLoad(adapterInput)
1573
+ );
1574
+ const loaded = adapter ? await adapter.load(adapterInput) : {};
1575
+ const id = stableId("src", `${contentHash3}:${input.uri}`);
1576
+ const targetRel = rawSourcePath(fileName, contentHash3, ".txt");
1577
+ const rawContent = text.endsWith("\n") ? text : `${text}
1578
+ `;
1579
+ return {
1580
+ record: {
1581
+ id,
1582
+ uri: targetRel,
1583
+ title: input.title ?? loaded.title ?? fileName,
1584
+ mediaType: input.mediaType ?? loaded.mediaType ?? "text/plain",
1585
+ contentHash: contentHash3,
1586
+ text: loaded.text ?? text.slice(0, 2e5),
1587
+ anchors: (loaded.anchors ?? []).map((anchor) => ({ ...anchor, sourceId: id })),
1588
+ ...input.validUntil === void 0 ? {} : { validUntil: input.validUntil },
1589
+ ...input.lastVerifiedAt === void 0 ? {} : { lastVerifiedAt: input.lastVerifiedAt },
1590
+ createdAt: (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
1591
+ metadata: {
1592
+ ...loaded.metadata ?? {},
1593
+ ...input.metadata ?? {},
1594
+ originalUri: input.uri,
1595
+ sizeBytes: Buffer.byteLength(text, "utf8")
1596
+ }
1597
+ },
1598
+ mutation: { path: targetRel, content: rawContent },
1599
+ purposeParts: [input.uri, contentHash3]
1600
+ };
1601
+ }
1602
+ async function commitSourceBatch(root, prepared, options) {
1603
+ if (prepared.length === 0) return [];
1604
+ const purpose = `source-import:${sha256(JSON.stringify(prepared.map((item) => item.purposeParts)))}`;
1605
+ return withKnowledgeMutation(
1606
+ root,
1607
+ async (lock2) => {
1608
+ const existing = await loadSourceRegistry(root);
1609
+ const records = uniqueSources(prepared.map((item) => item.record));
1610
+ const next = sourceRegistrySchema.parse({
1611
+ generatedAt: (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
1612
+ sources: [
1613
+ ...records,
1614
+ ...existing.sources.filter(
1615
+ (source) => !records.some((record) => record.id === source.id)
1616
+ )
1617
+ ]
1618
+ });
1619
+ await commitKnowledgeFileMutations({
1620
+ root,
1621
+ transactionRoot: lock2.transactionRoot,
1622
+ purpose,
1623
+ mutations: [
1624
+ ...uniqueMutations(prepared.flatMap((item) => item.mutation ? [item.mutation] : [])),
1625
+ {
1626
+ path: relative2(root, sourceRegistryPath(root)).replace(/\\/g, "/"),
1627
+ content: `${JSON.stringify(next, null, 2)}
1628
+ `
1629
+ }
1630
+ ],
1631
+ assertOwned: lock2.assertOwned
1632
+ });
1633
+ return records;
1634
+ },
1635
+ { resumeTransaction: { purpose } }
1636
+ );
1637
+ }
1638
+ function sourceRegistryPath(root) {
1639
+ return join4(layoutFor(root).cacheDir, "sources.json");
1640
+ }
1641
+ function uniqueSources(records) {
1642
+ const byId = /* @__PURE__ */ new Map();
1643
+ for (const record of records) byId.set(record.id, record);
1644
+ return [...byId.values()];
1645
+ }
1646
+ function uniqueMutations(mutations) {
1647
+ const byPath = /* @__PURE__ */ new Map();
1648
+ for (const mutation of mutations) {
1649
+ const existing = byPath.get(mutation.path);
1650
+ if (existing && !sameMutation(existing, mutation)) {
1651
+ throw new Error(`source import produced conflicting raw path: ${mutation.path}`);
1652
+ }
1653
+ byPath.set(mutation.path, mutation);
1654
+ }
1655
+ return [...byPath.values()];
1656
+ }
1657
+ function sameMutation(left, right) {
1658
+ if (left.mode !== right.mode) return false;
1659
+ if (Buffer.isBuffer(left.content) && Buffer.isBuffer(right.content)) {
1660
+ return left.content.equals(right.content);
1661
+ }
1662
+ return left.content === right.content;
1663
+ }
1664
+ async function listSourceFiles(root) {
1665
+ const entries = await readdir3(root, { withFileTypes: true });
1666
+ const out = [];
1667
+ for (const entry of entries) {
1668
+ const full = join4(root, entry.name);
1669
+ const entryStat = await lstat2(full);
1670
+ if (entryStat.isDirectory()) out.push(...await listSourceFiles(full));
1671
+ else if (entryStat.isFile()) out.push({ path: full, mode: entryStat.mode & 511 });
1672
+ else throw new Error(`unsupported directory entry in source import: ${full}`);
1673
+ }
1674
+ return out;
1675
+ }
1676
+ function rawSourcePath(fileName, contentHash3, extension) {
1677
+ return join4(
1678
+ "raw",
1679
+ "sources",
1680
+ `${slugify(fileName.replace(/\.[^.]+$/, ""))}-${contentHash3.slice(0, 8)}${extension}`
1681
+ ).replace(/\\/g, "/");
1682
+ }
1683
+ function ext(fileName) {
1684
+ const idx = fileName.lastIndexOf(".");
1685
+ return idx >= 0 ? fileName.slice(idx) : "";
1686
+ }
1687
+ function mediaTypeFor2(fileName) {
1688
+ const lower = fileName.toLowerCase();
1689
+ if (lower.endsWith(".md")) return "text/markdown";
1690
+ if (lower.endsWith(".txt")) return "text/plain";
1691
+ if (lower.endsWith(".json")) return "application/json";
1692
+ if (lower.endsWith(".csv")) return "text/csv";
1693
+ if (lower.endsWith(".pdf")) return "application/pdf";
1694
+ return "application/octet-stream";
1695
+ }
1696
+ function textPreview(fileName, bytes) {
1697
+ const mediaType = mediaTypeFor2(fileName);
1698
+ if (!mediaType.startsWith("text/") && mediaType !== "application/json") return void 0;
1699
+ return bytes.toString("utf8").slice(0, 2e5);
1700
+ }
1701
+
1702
+ // src/indexer.ts
1703
+ async function buildKnowledgeIndex(root) {
1704
+ return withKnowledgeRead(root, () => buildKnowledgeIndexUnlocked(root));
1705
+ }
1706
+ async function buildKnowledgeIndexUnlocked(root) {
1707
+ const [pages, sourceRegistry] = await Promise.all([
1708
+ loadKnowledgePages(root),
1709
+ loadSourceRegistry(root)
1710
+ ]);
1711
+ const index = {
1712
+ root,
1713
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1714
+ sources: sourceRegistry.sources,
1715
+ pages,
1716
+ graph: buildKnowledgeGraph(pages)
1717
+ };
1718
+ return index;
1719
+ }
1720
+ async function writeKnowledgeIndex(root) {
1721
+ return withKnowledgeMutation(root, async () => {
1722
+ const index = await buildKnowledgeIndexUnlocked(root);
1723
+ await writeJsonDurableWithinRoot(root, ".agent-knowledge/index.json", index);
1724
+ return index;
1725
+ });
1726
+ }
1727
+
1728
+ // src/lint.ts
1729
+ function lintKnowledgeIndex(index) {
1730
+ const findings = [];
1731
+ const byTarget = /* @__PURE__ */ new Set();
1732
+ const titles = /* @__PURE__ */ new Map();
1733
+ const sourceIds = new Set(index.sources.map((source) => source.id));
1734
+ const anchorIds = new Map(
1735
+ index.sources.map((source) => [
1736
+ source.id,
1737
+ new Set((source.anchors ?? []).map((anchor) => anchor.id))
1738
+ ])
1739
+ );
1740
+ const pageIds = /* @__PURE__ */ new Map();
1741
+ const sourceHashes = /* @__PURE__ */ new Map();
1742
+ for (const page of index.pages) {
1743
+ pageIds.set(page.id, [...pageIds.get(page.id) ?? [], page.path]);
1744
+ byTarget.add(normalizeLinkTarget(page.id));
1745
+ byTarget.add(normalizeLinkTarget(page.title));
1746
+ byTarget.add(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")));
1747
+ const titleKey = page.title.toLowerCase();
1748
+ titles.set(titleKey, [...titles.get(titleKey) ?? [], page.path]);
1749
+ }
1750
+ for (const source of index.sources) {
1751
+ sourceHashes.set(source.contentHash, [
1752
+ ...sourceHashes.get(source.contentHash) ?? [],
1753
+ source.id
1754
+ ]);
1755
+ }
1756
+ const inbound = /* @__PURE__ */ new Map();
1757
+ for (const page of index.pages) inbound.set(page.id, 0);
1758
+ for (const page of index.pages) {
1759
+ if (page.outLinks.length === 0 && !isScaffoldPath(page.path)) {
1760
+ findings.push({
1761
+ type: "no-outlinks",
1762
+ severity: "info",
1763
+ page: page.path,
1764
+ message: "Page has no wikilinks to other knowledge pages."
1765
+ });
1766
+ }
1767
+ for (const link of page.outLinks) {
1768
+ if (!byTarget.has(normalizeLinkTarget(link))) {
1769
+ findings.push({
1770
+ type: "broken-link",
1771
+ severity: "warning",
1772
+ page: page.path,
1773
+ message: `Broken wikilink [[${link}]].`
1774
+ });
1775
+ }
1776
+ }
1777
+ }
1778
+ for (const edge of index.graph.edges)
1779
+ inbound.set(edge.target, (inbound.get(edge.target) ?? 0) + 1);
1780
+ for (const page of index.pages) {
1781
+ if (!isScaffoldPath(page.path) && (inbound.get(page.id) ?? 0) === 0) {
1782
+ findings.push({
1783
+ type: "orphan",
1784
+ severity: "info",
1785
+ page: page.path,
1786
+ message: "No other page links to this page."
1787
+ });
1788
+ }
1789
+ if (/\bclaim\b/i.test(page.text) && page.sourceIds.length === 0) {
1790
+ findings.push({
1791
+ type: "uncited-claim",
1792
+ severity: "warning",
1793
+ page: page.path,
1794
+ message: "Page appears to contain claims but has no sources frontmatter."
1795
+ });
1796
+ }
1797
+ for (const sourceId of page.sourceIds) {
1798
+ if (!sourceIds.has(sourceId)) {
1799
+ findings.push({
1800
+ type: "missing-source",
1801
+ severity: "error",
1802
+ page: page.path,
1803
+ message: `Page cites unknown source "${sourceId}".`,
1804
+ metadata: { sourceId }
1805
+ });
1806
+ }
1807
+ }
1808
+ for (const ref of extractSourceRefs(page.text)) {
1809
+ if (!sourceIds.has(ref.sourceId)) {
1810
+ findings.push({
1811
+ type: "missing-source",
1812
+ severity: "error",
1813
+ page: page.path,
1814
+ message: `Page cites unknown source "${ref.sourceId}".`,
1815
+ metadata: ref
1816
+ });
1817
+ } else if (ref.anchorId && !anchorIds.get(ref.sourceId)?.has(ref.anchorId)) {
1818
+ findings.push({
1819
+ type: "missing-source",
1820
+ severity: "error",
1821
+ page: page.path,
1822
+ message: `Page cites unknown source anchor "${ref.sourceId}#${ref.anchorId}".`,
1823
+ metadata: ref
1824
+ });
1825
+ }
1826
+ }
1827
+ }
1828
+ for (const [title, paths] of titles) {
1829
+ if (title && paths.length > 1) {
1830
+ findings.push({
1831
+ type: "duplicate-title",
1832
+ severity: "warning",
1833
+ message: `Duplicate title "${title}" in ${paths.join(", ")}.`,
1834
+ metadata: { paths }
1835
+ });
1836
+ }
1837
+ }
1838
+ for (const [id, paths] of pageIds) {
1839
+ if (id && paths.length > 1) {
1840
+ findings.push({
1841
+ type: "duplicate-page-id",
1842
+ severity: "error",
1843
+ message: `Duplicate page id "${id}" in ${paths.join(", ")}.`,
1844
+ metadata: { paths }
1845
+ });
1846
+ }
1847
+ }
1848
+ for (const [hash, ids] of sourceHashes) {
1849
+ if (hash && ids.length > 1) {
1850
+ findings.push({
1851
+ type: "duplicate-source-hash",
1852
+ severity: "warning",
1853
+ message: `Duplicate source content hash across ${ids.join(", ")}.`,
1854
+ metadata: { sourceIds: ids }
1855
+ });
1856
+ }
1857
+ }
1858
+ return findings;
1859
+ }
1860
+ function extractSourceRefs(text) {
1861
+ const refs = [];
1862
+ const regex = /\[\^([A-Za-z0-9_-]+)(?:#([A-Za-z0-9_.:-]+))?\]/g;
1863
+ let match;
1864
+ match = regex.exec(text);
1865
+ while (match !== null) {
1866
+ refs.push({ sourceId: match[1], anchorId: match[2] });
1867
+ match = regex.exec(text);
1868
+ }
1869
+ return refs;
1870
+ }
1871
+
1872
+ // src/validate.ts
1873
+ function validateKnowledgeIndex(index, options = {}) {
1874
+ const findings = [...lintKnowledgeIndex(index)];
1875
+ const parsed = KnowledgeIndexSchema.safeParse(index);
1876
+ if (!parsed.success) {
1877
+ findings.push({
1878
+ type: "missing-frontmatter",
1879
+ severity: "error",
1880
+ message: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
1881
+ });
1882
+ }
1883
+ if (options.strict) {
1884
+ for (const page of index.pages) {
1885
+ if (isScaffoldPath(page.path)) continue;
1886
+ if (!page.frontmatter.id || !page.frontmatter.title) {
1887
+ findings.push({
1888
+ type: "missing-frontmatter",
1889
+ severity: "error",
1890
+ page: page.path,
1891
+ message: "Strict mode requires id and title frontmatter."
1892
+ });
1893
+ }
1894
+ }
1895
+ }
1896
+ return { ok: !findings.some((finding) => finding.severity === "error"), findings };
1897
+ }
1898
+
1899
+ // src/write-protocol.ts
1900
+ var OPENER_LINE = /^---\s*FILE:\s*(.+?)\s*---\s*$/i;
1901
+ var CLOSER_LINE = /^---\s*END\s+FILE\s*---\s*$/i;
1902
+ var FENCE_LINE = /^\s{0,3}(```+|~~~+)/;
1903
+ function isSafeKnowledgePath(path, allowedPrefixes = ["knowledge/"]) {
1904
+ if (typeof path !== "string" || path.trim() === "") return false;
1905
+ const controlRangeRegex = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}]`);
1906
+ if (controlRangeRegex.test(path)) return false;
1907
+ if (path.startsWith("/") || path.startsWith("\\")) return false;
1908
+ if (/^[a-zA-Z]:/.test(path)) return false;
1909
+ const normalized = path.replace(/\\/g, "/");
1910
+ if (normalized.split("/").some((part) => part === "..")) return false;
1911
+ return allowedPrefixes.some((prefix) => normalized.startsWith(prefix));
1912
+ }
1913
+ function parseKnowledgeWriteBlocks(text, allowedPrefixes = ["knowledge/"]) {
1914
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
1915
+ const blocks = [];
1916
+ const warnings = [];
1917
+ let i = 0;
1918
+ while (i < lines.length) {
1919
+ const opener = OPENER_LINE.exec(lines[i]);
1920
+ if (!opener) {
1921
+ i++;
1922
+ continue;
1923
+ }
1924
+ const path = opener[1].trim();
1925
+ i++;
1926
+ const contentLines = [];
1927
+ let fenceMarker = null;
1928
+ let fenceLen = 0;
1929
+ let closed = false;
1930
+ while (i < lines.length) {
1931
+ const line = lines[i];
1932
+ const fence = FENCE_LINE.exec(line);
1933
+ if (fence) {
1934
+ const run = fence[1];
1935
+ const char = run[0];
1936
+ if (fenceMarker === null) {
1937
+ fenceMarker = char;
1938
+ fenceLen = run.length;
1939
+ } else if (char === fenceMarker && run.length >= fenceLen) {
1940
+ fenceMarker = null;
1941
+ fenceLen = 0;
1942
+ }
1943
+ contentLines.push(line);
1944
+ i++;
1945
+ continue;
1946
+ }
1947
+ if (fenceMarker === null && CLOSER_LINE.test(line)) {
1948
+ closed = true;
1949
+ i++;
1950
+ break;
1951
+ }
1952
+ contentLines.push(line);
1953
+ i++;
1954
+ }
1955
+ if (!closed) {
1956
+ warnings.push(`FILE block "${path || "(empty)"}" was not closed before end of stream.`);
1957
+ continue;
1958
+ }
1959
+ if (!isSafeKnowledgePath(path, allowedPrefixes)) {
1960
+ warnings.push(`FILE block with unsafe path "${path}" rejected.`);
1961
+ continue;
1962
+ }
1963
+ blocks.push({ path, content: contentLines.join("\n") });
1964
+ }
1965
+ return { blocks, warnings };
1966
+ }
1967
+
1968
+ // src/proposals.ts
1969
+ import { readFile as readFile3 } from "fs/promises";
1970
+ import { contentHash as contentHash2 } from "@tangle-network/agent-eval";
1971
+ async function applyKnowledgeWriteBlocks(root, proposalText) {
1972
+ const parsed = parseKnowledgeWriteBlocks(proposalText);
1973
+ const purpose = `knowledge-proposal:${contentHash2(parsed.blocks)}`;
1974
+ return withKnowledgeMutation(
1975
+ root,
1976
+ async (lock2) => {
1977
+ if (parsed.blocks.length > 0) {
1978
+ await commitKnowledgeFileMutations({
1979
+ root,
1980
+ transactionRoot: lock2.transactionRoot,
1981
+ purpose,
1982
+ mutations: parsed.blocks.map((block) => ({
1983
+ path: block.path,
1984
+ content: block.content.endsWith("\n") ? block.content : `${block.content}
1985
+ `
1986
+ })),
1987
+ assertOwned: lock2.assertOwned
1988
+ });
1989
+ }
1990
+ return { written: parsed.blocks.map((block) => block.path), warnings: parsed.warnings };
1991
+ },
1992
+ { resumeTransaction: { purpose } }
1993
+ );
1994
+ }
1995
+ async function applyKnowledgeWriteBlocksFile(root, proposalPath) {
1996
+ return applyKnowledgeWriteBlocks(root, await readFile3(proposalPath, "utf8"));
1997
+ }
1998
+
1999
+ // src/metadata.ts
2000
+ function stringMetadata(metadata, key) {
2001
+ const value = metadata?.[key];
2002
+ return typeof value === "string" ? value : void 0;
2003
+ }
2004
+
2005
+ // src/inspect.ts
2006
+ function inspectKnowledgeIndex(index, options = {}) {
2007
+ const now = options.now ?? /* @__PURE__ */ new Date();
2008
+ const findings = lintKnowledgeIndex(index);
2009
+ const degree = new Map(index.graph.nodes.map((node) => [node.id, node.inDegree + node.outDegree]));
2010
+ const sourceFreshness = index.sources.map((source) => inspectSourceFreshness(source, now));
2011
+ return {
2012
+ pageCount: index.pages.length,
2013
+ sourceCount: index.sources.length,
2014
+ expiredSourceCount: sourceFreshness.filter((source) => source.status === "expired").length,
2015
+ staleSourceCount: sourceFreshness.filter((source) => source.status !== "fresh").length,
2016
+ edgeCount: index.graph.edges.length,
2017
+ findingCount: findings.length,
2018
+ blockingFindingCount: findings.filter((finding) => finding.severity === "error").length,
2019
+ topPages: [...index.pages].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, 10).map((page) => ({
2020
+ path: page.path,
2021
+ title: page.title,
2022
+ degree: degree.get(page.id) ?? 0,
2023
+ sources: page.sourceIds.length
2024
+ })),
2025
+ sourceFreshness,
2026
+ findings
2027
+ };
2028
+ }
2029
+ function inspectSourceFreshness(source, now) {
2030
+ const validUntil = source.validUntil ?? stringMetadata(source.metadata, "validUntil") ?? stringMetadata(source.metadata, "expiresAt");
2031
+ const lastVerifiedAt = source.lastVerifiedAt ?? stringMetadata(source.metadata, "lastVerifiedAt");
2032
+ const status = validUntil && Number.isFinite(Date.parse(validUntil)) ? Date.parse(validUntil) <= now.getTime() ? "expired" : "fresh" : "unknown";
2033
+ return { id: source.id, title: source.title, uri: source.uri, status, validUntil, lastVerifiedAt };
2034
+ }
2035
+ function explainKnowledgeTarget(index, target) {
2036
+ const page = index.pages.find(
2037
+ (candidate) => candidate.path === target || candidate.id === target || candidate.title.toLowerCase() === target.toLowerCase()
2038
+ );
2039
+ const inbound = page ? index.graph.edges.filter((edge) => edge.target === page.id).map(
2040
+ (edge) => index.pages.find((candidate) => candidate.id === edge.source)?.path ?? edge.source
2041
+ ) : [];
2042
+ const related = page ? searchKnowledge(index, `${page.title} ${page.tags.join(" ")}`, 6).filter((result) => result.page.id !== page.id).map((result) => ({
2043
+ path: result.page.path,
2044
+ title: result.page.title,
2045
+ score: result.score
2046
+ })) : searchKnowledge(index, target, 6).map((result) => ({
2047
+ path: result.page.path,
2048
+ title: result.page.title,
2049
+ score: result.score
2050
+ }));
2051
+ return {
2052
+ target,
2053
+ page,
2054
+ sources: page ? index.sources.filter((source) => page.sourceIds.includes(source.id)).map((source) => ({ id: source.id, title: source.title, uri: source.uri })) : [],
2055
+ links: page?.outLinks ?? [],
2056
+ inbound,
2057
+ related
2058
+ };
2059
+ }
2060
+
2061
+ export {
2062
+ textSourceAdapter,
2063
+ mediaTypeFor,
2064
+ writeFileDurableWithinRoot,
2065
+ writeJsonDurableWithinRoot,
2066
+ renameDurable,
2067
+ withSafeDirectory,
2068
+ readRegularFileWithinRoot,
2069
+ listRegularFilesWithinRoot,
2070
+ isMissingFile,
2071
+ knowledgeFileTransactionPlanHash,
2072
+ prepareKnowledgeFileTransaction,
2073
+ applyKnowledgeFileTransaction,
2074
+ finishKnowledgeFileTransaction,
2075
+ rollbackKnowledgeFileTransaction,
2076
+ assertKnowledgeMutationPath,
2077
+ WIKILINK_REGEX,
2078
+ extractWikilinks,
2079
+ normalizeLinkTarget,
2080
+ buildKnowledgeGraph,
2081
+ withKnowledgeMutation,
2082
+ inspectPendingKnowledgeMutation,
2083
+ recoverPendingKnowledgeMutation,
2084
+ withKnowledgeRead,
2085
+ acquireDurableFileLock,
2086
+ SourceAnchorSchema,
2087
+ SourceRecordSchema,
2088
+ KnowledgePageSchema,
2089
+ KnowledgeGraphNodeSchema,
2090
+ KnowledgeGraphEdgeSchema,
2091
+ KnowledgeIndexSchema,
2092
+ KnowledgeEventSchema,
2093
+ KnowledgeBaseCandidateSchema,
2094
+ parseFrontmatter,
2095
+ formatFrontmatter,
2096
+ layoutFor,
2097
+ SCAFFOLD_PAGE_BASENAMES,
2098
+ isScaffoldPath,
2099
+ initKnowledgeBase,
2100
+ loadKnowledgePages,
2101
+ writeJson,
2102
+ loadSourceRegistry,
2103
+ writeSourceRegistry,
2104
+ addSourcePath,
2105
+ addSourceText,
2106
+ sourceRegistryPath,
2107
+ buildKnowledgeIndex,
2108
+ writeKnowledgeIndex,
2109
+ lintKnowledgeIndex,
2110
+ validateKnowledgeIndex,
2111
+ isSafeKnowledgePath,
2112
+ parseKnowledgeWriteBlocks,
2113
+ applyKnowledgeWriteBlocks,
2114
+ applyKnowledgeWriteBlocksFile,
2115
+ stringMetadata,
2116
+ inspectKnowledgeIndex,
2117
+ explainKnowledgeTarget
2118
+ };
2119
+ //# sourceMappingURL=chunk-R7ADUA44.js.map