@tangle-network/agent-knowledge 8.0.9 → 10.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/AGENTS.md +14 -2
  2. package/CHANGELOG.md +99 -0
  3. package/README.md +127 -5
  4. package/dist/benchmarks/index.d.ts +1 -1
  5. package/dist/benchmarks/index.js +1 -1
  6. package/dist/{benchmarks-B_07vX0Q.js → benchmarks-B6fCb6AD.js} +2 -2
  7. package/dist/{benchmarks-B_07vX0Q.js.map → benchmarks-B6fCb6AD.js.map} +1 -1
  8. package/dist/cli.js +53 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/{index-DypNCZtP.d.ts → index-DZeFm-BP.d.ts} +2 -2
  11. package/dist/{index-DypNCZtP.d.ts.map → index-DZeFm-BP.d.ts.map} +1 -1
  12. package/dist/{index-Ij4giqqj.d.ts → index-eDsIXWyM.d.ts} +3 -3
  13. package/dist/{index-Ij4giqqj.d.ts.map → index-eDsIXWyM.d.ts.map} +1 -1
  14. package/dist/index.d.ts +910 -290
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1009 -382
  17. package/dist/index.js.map +1 -1
  18. package/dist/{inspect-0tmv3xV8.js → inspect-DALsvG10.js} +680 -108
  19. package/dist/inspect-DALsvG10.js.map +1 -0
  20. package/dist/memory/index.d.ts +2 -2
  21. package/dist/memory/index.js +2 -2
  22. package/dist/{memory-BPgPIEhj.js → memory-BRGsy2QN.js} +2 -2
  23. package/dist/{memory-BPgPIEhj.js.map → memory-BRGsy2QN.js.map} +1 -1
  24. package/dist/search-Cw6eYfSd.js +258 -0
  25. package/dist/search-Cw6eYfSd.js.map +1 -0
  26. package/dist/{types-CTT16XnO.d.ts → types-m2QB86fF.d.ts} +17 -3
  27. package/dist/types-m2QB86fF.d.ts.map +1 -0
  28. package/dist/viz/index.d.ts +1 -1
  29. package/docs/architecture.md +1 -0
  30. package/docs/knowledge-use-receipts.md +50 -12
  31. package/package.json +3 -3
  32. package/dist/inspect-0tmv3xV8.js.map +0 -1
  33. package/dist/search-CtVJ0PKX.js +0 -132
  34. package/dist/search-CtVJ0PKX.js.map +0 -1
  35. package/dist/types-CTT16XnO.d.ts.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { i as textSourceId, n as slugify, r as stableId, t as sha256 } from "./ids-Bevz_pXV.js";
2
- import { n as searchKnowledge } from "./search-CtVJ0PKX.js";
2
+ import { r as searchKnowledge } from "./search-Cw6eYfSd.js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm } from "node:fs/promises";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -273,10 +273,52 @@ function normalizeRelativePath(path) {
273
273
  return normalized;
274
274
  }
275
275
  //#endregion
