@tangle-network/agent-knowledge 0.1.1 → 1.1.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.
package/AGENTS.md CHANGED
@@ -23,6 +23,7 @@ agent-knowledge search "query" --json
23
23
  agent-knowledge inspect --json
24
24
  agent-knowledge explain knowledge/concepts/example.md --json
25
25
  agent-knowledge lint --json
26
+ agent-knowledge validate --strict --json
26
27
  agent-knowledge viz --json
27
28
  ```
28
29
 
@@ -49,3 +50,11 @@ The parser rejects absolute paths, `..`, control characters, and writes outside
49
50
  ## Eval Boundary
50
51
 
51
52
  Use `runKnowledgeBaseOptimization()` when comparing candidate knowledge bases on an actual task corpus. It delegates to `@tangle-network/agent-eval` multi-shot optimization, so single-turn and multi-turn agents share the same path.
53
+
54
+ Use `knowledgeReleaseReportFromOptimization()` before promotion. It projects optimizer traces and `RunRecord` rows into `agent-eval` release confidence evidence.
55
+
56
+ ## Integration Boundaries
57
+
58
+ - Use `KbStore` for storage. Implement D1 in the consuming app when needed.
59
+ - Use `KnowledgeDiscoveryDispatcher` for research workers. Production apps should wire this to their own swarm/fleet runtime.
60
+ - Do not bypass `lint` or `validate` before using generated knowledge in an agent.
package/README.md CHANGED
@@ -23,6 +23,8 @@ agent-knowledge inspect --root .
23
23
  agent-knowledge explain knowledge/concepts/risk.md --root .
24
24
  agent-knowledge graph --root . --format json
25
25
  agent-knowledge lint --root .
26
+ agent-knowledge validate --strict --root .
27
+ agent-knowledge export --root . --format json
26
28
  agent-knowledge viz --root .
27
29
  ```
28
30
 
@@ -32,13 +34,20 @@ The default layout is:
32
34
  raw/
33
35
  sources/
34
36
  knowledge/
35
- index.md
36
- log.md
37
+ index.md # scaffold: human-navigation only, excluded from the page index
38
+ log.md # scaffold: human-navigation only, excluded from the page index
37
39
  .agent-knowledge/
38
40
  sources.json
39
41
  index.json
