@tangle-network/agent-knowledge 20.0.0 → 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import { i as textSourceId, n as slugify, r as stableId, t as sha256 } from "./ids-Bevz_pXV.js";
2
+ import { r as searchKnowledge } from "./search-BBr5eUyT.js";
2
3
  import { canonicalCandidateBytes, canonicalCandidateDigest, sha256Bytes, sha256DigestSchema } from "@tangle-network/agent-interface";
3
4
  import { contentHash } from "@tangle-network/agent-eval";
4
5
  import { z } from "zod";
5
6
  import { createHash, randomUUID } from "node:crypto";
6
- import { constants, lstatSync } from "node:fs";
7
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
7
8
  import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm } from "node:fs/promises";
8
9
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
9
10
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -399,6 +400,19 @@ function normalizePagesDirectory(value = DEFAULT_PAGES_DIRECTORY) {
399
400
  return normalized;
400
401
  }
401
402
  //#endregion
403
+ //#region src/bounded-file-io.ts
404
+ /** Keep filesystem descriptors bounded and drain a failed batch before its owner releases handles. */
405
+ async function mapFileIo(values, operation) {
406
+ const results = [];
407
+ for (let start = 0; start < values.length; start += 8) {
408
+ const batch = await Promise.allSettled(values.slice(start, start + 8).map((value, offset) => Promise.resolve().then(() => operation(value, start + offset))));
409
+ const failed = batch.find((result) => result.status === "rejected");
410
+ if (failed?.status === "rejected") throw failed.reason;
411
+ for (const result of batch) if (result.status === "fulfilled") results.push(result.value);
412
+ }
413
+ return results;
414
+ }
415
+ //#endregion
402
416
  //#region src/file-transaction.ts
403
417
  const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
404
418
  const fileModeSchema = z.number().int().min(0).max(511);
@@ -460,7 +474,7 @@ async function prepareKnowledgeFileTransaction(input) {
460
474
  const activeTransactions = await activeTransactionDirectoryNames(transactionRoot);
461
475
  if (activeTransactions.length > 0) throw new Error(`knowledge file transaction is already active: ${activeTransactions.join(", ")}`);
462
476
  const paths = /* @__PURE__ */ new Set();
463
- const changed = (await Promise.all(input.mutations.map(async (mutation, index) => {
477
+ const changed = (await mapFileIo(input.mutations, async (mutation, index) => {
464
478
  const path = assertKnowledgeMutationPath(mutation.path, boundPagesDirectory, input.researchState);
465
479
  if (paths.has(path)) throw new Error(`knowledge file transaction repeats path: ${path}`);
466
480
  if (mutation.content === null && mutation.mode !== void 0) throw new Error(`deleted knowledge file cannot declare a mode: ${path}`);
@@ -485,7 +499,7 @@ async function prepareKnowledgeFileTransaction(input) {
485
499
  before: before?.bytes ?? null,
486
500
  after
487
501
  };
488
- }))).filter((entry) => entry !== null);
502
+ })).filter((entry) => entry !== null);
489
503
  if (changed.length === 0) return null;
490
504
  const preparationDir = await mkdtemp(join(transactionRoot, "prepare-"));
491
505
  let activated = false;
@@ -504,10 +518,14 @@ async function prepareKnowledgeFileTransaction(input) {
504
518
  entries: changed.map((item) => item.entry)
505
519
  });
506
520
  for (const side of ["before", "after"]) if (changed.some((item) => item[side])) await mkdir(join(preparationDir, side));
507
- await Promise.all([...changed.flatMap((item) => ["before", "after"].flatMap((side) => {
521
+ await mapFileIo(changed.flatMap((item) => ["before", "after"].flatMap((side) => {
508
522
  const bytes = item[side];
509
- return bytes ? [writeFileDurable(snapshotPath(preparationDir, side, item.entry.index), bytes)] : [];
510
- })), writeJsonDurable(join(preparationDir, "transaction.json"), transaction)]);
523
+ return bytes ? [{
524
+ path: snapshotPath(preparationDir, side, item.entry.index),
525
+ bytes
526
+ }] : [];
527
+ })), (item) => writeFileDurable(item.path, item.bytes));
528
+ await writeJsonDurable(join(preparationDir, "transaction.json"), transaction);
511
529
  const activeDir = join(transactionRoot, activeTransactionDirectoryName(transaction.transactionId));
512
530
  await renameDurable(preparationDir, activeDir);
513
531
  activated = true;
@@ -833,7 +851,7 @@ function knowledgeMutationPathPrefixes(pagesDirectory) {
833
851
  }
834
852
  function assertKnowledgeMutationPath(path, pagesDirectory, researchState = false) {
835
853
  const normalized = normalizeTransactionPath(path);
836
- if (normalized === SOURCE_REGISTRY_PATH || researchState && (normalized === ".agent-knowledge/events.json" || /^\.agent-knowledge\/claim-ledgers\/[^/]+\.json$/.test(normalized)) || knowledgeMutationPathPrefixes(pagesDirectory).some((prefix) => normalized.startsWith(prefix))) return normalized;
854
+ if (normalized === SOURCE_REGISTRY_PATH || researchState && (normalized === ".agent-knowledge/events.json" || normalized === "events.json" || /^(?:\.agent-knowledge\/)?event-log\/(?:state\.json|(?:records\/[a-f0-9]{64}|index\/[a-f0-9]{2,64})\.json)$/.test(normalized) || /^\.agent-knowledge\/claim-ledgers\/[^/]+\.json$/.test(normalized)) || knowledgeMutationPathPrefixes(pagesDirectory).some((prefix) => normalized.startsWith(prefix))) return normalized;
837
855
  throw new Error(`knowledge transaction contains an unsupported path: ${path}`);
838
856
  }
839
857
  function normalizeTransactionPath(path) {
@@ -2448,6 +2466,249 @@ function citedPageId(persisted) {
2448
2466
  }
2449
2467
  }
2450
2468
  //#endregion