276
+ //#region src/pages-directory.ts
277
+ /** Root-relative directory that holds Markdown pages when a caller names none. */
278
+ const DEFAULT_PAGES_DIRECTORY = "knowledge";
279
+ /**
280
+ * Validate a pages directory and return its canonical root-relative form.
281
+ *
282
+ * The result is a write allowlist prefix as well as a read location, so the
283
+ * check refuses every value that could reach outside the pages tree: absolute
284
+ * paths, drive letters, `..` or `.` segments, empty segments, control
285
+ * characters, and the package-owned `.agent-knowledge` and `raw` trees.
286
+ */
287
+ function normalizePagesDirectory(value = DEFAULT_PAGES_DIRECTORY) {
288
+ if (typeof value !== "string") throw new Error("pagesDirectory must be a string");
289
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
290
+ if (normalized.length === 0 || normalized === ".") throw new Error("pagesDirectory must name a root-relative directory");
291
+ if (new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}]`).test(normalized)) throw new Error("pagesDirectory must not contain control characters");
292
+ if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) throw new Error(`pagesDirectory must be root-relative, not absolute: ${value}`);
293
+ const segments = normalized.split("/");
294
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`pagesDirectory must not contain empty or dot segments: ${value}`);
295
+ if (segments[0] === ".agent-knowledge" || segments[0] === "raw") throw new Error(`pagesDirectory must not name the package-owned ${segments[0]} tree: ${value}`);
296
+ return normalized;
297
+ }
298
+ //#endregion
276
299
  //#region src/file-transaction.ts
277
300
  const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
278
301
  const fileModeSchema = z.number().int().min(0).max(511);
279
302
  const DEFAULT_FILE_MODE = 420;
303
+ const SOURCE_REGISTRY_PATH = ".agent-knowledge/sources.json";
304
+ /**
305
+ * A journal states the pages directory it was prepared under, already in
306
+ * canonical form, so replay and recovery enforce the same allowlist as the
307
+ * prepare step without any caller-supplied value.
308
+ */
309
+ const pagesDirectorySchema = z.string().superRefine((value, context) => {
310
+ try {
311
+ if (normalizePagesDirectory(value) !== value) context.addIssue({
312
+ code: "custom",
313
+ message: "pagesDirectory is not in canonical form"
314
+ });
315
+ } catch (error) {
316
+ context.addIssue({
317
+ code: "custom",
318
+ message: error instanceof Error ? error.message : String(error)
319
+ });
320
+ }
321
+ });
280
322
  const transactionEntrySchema = z.object({
281
323
  index: z.number().int().nonnegative(),
282
324
  path: z.string().min(1),
@@ -290,6 +332,7 @@ const transactionSchema = z.object({
290
332
  transactionId: z.string().uuid(),
291
333
  purpose: z.string().min(1),
292
334
  recoveryOwner: z.string().min(1).max(256).optional(),
335
+ pagesDirectory: pagesDirectorySchema.optional(),
293
336
  createdAt: z.string().min(1),
294
337
  entries: z.array(transactionEntrySchema).min(1)
295
338
  }).strict();
@@ -304,12 +347,13 @@ function knowledgeFileTransactionPlanHash(entries) {
304
347
  }
305
348
  async function prepareKnowledgeFileTransaction(input) {
306
349
  if (input.mutations.length === 0) throw new Error("knowledge file transaction has no mutations");
350
+ const pagesDirectory = input.pagesDirectory === void 0 ? void 0 : normalizePagesDirectory(input.pagesDirectory);
307
351
  return withTransactionRoot(input.root, input.transactionRoot, true, async (transactionRoot) => {
308
352
  const activeTransactions = await activeTransactionDirectoryNames(transactionRoot);
309
353
  if (activeTransactions.length > 0) throw new Error(`knowledge file transaction is already active: ${activeTransactions.join(", ")}`);
310
354
  const paths = /* @__PURE__ */ new Set();
311
355
  const changed = (await Promise.all(input.mutations.map(async (mutation, index) => {
312
- const path = assertKnowledgeMutationPath(mutation.path);
356
+ const path = assertKnowledgeMutationPath(mutation.path, pagesDirectory);
313
357
  if (paths.has(path)) throw new Error(`knowledge file transaction repeats path: ${path}`);
314
358
  if (mutation.content === null && mutation.mode !== void 0) throw new Error(`deleted knowledge file cannot declare a mode: ${path}`);
315
359
  paths.add(path);
@@ -343,6 +387,7 @@ async function prepareKnowledgeFileTransaction(input) {
343
387
  transactionId: randomUUID(),
344
388
  purpose: input.purpose,
345
389
  ...input.recoveryOwner ? { recoveryOwner: input.recoveryOwner } : {},
390
+ ...pagesDirectory === void 0 ? {} : { pagesDirectory },
346
391
  createdAt: (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
347
392
  entries: changed.map((item) => item.entry)
348
393
  });
@@ -377,6 +422,7 @@ async function commitKnowledgeFileMutations(input) {
377
422
  transactionRoot: input.transactionRoot,
378
423
  purpose: input.purpose,
379
424
  mutations: input.mutations,
425
+ ...input.pagesDirectory === void 0 ? {} : { pagesDirectory: input.pagesDirectory },
380
426
  now: input.now
381
427
  });
382
428
  if (!transaction) return false;
@@ -600,7 +646,7 @@ function assertTransactionEntries(transaction) {
600
646
  const indexes = /* @__PURE__ */ new Set();
601
647
  const paths = /* @__PURE__ */ new Set();
602
648
  for (const entry of transaction.entries) {
603
- const normalized = assertKnowledgeMutationPath(entry.path);
649
+ const normalized = assertKnowledgeMutationPath(entry.path, transaction.pagesDirectory);
604
650
  if (entry.path !== normalized || indexes.has(entry.index) || paths.has(entry.path)) throw new Error("knowledge file transaction has duplicate or unsafe entries");
605
651
  assertHashModePair(entry.path, "before", entry.beforeHash, entry.beforeMode);
606
652
  assertHashModePair(entry.path, "after", entry.afterHash, entry.afterMode);
@@ -624,9 +670,17 @@ function assertHashModePair(path, side, hash, mode) {
624
670
  if (hash === null !== (mode === void 0)) throw new Error(`knowledge transaction ${side} hash and mode disagree: ${path}`);
625
671
  if (mode !== void 0) fileModeSchema.parse(mode);
626
672
  }
627
- function assertKnowledgeMutationPath(path) {
673
+ /**
674
+ * Root-relative prefixes a knowledge transaction may write under. The pages
675
+ * directory is the only caller-selected member; the source registry file and
676
+ * the `raw/` evidence tree are package-owned and always present.
677
+ */
678
+ function knowledgeMutationPathPrefixes(pagesDirectory) {
679
+ return [`${normalizePagesDirectory(pagesDirectory)}/`, "raw/"];
680
+ }
681
+ function assertKnowledgeMutationPath(path, pagesDirectory) {
628
682
  const normalized = normalizeTransactionPath(path);
629
- if (normalized === ".agent-knowledge/sources.json" || normalized.startsWith("knowledge/") || normalized.startsWith("raw/")) return normalized;
683
+ if (normalized === SOURCE_REGISTRY_PATH || knowledgeMutationPathPrefixes(pagesDirectory).some((prefix) => normalized.startsWith(prefix))) return normalized;
630
684
  throw new Error(`knowledge transaction contains an unsupported path: ${path}`);
631
685
  }
632
686
  function normalizeTransactionPath(path) {
@@ -1492,6 +1546,23 @@ const KnowledgeGraphEdgeSchema = z.object({
1492
1546
  weight: z.number(),
1493
1547
  reasons: z.array(z.string())
1494
1548
  });
1549
+ const KnowledgeRelationSchema = z.object({
1550
+ sourceId: z.string(),
1551
+ targetId: z.string(),
1552
+ predicate: z.string(),
1553
+ weight: z.number().optional(),
1554
+ metadata: z.record(z.string(), z.unknown()).optional()
1555
+ });
1556
+ const KnowledgeRelationNodeSchema = z.object({
1557
+ id: z.string(),
1558
+ kind: z.string(),
1559
+ label: z.string().optional(),
1560
+ metadata: z.record(z.string(), z.unknown()).optional()
1561
+ });
1562
+ const KnowledgeRelationGraphSchema = z.object({
1563
+ nodes: z.array(KnowledgeRelationNodeSchema),
1564
+ edges: z.array(KnowledgeRelationSchema)
1565
+ });
1495
1566
  const KnowledgeIndexSchema = z.object({
1496
1567
  root: z.string(),
1497
1568
  generatedAt: z.string(),
@@ -1600,13 +1671,7 @@ const KnowledgeBaseCandidateSchema = z.object({
1600
1671
  ]).optional(),
1601
1672
  metadata: z.record(z.string(), z.unknown()).optional()
1602
1673
  })).optional(),
1603
- relations: z.array(z.object({
1604
- sourceId: z.string(),
1605
- targetId: z.string(),
1606
- predicate: z.string(),
1607
- weight: z.number().optional(),
1608
- metadata: z.record(z.string(), z.unknown()).optional()
1609
- })).optional(),
1674
+ relations: z.array(KnowledgeRelationSchema).optional(),
1610
1675
  sourceIds: z.array(z.string()).optional(),
1611
1676
  tags: z.array(z.string()).optional(),
1612
1677
  metadata: z.record(z.string(), z.unknown()).optional(),
@@ -1691,7 +1756,7 @@ async function loadKnowledgePages(root, options = {}) {
1691
1756
  return withKnowledgeRead(root, () => loadKnowledgePagesUnlocked(root, options));
1692
1757
  }
1693
1758
  async function loadKnowledgePagesUnlocked(root, options) {
1694
- const pagesDirectory = normalizePagesDirectory(options.pagesDirectory ?? "knowledge");
1759
+ const pagesDirectory = normalizePagesDirectory(options.pagesDirectory);
1695
1760
  let files;
1696
1761
  try {
1697
1762
  files = await listRegularFilesWithinRoot(root, pagesDirectory);
@@ -1700,36 +1765,48 @@ async function loadKnowledgePagesUnlocked(root, options) {
1700
1765
  throw error;
1701
1766
  }
1702
1767
  const pages = [];
1703
- const pagesPrefix = `${pagesDirectory}/`;
1704
1768
  for (const file of files) {
1705
1769
  const rel = file.path.replace(/\\/g, "/");
1706
- if (!rel.endsWith(".md")) continue;
1707
- if (isScaffoldPath(rel)) continue;
1708
- const { frontmatter, body } = parseFrontmatter(file.bytes.toString("utf8"));
1709
- const title = stringField(frontmatter.title) ?? firstHeading(body) ?? rel.split("/").pop().replace(/\.md$/, "");
1710
- const sourceIds = arrayField(frontmatter.sources);
1711
- const tags = arrayField(frontmatter.tags);
1712
- const cites = idListField(frontmatter.cites);
1713
- const contradicts = idListField(frontmatter.contradicts);
1714
- const invalidation = KnowledgePageInvalidationSchema.safeParse(frontmatter.invalidation);
1715
- const pageRelativePath = rel.startsWith(pagesPrefix) ? rel.slice(pagesPrefix.length) : rel;
1716
- pages.push({
1717
- id: stringField(frontmatter.id) ?? slugify(pageRelativePath.replace(/\.md$/, "")),
1718
- path: rel,
1719
- title,
1720
- text: body,
1721
- frontmatter,
1722
- sourceIds,
1723
- tags,
1724
- outLinks: extractWikilinks(body).map(normalizeLinkTarget),
1725
- ...cites.length > 0 ? { cites } : {},
1726
- ...contradicts.length > 0 ? { contradicts } : {},
1727
- ...invalidation.success ? { invalidation: invalidation.data } : {}
1728
- });
1770
+ if (!isKnowledgePagePath(rel)) continue;
1771
+ pages.push(knowledgePageFromMarkdown(rel, file.bytes.toString("utf8"), pagesDirectory));
1729
1772
  }
1730
1773
  pages.sort((a, b) => a.path.localeCompare(b.path));
1731
1774
  return pages;
1732
1775
  }
1776
+ /** True when a root-relative path names authored Markdown rather than scaffolding. */
1777
+ function isKnowledgePagePath(path) {
1778
+ const normalized = path.replace(/\\/g, "/");
1779
+ return normalized.endsWith(".md") && !isScaffoldPath(normalized);
1780
+ }
1781
+ /**
1782
+ * Build a page from the bytes of one Markdown file.
1783
+ *
1784
+ * The reader and every gate that inspects a page before it is written share
1785
+ * this constructor, so what a gate judges is exactly what the store loads back.
1786
+ */
1787
+ function knowledgePageFromMarkdown(path, content, pagesDirectory) {
1788
+ const rel = path.replace(/\\/g, "/");
1789
+ const pagesPrefix = `${normalizePagesDirectory(pagesDirectory)}/`;
1790
+ const { frontmatter, body } = parseFrontmatter(content);
1791
+ const title = stringField(frontmatter.title) ?? firstHeading(body) ?? rel.split("/").pop().replace(/\.md$/, "");
1792
+ const cites = idListField(frontmatter.cites);
1793
+ const contradicts = idListField(frontmatter.contradicts);
1794
+ const invalidation = KnowledgePageInvalidationSchema.safeParse(frontmatter.invalidation);
1795
+ const pageRelativePath = rel.startsWith(pagesPrefix) ? rel.slice(pagesPrefix.length) : rel;
1796
+ return {
1797
+ id: stringField(frontmatter.id) ?? slugify(pageRelativePath.replace(/\.md$/, "")),
1798
+ path: rel,
1799
+ title,
1800
+ text: body,
1801
+ frontmatter,
1802
+ sourceIds: arrayField(frontmatter.sources),
1803
+ tags: arrayField(frontmatter.tags),
1804
+ outLinks: extractWikilinks(body).map(normalizeLinkTarget),
1805
+ ...cites.length > 0 ? { cites } : {},
1806
+ ...contradicts.length > 0 ? { contradicts } : {},
1807
+ ...invalidation.success ? { invalidation: invalidation.data } : {}
1808
+ };
1809
+ }
1733
1810
  async function writeJson(path, value) {
1734
1811
  await writeJsonDurable(path, value);
1735
1812
  }
@@ -1755,11 +1832,6 @@ async function knowledgeBaseInitialized(layout) {
1755
1832
  throw error;
1756
1833
  }
1757
1834
  }
1758
- function normalizePagesDirectory(value) {
1759
- const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1760
- if (normalized.length === 0 || normalized === ".") throw new Error("pagesDirectory must name a root-relative directory");
1761
- return normalized;
1762
- }
1763
1835
  function stringField(value) {
1764
1836
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
1765
1837
  }
@@ -1927,7 +1999,18 @@ function isPageOrigin(value) {
1927
1999
  }
1928
2000
  //#endregion
1929
2001
  //#region src/graph.ts
1930
- function buildKnowledgeGraph(pages) {
2002
+ /**
2003
+ * The labeled relations between pages, one per `(sourceId, targetId, predicate)`.
2004
+ *
2005
+ * `wikilink`, `citation`, and `contradicts` follow an authored link, `cites`
2006
+ * entry, or `contradicts` entry to the one page it resolves to; a missing,
2007
+ * ambiguous, or self-referencing target emits nothing. `shared-source` joins
2008
+ * every pair of pages that registers a common source, in page order, weighted
2009
+ * 0.5 per shared source id, with the shared ids in `metadata.sourceIds`. It is
2010
+ * implicit, so it is listed after the authored relations and never counts
2011
+ * toward a page's degree in `buildKnowledgeGraph`.
2012
+ */
2013
+ function knowledgePageRelations(pages) {
1931
2014
  const bySlug = /* @__PURE__ */ new Map();
1932
2015
  const byId = /* @__PURE__ */ new Map();
1933
2016
  for (const page of pages) {
@@ -1936,26 +2019,93 @@ function buildKnowledgeGraph(pages) {
1936
2019
  bySlug.set(normalizeLinkTarget(page.title), page);
1937
2020
  bySlug.set(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")), page);
1938
2021
  }
2022
+ const authored = /* @__PURE__ */ new Map();
2023
+ const count = (source, target, predicate) => {
2024
+ const key = JSON.stringify([
2025
+ source.id,
2026
+ target.id,
2027
+ predicate
2028
+ ]);
2029
+ const relation = authored.get(key);
2030
+ if (relation) relation.weight += 1;
2031
+ else authored.set(key, {
2032
+ sourceId: source.id,
2033
+ targetId: target.id,
2034
+ predicate,
2035
+ weight: 1
2036
+ });
2037
+ };
2038
+ const uniqueTarget = (id) => {
2039
+ const targets = byId.get(id) ?? [];
2040
+ return targets.length === 1 ? targets[0] : void 0;
2041
+ };
2042
+ for (const page of pages) {
2043
+ for (const raw of page.outLinks) {
2044
+ const target = bySlug.get(normalizeLinkTarget(raw));
2045
+ if (!target || target.id === page.id) continue;
2046
+ count(page, target, "wikilink");
2047
+ }
2048
+ for (const persisted of page.cites ?? []) {
2049
+ const target = uniqueTarget(citedPageId(persisted));
2050
+ if (!target || target.id === page.id) continue;
2051
+ count(page, target, "citation");
2052
+ }
2053
+ for (const contradicted of page.contradicts ?? []) {
2054
+ const target = uniqueTarget(contradicted);
2055
+ if (!target || target.id === page.id) continue;
2056
+ count(page, target, "contradicts");
2057
+ }
2058
+ }
2059
+ const relations = [...authored.values()];
2060
+ for (let i = 0; i < pages.length; i++) for (let j = i + 1; j < pages.length; j++) {
2061
+ const a = pages[i];
2062
+ const b = pages[j];
2063
+ const overlap = a.sourceIds.filter((source) => b.sourceIds.includes(source));
2064
+ if (overlap.length === 0) continue;
2065
+ relations.push({
2066
+ sourceId: a.id,
2067
+ targetId: b.id,
2068
+ predicate: "shared-source",
2069
+ weight: overlap.length * .5,
2070
+ metadata: { sourceIds: overlap }
2071
+ });
2072
+ }
2073
+ return relations;
2074
+ }
2075
+ /**
2076
+ * The weighted page graph: `knowledgePageRelations` collapsed to one edge per
2077
+ * ordered page pair, with the predicates merged into `reasons` and the weights
2078
+ * summed. `contradicts` stays out of this projection; read it from the relation
2079
+ * list. Node degrees count the authored `wikilink` and `citation` occurrences.
2080
+ */
2081
+ function buildKnowledgeGraph(pages) {
1939
2082
  const incoming = /* @__PURE__ */ new Map();
1940
2083
  const outgoing = /* @__PURE__ */ new Map();
1941
- const edgesByKey = /* @__PURE__ */ new Map();
1942
2084
  for (const page of pages) {
1943
2085
  outgoing.set(page.id, 0);
1944
2086
  incoming.set(page.id, 0);
1945
2087
  }
1946
- for (const page of pages) {
1947
- for (const raw of page.outLinks) {
1948
- const target = bySlug.get(normalizeLinkTarget(raw));
1949
- if (!target || target.id === page.id) continue;
1950
- addDirectedEdge(page, target, "wikilink", edgesByKey, incoming, outgoing);
2088
+ const edgesByKey = /* @__PURE__ */ new Map();
2089
+ for (const relation of knowledgePageRelations(pages)) {
2090
+ if (relation.predicate === "contradicts") continue;
2091
+ const key = `${relation.sourceId}->${relation.targetId}`;
2092
+ let edge = edgesByKey.get(key);
2093
+ if (!edge) {
2094
+ edge = {
2095
+ source: relation.sourceId,
2096
+ target: relation.targetId,
2097
+ weight: 0,
2098
+ reasons: []
2099
+ };
2100
+ edgesByKey.set(key, edge);
1951
2101
  }
1952
- for (const persisted of page.cites ?? []) {
1953
- const targets = byId.get(citedPageId(persisted)) ?? [];
1954
- if (targets.length !== 1 || targets[0].id === page.id) continue;
1955
- addDirectedEdge(page, targets[0], "citation", edgesByKey, incoming, outgoing);
2102
+ edge.weight += relation.weight;
2103
+ if (!edge.reasons.includes(relation.predicate)) edge.reasons.push(relation.predicate);
2104
+ if (relation.predicate === "wikilink" || relation.predicate === "citation") {
2105
+ outgoing.set(relation.sourceId, (outgoing.get(relation.sourceId) ?? 0) + relation.weight);
2106
+ incoming.set(relation.targetId, (incoming.get(relation.targetId) ?? 0) + relation.weight);
1956
2107
  }
1957
2108
  }
1958
- addSourceOverlapEdges(pages, edgesByKey);
1959
2109
  return {
1960
2110
  nodes: pages.map((page) => ({
1961
2111
  id: page.id,
@@ -1982,40 +2132,6 @@ function citedPageId(persisted) {
1982
2132
  return persisted;
1983
2133
  }
1984
2134
  }
1985
- function addDirectedEdge(source, target, reason, edges, incoming, outgoing) {
1986
- const key = `${source.id}->${target.id}`;
1987
- const edge = edges.get(key);
1988
- if (edge) {
1989
- edge.weight += 1;
1990
- if (!edge.reasons.includes(reason)) edge.reasons.push(reason);
1991
- } else edges.set(key, {
1992
- source: source.id,
1993
- target: target.id,
1994
- weight: 1,
1995
- reasons: [reason]
1996
- });
1997
- outgoing.set(source.id, (outgoing.get(source.id) ?? 0) + 1);
1998
- incoming.set(target.id, (incoming.get(target.id) ?? 0) + 1);
1999
- }
2000
- function addSourceOverlapEdges(pages, edges) {
2001
- for (let i = 0; i < pages.length; i++) for (let j = i + 1; j < pages.length; j++) {
2002
- const a = pages[i];
2003
- const b = pages[j];
2004
- const overlap = a.sourceIds.filter((source) => b.sourceIds.includes(source));
2005
- if (overlap.length === 0) continue;
2006
- const key = `${a.id}->${b.id}`;
2007
- const edge = edges.get(key);
2008
- if (edge) {
2009
- edge.weight += overlap.length * .5;
2010
- if (!edge.reasons.includes("shared-source")) edge.reasons.push("shared-source");
2011
- } else edges.set(key, {
2012
- source: a.id,
2013
- target: b.id,
2014
- weight: overlap.length * .5,
2015
- reasons: ["shared-source"]
2016
- });
2017
- }
2018
- }
2019
2135
  //#endregion
2020
2136
  //#region src/kb-store.ts
2021
2137
  /**
@@ -2546,11 +2662,11 @@ function textPreview(fileName, bytes) {
2546
2662
  }
2547
2663
  //#endregion
2548
2664
  //#region src/indexer.ts
2549
- async function buildKnowledgeIndex(root) {
2550
- return withKnowledgeRead(root, () => buildKnowledgeIndexUnlocked(root));
2665
+ async function buildKnowledgeIndex(root, options = {}) {
2666
+ return withKnowledgeRead(root, () => buildKnowledgeIndexUnlocked(root, options));
2551
2667
  }
2552
- async function buildKnowledgeIndexUnlocked(root) {
2553
- const [pages, sourceRegistry] = await Promise.all([loadKnowledgePages(root), loadSourceRegistry(root)]);
2668
+ async function buildKnowledgeIndexUnlocked(root, options) {
2669
+ const [pages, sourceRegistry] = await Promise.all([loadKnowledgePages(root, options), loadSourceRegistry(root)]);
2554
2670
  return {
2555
2671
  root,
2556
2672
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2568,9 +2684,9 @@ async function buildKnowledgeIndexUnlocked(root) {
2568
2684
  * a store-based reader saw none of the indexer's work. One writer now, and it
2569
2685
  * validates through `KnowledgeIndexSchema` on the way out.
2570
2686
  */
2571
- async function writeKnowledgeIndex(root) {
2687
+ async function writeKnowledgeIndex(root, options = {}) {
2572
2688
  return withKnowledgeMutation(root, async () => {
2573
- const index = await buildKnowledgeIndexUnlocked(root);
2689
+ const index = await buildKnowledgeIndexUnlocked(root, options);
2574
2690
  await new FileSystemKbStore({ root }).putIndex(index);
2575
2691
  return index;
2576
2692
  });
@@ -2765,9 +2881,11 @@ function lintKnowledgeIndex(index) {
2765
2881
  const sourceIds = new Set(index.sources.map((source) => source.id));
2766
2882
  const anchorIds = new Map(index.sources.map((source) => [source.id, new Set((source.anchors ?? []).map((anchor) => anchor.id))]));
2767
2883
  const pageIds = /* @__PURE__ */ new Map();
2884
+ const invalidatedIds = /* @__PURE__ */ new Set();
2768
2885
  const sourceHashes = /* @__PURE__ */ new Map();
2769
2886
  for (const page of index.pages) {
2770
2887
  pageIds.set(page.id, [...pageIds.get(page.id) ?? [], page.path]);
2888
+ if (page.invalidation !== void 0) invalidatedIds.add(page.id);
2771
2889
  byTarget.add(normalizeLinkTarget(page.id));
2772
2890
  byTarget.add(normalizeLinkTarget(page.title));
2773
2891
  byTarget.add(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")));
@@ -2829,6 +2947,7 @@ function lintKnowledgeIndex(index) {
2829
2947
  findings.push(...lintPageEvidence(page));
2830
2948
  findings.push(...lintPageContradictions(page, pageIds));
2831
2949
  findings.push(...lintPageInvalidation(page));
2950
+ findings.push(...lintPageInvalidatedCitations(page, pageIds, invalidatedIds));
2832
2951
  }
2833
2952
  for (const [title, paths] of titles) if (title && paths.length > 1) findings.push({
2834
2953
  type: "duplicate-title",
@@ -2915,6 +3034,24 @@ function lintPageContradictions(page, pageIds) {
2915
3034
  }
2916
3035
  return findings;
2917
3036
  }
3037
+ /**
3038
+ * A citation into a page its own evidence refuted.
3039
+ *
3040
+ * The verdict lives on the cited page, so a reader arriving through the
3041
+ * citation never meets it. Ambiguous ids are left to the citation audit, which
3042
+ * owns that verdict.
3043
+ */
3044
+ function lintPageInvalidatedCitations(page, pageIds, invalidatedIds) {
3045
+ const targetIds = [...new Set((page.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted).pageId))].filter((targetId) => targetId !== page.id && pageIds.get(targetId)?.length === 1 && invalidatedIds.has(targetId)).sort();
3046
+ if (targetIds.length === 0) return [];
3047
+ return [{
3048
+ type: "cites-invalidated",
3049
+ severity: "warning",
3050
+ page: page.path,
3051
+ message: `Page cites invalidated ${targetIds.length === 1 ? "page" : "pages"} ${targetIds.join(", ")}.`,
3052
+ metadata: { targetIds }
3053
+ }];
3054
+ }
2918
3055
  function lintPageInvalidation(page) {
2919
3056
  if (page.frontmatter.invalidation === void 0) return [];
2920
3057
  const parsed = KnowledgePageInvalidationSchema.safeParse(page.frontmatter.invalidation);
@@ -2973,6 +3110,420 @@ function validateKnowledgeIndex(index, options = {}) {
2973
3110
  };
2974
3111
  }
2975
3112
  //#endregion
3113
+ //#region src/rag-eval/near-duplicates.ts
3114
+ /**
3115
+ * Detect exact and near-duplicate pages without a model, embedding service, or clock.
3116
+ *
3117
+ * The detector uses word shingles when prose has enough tokens and Unicode
3118
+ * character shingles otherwise. An inverted index proposes candidate pairs;
3119
+ * high-document-frequency shingles are ignored so common boilerplate cannot
3120
+ * make candidate generation quadratic. Exact normalized copies are always
3121
+ * proposed before ordinary shingle candidates, even when every shingle is
3122
+ * common. Every limit and sort order is explicit so the same corpus produces
3123
+ * the same report on every run.
3124
+ */
3125
+ function detectNearDuplicatePages(pages, options = {}) {
3126
+ const threshold = finiteUnitInterval(options.threshold ?? .82, "threshold");
3127
+ const minCharacters = nonNegativeInteger(options.minCharacters ?? 80, "minCharacters");
3128
+ const wordShingleSize = positiveInteger(options.wordShingleSize ?? 5, "wordShingleSize");
3129
+ const characterShingleSize = positiveInteger(options.characterShingleSize ?? 16, "characterShingleSize");
3130
+ const maxDocumentFrequency = positiveInteger(options.maxDocumentFrequency ?? 64, "maxDocumentFrequency");
3131
+ const maxCandidatePairs = positiveInteger(options.maxCandidatePairs ?? 25e4, "maxCandidatePairs");
3132
+ const maxReportedPairs = nonNegativeInteger(options.maxReportedPairs ?? 1e3, "maxReportedPairs");
3133
+ const prepared = pages.map((page) => preparePage(page, wordShingleSize, characterShingleSize)).filter((item) => item !== null && item.normalized.length >= minCharacters).sort((left, right) => left.page.path.localeCompare(right.page.path));
3134
+ const candidateKeys = /* @__PURE__ */ new Set();
3135
+ const exactGroups = /* @__PURE__ */ new Map();
3136
+ const byShingle = /* @__PURE__ */ new Map();
3137
+ for (const [index, item] of prepared.entries()) {
3138
+ exactGroups.set(item.normalized, [...exactGroups.get(item.normalized) ?? [], index]);
3139
+ for (const shingle of item.shingles) byShingle.set(shingle, [...byShingle.get(shingle) ?? [], index]);
3140
+ }
3141
+ let truncated = false;
3142
+ const addPair = (left, right) => {
3143
+ if (left === right) return true;
3144
+ const key = pairKey(Math.min(left, right), Math.max(left, right));
3145
+ if (candidateKeys.has(key)) return true;
3146
+ if (candidateKeys.size >= maxCandidatePairs) {
3147
+ truncated = true;
3148
+ return false;
3149
+ }
3150
+ candidateKeys.add(key);
3151
+ return true;
3152
+ };
3153
+ for (const normalized of [...exactGroups.keys()].sort()) if (!addGroupPairs(exactGroups.get(normalized), addPair)) break;
3154
+ if (!truncated) for (const shingle of [...byShingle.keys()].sort()) {
3155
+ const group = byShingle.get(shingle);
3156
+ if (group.length > maxDocumentFrequency) continue;
3157
+ if (!addGroupPairs(group, addPair)) break;
3158
+ }
3159
+ const duplicatePageIndices = /* @__PURE__ */ new Set();
3160
+ const pairs = [];
3161
+ let duplicatePairCount = 0;
3162
+ let comparedPairCount = 0;
3163
+ for (const key of [...candidateKeys].sort(pairKeyOrder)) {
3164
+ const [leftIndex, rightIndex] = parsePairKey(key);
3165
+ const left = prepared[leftIndex];
3166
+ const right = prepared[rightIndex];
3167
+ comparedPairCount += 1;
3168
+ const exact = left.normalized === right.normalized;
3169
+ const overlap = jaccard(left.shingles, right.shingles);
3170
+ if (!exact && overlap.similarity < threshold) continue;
3171
+ duplicatePairCount += 1;
3172
+ duplicatePageIndices.add(leftIndex);
3173
+ duplicatePageIndices.add(rightIndex);
3174
+ if (pairs.length >= maxReportedPairs) {
3175
+ truncated = true;
3176
+ continue;
3177
+ }
3178
+ pairs.push({
3179
+ leftPageId: left.page.id,
3180
+ leftPath: left.page.path,
3181
+ rightPageId: right.page.id,
3182
+ rightPath: right.page.path,
3183
+ similarity: exact ? 1 : round(overlap.similarity),
3184
+ intersectionSize: overlap.intersectionSize,
3185
+ unionSize: overlap.unionSize,
3186
+ exact
3187
+ });
3188
+ }
3189
+ pairs.sort((left, right) => right.similarity - left.similarity || left.leftPath.localeCompare(right.leftPath) || left.rightPath.localeCompare(right.rightPath));
3190
+ return {
3191
+ algorithm: "deterministic-shingle-jaccard-v1",
3192
+ threshold,
3193
+ pageCount: pages.length,
3194
+ eligiblePageCount: prepared.length,
3195
+ candidatePairCount: candidateKeys.size,
3196
+ comparedPairCount,
3197
+ duplicatePairCount,
3198
+ duplicatePageCount: duplicatePageIndices.size,
3199
+ duplicatePageRate: prepared.length === 0 ? 0 : round(duplicatePageIndices.size / prepared.length),
3200
+ truncated,
3201
+ pairs
3202
+ };
3203
+ }
3204
+ function preparePage(page, wordShingleSize, characterShingleSize) {
3205
+ const normalized = normalizePageText(`${page.title}\n${page.text}`);
3206
+ if (normalized.length === 0) return null;
3207
+ const words = normalized.match(/[\p{L}\p{N}]+/gu) ?? [];
3208
+ const shingles = words.length >= wordShingleSize + 2 ? sequenceShingles(words, wordShingleSize) : characterShingles(normalized.replace(/\s+/g, ""), characterShingleSize);
3209
+ if (shingles.size === 0) shingles.add(normalized);
3210
+ return {
3211
+ page,
3212
+ normalized,
3213
+ shingles
3214
+ };
3215
+ }
3216
+ function normalizePageText(value) {
3217
+ return value.normalize("NFKC").toLowerCase().replace(/https?:\/\/\S+/g, "<url>").replace(/\s+/g, " ").trim();
3218
+ }
3219
+ function sequenceShingles(items, width) {
3220
+ const out = /* @__PURE__ */ new Set();
3221
+ if (items.length < width) return out;
3222
+ for (let index = 0; index <= items.length - width; index += 1) out.add(items.slice(index, index + width).join(""));
3223
+ return out;
3224
+ }
3225
+ function characterShingles(value, width) {
3226
+ const chars = [...value];
3227
+ const out = /* @__PURE__ */ new Set();
3228
+ if (chars.length < width) return out;
3229
+ for (let index = 0; index <= chars.length - width; index += 1) out.add(chars.slice(index, index + width).join(""));
3230
+ return out;
3231
+ }
3232
+ function addGroupPairs(group, add) {
3233
+ for (let left = 0; left < group.length; left += 1) for (let right = left + 1; right < group.length; right += 1) if (!add(group[left], group[right])) return false;
3234
+ return true;
3235
+ }
3236
+ function jaccard(left, right) {
3237
+ const [small, large] = left.size <= right.size ? [left, right] : [right, left];
3238
+ let intersectionSize = 0;
3239
+ for (const item of small) if (large.has(item)) intersectionSize += 1;
3240
+ const unionSize = left.size + right.size - intersectionSize;
3241
+ return {
3242
+ similarity: unionSize === 0 ? 1 : intersectionSize / unionSize,
3243
+ intersectionSize,
3244
+ unionSize
3245
+ };
3246
+ }
3247
+ function pairKey(left, right) {
3248
+ return `${left}:${right}`;
3249
+ }
3250
+ function parsePairKey(value) {
3251
+ const [left, right] = value.split(":");
3252
+ return [Number(left), Number(right)];
3253
+ }
3254
+ function pairKeyOrder(left, right) {
3255
+ const [leftA, leftB] = parsePairKey(left);
3256
+ const [rightA, rightB] = parsePairKey(right);
3257
+ return leftA - rightA || leftB - rightB;
3258
+ }
3259
+ function finiteUnitInterval(value, name) {
3260
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new Error(`${name} must be a finite number in [0, 1]`);
3261
+ return value;
3262
+ }
3263
+ function positiveInteger(value, name) {
3264
+ if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`);
3265
+ return value;
3266
+ }
3267
+ function nonNegativeInteger(value, name) {
3268
+ if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
3269
+ return value;
3270
+ }
3271
+ function round(value) {
3272
+ return Math.round(value * 1e6) / 1e6;
3273
+ }
3274
+ //#endregion
3275
+ //#region src/run-scoped.ts
3276
+ /**
3277
+ * Run-scoped knowledge stores with lineage-chain reads.
3278
+ *
3279
+ * One store exists per run, physically isolated, with inheritance only through
3280
+ * declared ancestry. Reads expose the current store, every declared ancestor,
3281
+ * and an optional curated shared store with an explicit origin label.
3282
+ */
3283
+ /**
3284
+ * Present plain pages as a visibility chain of one origin.
3285
+ *
3286
+ * A store read directly is the `here` origin of a chain with no ancestry, so
3287
+ * the chain-shaped APIs — citation resolution, the write intake gate,
3288
+ * invalidation propagation — take one page shape whether or not the caller
3289
+ * runs run-scoped stores.
3290
+ */
3291
+ function originatedPages(pages, origin = "here") {
3292
+ return pages.map((page) => ({
3293
+ page,
3294
+ origin
3295
+ }));
3296
+ }
3297
+ const RUN_LINEAGE_BASENAME = "lineage.json";
3298
+ /** Ancestry beyond this bound is invalid durable state. */
3299
+ const MAX_LINEAGE_HOPS = 64;
3300
+ function createRunScopedStores(options) {
3301
+ if (!options || typeof options !== "object") throw new TypeError("createRunScopedStores options are required");
3302
+ if (typeof options.root !== "string" || options.root.trim().length === 0) throw new TypeError("createRunScopedStores root must be a non-empty string");
3303
+ if (options.runStorePath !== void 0 && typeof options.runStorePath !== "function") throw new TypeError("createRunScopedStores runStorePath must be a function when present");
3304
+ if (options.lineageAuthority !== void 0) validateLineageAuthority(options.lineageAuthority);
3305
+ const pagesDirectory = normalizePagesDirectory(options.pagesDirectory);
3306
+ const storePath = options.runStorePath ?? ((runId) => join(options.root, runId, "knowledge-base"));
3307
+ const internalAuthority = createFileRunLineageAuthority(storePath);
3308
+ const authority = options.lineageAuthority ?? internalAuthority;
3309
+ const resolveLineage = async (runId) => {
3310
+ assertRunId(runId);
3311
+ const chain = [];
3312
+ const seen = /* @__PURE__ */ new Set([runId]);
3313
+ let current = runId;
3314
+ for (let hop = 0; hop <= MAX_LINEAGE_HOPS; hop += 1) {
3315
+ const parent = await authority.parentOf(current);
3316
+ if (parent === null) return chain;
3317
+ assertRunId(parent, `parent of '${current}'`);
3318
+ if (seen.has(parent)) throw new Error(`run lineage cycle: ${parent} is its own ancestor (via ${runId})`);
3319
+ seen.add(parent);
3320
+ chain.push(parent);
3321
+ current = parent;
3322
+ }
3323
+ throw new Error(`run lineage for '${runId}' exceeds ${MAX_LINEAGE_HOPS} ancestors`);
3324
+ };
3325
+ return {
3326
+ storePath(runId) {
3327
+ assertRunId(runId);
3328
+ return storePath(runId);
3329
+ },
3330
+ async init(runId, initOptions = {}) {
3331
+ assertRunId(runId);
3332
+ const parentRunId = initOptions.parentRunId ?? null;
3333
+ if (parentRunId !== null) assertRunId(parentRunId, `parent of '${runId}'`);
3334
+ if (!authority.record) {
3335
+ const existingParent = await authority.parentOf(runId);
3336
+ if (existingParent !== parentRunId) throw new Error(`external lineage authority disagrees for '${runId}': expected ${renderParent(parentRunId)}, observed ${renderParent(existingParent)}`);
3337
+ }
3338
+ const layout = await initKnowledgeBase(storePath(runId));
3339
+ if (authority.record) await authority.record(Object.freeze({
3340
+ runId,
3341
+ parentRunId,
3342
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3343
+ }));
3344
+ return layout;
3345
+ },
3346
+ lineage: resolveLineage,
3347
+ async loadChain(runId) {
3348
+ assertRunId(runId);
3349
+ const out = [];
3350
+ const readInto = async (root, origin) => {
3351
+ let pages;
3352
+ try {
3353
+ pages = await loadKnowledgePages(root, { pagesDirectory });
3354
+ } catch (error) {
3355
+ if (isMissingFile(error)) return;
3356
+ throw error;
3357
+ }
3358
+ for (const page of pages) out.push({
3359
+ page,
3360
+ origin
3361
+ });
3362
+ };
3363
+ await readInto(storePath(runId), "here");
3364
+ for (const ancestor of await resolveLineage(runId)) await readInto(storePath(ancestor), `inherited:${ancestor}`);
3365
+ if (options.sharedRoot) await readInto(options.sharedRoot, "shared");
3366
+ return out;
3367
+ }
3368
+ };
3369
+ }
3370
+ /** File-backed authority used when the product has no separate run manifest. */
3371
+ function createFileRunLineageAuthority(runStorePath) {
3372
+ if (typeof runStorePath !== "function") throw new TypeError("createFileRunLineageAuthority requires a runStorePath function");
3373
+ const readRecord = async (runId) => {
3374
+ assertRunId(runId);
3375
+ const root = runStorePath(runId);
3376
+ try {
3377
+ return parseLineageRecord((await readRegularFileWithinRoot(root, RUN_LINEAGE_BASENAME)).bytes.toString("utf8"), runId);
3378
+ } catch (error) {
3379
+ if (isMissingFile(error)) return null;
3380
+ throw error;
3381
+ }
3382
+ };
3383
+ return {
3384
+ async parentOf(runId) {
3385
+ return (await readRecord(runId))?.parentRunId ?? null;
3386
+ },
3387
+ async record(record) {
3388
+ const validated = validateLineageRecord(record);
3389
+ const root = runStorePath(validated.runId);
3390
+ await withKnowledgeMutation(root, async () => {
3391
+ const existing = await readRecord(validated.runId);
3392
+ if (existing) {
3393
+ if (existing.parentRunId !== validated.parentRunId) throw new Error(`run lineage conflict for '${validated.runId}': existing parent ${renderParent(existing.parentRunId)}, requested ${renderParent(validated.parentRunId)}`);
3394
+ return;
3395
+ }
3396
+ await writeJson(join(root, RUN_LINEAGE_BASENAME), validated);
3397
+ });
3398
+ }
3399
+ };
3400
+ }
3401
+ function validateLineageAuthority(authority) {
3402
+ if (!authority || typeof authority !== "object") throw new TypeError("lineageAuthority must be an object");
3403
+ if (typeof authority.parentOf !== "function") throw new TypeError("lineageAuthority.parentOf must be a function");
3404
+ if (authority.record !== void 0 && typeof authority.record !== "function") throw new TypeError("lineageAuthority.record must be a function when present");
3405
+ }
3406
+ function parseLineageRecord(text, expectedRunId) {
3407
+ let parsed;
3408
+ try {
3409
+ parsed = JSON.parse(text);
3410
+ } catch (cause) {
3411
+ throw new Error(`invalid lineage JSON for '${expectedRunId}'`, { cause });
3412
+ }
3413
+ const record = validateLineageRecord(parsed);
3414
+ if (record.runId !== expectedRunId) throw new Error(`lineage record identity mismatch: expected '${expectedRunId}', found '${record.runId}'`);
3415
+ return record;
3416
+ }
3417
+ function validateLineageRecord(value) {
3418
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("run lineage record must be an object");
3419
+ const record = value;
3420
+ assertRunId(record.runId, "lineage record runId");
3421
+ if (record.parentRunId !== null) assertRunId(record.parentRunId, `parent of '${record.runId}'`);
3422
+ if (typeof record.createdAt !== "string" || record.createdAt.trim().length === 0 || !Number.isFinite(Date.parse(record.createdAt))) throw new TypeError("run lineage record createdAt must be an ISO timestamp");
3423
+ return {
3424
+ runId: record.runId,
3425
+ parentRunId: record.parentRunId,
3426
+ createdAt: record.createdAt
3427
+ };
3428
+ }
3429
+ function assertRunId(value, label = "runId") {
3430
+ if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${label} must be a non-empty string`);
3431
+ const trimmed = value.trim();
3432
+ if (trimmed === "." || trimmed === ".." || /[/\\\0]/.test(value)) throw new TypeError(`${label} must not contain path separators or dot segments`);
3433
+ }
3434
+ function renderParent(parentRunId) {
3435
+ return parentRunId === null ? "none" : `'${parentRunId}'`;
3436
+ }
3437
+ //#endregion
3438
+ //#region src/write-intake.ts
3439
+ /**
3440
+ * Write-time intake gate.
3441
+ *
3442
+ * A store degrades in two ways that no post-hoc report reverses: a page lands
3443
+ * that restates knowledge already present without relating itself to it, and a
3444
+ * page lands citing an id that exists nowhere. Both are cheap to refuse at the
3445
+ * moment of the write and expensive to repair afterwards, so the gate runs
3446
+ * before bytes are committed.
3447
+ */
3448
+ /**
3449
+ * A write duplicates visible knowledge and declares no relation to it.
3450
+ *
3451
+ * Every listed pair is cleared by one authoring action: cite the matched page,
3452
+ * name it in `contradicts`, or give the candidate the matched page's id so the
3453
+ * write is an update of it.
3454
+ */
3455
+ var KnowledgeDuplicateIntakeError = class extends Error {
3456
+ pairs;
3457
+ threshold;
3458
+ constructor(pairs, threshold) {
3459
+ const summary = pairs.map((pair) => `${pair.candidatePath} ~ ${pair.matchedPath} (${pair.exact ? "exact copy" : pair.similarity.toFixed(3)})`).join("; ");
3460
+ super(`knowledge write intake refused ${pairs.length} duplicate ${pairs.length === 1 ? "pair" : "pairs"}: ${summary}. Cite the matched page, name it in contradicts, or reuse its id to update it.`);
3461
+ this.name = "KnowledgeDuplicateIntakeError";
3462
+ this.pairs = Object.freeze([...pairs]);
3463
+ this.threshold = threshold;
3464
+ }
3465
+ };
3466
+ /**
3467
+ * Refuse a write that duplicates visible knowledge or cites a page that exists
3468
+ * nowhere.
3469
+ *
3470
+ * The candidates are part of the corpus both checks see, so a batch may cite a
3471
+ * page it writes in the same call, and two candidates that duplicate each other
3472
+ * are caught before either lands.
3473
+ *
3474
+ * Returns the resolved citation candidates in the order the candidates declare
3475
+ * them.
3476
+ */
3477
+ function assertKnowledgeWriteIntake(candidates, options) {
3478
+ if (!Array.isArray(candidates)) throw new TypeError("knowledge write intake candidates must be an array");
3479
+ if (!options || typeof options !== "object" || !Array.isArray(options.visiblePages)) throw new TypeError("knowledge write intake requires the visible pages of the target chain");
3480
+ const candidatePaths = new Set(candidates.map((candidate) => candidate.path));
3481
+ const corpus = [...options.visiblePages.filter((entry) => !(entry.origin === "here" && candidatePaths.has(entry.page.path))), ...candidates.map((page) => ({
3482
+ page,
3483
+ origin: "here"
3484
+ }))];
3485
+ assertNoUnrelatedDuplicate(candidatePaths, corpus, options.nearDuplicates ?? {});
3486
+ if (options.citations === false) return Object.freeze([]);
3487
+ return assertKnowledgeCitationsResolved(corpus, candidates.flatMap((candidate) => (candidate.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted))));
3488
+ }
3489
+ function assertNoUnrelatedDuplicate(candidatePaths, corpus, nearDuplicates) {
3490
+ if (candidatePaths.size === 0) return;
3491
+ const report = detectNearDuplicatePages(corpus.map((entry) => entry.page), nearDuplicates);
3492
+ const byIdentity = /* @__PURE__ */ new Map();
3493
+ for (const entry of corpus) {
3494
+ const key = identityKey(entry.page.path, entry.page.id);
3495
+ if (!byIdentity.has(key)) byIdentity.set(key, entry.page);
3496
+ }
3497
+ const offending = [];
3498
+ for (const pair of report.pairs) {
3499
+ const leftIsCandidate = candidatePaths.has(pair.leftPath);
3500
+ if (!leftIsCandidate && !candidatePaths.has(pair.rightPath)) continue;
3501
+ if (pair.leftPageId === pair.rightPageId) continue;
3502
+ const left = byIdentity.get(identityKey(pair.leftPath, pair.leftPageId));
3503
+ const right = byIdentity.get(identityKey(pair.rightPath, pair.rightPageId));
3504
+ if (declaresRelation(left, right.id) || declaresRelation(right, left.id)) continue;
3505
+ const candidate = leftIsCandidate ? left : right;
3506
+ const matched = leftIsCandidate ? right : left;
3507
+ offending.push({
3508
+ candidatePageId: candidate.id,
3509
+ candidatePath: candidate.path,
3510
+ matchedPageId: matched.id,
3511
+ matchedPath: matched.path,
3512
+ similarity: pair.similarity,
3513
+ exact: pair.exact
3514
+ });
3515
+ }
3516
+ if (offending.length > 0) throw new KnowledgeDuplicateIntakeError(offending, report.threshold);
3517
+ }
3518
+ function identityKey(path, id) {
3519
+ return `${path}\u0000${id}`;
3520
+ }
3521
+ /** Whether `page` already relates itself to `targetId` through `cites` or `contradicts`. */
3522
+ function declaresRelation(page, targetId) {
3523
+ for (const persisted of page.cites ?? []) if (parseKnowledgeCitationReference(persisted).pageId === targetId) return true;
3524
+ return (page.contradicts ?? []).includes(targetId);
3525
+ }
3526
+ //#endregion
2976
3527
  //#region src/write-protocol.ts
2977
3528
  const OPENER_LINE = /^---\s*FILE:\s*(.+?)\s*---\s*$/i;
2978
3529
  const CLOSER_LINE = /^---\s*END\s+FILE\s*---\s*$/i;
@@ -3048,28 +3599,49 @@ function parseKnowledgeWriteBlocks(text, allowedPrefixes = ["knowledge/"]) {
3048
3599
  }
3049
3600
  //#endregion
3050
3601
  //#region src/proposals.ts
3051
- async function applyKnowledgeWriteBlocks(root, proposalText) {
3052
- const parsed = parseKnowledgeWriteBlocks(proposalText);
3602
+ /**
3603
+ * Apply the FILE blocks of a proposal under the pages directory.
3604
+ *
3605
+ * A block whose path lies outside `<pagesDirectory>/` is refused by the parser
3606
+ * and again by the file transaction, so one option value bounds the whole
3607
+ * write.
3608
+ */
3609
+ async function applyKnowledgeWriteBlocks(root, proposalText, options = {}) {
3610
+ const pagesDirectory = normalizePagesDirectory(options.pagesDirectory);
3611
+ const parsed = parseKnowledgeWriteBlocks(proposalText, [`${pagesDirectory}/`]);
3053
3612
  const purpose = `knowledge-proposal:${contentHash(parsed.blocks)}`;
3613
+ const intake = options.intake;
3054
3614
  return withKnowledgeMutation(root, async (lock) => {
3055
- if (parsed.blocks.length > 0) await commitKnowledgeFileMutations({
3056
- root,
3057
- transactionRoot: lock.transactionRoot,
3058
- purpose,
3059
- mutations: parsed.blocks.map((block) => ({
3615
+ if (parsed.blocks.length > 0) {
3616
+ const mutations = parsed.blocks.map((block) => ({
3060
3617
  path: block.path,
3061
3618
  content: block.content.endsWith("\n") ? block.content : `${block.content}\n`
3062
- })),
3063
- assertOwned: lock.assertOwned
3064
- });
3619
+ }));
3620
+ if (intake) {
3621
+ const { inheritedPages = [], ...settings } = intake;
3622
+ const here = await loadKnowledgePages(root, { pagesDirectory });
3623
+ assertKnowledgeWriteIntake(mutations.filter((mutation) => isKnowledgePagePath(mutation.path)).map((mutation) => knowledgePageFromMarkdown(mutation.path, mutation.content, pagesDirectory)), {
3624
+ ...settings,
3625
+ visiblePages: [...originatedPages(here), ...inheritedPages]
3626
+ });
3627
+ }
3628
+ await commitKnowledgeFileMutations({
3629
+ root,
3630
+ transactionRoot: lock.transactionRoot,
3631
+ purpose,
3632
+ mutations,
3633
+ pagesDirectory,
3634
+ assertOwned: lock.assertOwned
3635
+ });
3636
+ }
3065
3637
  return {
3066
3638
  written: parsed.blocks.map((block) => block.path),
3067
3639
  warnings: parsed.warnings
3068
3640
  };
3069
3641
  }, { resumeTransaction: { purpose } });
3070
3642
  }
3071
- async function applyKnowledgeWriteBlocksFile(root, proposalPath) {
3072
- return applyKnowledgeWriteBlocks(root, await readFile(proposalPath, "utf8"));
3643
+ async function applyKnowledgeWriteBlocksFile(root, proposalPath, options = {}) {
3644
+ return applyKnowledgeWriteBlocks(root, await readFile(proposalPath, "utf8"), options);
3073
3645
  }
3074
3646
  //#endregion
3075
3647
  //#region src/metadata.ts
@@ -3149,6 +3721,6 @@ function explainKnowledgeTarget(index, target) {
3149
3721
  };
3150
3722
  }
3151
3723
  //#endregion
3152
- export { WIKILINK_REGEX as $, syncDirectory as $t, MemoryKbStore as A, mergeTrackedClaims as At, auditKnowledgeCitations as B, applyKnowledgeFileTransaction as Bt, writeSourceRegistry as C, claimId as Ct, KB_EVENTS_PATH as D, linkClaimContradictions as Dt, KB_CLAIM_LEDGER_DIR as E, emptyClaimLedger as Et, assertCurrentRunCitationsResolved as F, acquireDurableFileLock as Ft, resolveRunScopedCitation as G, rollbackKnowledgeFileTransaction as Gt, parseKnowledgeCitationReference as H, finishKnowledgeFileTransaction as Ht, assertKnowledgeCitationAudit as I, inspectPendingKnowledgeMutation as It, initKnowledgeBase as J, listRegularFilesWithinRoot as Jt, resolveRunScopedCitations as K, isKernelAnchoredPath as Kt, assertKnowledgeCitationsResolved as L, recoverPendingKnowledgeMutation as Lt, buildKnowledgeGraph as M, researchSourceVersionKey as Mt, KnowledgeCitationAuditError as N, formatFrontmatter as Nt, KB_INDEX_PATH as O, materializeRegisteredClaimEvidence as Ot, KnowledgeCitationResolutionError as P, parseFrontmatter as Pt, writeJson as Q, renameDurable as Qt, assertRunScopedCitationsResolved as R, withKnowledgeMutation as Rt, sourceRegistryPath as S, claimEvidenceId as St, FileSystemKbStore as T, deepQuestionId as Tt, resolveKnowledgeCitation as U, knowledgeFileTransactionPlanHash as Ut, formatKnowledgeCitationReference as V, assertKnowledgeMutationPath as Vt, resolveKnowledgeCitations as W, prepareKnowledgeFileTransaction as Wt, layoutFor as X, readRegularFileWithinRoot as Xt, isScaffoldPath as Y, readRegularFileNoFollow as Yt, loadKnowledgePages as Z, removeDurable as Zt, writeKnowledgeIndex as _, ClaimLedgerGoalConflictError as _t, applyKnowledgeWriteBlocksFile as a, writeJsonDurableWithinRoot as an, KnowledgeGraphEdgeSchema as at, loadSourceRegistry as b, assertResearchClaimLedgerIntegrity as bt, validateKnowledgeIndex as c, KnowledgePageInvalidationSchema as ct, UncheckableClaimError as d, ResearchClaimLedgerSchema as dt, withSafeDescendant as en, extractWikilinks as et, assertGradeableEvidence as f, ResearchClaimRecordSchema as ft, buildKnowledgeIndex as g, KNOWLEDGE_EVENT_TYPES as gt, verifyGradeableEvidence as h, SourceRecordSchema as ht, applyKnowledgeWriteBlocks as i, writeJsonDurable as in, KnowledgeEventSchema as it, assertClaimLedgerId as j, normalizeClaimText as jt, KB_STORE_DIR as k, mergeClaimLedgers as kt, lintKnowledgeIndex as l, KnowledgePageSchema as lt, verdictFor as m, SourceAnchorSchema as mt, inspectKnowledgeIndex as n, writeFileDurable as nn, DeepQuestionSchema as nt, isSafeKnowledgePath as o, mediaTypeFor$1 as on, KnowledgeGraphNodeSchema as ot, gradeFor as p, ResearchSourceVersionSchema as pt, SCAFFOLD_PAGE_BASENAMES as q, isMissingFile as qt, stringMetadata as r, writeFileDurableWithinRoot as rn, KnowledgeBaseCandidateSchema as rt, parseKnowledgeWriteBlocks as s, textSourceAdapter as sn, KnowledgeIndexSchema as st, explainKnowledgeTarget as t, withSafeDirectory as tn, normalizeLinkTarget as tt, CHECKABLE_RUNG_THRESHOLD as u, ResearchClaimEvidenceSchema as ut, addSourcePath as v, assertDeepQuestionIntegrity as vt, ClaimLedgerMigrationRequiredError as w, claimSourceHost as wt, snapshotSourceTextInput as x, assertTrackedClaimIntegrity as xt, addSourceText as y, assertResearchClaimEvidenceIntegrity as yt, auditCurrentRunCitations as z, withKnowledgeRead as zt };
3724
+ export { resolveKnowledgeCitations as $, withKnowledgeRead as $t, sourceRegistryPath as A, KNOWLEDGE_EVENT_TYPES as At, buildKnowledgeGraph as B, emptyClaimLedger as Bt, verifyGradeableEvidence as C, mediaTypeFor$1 as Cn, KnowledgeRelationSchema as Ct, addSourceText as D, ResearchSourceVersionSchema as Dt, addSourcePath as E, ResearchClaimRecordSchema as Et, KB_EVENTS_PATH as F, assertTrackedClaimIntegrity as Ft, assertKnowledgeCitationAudit as G, normalizeClaimText as Gt, KnowledgeCitationAuditError as H, materializeRegisteredClaimEvidence as Ht, KB_INDEX_PATH as I, claimEvidenceId as It, auditCurrentRunCitations as J, parseFrontmatter as Jt, assertKnowledgeCitationsResolved as K, researchSourceVersionKey as Kt, KB_STORE_DIR as L, claimId as Lt, ClaimLedgerMigrationRequiredError as M, assertDeepQuestionIntegrity as Mt, FileSystemKbStore as N, assertResearchClaimEvidenceIntegrity as Nt, loadSourceRegistry as O, SourceAnchorSchema as Ot, KB_CLAIM_LEDGER_DIR as P, assertResearchClaimLedgerIntegrity as Pt, resolveKnowledgeCitation as Q, withKnowledgeMutation as Qt, MemoryKbStore as R, claimSourceHost as Rt, verdictFor as S, writeJsonDurableWithinRoot as Sn, KnowledgeRelationNodeSchema as St, writeKnowledgeIndex as T, ResearchClaimLedgerSchema as Tt, KnowledgeCitationResolutionError as U, mergeClaimLedgers as Ut, knowledgePageRelations as V, linkClaimContradictions as Vt, assertCurrentRunCitationsResolved as W, mergeTrackedClaims as Wt, formatKnowledgeCitationReference as X, inspectPendingKnowledgeMutation as Xt, auditKnowledgeCitations as Y, acquireDurableFileLock as Yt, parseKnowledgeCitationReference as Z, recoverPendingKnowledgeMutation as Zt, lintKnowledgeIndex as _, withSafeDescendant as _n, KnowledgeGraphNodeSchema as _t, applyKnowledgeWriteBlocksFile as a, prepareKnowledgeFileTransaction as an, isScaffoldPath as at, assertGradeableEvidence as b, writeFileDurableWithinRoot as bn, KnowledgePageSchema as bt, KnowledgeDuplicateIntakeError as c, normalizePagesDirectory as cn, loadKnowledgePages as ct, createFileRunLineageAuthority as d, listRegularFilesWithinRoot as dn, extractWikilinks as dt, applyKnowledgeFileTransaction as en, resolveRunScopedCitation as et, createRunScopedStores as f, readRegularFileNoFollow as fn, normalizeLinkTarget as ft, validateKnowledgeIndex as g, syncDirectory as gn, KnowledgeGraphEdgeSchema as gt, normalizePageText as h, renameDurable as hn, KnowledgeEventSchema as ht, applyKnowledgeWriteBlocks as i, knowledgeFileTransactionPlanHash as in, isKnowledgePagePath as it, writeSourceRegistry as j, ClaimLedgerGoalConflictError as jt, snapshotSourceTextInput as k, SourceRecordSchema as kt, assertKnowledgeWriteIntake as l, isKernelAnchoredPath as ln, writeJson as lt, detectNearDuplicatePages as m, removeDurable as mn, KnowledgeBaseCandidateSchema as mt, inspectKnowledgeIndex as n, commitKnowledgeFileMutations as nn, SCAFFOLD_PAGE_BASENAMES as nt, isSafeKnowledgePath as o, rollbackKnowledgeFileTransaction as on, knowledgePageFromMarkdown as ot, originatedPages as p, readRegularFileWithinRoot as pn, DeepQuestionSchema as pt, assertRunScopedCitationsResolved as q, formatFrontmatter as qt, stringMetadata as r, finishKnowledgeFileTransaction as rn, initKnowledgeBase as rt, parseKnowledgeWriteBlocks as s, DEFAULT_PAGES_DIRECTORY as sn, layoutFor as st, explainKnowledgeTarget as t, assertKnowledgeMutationPath as tn, resolveRunScopedCitations as tt, RUN_LINEAGE_BASENAME as u, isMissingFile as un, WIKILINK_REGEX as ut, CHECKABLE_RUNG_THRESHOLD as v, withSafeDirectory as vn, KnowledgeIndexSchema as vt, buildKnowledgeIndex as w, textSourceAdapter as wn, ResearchClaimEvidenceSchema as wt, gradeFor as x, writeJsonDurable as xn, KnowledgeRelationGraphSchema as xt, UncheckableClaimError as y, writeFileDurable as yn, KnowledgePageInvalidationSchema as yt, assertClaimLedgerId as z, deepQuestionId as zt };
3153
3725
 
3154
- //# sourceMappingURL=inspect-0tmv3xV8.js.map
3726
+ //# sourceMappingURL=inspect-DALsvG10.js.map