40
42
  ```
41
43
 
44
+ `initKnowledgeBase` writes `knowledge/index.md` and `knowledge/log.md` for
45
+ authors to curate by hand. They are deliberately excluded from
46
+ `buildKnowledgeIndex` / `searchKnowledge` so they do not inflate page counts
47
+ or pollute search hits. Any nested `<dir>/index.md` or `<dir>/log.md` is
48
+ treated the same way. The shared predicate is `isScaffoldPath`, exported
49
+ from `@tangle-network/agent-knowledge`.
50
+
42
51
  ## Design
43
52
 
44
53
  - Raw sources are immutable evidence.
@@ -47,11 +56,58 @@ knowledge/
47
56
  - Lint fails on pages that cite unknown source IDs.
48
57
  - Text sources get deterministic anchors (`all`, `l1`, `l51`, ...) for precise citations like `[^src_id#all]`.
49
58
  - Agent write proposals can be safely applied with `apply-write-blocks`.
59
+ - `KbStore` keeps storage consumer-owned; use `MemoryKbStore`, `FileSystemKbStore`, or implement D1 in the app.
60
+ - Discovery uses worker/dispatcher contracts, with a local dispatcher for dev and tests.
61
+ - Zod schemas define the stable wire shape.
50
62
  - Graph/search/lint are deterministic and fast.
63
+ - `searchKnowledge` returns hits with three score fields. `score` and
64
+ `rrfScore` are the raw reciprocal-rank-fusion value (typically 0.01–0.05);
65
+ use them when intent matters or when fusing across queries.
66
+ `normalizedScore` is the same value scaled into [0, 1] relative to the top
67
+ hit *in this result set* (top hit = 1, others = score / topScore) — use it
68
+ when comparing against natural confidence thresholds. The normalization is
69
+ within-set ranking, not a cross-query absolute confidence.
51
70
  - Optimization uses `@tangle-network/agent-eval` internally instead of reimplementing eval gates.
71
+ - `buildEvalKnowledgeBundle()` maps wiki/search evidence into
72
+ `agent-eval` `KnowledgeRequirement`, `KnowledgeBundle`, and
73
+ `KnowledgeReadinessReport` contracts so control loops can block, ask, or
74
+ acquire data before running an agent.
52
75
 
53
76
  The `/viz` subpath exports graph insight helpers without UI dependencies.
54
77
 
55
78
  ## Agent-Eval Integration
56
79
 
57
80
  Use `runKnowledgeBaseOptimization()` when the question is whether a candidate knowledge base actually improves agent task success. The candidate is passed through `runMultiShotOptimization`, so `n=1` single-turn tasks and variable-length multi-turn traces use the same path.
81
+
82
+ Use `knowledgeReleaseReportFromOptimization()` to turn optimizer output into release confidence evidence using `agent-eval` release gates and `RunRecord` validation.
83
+
84
+ Use `buildEvalKnowledgeBundle()` before execution when the question is whether
85
+ the agent has enough task-world context to run:
86
+
87
+ ```ts
88
+ import { buildEvalKnowledgeBundle } from '@tangle-network/agent-knowledge'
89
+
90
+ const readiness = buildEvalKnowledgeBundle({
91
+ taskId: 'sdk-migration',
92
+ index,
93
+ specs: [{
94
+ id: 'repo-build-command',
95
+ description: 'Repository build and typecheck command',
96
+ query: 'build typecheck command',
97
+ requiredFor: ['coding'],
98
+ category: 'codebase_specific',
99
+ acquisitionMode: 'inspect_repo',
100
+ importance: 'blocking',
101
+ freshness: 'weekly',
102
+ sensitivity: 'public',
103
+ confidenceNeeded: 0.9,
104
+ minSources: 1,
105
+ }],
106
+ })
107
+
108
+ console.log(readiness.report.recommendedAction)
109
+ ```
110
+
111
+ Pass `readiness.report` to `blockingKnowledgeEval()` from
112
+ `@tangle-network/agent-eval`; use `readiness.questions` and
113
+ `readiness.acquisitionPlans` to drive UI or connector workflows.
@@ -215,6 +215,176 @@ async function applyKnowledgeWriteBlocksFile(root, proposalPath) {
215
215
  return applyKnowledgeWriteBlocks(root, await readFile(proposalPath, "utf8"));
216
216
  }
217
217
 
218
+ // src/schemas.ts
219
+ import { z } from "zod";
220
+ var SourceAnchorSchema = z.object({
221
+ id: z.string().min(1),
222
+ sourceId: z.string().min(1),
223
+ label: z.string().optional(),
224
+ page: z.number().int().positive().optional(),
225
+ lineStart: z.number().int().positive().optional(),
226
+ lineEnd: z.number().int().positive().optional(),
227
+ charStart: z.number().int().nonnegative().optional(),
228
+ charEnd: z.number().int().nonnegative().optional(),
229
+ timestampMs: z.number().nonnegative().optional(),
230
+ metadata: z.record(z.string(), z.unknown()).optional()
231
+ });
232
+ var SourceRecordSchema = z.object({
233
+ id: z.string().min(1),
234
+ uri: z.string().min(1),
235
+ title: z.string().optional(),
236
+ mediaType: z.string().optional(),
237
+ contentHash: z.string().min(16),
238
+ text: z.string().optional(),
239
+ anchors: z.array(SourceAnchorSchema).optional(),
240
+ metadata: z.record(z.string(), z.unknown()).optional(),
241
+ createdAt: z.string().min(1)
242
+ });
243
+ var KnowledgePageSchema = z.object({
244
+ id: z.string().min(1),
245
+ path: z.string().min(1),
246
+ title: z.string().min(1),
247
+ text: z.string(),
248
+ frontmatter: z.record(z.string(), z.unknown()),
249
+ sourceIds: z.array(z.string()),
250
+ tags: z.array(z.string()),
251
+ outLinks: z.array(z.string())
252
+ });
253
+ var KnowledgeGraphNodeSchema = z.object({
254
+ id: z.string(),
255
+ title: z.string(),
256
+ path: z.string(),
257
+ tags: z.array(z.string()),
258
+ sourceIds: z.array(z.string()),
259
+ outDegree: z.number().int().nonnegative(),
260
+ inDegree: z.number().int().nonnegative()
261
+ });
262
+ var KnowledgeGraphEdgeSchema = z.object({
263
+ source: z.string(),
264
+ target: z.string(),
265
+ weight: z.number(),
266
+ reasons: z.array(z.string())
267
+ });
268
+ var KnowledgeIndexSchema = z.object({
269
+ root: z.string(),
270
+ generatedAt: z.string(),
271
+ sources: z.array(SourceRecordSchema),
272
+ pages: z.array(KnowledgePageSchema),
273
+ graph: z.object({
274
+ nodes: z.array(KnowledgeGraphNodeSchema),
275
+ edges: z.array(KnowledgeGraphEdgeSchema)
276
+ })
277
+ });
278
+ var KnowledgeEventSchema = z.object({
279
+ id: z.string().min(1),
280
+ type: z.enum(["source.added", "proposal.applied", "index.built", "lint.run", "optimization.run", "release.promoted", "release.rejected"]),
281
+ createdAt: z.string().min(1),
282
+ actor: z.string().optional(),
283
+ target: z.string().optional(),
284
+ metadata: z.record(z.string(), z.unknown()).optional()
285
+ });
286
+ var KnowledgeBaseCandidateSchema = z.object({
287
+ id: z.string().min(1),
288
+ units: z.array(z.object({
289
+ id: z.string().min(1),
290
+ title: z.string().min(1),
291
+ text: z.string(),
292
+ claims: z.array(z.object({
293
+ id: z.string().min(1),
294
+ text: z.string().min(1),
295
+ refs: z.array(z.object({
296
+ sourceId: z.string().min(1),
297
+ anchorId: z.string().optional(),
298
+ quote: z.string().optional()
299
+ })),
300
+ confidence: z.number().min(0).max(1).optional(),
301
+ status: z.enum(["draft", "active", "superseded", "rejected"]).optional(),
302
+ metadata: z.record(z.string(), z.unknown()).optional()
303
+ })).optional(),
304
+ relations: z.array(z.object({
305
+ sourceId: z.string(),
306
+ targetId: z.string(),
307
+ predicate: z.string(),
308
+ weight: z.number().optional(),
309
+ metadata: z.record(z.string(), z.unknown()).optional()
310
+ })).optional(),
311
+ sourceIds: z.array(z.string()).optional(),
312
+ tags: z.array(z.string()).optional(),
313
+ metadata: z.record(z.string(), z.unknown()).optional(),
314
+ updatedAt: z.string().optional()
315
+ })),
316
+ retrievalPolicy: z.string().optional(),
317
+ synthesisPolicy: z.string().optional(),
318
+ questionPolicy: z.string().optional(),
319
+ updatePolicy: z.string().optional(),
320
+ metadata: z.record(z.string(), z.unknown()).optional()
321
+ });
322
+
323
+ // src/graph.ts
324
+ function buildKnowledgeGraph(pages) {
325
+ const byId = /* @__PURE__ */ new Map();
326
+ const bySlug = /* @__PURE__ */ new Map();
327
+ for (const page of pages) {
328
+ byId.set(page.id, page);
329
+ bySlug.set(normalizeLinkTarget(page.id), page);
330
+ bySlug.set(normalizeLinkTarget(page.title), page);
331
+ bySlug.set(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")), page);
332
+ }
333
+ const incoming = /* @__PURE__ */ new Map();
334
+ const outgoing = /* @__PURE__ */ new Map();
335
+ const edgesByKey = /* @__PURE__ */ new Map();
336
+ for (const page of pages) {
337
+ outgoing.set(page.id, 0);
338
+ incoming.set(page.id, 0);
339
+ }
340
+ for (const page of pages) {
341
+ for (const raw of page.outLinks) {
342
+ const target = bySlug.get(normalizeLinkTarget(raw));
343
+ if (!target || target.id === page.id) continue;
344
+ const key = `${page.id}->${target.id}`;
345
+ const edge = edgesByKey.get(key);
346
+ if (edge) edge.weight += 1;
347
+ else edgesByKey.set(key, { source: page.id, target: target.id, weight: 1, reasons: ["wikilink"] });
348
+ outgoing.set(page.id, (outgoing.get(page.id) ?? 0) + 1);
349
+ incoming.set(target.id, (incoming.get(target.id) ?? 0) + 1);
350
+ }
351
+ }
352
+ addSourceOverlapEdges(pages, edgesByKey);
353
+ const nodes = pages.map((page) => ({
354
+ id: page.id,
355
+ title: page.title,
356
+ path: page.path,
357
+ tags: page.tags,
358
+ sourceIds: page.sourceIds,
359
+ outDegree: outgoing.get(page.id) ?? 0,
360
+ inDegree: incoming.get(page.id) ?? 0
361
+ }));
362
+ return { nodes, edges: [...edgesByKey.values()].sort((a, b) => b.weight - a.weight) };
363
+ }
364
+ function addSourceOverlapEdges(pages, edges) {
365
+ for (let i = 0; i < pages.length; i++) {
366
+ for (let j = i + 1; j < pages.length; j++) {
367
+ const a = pages[i];
368
+ const b = pages[j];
369
+ const overlap = a.sourceIds.filter((source) => b.sourceIds.includes(source));
370
+ if (overlap.length === 0) continue;
371
+ const key = `${a.id}->${b.id}`;
372
+ const edge = edges.get(key);
373
+ if (edge) {
374
+ edge.weight += overlap.length * 0.5;
375
+ edge.reasons.push("shared-source");
376
+ } else {
377
+ edges.set(key, {
378
+ source: a.id,
379
+ target: b.id,
380
+ weight: overlap.length * 0.5,
381
+ reasons: ["shared-source"]
382
+ });
383
+ }
384
+ }
385
+ }
386
+ }
387
+
218
388
  // src/store.ts
219
389
  import { mkdir as mkdir2, readFile as readFile2, readdir, stat, writeFile as writeFile2 } from "fs/promises";
220
390
  import { dirname as dirname2, join as join2, relative } from "path";
@@ -229,6 +399,15 @@ function layoutFor(root) {
229
399
  cacheDir: join2(root, ".agent-knowledge")
230
400
  };
231
401
  }
402
+ var SCAFFOLD_PAGE_BASENAMES = ["index.md", "log.md"];
403
+ function isScaffoldPath(path) {
404
+ const normalized = path.replace(/\\/g, "/");
405
+ for (const basename2 of SCAFFOLD_PAGE_BASENAMES) {
406
+ if (normalized === `knowledge/${basename2}`) return true;
407
+ if (normalized.endsWith(`/${basename2}`)) return true;
408
+ }
409
+ return false;
410
+ }
232
411
  async function initKnowledgeBase(root) {
233
412
  const layout = layoutFor(root);
234
413
  await mkdir2(layout.knowledgeDir, { recursive: true });
@@ -244,9 +423,10 @@ async function loadKnowledgePages(root) {
244
423
  const files = await listMarkdownFiles(layout.knowledgeDir);
245
424
  const pages = [];
246
425
  for (const file of files) {
426
+ const rel = relative(root, file).replace(/\\/g, "/");
427
+ if (isScaffoldPath(rel)) continue;
247
428
  const content = await readFile2(file, "utf8");
248
429
  const { frontmatter, body } = parseFrontmatter(content);
249
- const rel = relative(root, file).replace(/\\/g, "/");
250
430
  const title = stringField(frontmatter.title) ?? firstHeading(body) ?? rel.split("/").pop().replace(/\.md$/, "");
251
431
  const sourceIds = arrayField(frontmatter.sources);
252
432
  const tags = arrayField(frontmatter.tags);
@@ -400,71 +580,6 @@ function textPreview(fileName, bytes) {
400
580
  return bytes.toString("utf8").slice(0, 2e5);
401
581
  }
402
582
 
403
- // src/graph.ts
404
- function buildKnowledgeGraph(pages) {
405
- const byId = /* @__PURE__ */ new Map();
406
- const bySlug = /* @__PURE__ */ new Map();
407
- for (const page of pages) {
408
- byId.set(page.id, page);
409
- bySlug.set(normalizeLinkTarget(page.id), page);
410
- bySlug.set(normalizeLinkTarget(page.title), page);
411
- bySlug.set(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")), page);
412
- }
413
- const incoming = /* @__PURE__ */ new Map();
414
- const outgoing = /* @__PURE__ */ new Map();
415
- const edgesByKey = /* @__PURE__ */ new Map();
416
- for (const page of pages) {
417
- outgoing.set(page.id, 0);
418
- incoming.set(page.id, 0);
419
- }
420
- for (const page of pages) {
421
- for (const raw of page.outLinks) {
422
- const target = bySlug.get(normalizeLinkTarget(raw));
423
- if (!target || target.id === page.id) continue;
424
- const key = `${page.id}->${target.id}`;
425
- const edge = edgesByKey.get(key);
426
- if (edge) edge.weight += 1;
427
- else edgesByKey.set(key, { source: page.id, target: target.id, weight: 1, reasons: ["wikilink"] });
428
- outgoing.set(page.id, (outgoing.get(page.id) ?? 0) + 1);
429
- incoming.set(target.id, (incoming.get(target.id) ?? 0) + 1);
430
- }
431
- }
432
- addSourceOverlapEdges(pages, edgesByKey);
433
- const nodes = pages.map((page) => ({
434
- id: page.id,
435
- title: page.title,
436
- path: page.path,
437
- tags: page.tags,
438
- sourceIds: page.sourceIds,
439
- outDegree: outgoing.get(page.id) ?? 0,
440
- inDegree: incoming.get(page.id) ?? 0
441
- }));
442
- return { nodes, edges: [...edgesByKey.values()].sort((a, b) => b.weight - a.weight) };
443
- }
444
- function addSourceOverlapEdges(pages, edges) {
445
- for (let i = 0; i < pages.length; i++) {
446
- for (let j = i + 1; j < pages.length; j++) {
447
- const a = pages[i];
448
- const b = pages[j];
449
- const overlap = a.sourceIds.filter((source) => b.sourceIds.includes(source));
450
- if (overlap.length === 0) continue;
451
- const key = `${a.id}->${b.id}`;
452
- const edge = edges.get(key);
453
- if (edge) {
454
- edge.weight += overlap.length * 0.5;
455
- edge.reasons.push("shared-source");
456
- } else {
457
- edges.set(key, {
458
- source: a.id,
459
- target: b.id,
460
- weight: overlap.length * 0.5,
461
- reasons: ["shared-source"]
462
- });
463
- }
464
- }
465
- }
466
- }
467
-
468
583
  // src/search.ts
469
584
  var RRF_K = 60;
470
585
  var STOP_WORDS = /* @__PURE__ */ new Set(["the", "is", "a", "an", "what", "how", "are", "was", "were", "to", "for", "of", "with", "by", "in", "on", "and"]);
@@ -475,9 +590,13 @@ function searchKnowledge(index, query, limit = 10) {
475
590
  const graphRanked = rankByGraph(index.pages, tokenRanked);
476
591
  const scores = reciprocalRankFusion([tokenRanked.map((p) => p.id), graphRanked.map((p) => p.id)]);
477
592
  const byId = new Map(index.pages.map((page) => [page.id, page]));
478
- return [...scores.entries()].map(([id, score]) => ({ page: byId.get(id), score })).filter((item) => Boolean(item.page)).sort((a, b) => b.score - a.score || a.page.path.localeCompare(b.page.path)).slice(0, limit).map((item, i) => ({
593
+ const ranked = [...scores.entries()].map(([id, score]) => ({ page: byId.get(id), score })).filter((item) => Boolean(item.page)).sort((a, b) => b.score - a.score || a.page.path.localeCompare(b.page.path)).slice(0, limit);
594
+ const topScore = ranked[0]?.score ?? 0;
595
+ return ranked.map((item, i) => ({
479
596
  page: item.page,
480
597
  score: item.score,
598
+ rrfScore: item.score,
599
+ normalizedScore: topScore > 0 ? item.score / topScore : 0,
481
600
  rank: i + 1,
482
601
  snippet: buildSnippet(item.page.text, trimmed),
483
602
  reasons: reasonsFor(item.page, trimmed)
@@ -579,13 +698,19 @@ function lintKnowledgeIndex(index) {
579
698
  const titles = /* @__PURE__ */ new Map();
580
699
  const sourceIds = new Set(index.sources.map((source) => source.id));
581
700
  const anchorIds = new Map(index.sources.map((source) => [source.id, new Set((source.anchors ?? []).map((anchor) => anchor.id))]));
701
+ const pageIds = /* @__PURE__ */ new Map();
702
+ const sourceHashes = /* @__PURE__ */ new Map();
582
703
  for (const page of index.pages) {
704
+ pageIds.set(page.id, [...pageIds.get(page.id) ?? [], page.path]);
583
705
  byTarget.add(normalizeLinkTarget(page.id));
584
706
  byTarget.add(normalizeLinkTarget(page.title));
585
707
  byTarget.add(normalizeLinkTarget(page.path.split("/").pop().replace(/\.md$/, "")));
586
708
  const titleKey = page.title.toLowerCase();
587
709
  titles.set(titleKey, [...titles.get(titleKey) ?? [], page.path]);
588
710
  }
711
+ for (const source of index.sources) {
712
+ sourceHashes.set(source.contentHash, [...sourceHashes.get(source.contentHash) ?? [], source.id]);
713
+ }
589
714
  const inbound = /* @__PURE__ */ new Map();
590
715
  for (const page of index.pages) inbound.set(page.id, 0);
591
716
  for (const page of index.pages) {
@@ -624,6 +749,16 @@ function lintKnowledgeIndex(index) {
624
749
  findings.push({ type: "duplicate-title", severity: "warning", message: `Duplicate title "${title}" in ${paths.join(", ")}.`, metadata: { paths } });
625
750
  }
626
751
  }
752
+ for (const [id, paths] of pageIds) {
753
+ if (id && paths.length > 1) {
754
+ findings.push({ type: "duplicate-page-id", severity: "error", message: `Duplicate page id "${id}" in ${paths.join(", ")}.`, metadata: { paths } });
755
+ }
756
+ }
757
+ for (const [hash, ids] of sourceHashes) {
758
+ if (hash && ids.length > 1) {
759
+ findings.push({ type: "duplicate-source-hash", severity: "warning", message: `Duplicate source content hash across ${ids.join(", ")}.`, metadata: { sourceIds: ids } });
760
+ }
761
+ }
627
762
  return findings;
628
763
  }
629
764
  function isStructural(path) {
@@ -667,6 +802,36 @@ function explainKnowledgeTarget(index, target) {
667
802
  };
668
803
  }
669
804
 
805
+ // src/validate.ts
806
+ function validateKnowledgeIndex(index, options = {}) {
807
+ const findings = [...lintKnowledgeIndex(index)];
808
+ const parsed = KnowledgeIndexSchema.safeParse(index);
809
+ if (!parsed.success) {
810
+ findings.push({
811
+ type: "missing-frontmatter",
812
+ severity: "error",
813
+ message: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
814
+ });
815
+ }
816
+ if (options.strict) {
817
+ for (const page of index.pages) {
818
+ if (isStructuralPage(page.path)) continue;
819
+ if (!page.frontmatter.id || !page.frontmatter.title) {
820
+ findings.push({
821
+ type: "missing-frontmatter",
822
+ severity: "error",
823
+ page: page.path,
824
+ message: "Strict mode requires id and title frontmatter."
825
+ });
826
+ }
827
+ }
828
+ }
829
+ return { ok: !findings.some((finding) => finding.severity === "error"), findings };
830
+ }
831
+ function isStructuralPage(path) {
832
+ return path === "knowledge/index.md" || path === "knowledge/log.md" || path.endsWith("/index.md") || path.endsWith("/log.md");
833
+ }
834
+
670
835
  export {
671
836
  sha256,
672
837
  slugify,
@@ -682,7 +847,18 @@ export {
682
847
  mediaTypeFor,
683
848
  applyKnowledgeWriteBlocks,
684
849
  applyKnowledgeWriteBlocksFile,
850
+ SourceAnchorSchema,
851
+ SourceRecordSchema,
852
+ KnowledgePageSchema,
853
+ KnowledgeGraphNodeSchema,
854
+ KnowledgeGraphEdgeSchema,
855
+ KnowledgeIndexSchema,
856
+ KnowledgeEventSchema,
857
+ KnowledgeBaseCandidateSchema,
858
+ buildKnowledgeGraph,
685
859
  layoutFor,
860
+ SCAFFOLD_PAGE_BASENAMES,
861
+ isScaffoldPath,
686
862
  initKnowledgeBase,
687
863
  loadKnowledgePages,
688
864
  writeJson,
@@ -690,7 +866,6 @@ export {
690
866
  writeSourceRegistry,
691
867
  addSourcePath,
692
868
  sourceRegistryPath,
693
- buildKnowledgeGraph,
694
869
  searchKnowledge,
695
870
  tokenizeQuery,
696
871
  reciprocalRankFusion,
@@ -698,6 +873,7 @@ export {
698
873
  writeKnowledgeIndex,
699
874
  lintKnowledgeIndex,
700
875
  inspectKnowledgeIndex,
701
- explainKnowledgeTarget
876
+ explainKnowledgeTarget,
877
+ validateKnowledgeIndex
702
878
  };
703
- //# sourceMappingURL=chunk-XDXIBHPS.js.map
879
+ //# sourceMappingURL=chunk-BEFAI2HO.js.map