2469
+ //#region src/event-store.ts
2470
+ /** Durable immutable event payloads with bounded, hash-partitioned metadata buckets. */
2471
+ const stateSchema = z.object({
2472
+ version: z.literal(1),
2473
+ sequence: z.number().int().nonnegative().safe(),
2474
+ count: z.number().int().nonnegative().safe()
2475
+ }).strict();
2476
+ const entrySchema = z.object({
2477
+ id: z.string().min(1),
2478
+ createdAt: z.string().min(1),
2479
+ type: KnowledgeEventSchema.shape.type,
2480
+ target: z.string().optional(),
2481
+ sequence: z.number().int().nonnegative().safe(),
2482
+ digest: z.string().regex(/^[a-f0-9]{64}$/)
2483
+ }).strict();
2484
+ const MAX_BUCKET_ENTRIES = 256;
2485
+ const bucketSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("branch") }).strict(), z.object({
2486
+ kind: z.literal("leaf"),
2487
+ entries: z.array(entrySchema).max(MAX_BUCKET_ENTRIES)
2488
+ }).strict()]);
2489
+ const encode = (value) => `${JSON.stringify(value)}\n`;
2490
+ const recordPath = (directory, digest) => `${directory}/records/${digest}.json`;
2491
+ const bucketPath = (directory, prefix) => `${directory}/index/${prefix}.json`;
2492
+ async function readOptional(root, path) {
2493
+ try {
2494
+ return (await readRegularFileWithinRoot(root, path)).bytes;
2495
+ } catch (error) {
2496
+ if (isMissingFile(error)) return void 0;
2497
+ throw error;
2498
+ }
2499
+ }
2500
+ async function assertNoLegacy(root, path) {
2501
+ if (await withSafeDescendant(root, path, async (target) => {
2502
+ try {
2503
+ await lstat(target);
2504
+ return true;
2505
+ } catch (error) {
2506
+ if (isMissingFile(error)) return false;
2507
+ throw error;
2508
+ }
2509
+ })) throw new Error("legacy event history reappeared after migration; reconcile the older writer before proceeding");
2510
+ }
2511
+ function ordered(entries) {
2512
+ return entries.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.sequence - b.sequence);
2513
+ }
2514
+ async function putStoredEvent(root, legacyPath, event) {
2515
+ const directory = legacyPath === "events.json" ? "event-log" : ".agent-knowledge/event-log";
2516
+ const statePath = `${directory}/state.json`;
2517
+ const parsed = KnowledgeEventSchema.parse(structuredClone(event));
2518
+ const purpose = `event-put:${sha256(encode({
2519
+ legacyPath,
2520
+ event: parsed
2521
+ }))}`;
2522
+ await withKnowledgeMutation(root, async (lock) => {
2523
+ const saved = await readOptional(root, statePath);
2524
+ let sequence = 0;
2525
+ let count = 0;
2526
+ const mutations = /* @__PURE__ */ new Map();
2527
+ const add = (path, content) => mutations.set(path, {
2528
+ path,
2529
+ content
2530
+ });
2531
+ const bucketCache = /* @__PURE__ */ new Map();
2532
+ const writeBucket = (prefix, entries) => {
2533
+ const writeTree = (prefix, entries) => {
2534
+ if (entries.length <= MAX_BUCKET_ENTRIES) {
2535
+ const bucket = {
2536
+ kind: "leaf",
2537
+ entries: entries.sort((a, b) => a.id.localeCompare(b.id))
2538
+ };
2539
+ bucketCache.set(prefix, bucket);
2540
+ add(bucketPath(directory, prefix), encode(bucket));
2541
+ return;
2542
+ }
2543
+ if (prefix.length >= 64) throw new Error("event id hash collision");
2544
+ const branch = { kind: "branch" };
2545
+ bucketCache.set(prefix, branch);
2546
+ add(bucketPath(directory, prefix), encode(branch));
2547
+ const groups = /* @__PURE__ */ new Map();
2548
+ for (const entry of entries) {
2549
+ const child = sha256(entry.id).slice(0, prefix.length + 1);
2550
+ const group = groups.get(child) ?? [];
2551
+ group.push(entry);
2552
+ groups.set(child, group);
2553
+ }
2554
+ for (const [child, group] of groups) writeTree(child, group);
2555
+ };
2556
+ writeTree(prefix, entries);
2557
+ };
2558
+ const putIndexEntry = async (entry) => {
2559
+ const idHash = sha256(entry.id);
2560
+ for (let length = 2; length <= 64; length++) {
2561
+ const prefix = idHash.slice(0, length);
2562
+ let bucket = bucketCache.get(prefix);
2563
+ if (!bucket) {
2564
+ const bytes = await readOptional(root, bucketPath(directory, prefix));
2565
+ bucket = bytes ? parseBucket(prefix, bytes, sequence) : {
2566
+ kind: "leaf",
2567
+ entries: []
2568
+ };
2569
+ bucketCache.set(prefix, bucket);
2570
+ }
2571
+ if (bucket.kind === "branch") continue;
2572
+ if (!bucket.entries.some((value) => value.id === entry.id)) count += 1;
2573
+ writeBucket(prefix, [...bucket.entries.filter((value) => value.id !== entry.id), entry]);
2574
+ return;
2575
+ }
2576
+ throw new Error("event index has an invalid terminal branch");
2577
+ };
2578
+ const append = async (value) => {
2579
+ if (sequence >= Number.MAX_SAFE_INTEGER) throw new Error("event sequence exhausted");
2580
+ sequence += 1;
2581
+ const content = encode(value);
2582
+ const digest = sha256(content);
2583
+ add(recordPath(directory, digest), content);
2584
+ await putIndexEntry({
2585
+ id: value.id,
2586
+ createdAt: value.createdAt,
2587
+ type: value.type,
2588
+ ...value.target === void 0 ? {} : { target: value.target },
2589
+ sequence,
2590
+ digest
2591
+ });
2592
+ };
2593
+ if (saved) {
2594
+ const state = stateSchema.parse(JSON.parse(saved.toString("utf8")));
2595
+ sequence = state.sequence;
2596
+ count = state.count;
2597
+ await assertNoLegacy(root, legacyPath);
2598
+ } else {
2599
+ await assertEmptyLog(root, directory);
2600
+ const legacy = await readOptional(root, legacyPath);
2601
+ if (legacy) {
2602
+ const events = z.array(KnowledgeEventSchema).parse(JSON.parse(legacy.toString("utf8")));
2603
+ for (const value of events) await append(value);
2604
+ add(legacyPath, null);
2605
+ }
2606
+ }
2607
+ await append(parsed);
2608
+ add(statePath, encode({
2609
+ version: 1,
2610
+ sequence,
2611
+ count
2612
+ }));
2613
+ await commitKnowledgeFileMutations({
2614
+ root,
2615
+ transactionRoot: lock.transactionRoot,
2616
+ purpose,
2617
+ researchState: true,
2618
+ mutations: [...mutations.values()],
2619
+ assertOwned: lock.assertOwned
2620
+ });
2621
+ }, { resumeTransaction: { purpose } });
2622
+ }
2623
+ async function listStoredEvents(root, legacyPath, query) {
2624
+ const directory = legacyPath === "events.json" ? "event-log" : ".agent-knowledge/event-log";
2625
+ const statePath = `${directory}/state.json`;
2626
+ return withKnowledgeRead(root, async () => {
2627
+ const saved = await readOptional(root, statePath);
2628
+ if (!saved) {
2629
+ await assertEmptyLog(root, directory);
2630
+ const bytes = await readOptional(root, legacyPath);
2631
+ let events = bytes ? z.array(KnowledgeEventSchema).parse(JSON.parse(bytes.toString("utf8"))) : [];
2632
+ if (query.type) events = events.filter((event) => event.type === query.type);
2633
+ if (query.target) events = events.filter((event) => event.target === query.target);
2634
+ return events.slice(-(query.limit ?? events.length));
2635
+ }
2636
+ const state = stateSchema.parse(JSON.parse(saved.toString("utf8")));
2637
+ await assertNoLegacy(root, legacyPath);
2638
+ const entries = [];
2639
+ const buckets = /* @__PURE__ */ new Map();
2640
+ for (const file of await readEventIndex(root, `${directory}/index`)) {
2641
+ const prefix = file.path.slice(`${directory}/index/`.length, -5);
2642
+ buckets.set(prefix, parseBucket(prefix, file.bytes, state.sequence));
2643
+ }
2644
+ let count = 0;
2645
+ for (const [prefix, bucket] of buckets) {
2646
+ for (let length = 2; length < prefix.length; length++) if (buckets.get(prefix.slice(0, length))?.kind !== "branch") throw new Error("event index has an orphaned bucket");
2647
+ if (bucket.kind === "branch") {
2648
+ if (![...buckets.keys()].some((child) => child.length === prefix.length + 1 && child.startsWith(prefix))) throw new Error("event index branch has no children");
2649
+ continue;
2650
+ }
2651
+ count += bucket.entries.length;
2652
+ for (const entry of bucket.entries) {
2653
+ if (query.type && entry.type !== query.type) continue;
2654
+ if (query.target && entry.target !== query.target) continue;
2655
+ entries.push(entry);
2656
+ }
2657
+ }
2658
+ if (count !== state.count) throw new Error("event index entry count mismatch");
2659
+ return mapFileIo(ordered(entries).slice(-(query.limit ?? entries.length)), async (entry) => {
2660
+ const bytes = (await readRegularFileWithinRoot(root, recordPath(directory, entry.digest))).bytes;
2661
+ if (createHash("sha256").update(bytes).digest("hex") !== entry.digest) throw new Error("event payload digest mismatch");
2662
+ const event = KnowledgeEventSchema.parse(JSON.parse(bytes.toString("utf8")));
2663
+ if (event.id !== entry.id || event.createdAt !== entry.createdAt || event.type !== entry.type || event.target !== entry.target) throw new Error("event payload index mismatch");
2664
+ return event;
2665
+ });
2666
+ });
2667
+ }
2668
+ async function assertEmptyLog(root, directory) {
2669
+ try {
2670
+ if ((await listRegularFilesWithinRoot(root, directory)).length) throw new Error("event log state is missing; recover the log before proceeding");
2671
+ } catch (error) {
2672
+ if (!isMissingFile(error)) throw error;
2673
+ }
2674
+ }
2675
+ async function readEventIndex(root, relativeDirectory) {
2676
+ return withSafeDirectory(root, relativeDirectory, false, async (directory) => {
2677
+ const entries = await readdir(directory, { withFileTypes: true });
2678
+ const files = [];
2679
+ for (let start = 0; start < entries.length; start += 64) {
2680
+ if (start > 0) await new Promise((resolve) => setImmediate(resolve));
2681
+ for (const entry of entries.slice(start, start + 64)) {
2682
+ if (!entry.isFile() || !/^[a-f0-9]{2,64}\.json$/.test(entry.name)) throw new Error("event index contains an unsupported entry");
2683
+ const fd = openSync(join(directory, entry.name), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
2684
+ try {
2685
+ if (!fstatSync(fd).isFile()) throw new Error("event index contains an unsupported entry");
2686
+ files.push({
2687
+ path: `${relativeDirectory}/${entry.name}`,
2688
+ bytes: readFileSync(fd)
2689
+ });
2690
+ } finally {
2691
+ closeSync(fd);
2692
+ }
2693
+ }
2694
+ }
2695
+ return files;
2696
+ });
2697
+ }
2698
+ function parseBucket(prefix, bytes, sequence) {
2699
+ const bucket = bucketSchema.parse(JSON.parse(bytes.toString("utf8")));
2700
+ if (bucket.kind === "branch") {
2701
+ if (prefix.length >= 64) throw new Error("event index has an invalid terminal branch");
2702
+ return bucket;
2703
+ }
2704
+ const ids = /* @__PURE__ */ new Set();
2705
+ for (const entry of bucket.entries) {
2706
+ if (!sha256(entry.id).startsWith(prefix) || entry.sequence > sequence || ids.has(entry.id)) throw new Error("event index identity mismatch");
2707
+ ids.add(entry.id);
2708
+ }
2709
+ return bucket;
2710
+ }
2711
+ //#endregion
2451
2712
  //#region src/kb-store.ts
2452
2713
  /**
2453
2714
  * Where a filesystem knowledge base keeps its machine-written records, relative
@@ -2462,6 +2723,7 @@ function citedPageId(persisted) {
2462
2723
  */
2463
2724
  const KB_STORE_DIR = ".agent-knowledge";
2464
2725
  const KB_INDEX_PATH = `${KB_STORE_DIR}/index.json`;
2726
+ /** Legacy array import path; new events live in the adjacent event-log directory. */
2465
2727
  const KB_EVENTS_PATH = `${KB_STORE_DIR}/events.json`;
2466
2728
  const KB_CLAIM_LEDGER_DIR = `${KB_STORE_DIR}/claim-ledgers`;
2467
2729
  /**
@@ -2525,6 +2787,8 @@ var MemoryKbStore = class {
2525
2787
  };
2526
2788
  }
2527
2789
  async putEvent(event) {
2790
+ const position = this.events.findIndex((entry) => entry.id === event.id);
2791
+ if (position !== -1) this.events.splice(position, 1);
2528
2792
  this.events.push(clone(event));
2529
2793
  }
2530
2794
  async listEvents(query = {}) {
@@ -2551,7 +2815,6 @@ var MemoryKbStore = class {
2551
2815
  return clone(next);
2552
2816
  }
2553
2817
  };
2554
- const knowledgeEventsSchema = z.array(KnowledgeEventSchema);
2555
2818
  var FileSystemKbStore = class {
2556
2819
  root;
2557
2820
  indexPath;
@@ -2616,19 +2879,10 @@ var FileSystemKbStore = class {
2616
2879
  return withKnowledgeRead(this.root, () => this.readIndex());
2617
2880
  }
2618
2881
  async putEvent(event) {
2619
- const parsed = KnowledgeEventSchema.parse(event);
2620
- await withKnowledgeMutation(this.root, async () => {
2621
- const next = [...(await this.readEvents()).filter((entry) => entry.id !== parsed.id), parsed].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2622
- await writeJsonDurableWithinRoot(this.root, this.eventsPath, next);
2623
- });
2882
+ await putStoredEvent(this.root, this.eventsPath, event);
2624
2883
  }
2625
2884
  async listEvents(query = {}) {
2626
- return withKnowledgeRead(this.root, async () => {
2627
- let events = await this.readEvents();
2628
- if (query.type) events = events.filter((event) => event.type === query.type);
2629
- if (query.target) events = events.filter((event) => event.target === query.target);
2630
- return clone(events.slice(-(query.limit ?? events.length)));
2631
- });
2885
+ return this.readEvents(query);
2632
2886
  }
2633
2887
  async putClaimLedger(ledger) {
2634
2888
  const parsed = parseResearchClaimLedger(ledger);
@@ -2678,8 +2932,8 @@ var FileSystemKbStore = class {
2678
2932
  async readIndex() {
2679
2933
  return readJsonFile(this.root, this.indexPath, KnowledgeIndexSchema);
2680
2934
  }
2681
- async readEvents() {
2682
- return await readJsonFile(this.root, this.eventsPath, knowledgeEventsSchema) ?? [];
2935
+ async readEvents(query = {}) {
2936
+ return listStoredEvents(this.root, this.eventsPath, query);
2683
2937
  }
2684
2938
  claimLedgerPath(id) {
2685
2939
  return `${this.claimLedgerDir}/${assertClaimLedgerId(id)}.json`;
@@ -4556,278 +4810,6 @@ function stringMetadata(metadata, key) {
4556
4810
  return typeof value === "string" ? value : void 0;
4557
4811
  }
4558
4812
  //#endregion
4559
- //#region src/lexical-postings.ts
4560
- const DEFAULT_FIELD_BOOSTS = Object.freeze({
4561
- title: 3,
4562
- path: 2,
4563
- text: 1
4564
- });
4565
- function lexicalPageTerms(page, tokenize, fieldBoosts) {
4566
- const frequencies = /* @__PURE__ */ new Map();
4567
- let length = 0;
4568
- for (const [text, boost] of [
4569
- [page.title, fieldBoosts.title],
4570
- [page.path.replace(/\.md$/, ""), fieldBoosts.path],
4571
- [page.text, fieldBoosts.text]
4572
- ]) {
4573
- if (boost === 0) continue;
4574
- for (const token of tokenize(text)) {
4575
- frequencies.set(token, (frequencies.get(token) ?? 0) + boost);
4576
- length += boost;
4577
- }
4578
- }
4579
- return {
4580
- frequencies,
4581
- length
4582
- };
4583
- }
4584
- /** Inverted index from per-page terms already computed with `tokenize` and `fieldBoosts`. */
4585
- function assembleLexicalIndex(pages, terms, tokenize, fieldBoosts) {
4586
- const postings = /* @__PURE__ */ new Map();
4587
- const documentLengths = [];
4588
- let totalLength = 0;
4589
- terms.forEach(({ frequencies, length }, ordinal) => {
4590
- documentLengths.push(length);
4591
- totalLength += length;
4592
- for (const [term, tf] of frequencies) {
4593
- let list = postings.get(term);
4594
- if (!list) {
4595
- list = [];
4596
- postings.set(term, list);
4597
- }
4598
- list.push({
4599
- ordinal,
4600
- tf
4601
- });
4602
- }
4603
- });
4604
- return {
4605
- pages,
4606
- postings,
4607
- documentLengths,
4608
- averageDocumentLength: pages.length > 0 ? totalLength / pages.length : 0,
4609
- documentCount: pages.length,
4610
- tokenize,
4611
- fieldBoosts
4612
- };
4613
- }
4614
- //#endregion
4615
- //#region src/lexical-index.ts
4616
- const STOP_WORDS = /* @__PURE__ */ new Set([
4617
- "the",
4618
- "is",
4619
- "a",
4620
- "an",
4621
- "what",
4622
- "how",
4623
- "are",
4624
- "was",
4625
- "were",
4626
- "to",
4627
- "for",
4628
- "of",
4629
- "with",
4630
- "by",
4631
- "in",
4632
- "on",
4633
- "and"
4634
- ]);
4635
- /**
4636
- * The token stream of one text: lower-cased, split on whitespace and
4637
- * punctuation, single characters and stop words removed, and a CJK run
4638
- * expanded into its bigrams and characters. Repeats are kept so a term
4639
- * frequency can be counted. Indexing and querying share this function, so the
4640
- * two vocabularies cannot drift.
4641
- */
4642
- function tokenizeText(text) {
4643
- const raw = text.toLowerCase().split(/[\s,,。!?、;:""''()()\-_/\\·~~…]+/).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
4644
- const tokens = [];
4645
- for (const token of raw) {
4646
- if (/[\u4e00-\u9fff\u3400-\u4dbf]/.test(token) && token.length > 2) {
4647
- const chars = [...token];
4648
- for (let i = 0; i < chars.length - 1; i++) tokens.push(chars[i] + chars[i + 1]);
4649
- tokens.push(...chars);
4650
- }
4651
- tokens.push(token);
4652
- }
4653
- return tokens;
4654
- }
4655
- /** The distinct query terms, in first-occurrence order. */
4656
- function tokenizeQuery(query) {
4657
- return [...new Set(tokenizeText(query))];
4658
- }
4659
- function buildKnowledgeLexicalIndex(pages, options = {}) {
4660
- const tokenize = options.tokenize ?? tokenizeText;
4661
- const fieldBoosts = resolveFieldBoosts(options.fieldBoosts);
4662
- return assembleLexicalIndex(pages, pages.map((page) => lexicalPageTerms(page, tokenize, fieldBoosts)), tokenize, fieldBoosts);
4663
- }
4664
- /**
4665
- * Okapi BM25 over the distinct query terms, with the Lucene inverse document
4666
- * frequency `ln(1 + (N - df + 0.5) / (df + 0.5))`, which is positive for every
4667
- * indexed term. Pages with no matching term are absent. The result is ordered
4668
- * by score, then by path, so it does not depend on page order.
4669
- */
4670
- function scoreBm25(index, tokens, options = {}) {
4671
- const k1 = options.k1 ?? 1.2;
4672
- const b = options.b ?? .75;
4673
- if (!Number.isFinite(k1) || k1 < 0) throw new Error(`bm25 k1 must be >= 0, got ${String(k1)}`);
4674
- if (!Number.isFinite(b) || b < 0 || b > 1) throw new Error(`bm25 b must lie in [0, 1], got ${String(b)}`);
4675
- const scores = /* @__PURE__ */ new Map();
4676
- const { documentCount, averageDocumentLength, documentLengths } = index;
4677
- for (const term of new Set(tokens)) {
4678
- const list = index.postings.get(term);
4679
- if (!list) continue;
4680
- const df = list.length;
4681
- const idf = Math.log(1 + (documentCount - df + .5) / (df + .5));
4682
- for (const { ordinal, tf } of list) {
4683
- const lengthRatio = averageDocumentLength > 0 ? documentLengths[ordinal] / averageDocumentLength : 1;
4684
- const saturated = tf * (k1 + 1) / (tf + k1 * (1 - b + b * lengthRatio));
4685
- scores.set(ordinal, (scores.get(ordinal) ?? 0) + idf * saturated);
4686
- }
4687
- }
4688
- return [...scores.entries()].map(([ordinal, score]) => ({
4689
- page: index.pages[ordinal],
4690
- score
4691
- })).sort((left, right) => right.score - left.score || left.page.path.localeCompare(right.page.path));
4692
- }
4693
- function resolveFieldBoosts(boosts) {
4694
- const resolved = {
4695
- ...DEFAULT_FIELD_BOOSTS,
4696
- ...boosts
4697
- };
4698
- for (const [field, boost] of Object.entries(resolved)) if (!Number.isFinite(boost) || boost < 0) throw new Error(`lexical field boost ${field} must be >= 0, got ${String(boost)}`);
4699
- return Object.freeze(resolved);
4700
- }
4701
- //#endregion
4702
- //#region src/search.ts
4703
- const RRF_K = 60;
4704
- /**
4705
- * Identity of the ranking `searchKnowledge` performs: BM25 over title, path,
4706
- * and body, exact-title and phrase matches ahead of bag-of-words matches, and
4707
- * reciprocal rank fusion with link and shared-source structure. Declare it as
4708
- * the retriever id of a retrieval receipt minted from these results.
4709
- */
4710
- const KNOWLEDGE_SEARCH_RETRIEVER_ID = "bm25-rrf-v1";
4711
- function searchKnowledge(index, query, limitOrOptions = 10) {
4712
- return searchKnowledgePages(index.pages, query, limitOrOptions);
4713
- }
4714
- /**
4715
- * Rank a page set that is not a built index, such as the chain a run can see.
4716
- *
4717
- * `searchKnowledge` is this function over `index.pages`, so both entry points
4718
- * rank identically and neither can drift from the other.
4719
- */
4720
- function searchKnowledgePages(pages, query, limitOrOptions = 10) {
4721
- const trimmed = query.trim();
4722
- if (trimmed === "") return [];
4723
- const options = typeof limitOrOptions === "number" ? { limit: limitOrOptions } : { ...limitOrOptions };
4724
- const limit = options.limit ?? 10;
4725
- if (!Number.isInteger(limit) || limit < 0) throw new Error(`search limit must be a non-negative integer, got ${String(limit)}`);
4726
- const matched = filterPages(pages, options);
4727
- const lexicalRanked = rankLexical(matched, trimmed, options.lexicalIndex ? assertLexicalIndexMatches(options.lexicalIndex, pages) : buildKnowledgeLexicalIndex(pages));
4728
- const ranked = [...fuseRanks([lexicalRanked, rankByGraph(matched, lexicalRanked, pages)]).entries()].map(([page, score]) => ({
4729
- page,
4730
- score
4731
- })).sort((a, b) => b.score - a.score || comparePages(a.page, b.page)).slice(0, limit);
4732
- const topScore = ranked[0]?.score ?? 0;
4733
- return ranked.map((item, i) => ({
4734
- citationId: item.page.id,
4735
- page: item.page,
4736
- score: item.score,
4737
- rrfScore: item.score,
4738
- normalizedScore: topScore > 0 ? item.score / topScore : 0,
4739
- rank: i + 1,
4740
- snippet: buildSnippet(item.page.text, trimmed),
4741
- reasons: reasonsFor(item.page, trimmed)
4742
- }));
4743
- }
4744
- function reciprocalRankFusion(rankLists, k = RRF_K) {
4745
- return fuseRanks(rankLists, k);
4746
- }
4747
- function fuseRanks(rankLists, k = RRF_K) {
4748
- const scores = /* @__PURE__ */ new Map();
4749
- for (const list of rankLists) list.forEach((id, idx) => {
4750
- scores.set(id, (scores.get(id) ?? 0) + 1 / (k + idx + 1));
4751
- });
4752
- return scores;
4753
- }
4754
- function filterPages(pages, options) {
4755
- const pageIds = options.pageIds ? new Set(options.pageIds) : null;
4756
- const tags = options.tags ? new Set(options.tags) : null;
4757
- const kinds = options.kinds ? new Set(options.kinds) : null;
4758
- return pages.filter((page) => {
4759
- if (options.excludeInvalidated && page.invalidation !== void 0) return false;
4760
- if (pageIds && !pageIds.has(page.id)) return false;
4761
- if (tags && !page.tags.some((tag) => tags.has(tag))) return false;
4762
- if (kinds) {
4763
- const kind = page.frontmatter.kind;
4764
- if (typeof kind !== "string" || !kinds.has(kind)) return false;
4765
- }
4766
- return options.predicate?.(page) ?? true;
4767
- });
4768
- }
4769
- function assertLexicalIndexMatches(lexicalIndex, pages) {
4770
- if (lexicalIndex.pages.length !== pages.length || lexicalIndex.pages.some((page, ordinal) => page !== pages[ordinal])) throw new Error("lexical index was not built from the searched pages");
4771
- return lexicalIndex;
4772
- }
4773
- /**
4774
- * The lexical rank list. An exact title or path match outranks a title that
4775
- * contains the query, which outranks a body that contains the query, which
4776
- * outranks a bag-of-words match; BM25 orders pages inside each of those tiers.
4777
- * The tiers are an ordering, not a score, so a strong bag-of-words page cannot
4778
- * overtake an exact match by term repetition alone.
4779
- */
4780
- function rankLexical(pages, query, lexicalIndex) {
4781
- const tokens = [...new Set(lexicalIndex.tokenize(query))];
4782
- const bm25 = new Map(scoreBm25(lexicalIndex, tokens).map((hit) => [hit.page, hit.score]));
4783
- const phrase = query.toLowerCase();
4784
- return pages.flatMap((page) => {
4785
- const score = bm25.get(page) ?? 0;
4786
- if (score === 0 && tokens.length > 0) return [];
4787
- const tier = phraseTier(page, phrase);
4788
- if (score === 0 && tier === 0) return [];
4789
- return [{
4790
- page,
4791
- tier,
4792
- score
4793
- }];
4794
- }).sort((a, b) => b.tier - a.tier || b.score - a.score || comparePages(a.page, b.page)).map((item) => item.page);
4795
- }
4796
- function phraseTier(page, phrase) {
4797
- const title = page.title.toLowerCase();
4798
- if (title === phrase || page.path.toLowerCase().endsWith(`${phrase}.md`)) return 3;
4799
- if (title.includes(phrase)) return 2;
4800
- if (page.text.toLowerCase().includes(phrase)) return 1;
4801
- return 0;
4802
- }
4803
- function rankByGraph(pages, lexicalRanked, visiblePages) {
4804
- if (lexicalRanked.length === 0) return [];
4805
- const counts = /* @__PURE__ */ new Map();
4806
- for (const page of visiblePages) counts.set(page.id, (counts.get(page.id) ?? 0) + 1);
4807
- const seeds = new Set(lexicalRanked.slice(0, 5).filter((page) => counts.get(page.id) === 1).map((page) => page.id));
4808
- return pages.map((page) => ({
4809
- page,
4810
- score: page.outLinks.filter((link) => seeds.has(link)).length + page.sourceIds.filter((source) => lexicalRanked.some((seed) => seed.sourceIds.includes(source))).length
4811
- })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || comparePages(a.page, b.page)).map((item) => item.page);
4812
- }
4813
- function comparePages(a, b) {
4814
- return a.path.localeCompare(b.path) || a.id.localeCompare(b.id);
4815
- }
4816
- function buildSnippet(text, query) {
4817
- const compact = text.replace(/\s+/g, " ").trim();
4818
- const idx = compact.toLowerCase().indexOf(query.toLowerCase());
4819
- if (idx < 0) return compact.slice(0, 180);
4820
- return compact.slice(Math.max(0, idx - 80), Math.min(compact.length, idx + query.length + 100));
4821
- }
4822
- function reasonsFor(page, query) {
4823
- const lower = `${page.title}\n${page.text}`.toLowerCase();
4824
- const reasons = [];
4825
- if (lower.includes(query.toLowerCase())) reasons.push("phrase");
4826
- if (page.sourceIds.length > 0) reasons.push("sourced");
4827
- if (page.outLinks.length > 0) reasons.push("linked");
4828
- return reasons;
4829
- }
4830
- //#endregion
4831
4813
  //#region src/inspect.ts
4832
4814
  function inspectKnowledgeIndex(index, options = {}) {
4833
4815
  const now = options.now ?? /* @__PURE__ */ new Date();
@@ -4891,6 +4873,6 @@ function explainKnowledgeTarget(index, target) {
4891
4873
  };
4892
4874
  }
4893
4875
  //#endregion
4894
- export { assertGradeableEvidence as $, removeDurable as $n, KnowledgeIndexSchema as $t, createKnowledgeVisibilitySnapshot as A, isKnowledgeMutationHeld as An, auditKnowledgeCitations as At, snapshotImmutableVisibility as B, knowledgeFileTransactionPlanHash as Bn, isScaffoldPath as Bt, createRunScopedStores as C, mergeTrackedClaims as Cn, KnowledgeCitationAuditError as Ct, KNOWLEDGE_RECEIPT_DIGEST_ALGORITHM as D, parseFrontmatter as Dn, assertKnowledgeCitationsResolved as Dt, originatedPages as E, formatFrontmatter as En, assertKnowledgeCitationAudit as Et, knowledgePageDigest as F, withKnowledgeRead as Fn, resolveRunScopedCitation as Ft, visibilityIndexOf as G, canonicalPathsEqual as Gn, WIKILINK_REGEX as Gt, validateOrigin as H, rollbackKnowledgeFileTransaction as Hn, layoutFor as Ht, knowledgeVisibilityArtifactRef as I, applyKnowledgeFileTransaction as In, resolveRunScopedCitations as It, validateKnowledgeIndex as J, isMissingFile as Jn, DeepQuestionSchema as Jt, detectNearDuplicatePages as K, canonicalRelativeWithinRoot as Kn, extractWikilinks as Kt, nonEmpty as L, assertKnowledgeMutationPath as Ln, SCAFFOLD_PAGE_BASENAMES as Lt, digest as M, recoverPendingKnowledgeMutation as Mn, parseKnowledgeCitationReference as Mt, encodeKnowledgeVisibilitySnapshot as N, runInKnowledgeMutationScope as Nn, resolveKnowledgeCitation as Nt, KNOWLEDGE_USE_RECEIPT_SCHEMA_VERSION as O, acquireDurableFileLock as On, assertRunScopedCitationsResolved as Ot, immutablePageDigest as P, withKnowledgeMutation as Pn, resolveKnowledgeCitations as Pt, UncheckableClaimError as Q, relativeWithinRoot as Qn, KnowledgeGraphNodeSchema as Qt, normalizeVisibilityArtifact as R, commitKnowledgeFileMutations as Rn, initKnowledgeBase as Rt, createFileRunLineageAuthority as S, mergeClaimLedgers as Sn, knowledgePageRelations as St, loadKnowledgeToolChain as T, researchSourceVersionKey as Tn, assertCurrentRunCitationsResolved as Tt, verifyKnowledgeVisibilitySnapshot as U, DEFAULT_PAGES_DIRECTORY as Un, loadKnowledgePages as Ut, validateKnowledgePage as V, prepareKnowledgeFileTransaction as Vn, knowledgePageFromMarkdown as Vt, visibilityIdentity as W, normalizePagesDirectory as Wn, writeJson as Wt, CHECKABLE_RUNG_THRESHOLD as X, readRegularFileNoFollow as Xn, KnowledgeEventSchema as Xt, lintKnowledgeIndex as Y, listRegularFilesWithinRoot as Yn, KnowledgeBaseCandidateSchema as Yt, DEADLINE_EXIT_CODE as Z, readRegularFileWithinRoot as Zn, KnowledgeGraphEdgeSchema as Zt, isSafeKnowledgePath as _, claimSourceHost as _n, KB_INDEX_PATH as _t, searchKnowledge as a, ResearchClaimRecordSchema as an, writeFileDurableWithinRoot as ar, buildKnowledgeIndex as at, assertKnowledgeWriteIntake as b, linkClaimContradictions as bn, assertClaimLedgerId as bt, scoreBm25 as c, SourceRecordSchema as cn, mediaTypeFor$1 as cr, addSourceText as ct, DEFAULT_FIELD_BOOSTS as d, assertDeepQuestionIntegrity as dn, sourceRegistryPath as dt, KnowledgePageInvalidationSchema as en, renameDurable as er, claimCheckKey as et, assembleLexicalIndex as f, assertResearchClaimEvidenceIntegrity as fn, writeSourceRegistry as ft, applyKnowledgeWriteBlocksFile as g, claimId as gn, KB_EVENTS_PATH as gt, applyKnowledgeWriteBlocks as h, claimEvidenceId as hn, KB_CLAIM_LEDGER_DIR as ht, reciprocalRankFusion as i, ResearchClaimLedgerSchema as in, writeFileDurable as ir, verifyGradeableEvidence as it, decodeKnowledgeVisibilitySnapshot as j, measureKnowledgeMutation as jn, formatKnowledgeCitationReference as jt, assertSchemaVersion as k, inspectPendingKnowledgeMutation as kn, auditCurrentRunCitations as kt, tokenizeQuery as l, KNOWLEDGE_EVENT_TYPES as ln, textSourceAdapter as lr, loadSourceRegistry as lt, stringMetadata as m, assertTrackedClaimIntegrity as mn, FileSystemKbStore as mt, inspectKnowledgeIndex as n, KnowledgeRelationSchema as nn, withSafeDescendant as nr, gradeFor as nt, searchKnowledgePages as o, ResearchSourceVersionSchema as on, writeJsonDurable as or, writeKnowledgeIndex as ot, lexicalPageTerms as p, assertResearchClaimLedgerIntegrity as pn, ClaimLedgerMigrationRequiredError as pt, normalizePageText as q, isKernelAnchoredPath as qn, normalizeLinkTarget as qt, KNOWLEDGE_SEARCH_RETRIEVER_ID as r, ResearchClaimEvidenceSchema as rn, withSafeDirectory as rr, verdictFor as rt, buildKnowledgeLexicalIndex as s, SourceAnchorSchema as sn, writeJsonDurableWithinRoot as sr, addSourcePath as st, explainKnowledgeTarget as t, KnowledgePageSchema as tn, syncDirectory as tr, gradeClaims as tt, tokenizeText as u, ClaimLedgerGoalConflictError as un, snapshotSourceTextInput as ut, parseKnowledgeWriteBlocks as v, deepQuestionId as vn, KB_STORE_DIR as vt, isKnowledgeToolChain as w, normalizeClaimText as wn, KnowledgeCitationResolutionError as wt, RUN_LINEAGE_BASENAME as x, materializeRegisteredClaimEvidence as xn, buildKnowledgeGraph as xt, KnowledgeDuplicateIntakeError as y, emptyClaimLedger as yn, MemoryKbStore as yt, normalizeVisibilityRef as z, finishKnowledgeFileTransaction as zn, isKnowledgePagePath as zt };
4876
+ export { snapshotSourceTextInput as $, ClaimLedgerGoalConflictError as $t, validateKnowledgePage as A, prepareKnowledgeFileTransaction as An, knowledgePageFromMarkdown as At, DEADLINE_EXIT_CODE as B, readRegularFileWithinRoot as Bn, KnowledgeGraphEdgeSchema as Bt, immutablePageDigest as C, withKnowledgeMutation as Cn, resolveKnowledgeCitations as Ct, normalizeVisibilityArtifact as D, commitKnowledgeFileMutations as Dn, initKnowledgeBase as Dt, nonEmpty as E, assertKnowledgeMutationPath as En, SCAFFOLD_PAGE_BASENAMES as Et, detectNearDuplicatePages as F, canonicalRelativeWithinRoot as Fn, extractWikilinks as Ft, gradeFor as G, withSafeDescendant as Gn, KnowledgeRelationSchema as Gt, assertGradeableEvidence as H, removeDurable as Hn, KnowledgeIndexSchema as Ht, normalizePageText as I, isKernelAnchoredPath as In, normalizeLinkTarget as It, buildKnowledgeIndex as J, writeFileDurableWithinRoot as Jn, ResearchClaimRecordSchema as Jt, verdictFor as K, withSafeDirectory as Kn, ResearchClaimEvidenceSchema as Kt, validateKnowledgeIndex as L, isMissingFile as Ln, DeepQuestionSchema as Lt, verifyKnowledgeVisibilitySnapshot as M, DEFAULT_PAGES_DIRECTORY as Mn, loadKnowledgePages as Mt, visibilityIdentity as N, normalizePagesDirectory as Nn, writeJson as Nt, normalizeVisibilityRef as O, finishKnowledgeFileTransaction as On, isKnowledgePagePath as Ot, visibilityIndexOf as P, canonicalPathsEqual as Pn, WIKILINK_REGEX as Pt, loadSourceRegistry as Q, textSourceAdapter as Qn, KNOWLEDGE_EVENT_TYPES as Qt, lintKnowledgeIndex as R, listRegularFilesWithinRoot as Rn, KnowledgeBaseCandidateSchema as Rt, encodeKnowledgeVisibilitySnapshot as S, runInKnowledgeMutationScope as Sn, resolveKnowledgeCitation as St, knowledgeVisibilityArtifactRef as T, applyKnowledgeFileTransaction as Tn, resolveRunScopedCitations as Tt, claimCheckKey as U, renameDurable as Un, KnowledgePageInvalidationSchema as Ut, UncheckableClaimError as V, relativeWithinRoot as Vn, KnowledgeGraphNodeSchema as Vt, gradeClaims as W, syncDirectory as Wn, KnowledgePageSchema as Wt, addSourcePath as X, writeJsonDurableWithinRoot as Xn, SourceAnchorSchema as Xt, writeKnowledgeIndex as Y, writeJsonDurable as Yn, ResearchSourceVersionSchema as Yt, addSourceText as Z, mediaTypeFor$1 as Zn, SourceRecordSchema as Zt, KNOWLEDGE_USE_RECEIPT_SCHEMA_VERSION as _, acquireDurableFileLock as _n, assertRunScopedCitationsResolved as _t, applyKnowledgeWriteBlocksFile as a, claimId as an, KB_EVENTS_PATH as at, decodeKnowledgeVisibilitySnapshot as b, measureKnowledgeMutation as bn, formatKnowledgeCitationReference as bt, KnowledgeDuplicateIntakeError as c, emptyClaimLedger as cn, MemoryKbStore as ct, createFileRunLineageAuthority as d, mergeClaimLedgers as dn, knowledgePageRelations as dt, assertDeepQuestionIntegrity as en, sourceRegistryPath as et, createRunScopedStores as f, mergeTrackedClaims as fn, KnowledgeCitationAuditError as ft, KNOWLEDGE_RECEIPT_DIGEST_ALGORITHM as g, parseFrontmatter as gn, assertKnowledgeCitationsResolved as gt, originatedPages as h, formatFrontmatter as hn, assertKnowledgeCitationAudit as ht, applyKnowledgeWriteBlocks as i, claimEvidenceId as in, KB_CLAIM_LEDGER_DIR as it, validateOrigin as j, rollbackKnowledgeFileTransaction as jn, layoutFor as jt, snapshotImmutableVisibility as k, knowledgeFileTransactionPlanHash as kn, isScaffoldPath as kt, assertKnowledgeWriteIntake as l, linkClaimContradictions as ln, assertClaimLedgerId as lt, loadKnowledgeToolChain as m, researchSourceVersionKey as mn, assertCurrentRunCitationsResolved as mt, inspectKnowledgeIndex as n, assertResearchClaimLedgerIntegrity as nn, ClaimLedgerMigrationRequiredError as nt, isSafeKnowledgePath as o, claimSourceHost as on, KB_INDEX_PATH as ot, isKnowledgeToolChain as p, normalizeClaimText as pn, KnowledgeCitationResolutionError as pt, verifyGradeableEvidence as q, writeFileDurable as qn, ResearchClaimLedgerSchema as qt, stringMetadata as r, assertTrackedClaimIntegrity as rn, FileSystemKbStore as rt, parseKnowledgeWriteBlocks as s, deepQuestionId as sn, KB_STORE_DIR as st, explainKnowledgeTarget as t, assertResearchClaimEvidenceIntegrity as tn, writeSourceRegistry as tt, RUN_LINEAGE_BASENAME as u, materializeRegisteredClaimEvidence as un, buildKnowledgeGraph as ut, assertSchemaVersion as v, inspectPendingKnowledgeMutation as vn, auditCurrentRunCitations as vt, knowledgePageDigest as w, withKnowledgeRead as wn, resolveRunScopedCitation as wt, digest as x, recoverPendingKnowledgeMutation as xn, parseKnowledgeCitationReference as xt, createKnowledgeVisibilitySnapshot as y, isKnowledgeMutationHeld as yn, auditKnowledgeCitations as yt, CHECKABLE_RUNG_THRESHOLD as z, readRegularFileNoFollow as zn, KnowledgeEventSchema as zt };
4895
4877
 
4896
- //# sourceMappingURL=inspect-CMigdsD5.js.map
4878
+ //# sourceMappingURL=inspect-DXN6SE2o.js.map