@swarmvaultai/engine 0.1.18 → 0.1.19

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/dist/index.js CHANGED
@@ -38,7 +38,7 @@ function buildManagedBlock(target) {
38
38
  "- Read `swarmvault.schema.md` before compile or query style work. It is the canonical schema path.",
39
39
  "- Treat `raw/` as immutable source input.",
40
40
  "- Treat `wiki/` as generated markdown owned by the agent and compiler workflow.",
41
- "- Read `wiki/index.md` before broad file searching when answering SwarmVault questions.",
41
+ "- Read `wiki/graph/report.md` before broad file searching when it exists; otherwise start with `wiki/index.md`.",
42
42
  "- Preserve frontmatter fields including `page_id`, `source_ids`, `node_ids`, `freshness`, and `source_hashes`.",
43
43
  "- Save high-value answers back into `wiki/outputs/` instead of leaving them only in chat.",
44
44
  "- Prefer `swarmvault ingest`, `swarmvault compile`, `swarmvault query`, and `swarmvault lint` for SwarmVault maintenance tasks.",
@@ -148,6 +148,16 @@ import { createRequire } from "module";
148
148
  import path2 from "path";
149
149
  var require2 = createRequire(import.meta.url);
150
150
  var TREE_SITTER_PACKAGE_ROOT = path2.dirname(path2.dirname(require2.resolve("@vscode/tree-sitter-wasm")));
151
+ var RATIONALE_MARKERS = ["NOTE:", "IMPORTANT:", "HACK:", "WHY:", "RATIONALE:"];
152
+ function stripKnownCommentPrefix(line) {
153
+ let next = line.trim();
154
+ for (const prefix of ["/**", "/*", "*/", "//", "#", "--", "*"]) {
155
+ if (next.startsWith(prefix)) {
156
+ next = next.slice(prefix.length).trimStart();
157
+ }
158
+ }
159
+ return next;
160
+ }
151
161
  var treeSitterModulePromise;
152
162
  var treeSitterInitPromise;
153
163
  var languageCache = /* @__PURE__ */ new Map();
@@ -279,9 +289,107 @@ function finalizeCodeAnalysis(manifest, language, imports, draftSymbols, exportL
279
289
  diagnostics
280
290
  };
281
291
  }
292
+ function cleanCommentText(value) {
293
+ return normalizeWhitespace(
294
+ value.split(/\r?\n/).map((line) => stripKnownCommentPrefix(line)).join("\n").trim()
295
+ );
296
+ }
297
+ function normalizeRationaleText(value) {
298
+ let next = normalizeWhitespace(value.trim());
299
+ const upper = next.toUpperCase();
300
+ for (const marker of RATIONALE_MARKERS) {
301
+ if (upper.startsWith(marker)) {
302
+ next = next.slice(marker.length).trimStart();
303
+ break;
304
+ }
305
+ }
306
+ return next;
307
+ }
308
+ function rationaleKindFromText(text) {
309
+ const upper = text.toUpperCase();
310
+ return RATIONALE_MARKERS.some((marker) => upper.startsWith(marker)) ? "marker" : "comment";
311
+ }
312
+ function isLikelyRationaleText(value) {
313
+ if (value.length < 20) {
314
+ return false;
315
+ }
316
+ const upper = value.toUpperCase();
317
+ if (RATIONALE_MARKERS.some((marker) => upper.startsWith(marker))) {
318
+ return true;
319
+ }
320
+ const lower = value.toLowerCase();
321
+ return ["why", "because", "tradeoff", "important", "avoid", "workaround", "reason", "so that", "in order to"].some(
322
+ (needle) => lower.includes(needle)
323
+ );
324
+ }
325
+ function makeRationale(manifest, index, text, kind, symbolName) {
326
+ const normalized = normalizeRationaleText(cleanCommentText(text));
327
+ if (!isLikelyRationaleText(normalized)) {
328
+ return null;
329
+ }
330
+ return {
331
+ id: `rationale:${manifest.sourceId}:${index}`,
332
+ text: truncate(normalized, 280),
333
+ citation: manifest.sourceId,
334
+ kind,
335
+ symbolName
336
+ };
337
+ }
282
338
  function nodeText(node) {
283
339
  return node?.text ?? "";
284
340
  }
341
+ function moduleDocstringNode(rootNode) {
342
+ const first = rootNode.namedChildren[0];
343
+ if (!first || first.type !== "expression_statement") {
344
+ return null;
345
+ }
346
+ return first.namedChildren.find((child) => child?.type === "string" || child?.type === "concatenated_string") ?? null;
347
+ }
348
+ function bodyDocstringNode(node) {
349
+ const body = node?.childForFieldName("body");
350
+ if (!body) {
351
+ return null;
352
+ }
353
+ const first = body.namedChildren[0];
354
+ if (!first || first.type !== "expression_statement") {
355
+ return null;
356
+ }
357
+ return first.namedChildren.find((child) => child?.type === "string" || child?.type === "concatenated_string") ?? null;
358
+ }
359
+ function unquoteDocstringText(value) {
360
+ return value.trim().replace(/^("""|'''|"|')/, "").replace(/("""|'''|"|')$/, "");
361
+ }
362
+ function commentNodes(rootNode) {
363
+ return rootNode.descendantsOfType("comment").filter((node) => node !== null);
364
+ }
365
+ function extractTreeSitterRationales(manifest, language, rootNode) {
366
+ const results = [];
367
+ let index = 0;
368
+ const push = (text, kind, symbolName) => {
369
+ const rationale = makeRationale(manifest, index + 1, text, kind, symbolName);
370
+ if (rationale) {
371
+ results.push(rationale);
372
+ index += 1;
373
+ }
374
+ };
375
+ if (language === "python") {
376
+ const moduleDocstring = moduleDocstringNode(rootNode);
377
+ if (moduleDocstring) {
378
+ push(unquoteDocstringText(moduleDocstring.text), "docstring");
379
+ }
380
+ for (const node of rootNode.descendantsOfType(["class_definition", "function_definition"]).filter((item) => item !== null)) {
381
+ const name = extractIdentifier(node.childForFieldName("name"));
382
+ const docstring = bodyDocstringNode(node);
383
+ if (docstring) {
384
+ push(unquoteDocstringText(docstring.text), "docstring", name);
385
+ }
386
+ }
387
+ }
388
+ for (const commentNode of commentNodes(rootNode)) {
389
+ push(commentNode.text, rationaleKindFromText(commentNode.text));
390
+ }
391
+ return uniqueBy(results, (item) => `${item.symbolName ?? ""}:${item.text.toLowerCase()}`);
392
+ }
285
393
  function findNamedChild(node, type) {
286
394
  return node?.namedChildren.find((child) => child?.type === type) ?? null;
287
395
  }
@@ -950,41 +1058,45 @@ async function analyzeTreeSitterCode(manifest, content, language) {
950
1058
  parser.setLanguage(await loadLanguage(language));
951
1059
  const tree = parser.parse(content);
952
1060
  if (!tree) {
953
- return finalizeCodeAnalysis(
954
- manifest,
955
- language,
956
- [],
957
- [],
958
- [],
959
- [
960
- {
961
- code: 9e3,
962
- category: "error",
963
- message: `Failed to parse ${language} source.`,
964
- line: 1,
965
- column: 1
966
- }
967
- ]
968
- );
1061
+ return {
1062
+ code: finalizeCodeAnalysis(
1063
+ manifest,
1064
+ language,
1065
+ [],
1066
+ [],
1067
+ [],
1068
+ [
1069
+ {
1070
+ code: 9e3,
1071
+ category: "error",
1072
+ message: `Failed to parse ${language} source.`,
1073
+ line: 1,
1074
+ column: 1
1075
+ }
1076
+ ]
1077
+ ),
1078
+ rationales: []
1079
+ };
969
1080
  }
970
1081
  try {
971
1082
  const diagnostics = diagnosticsFromTree(tree.rootNode);
1083
+ const rationales = extractTreeSitterRationales(manifest, language, tree.rootNode);
972
1084
  switch (language) {
973
1085
  case "python":
974
- return pythonCodeAnalysis(manifest, tree.rootNode, diagnostics);
1086
+ return { code: pythonCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
975
1087
  case "go":
976
- return goCodeAnalysis(manifest, tree.rootNode, diagnostics);
1088
+ return { code: goCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
977
1089
  case "rust":
978
- return rustCodeAnalysis(manifest, tree.rootNode, diagnostics);
1090
+ return { code: rustCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
979
1091
  case "java":
980
- return javaCodeAnalysis(manifest, tree.rootNode, diagnostics);
1092
+ return { code: javaCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
981
1093
  case "csharp":
982
- return csharpCodeAnalysis(manifest, tree.rootNode, diagnostics);
1094
+ return { code: csharpCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
983
1095
  case "php":
984
- return phpCodeAnalysis(manifest, tree.rootNode, diagnostics);
1096
+ return { code: phpCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
985
1097
  case "c":
986
1098
  case "cpp":
987
- return cFamilyCodeAnalysis(manifest, language, tree.rootNode, diagnostics);
1099
+ return { code: cFamilyCodeAnalysis(manifest, language, tree.rootNode, diagnostics), rationales };
988
1100
  }
989
1101
  } finally {
990
1102
  tree.delete();
@@ -1183,6 +1295,62 @@ function normalizeSymbolReference2(value) {
1183
1295
  const lastSegment = trimmed.split(/::|\./).filter(Boolean).at(-1) ?? trimmed;
1184
1296
  return lastSegment.replace(/[,:;]+$/g, "").trim();
1185
1297
  }
1298
+ var RATIONALE_MARKERS2 = ["NOTE:", "IMPORTANT:", "HACK:", "WHY:", "RATIONALE:"];
1299
+ function stripKnownCommentPrefix2(line) {
1300
+ let next = line.trim();
1301
+ for (const prefix of ["/**", "/*", "*/", "//", "#", "*"]) {
1302
+ if (next.startsWith(prefix)) {
1303
+ next = next.slice(prefix.length).trimStart();
1304
+ }
1305
+ }
1306
+ return next;
1307
+ }
1308
+ function cleanCommentText2(value) {
1309
+ return normalizeWhitespace(
1310
+ value.split(/\r?\n/).map((line) => stripKnownCommentPrefix2(line)).join("\n").trim()
1311
+ );
1312
+ }
1313
+ function rationaleKindFromText2(text) {
1314
+ const upper = text.toUpperCase();
1315
+ return RATIONALE_MARKERS2.some((marker) => upper.startsWith(marker)) ? "marker" : "comment";
1316
+ }
1317
+ function normalizeRationaleText2(value) {
1318
+ let next = normalizeWhitespace(value.trim());
1319
+ const upper = next.toUpperCase();
1320
+ for (const marker of RATIONALE_MARKERS2) {
1321
+ if (upper.startsWith(marker)) {
1322
+ next = next.slice(marker.length).trimStart();
1323
+ break;
1324
+ }
1325
+ }
1326
+ return next;
1327
+ }
1328
+ function isLikelyRationaleText2(value) {
1329
+ if (value.length < 20) {
1330
+ return false;
1331
+ }
1332
+ const upper = value.toUpperCase();
1333
+ if (RATIONALE_MARKERS2.some((marker) => upper.startsWith(marker))) {
1334
+ return true;
1335
+ }
1336
+ const lower = value.toLowerCase();
1337
+ return ["why", "because", "tradeoff", "important", "avoid", "workaround", "reason", "so that", "in order to"].some(
1338
+ (needle) => lower.includes(needle)
1339
+ );
1340
+ }
1341
+ function makeRationale2(manifest, index, text, kind, symbolName) {
1342
+ const normalized = normalizeRationaleText2(cleanCommentText2(text));
1343
+ if (!isLikelyRationaleText2(normalized)) {
1344
+ return null;
1345
+ }
1346
+ return {
1347
+ id: `rationale:${manifest.sourceId}:${index}`,
1348
+ text: truncate(normalized, 280),
1349
+ citation: manifest.sourceId,
1350
+ kind,
1351
+ symbolName
1352
+ };
1353
+ }
1186
1354
  function stripCodeExtension2(filePath) {
1187
1355
  return filePath.replace(/\.(?:[cm]?jsx?|tsx?|mts|cts|py|go|rs|java|cs|php|c|cc|cpp|cxx|h|hh|hpp|hxx)$/i, "");
1188
1356
  }
@@ -1231,6 +1399,46 @@ function finalizeCodeAnalysis2(manifest, language, imports, draftSymbols, export
1231
1399
  diagnostics
1232
1400
  };
1233
1401
  }
1402
+ function statementRationaleSymbolName(statement) {
1403
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isEnumDeclaration(statement)) && statement.name) {
1404
+ return statement.name.text;
1405
+ }
1406
+ if (ts.isTypeAliasDeclaration(statement)) {
1407
+ return statement.name.text;
1408
+ }
1409
+ if (ts.isVariableStatement(statement)) {
1410
+ const first = statement.declarationList.declarations[0];
1411
+ return first && ts.isIdentifier(first.name) ? first.name.text : void 0;
1412
+ }
1413
+ return void 0;
1414
+ }
1415
+ function extractTypeScriptRationales(manifest, content, sourceFile) {
1416
+ const rationales = [];
1417
+ let index = 0;
1418
+ const pushRationale = (rawText, symbolName, kind) => {
1419
+ const rationale = makeRationale2(manifest, index + 1, rawText, kind ?? rationaleKindFromText2(rawText), symbolName);
1420
+ if (rationale) {
1421
+ rationales.push(rationale);
1422
+ index += 1;
1423
+ }
1424
+ };
1425
+ const firstStatement = sourceFile.statements[0];
1426
+ if (firstStatement) {
1427
+ for (const range of ts.getLeadingCommentRanges(content, firstStatement.getFullStart()) ?? []) {
1428
+ pushRationale(content.slice(range.pos, range.end));
1429
+ }
1430
+ }
1431
+ for (const statement of sourceFile.statements) {
1432
+ const symbolName = statementRationaleSymbolName(statement);
1433
+ for (const jsDoc of statement.jsDoc ?? []) {
1434
+ pushRationale(jsDoc.getText(sourceFile), symbolName, "docstring");
1435
+ }
1436
+ for (const range of ts.getLeadingCommentRanges(content, statement.getFullStart()) ?? []) {
1437
+ pushRationale(content.slice(range.pos, range.end), symbolName);
1438
+ }
1439
+ }
1440
+ return uniqueBy(rationales, (item) => `${item.symbolName ?? ""}:${item.text.toLowerCase()}`);
1441
+ }
1234
1442
  function analyzeTypeScriptLikeCode(manifest, content) {
1235
1443
  const language = manifest.language ?? inferCodeLanguage(manifest.originalPath ?? manifest.storedPath, manifest.mimeType) ?? "typescript";
1236
1444
  const sourceFile = ts.createSourceFile(
@@ -1443,7 +1651,10 @@ function analyzeTypeScriptLikeCode(manifest, content) {
1443
1651
  column: (position?.character ?? 0) + 1
1444
1652
  };
1445
1653
  });
1446
- return finalizeCodeAnalysis2(manifest, language, imports, draftSymbols, exportLabels, diagnostics);
1654
+ return {
1655
+ code: finalizeCodeAnalysis2(manifest, language, imports, draftSymbols, exportLabels, diagnostics),
1656
+ rationales: extractTypeScriptRationales(manifest, content, sourceFile)
1657
+ };
1447
1658
  }
1448
1659
  function inferCodeLanguage(filePath, mimeType = "") {
1449
1660
  const extension = path3.extname(filePath).toLowerCase();
@@ -1806,9 +2017,9 @@ function escapeRegExp2(value) {
1806
2017
  }
1807
2018
  async function analyzeCodeSource(manifest, extractedText, schemaHash) {
1808
2019
  const language = manifest.language ?? inferCodeLanguage(manifest.originalPath ?? manifest.storedPath, manifest.mimeType) ?? "typescript";
1809
- const code = language === "javascript" || language === "jsx" || language === "typescript" || language === "tsx" ? analyzeTypeScriptLikeCode(manifest, extractedText) : await analyzeTreeSitterCode(manifest, extractedText, language);
2020
+ const { code, rationales } = language === "javascript" || language === "jsx" || language === "typescript" || language === "tsx" ? analyzeTypeScriptLikeCode(manifest, extractedText) : await analyzeTreeSitterCode(manifest, extractedText, language);
1810
2021
  return {
1811
- analysisVersion: 3,
2022
+ analysisVersion: 4,
1812
2023
  sourceId: manifest.sourceId,
1813
2024
  sourceHash: manifest.contentHash,
1814
2025
  schemaHash,
@@ -1818,6 +2029,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
1818
2029
  entities: [],
1819
2030
  claims: codeClaims(manifest, code),
1820
2031
  questions: codeQuestions(manifest, code),
2032
+ rationales,
1821
2033
  code,
1822
2034
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
1823
2035
  };
@@ -2871,7 +3083,7 @@ import { z as z6 } from "zod";
2871
3083
  // src/analysis.ts
2872
3084
  import path7 from "path";
2873
3085
  import { z } from "zod";
2874
- var ANALYSIS_FORMAT_VERSION = 3;
3086
+ var ANALYSIS_FORMAT_VERSION = 4;
2875
3087
  var sourceAnalysisSchema = z.object({
2876
3088
  title: z.string().min(1),
2877
3089
  summary: z.string().min(1),
@@ -2989,6 +3201,7 @@ function heuristicAnalysis(manifest, text, schemaHash) {
2989
3201
  citation: manifest.sourceId
2990
3202
  })),
2991
3203
  questions: concepts.slice(0, 3).map((term) => `How does ${term.name} relate to ${manifest.title}?`),
3204
+ rationales: [],
2992
3205
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
2993
3206
  };
2994
3207
  }
@@ -3042,6 +3255,7 @@ ${truncate(text, 18e3)}`
3042
3255
  citation: claim.citation
3043
3256
  })),
3044
3257
  questions: parsed.questions,
3258
+ rationales: [],
3045
3259
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
3046
3260
  };
3047
3261
  }
@@ -3067,6 +3281,7 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
3067
3281
  entities: [],
3068
3282
  claims: [],
3069
3283
  questions: [],
3284
+ rationales: [],
3070
3285
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
3071
3286
  };
3072
3287
  } else if (provider.type === "heuristic") {
@@ -3695,6 +3910,309 @@ async function runDeepLint(rootDir, structuralFindings, options = {}) {
3695
3910
  );
3696
3911
  }
3697
3912
 
3913
+ // src/graph-tools.ts
3914
+ function normalizeTarget(value) {
3915
+ return normalizeWhitespace(value).toLowerCase();
3916
+ }
3917
+ function nodeById(graph) {
3918
+ return new Map(graph.nodes.map((node) => [node.id, node]));
3919
+ }
3920
+ function pageById(graph) {
3921
+ return new Map(graph.pages.map((page) => [page.id, page]));
3922
+ }
3923
+ function scoreMatch(query, candidate) {
3924
+ const normalizedQuery = normalizeTarget(query);
3925
+ const normalizedCandidate = normalizeTarget(candidate);
3926
+ if (!normalizedQuery || !normalizedCandidate) {
3927
+ return 0;
3928
+ }
3929
+ if (normalizedCandidate === normalizedQuery) {
3930
+ return 100;
3931
+ }
3932
+ if (normalizedCandidate.startsWith(normalizedQuery)) {
3933
+ return 80;
3934
+ }
3935
+ if (normalizedCandidate.includes(normalizedQuery)) {
3936
+ return 60;
3937
+ }
3938
+ const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
3939
+ const candidateTokens = new Set(normalizedCandidate.split(/\s+/).filter(Boolean));
3940
+ const overlap = queryTokens.filter((token) => candidateTokens.has(token)).length;
3941
+ return overlap ? overlap * 10 : 0;
3942
+ }
3943
+ function primaryNodeForPage(graph, page) {
3944
+ const byId = nodeById(graph);
3945
+ return page.nodeIds.map((nodeId) => byId.get(nodeId)).find((node) => Boolean(node));
3946
+ }
3947
+ function pageSearchMatches(graph, question, searchResults) {
3948
+ const pages = pageById(graph);
3949
+ return searchResults.map((result) => {
3950
+ const page = pages.get(result.pageId);
3951
+ const score = Math.max(scoreMatch(question, result.title), scoreMatch(question, result.path));
3952
+ if (!page || score <= 0) {
3953
+ return null;
3954
+ }
3955
+ return {
3956
+ type: "page",
3957
+ id: page.id,
3958
+ label: page.title,
3959
+ score
3960
+ };
3961
+ }).filter((match) => Boolean(match));
3962
+ }
3963
+ function nodeMatches(graph, query) {
3964
+ return graph.nodes.map((node) => ({
3965
+ type: "node",
3966
+ id: node.id,
3967
+ label: node.label,
3968
+ score: Math.max(scoreMatch(query, node.label), scoreMatch(query, node.id))
3969
+ })).filter((match) => match.score > 0).sort((left, right) => right.score - left.score || left.label.localeCompare(right.label));
3970
+ }
3971
+ function graphAdjacency(graph) {
3972
+ const adjacency = /* @__PURE__ */ new Map();
3973
+ const push = (nodeId, item) => {
3974
+ if (!adjacency.has(nodeId)) {
3975
+ adjacency.set(nodeId, []);
3976
+ }
3977
+ adjacency.get(nodeId)?.push(item);
3978
+ };
3979
+ for (const edge of graph.edges) {
3980
+ push(edge.source, { edge, nodeId: edge.target, direction: "outgoing" });
3981
+ push(edge.target, { edge, nodeId: edge.source, direction: "incoming" });
3982
+ }
3983
+ for (const [nodeId, items] of adjacency.entries()) {
3984
+ items.sort((left, right) => right.edge.confidence - left.edge.confidence || left.edge.relation.localeCompare(right.edge.relation));
3985
+ adjacency.set(nodeId, items);
3986
+ }
3987
+ return adjacency;
3988
+ }
3989
+ function resolveNode(graph, target) {
3990
+ const normalized = normalizeTarget(target);
3991
+ const byId = nodeById(graph);
3992
+ if (byId.has(target)) {
3993
+ return byId.get(target);
3994
+ }
3995
+ const exact = graph.nodes.find((node) => normalizeTarget(node.label) === normalized || normalizeTarget(node.id) === normalized);
3996
+ if (exact) {
3997
+ return exact;
3998
+ }
3999
+ const pages = graph.pages.map((page) => ({
4000
+ page,
4001
+ score: Math.max(scoreMatch(target, page.title), scoreMatch(target, page.path))
4002
+ })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.page.title.localeCompare(right.page.title));
4003
+ if (pages[0]) {
4004
+ return primaryNodeForPage(graph, pages[0].page);
4005
+ }
4006
+ return graph.nodes.map((node) => ({ node, score: Math.max(scoreMatch(target, node.label), scoreMatch(target, node.id)) })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.node.label.localeCompare(right.node.label))[0]?.node;
4007
+ }
4008
+ function communityLabel(graph, communityId) {
4009
+ if (!communityId) {
4010
+ return void 0;
4011
+ }
4012
+ const community = graph.communities?.find((item) => item.id === communityId);
4013
+ return community ? { id: community.id, label: community.label } : void 0;
4014
+ }
4015
+ function queryGraph(graph, question, searchResults, options) {
4016
+ const traversal = options?.traversal ?? "bfs";
4017
+ const budget = Math.max(3, Math.min(options?.budget ?? 12, 50));
4018
+ const matches = uniqueBy(
4019
+ [...pageSearchMatches(graph, question, searchResults), ...nodeMatches(graph, question)],
4020
+ (match) => `${match.type}:${match.id}`
4021
+ ).sort((left, right) => right.score - left.score || left.label.localeCompare(right.label)).slice(0, 12);
4022
+ const pages = pageById(graph);
4023
+ const seeds = uniqueBy(
4024
+ [
4025
+ ...searchResults.flatMap((result) => pages.get(result.pageId)?.nodeIds ?? []),
4026
+ ...matches.filter((match) => match.type === "node").map((match) => match.id)
4027
+ ],
4028
+ (item) => item
4029
+ ).filter(Boolean);
4030
+ const adjacency = graphAdjacency(graph);
4031
+ const visitedNodeIds = [];
4032
+ const visitedEdgeIds = /* @__PURE__ */ new Set();
4033
+ const seen = /* @__PURE__ */ new Set();
4034
+ const frontier = [...seeds];
4035
+ while (frontier.length && visitedNodeIds.length < budget) {
4036
+ const current = traversal === "dfs" ? frontier.pop() : frontier.shift();
4037
+ if (!current || seen.has(current)) {
4038
+ continue;
4039
+ }
4040
+ seen.add(current);
4041
+ visitedNodeIds.push(current);
4042
+ for (const neighbor of adjacency.get(current) ?? []) {
4043
+ visitedEdgeIds.add(neighbor.edge.id);
4044
+ if (!seen.has(neighbor.nodeId)) {
4045
+ frontier.push(neighbor.nodeId);
4046
+ }
4047
+ if (visitedNodeIds.length + frontier.length >= budget * 2) {
4048
+ break;
4049
+ }
4050
+ }
4051
+ }
4052
+ const nodes = nodeById(graph);
4053
+ const pageIds = uniqueBy(
4054
+ [
4055
+ ...searchResults.map((result) => result.pageId),
4056
+ ...visitedNodeIds.flatMap((nodeId) => {
4057
+ const node = nodes.get(nodeId);
4058
+ return node?.pageId ? [node.pageId] : [];
4059
+ })
4060
+ ],
4061
+ (item) => item
4062
+ );
4063
+ const communities = uniqueBy(
4064
+ visitedNodeIds.map((nodeId) => nodes.get(nodeId)?.communityId).filter((communityId) => Boolean(communityId)),
4065
+ (item) => item
4066
+ );
4067
+ return {
4068
+ question,
4069
+ traversal,
4070
+ seedNodeIds: seeds,
4071
+ seedPageIds: uniqueBy(
4072
+ searchResults.map((result) => result.pageId),
4073
+ (item) => item
4074
+ ),
4075
+ visitedNodeIds,
4076
+ visitedEdgeIds: [...visitedEdgeIds],
4077
+ pageIds,
4078
+ communities,
4079
+ matches,
4080
+ summary: [
4081
+ `Seeds: ${seeds.join(", ") || "none"}`,
4082
+ `Visited nodes: ${visitedNodeIds.length}`,
4083
+ `Visited edges: ${visitedEdgeIds.size}`,
4084
+ `Communities: ${communities.join(", ") || "none"}`,
4085
+ `Pages: ${pageIds.join(", ") || "none"}`
4086
+ ].join("\n")
4087
+ };
4088
+ }
4089
+ function shortestGraphPath(graph, from, to) {
4090
+ const start = resolveNode(graph, from);
4091
+ const end = resolveNode(graph, to);
4092
+ if (!start || !end) {
4093
+ return {
4094
+ from,
4095
+ to,
4096
+ resolvedFromNodeId: start?.id,
4097
+ resolvedToNodeId: end?.id,
4098
+ found: false,
4099
+ nodeIds: [],
4100
+ edgeIds: [],
4101
+ pageIds: [],
4102
+ summary: "Could not resolve one or both graph targets."
4103
+ };
4104
+ }
4105
+ const adjacency = graphAdjacency(graph);
4106
+ const queue = [start.id];
4107
+ const visited = /* @__PURE__ */ new Set([start.id]);
4108
+ const previous = /* @__PURE__ */ new Map();
4109
+ while (queue.length) {
4110
+ const current2 = queue.shift();
4111
+ if (current2 === end.id) {
4112
+ break;
4113
+ }
4114
+ for (const neighbor of adjacency.get(current2) ?? []) {
4115
+ if (visited.has(neighbor.nodeId)) {
4116
+ continue;
4117
+ }
4118
+ visited.add(neighbor.nodeId);
4119
+ previous.set(neighbor.nodeId, { nodeId: current2, edgeId: neighbor.edge.id });
4120
+ queue.push(neighbor.nodeId);
4121
+ }
4122
+ }
4123
+ if (!visited.has(end.id)) {
4124
+ return {
4125
+ from,
4126
+ to,
4127
+ resolvedFromNodeId: start.id,
4128
+ resolvedToNodeId: end.id,
4129
+ found: false,
4130
+ nodeIds: [],
4131
+ edgeIds: [],
4132
+ pageIds: [],
4133
+ summary: `No path found between ${start.label} and ${end.label}.`
4134
+ };
4135
+ }
4136
+ const nodeIds = [];
4137
+ const edgeIds = [];
4138
+ let current = end.id;
4139
+ while (current !== start.id) {
4140
+ nodeIds.push(current);
4141
+ const prev = previous.get(current);
4142
+ if (!prev) {
4143
+ break;
4144
+ }
4145
+ edgeIds.push(prev.edgeId);
4146
+ current = prev.nodeId;
4147
+ }
4148
+ nodeIds.push(start.id);
4149
+ nodeIds.reverse();
4150
+ edgeIds.reverse();
4151
+ const nodes = nodeById(graph);
4152
+ const pageIds = uniqueBy(
4153
+ nodeIds.flatMap((nodeId) => {
4154
+ const node = nodes.get(nodeId);
4155
+ return node?.pageId ? [node.pageId] : [];
4156
+ }),
4157
+ (item) => item
4158
+ );
4159
+ return {
4160
+ from,
4161
+ to,
4162
+ resolvedFromNodeId: start.id,
4163
+ resolvedToNodeId: end.id,
4164
+ found: true,
4165
+ nodeIds,
4166
+ edgeIds,
4167
+ pageIds,
4168
+ summary: nodeIds.map((nodeId) => nodes.get(nodeId)?.label ?? nodeId).join(" -> ")
4169
+ };
4170
+ }
4171
+ function explainGraphTarget(graph, target) {
4172
+ const node = resolveNode(graph, target);
4173
+ if (!node) {
4174
+ throw new Error(`Could not resolve graph target: ${target}`);
4175
+ }
4176
+ const pages = pageById(graph);
4177
+ const page = node.pageId ? pages.get(node.pageId) : void 0;
4178
+ const neighbors = [];
4179
+ const nodes = nodeById(graph);
4180
+ for (const neighbor of graphAdjacency(graph).get(node.id) ?? []) {
4181
+ const targetNode = nodes.get(neighbor.nodeId);
4182
+ if (!targetNode) {
4183
+ continue;
4184
+ }
4185
+ neighbors.push({
4186
+ nodeId: targetNode.id,
4187
+ label: targetNode.label,
4188
+ type: targetNode.type,
4189
+ pageId: targetNode.pageId,
4190
+ relation: neighbor.edge.relation,
4191
+ direction: neighbor.direction,
4192
+ confidence: neighbor.edge.confidence,
4193
+ evidenceClass: neighbor.edge.evidenceClass
4194
+ });
4195
+ }
4196
+ neighbors.sort((left, right) => right.confidence - left.confidence || left.label.localeCompare(right.label));
4197
+ return {
4198
+ target,
4199
+ node,
4200
+ page,
4201
+ community: communityLabel(graph, node.communityId),
4202
+ neighbors,
4203
+ summary: [
4204
+ `Node: ${node.label}`,
4205
+ `Type: ${node.type}`,
4206
+ `Community: ${node.communityId ?? "none"}`,
4207
+ `Neighbors: ${neighbors.length}`,
4208
+ `Page: ${page?.path ?? "none"}`
4209
+ ].join("\n")
4210
+ };
4211
+ }
4212
+ function topGodNodes(graph, limit = 10) {
4213
+ return graph.nodes.filter((node) => node.isGodNode).sort((left, right) => (right.degree ?? 0) - (left.degree ?? 0)).slice(0, limit);
4214
+ }
4215
+
3698
4216
  // src/markdown.ts
3699
4217
  import matter3 from "gray-matter";
3700
4218
  function uniqueStrings(values) {
@@ -3719,6 +4237,10 @@ function pagePathFor(kind, slug) {
3719
4237
  return `entities/${slug}.md`;
3720
4238
  case "output":
3721
4239
  return `outputs/${slug}.md`;
4240
+ case "graph_report":
4241
+ return "graph/report.md";
4242
+ case "community_summary":
4243
+ return `graph/communities/${slug}.md`;
3722
4244
  default:
3723
4245
  return `${slug}.md`;
3724
4246
  }
@@ -3729,6 +4251,10 @@ function candidatePagePathFor(kind, slug) {
3729
4251
  function pageLink(page) {
3730
4252
  return `[[${page.path.replace(/\.md$/, "")}|${page.title}]]`;
3731
4253
  }
4254
+ function graphNodeLink(node, pagesById) {
4255
+ const page = node.pageId ? pagesById.get(node.pageId) : void 0;
4256
+ return page ? pageLink(page) : `\`${node.label}\``;
4257
+ }
3732
4258
  function assetMarkdownPath(assetPath) {
3733
4259
  return `./${assetPath.replace(/^outputs\//, "")}`;
3734
4260
  }
@@ -4084,6 +4610,7 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
4084
4610
  const candidates = pages.filter((page) => page.status === "candidate");
4085
4611
  const outputs = pages.filter((page) => page.kind === "output");
4086
4612
  const insights = pages.filter((page) => page.kind === "insight");
4613
+ const graphPages = pages.filter((page) => page.kind === "graph_report" || page.kind === "community_summary");
4087
4614
  return [
4088
4615
  "---",
4089
4616
  "page_id: index",
@@ -4128,6 +4655,10 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
4128
4655
  "",
4129
4656
  ...outputs.length ? outputs.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No saved outputs yet."],
4130
4657
  "",
4658
+ "## Graph",
4659
+ "",
4660
+ ...graphPages.length ? graphPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No graph reports yet."],
4661
+ "",
4131
4662
  "## Projects",
4132
4663
  "",
4133
4664
  ...projectPages.length ? projectPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No projects configured."],
@@ -4167,6 +4698,215 @@ function buildSectionIndex(kind, pages, schemaHash, metadata, projectIds = []) {
4167
4698
  }
4168
4699
  );
4169
4700
  }
4701
+ function communityPagePath(communityId) {
4702
+ return pagePathFor("community_summary", communityId.replace(/^community:/, ""));
4703
+ }
4704
+ function nodeSummary(node) {
4705
+ const degree = typeof node.degree === "number" ? `degree=${node.degree}` : "";
4706
+ const bridge = typeof node.bridgeScore === "number" ? `bridge=${node.bridgeScore}` : "";
4707
+ return [node.type, degree, bridge].filter(Boolean).join(", ");
4708
+ }
4709
+ function crossCommunityEdges(graph) {
4710
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
4711
+ return graph.edges.filter((edge) => {
4712
+ const source = nodesById.get(edge.source);
4713
+ const target = nodesById.get(edge.target);
4714
+ return source?.communityId && target?.communityId && source.communityId !== target.communityId;
4715
+ }).sort((left, right) => right.confidence - left.confidence || left.relation.localeCompare(right.relation));
4716
+ }
4717
+ function suggestedGraphQuestions(graph) {
4718
+ const thinCommunities = (graph.communities ?? []).filter((community) => community.nodeIds.length <= 2);
4719
+ const bridgeNodes = graph.nodes.filter((node) => (node.bridgeScore ?? 0) > 0).sort((left, right) => (right.bridgeScore ?? 0) - (left.bridgeScore ?? 0)).slice(0, 3);
4720
+ return uniqueStrings([
4721
+ ...thinCommunities.map((community) => `What sources would strengthen community ${community.label}?`),
4722
+ ...bridgeNodes.map((node) => `Why does ${node.label} connect multiple communities in the vault?`)
4723
+ ]).slice(0, 6);
4724
+ }
4725
+ function buildGraphReportPage(input) {
4726
+ const pageId = "graph:report";
4727
+ const pathValue = pagePathFor("graph_report", "report");
4728
+ const pagesById = new Map(input.graph.pages.map((page) => [page.id, page]));
4729
+ const nodesById = new Map(input.graph.nodes.map((node) => [node.id, node]));
4730
+ const godNodes = input.graph.nodes.filter((node) => node.isGodNode).sort((left, right) => (right.degree ?? 0) - (left.degree ?? 0)).slice(0, 8);
4731
+ const bridgeNodes = input.graph.nodes.filter((node) => (node.bridgeScore ?? 0) > 0).sort((left, right) => (right.bridgeScore ?? 0) - (left.bridgeScore ?? 0)).slice(0, 8);
4732
+ const surprisingEdges = crossCommunityEdges(input.graph).slice(0, 8);
4733
+ const thinCommunities = (input.graph.communities ?? []).filter((community) => community.nodeIds.length <= 2);
4734
+ const relatedNodeIds = uniqueStrings([...godNodes, ...bridgeNodes].map((node) => node.id));
4735
+ const relatedPageIds = uniqueStrings([
4736
+ ...godNodes.map((node) => node.pageId ?? ""),
4737
+ ...bridgeNodes.map((node) => node.pageId ?? ""),
4738
+ ...input.communityPages.map((page) => page.id)
4739
+ ]);
4740
+ const relatedSourceIds = uniqueStrings(relatedNodeIds.flatMap((nodeId) => nodesById.get(nodeId)?.sourceIds ?? []));
4741
+ const frontmatter = {
4742
+ page_id: pageId,
4743
+ kind: "graph_report",
4744
+ title: "Graph Report",
4745
+ tags: ["graph", "report"],
4746
+ source_ids: relatedSourceIds,
4747
+ project_ids: [],
4748
+ node_ids: relatedNodeIds,
4749
+ freshness: "fresh",
4750
+ status: input.metadata.status,
4751
+ confidence: input.metadata.confidence,
4752
+ created_at: input.metadata.createdAt,
4753
+ updated_at: input.metadata.updatedAt,
4754
+ compiled_from: input.metadata.compiledFrom,
4755
+ managed_by: input.metadata.managedBy,
4756
+ backlinks: [],
4757
+ schema_hash: input.schemaHash,
4758
+ source_hashes: {},
4759
+ related_page_ids: relatedPageIds,
4760
+ related_node_ids: relatedNodeIds,
4761
+ related_source_ids: relatedSourceIds
4762
+ };
4763
+ const body = [
4764
+ "# Graph Report",
4765
+ "",
4766
+ "## Overview",
4767
+ "",
4768
+ `- Nodes: ${input.graph.nodes.length}`,
4769
+ `- Edges: ${input.graph.edges.length}`,
4770
+ `- Pages: ${input.graph.pages.length}`,
4771
+ `- Communities: ${input.graph.communities?.length ?? 0}`,
4772
+ "",
4773
+ "## God Nodes",
4774
+ "",
4775
+ ...godNodes.length ? godNodes.map((node) => `- ${graphNodeLink(node, pagesById)} (${nodeSummary(node)})`) : ["- No high-connectivity nodes detected."],
4776
+ "",
4777
+ "## Bridge Nodes",
4778
+ "",
4779
+ ...bridgeNodes.length ? bridgeNodes.map((node) => `- ${graphNodeLink(node, pagesById)} (${nodeSummary(node)})`) : ["- No cross-community bridge nodes detected."],
4780
+ "",
4781
+ "## Communities",
4782
+ "",
4783
+ ...input.communityPages.length ? input.communityPages.map((page) => `- ${pageLink(page)}`) : ["- No community summaries generated yet."],
4784
+ "",
4785
+ "## Thin Communities",
4786
+ "",
4787
+ ...thinCommunities.length ? thinCommunities.map((community) => `- ${community.label} (${community.nodeIds.length} node(s))`) : ["- No thin communities detected."],
4788
+ "",
4789
+ "## Cross-Community Surprises",
4790
+ "",
4791
+ ...surprisingEdges.length ? surprisingEdges.map((edge) => {
4792
+ const source = nodesById.get(edge.source);
4793
+ const target = nodesById.get(edge.target);
4794
+ return `- ${source ? graphNodeLink(source, pagesById) : `\`${edge.source}\``} ${edge.relation} ${target ? graphNodeLink(target, pagesById) : `\`${edge.target}\``} (${edge.evidenceClass}, ${edge.confidence.toFixed(2)})`;
4795
+ }) : ["- No cross-community links detected."],
4796
+ "",
4797
+ "## Suggested Follow-Up Questions",
4798
+ "",
4799
+ ...suggestedGraphQuestions(input.graph).map((question) => `- ${question}`),
4800
+ ""
4801
+ ].join("\n");
4802
+ return {
4803
+ page: {
4804
+ id: pageId,
4805
+ path: pathValue,
4806
+ title: "Graph Report",
4807
+ kind: "graph_report",
4808
+ sourceIds: relatedSourceIds,
4809
+ projectIds: [],
4810
+ nodeIds: relatedNodeIds,
4811
+ freshness: "fresh",
4812
+ status: input.metadata.status,
4813
+ confidence: input.metadata.confidence,
4814
+ backlinks: [],
4815
+ schemaHash: input.schemaHash,
4816
+ sourceHashes: {},
4817
+ relatedPageIds,
4818
+ relatedNodeIds,
4819
+ relatedSourceIds,
4820
+ createdAt: input.metadata.createdAt,
4821
+ updatedAt: input.metadata.updatedAt,
4822
+ compiledFrom: input.metadata.compiledFrom,
4823
+ managedBy: input.metadata.managedBy
4824
+ },
4825
+ content: matter3.stringify(body, frontmatter)
4826
+ };
4827
+ }
4828
+ function buildCommunitySummaryPage(input) {
4829
+ const pageId = `graph:${input.community.id}`;
4830
+ const pathValue = communityPagePath(input.community.id);
4831
+ const nodesById = new Map(input.graph.nodes.map((node) => [node.id, node]));
4832
+ const pagesById = new Map(input.graph.pages.map((page) => [page.id, page]));
4833
+ const communityNodes = input.community.nodeIds.map((nodeId) => nodesById.get(nodeId)).filter((node) => Boolean(node));
4834
+ const communityPageIds = uniqueStrings(communityNodes.map((node) => node.pageId ?? ""));
4835
+ const communityPages = communityPageIds.map((id) => pagesById.get(id)).filter((page) => Boolean(page));
4836
+ const externalEdges = input.graph.edges.filter((edge) => {
4837
+ const source = nodesById.get(edge.source);
4838
+ const target = nodesById.get(edge.target);
4839
+ return source?.communityId === input.community.id && target?.communityId && target.communityId !== input.community.id;
4840
+ }).slice(0, 8);
4841
+ const relatedSourceIds = uniqueStrings(communityNodes.flatMap((node) => node.sourceIds));
4842
+ const frontmatter = {
4843
+ page_id: pageId,
4844
+ kind: "community_summary",
4845
+ title: `Community: ${input.community.label}`,
4846
+ tags: ["graph", "community"],
4847
+ source_ids: relatedSourceIds,
4848
+ project_ids: [],
4849
+ node_ids: input.community.nodeIds,
4850
+ freshness: "fresh",
4851
+ status: input.metadata.status,
4852
+ confidence: input.metadata.confidence,
4853
+ created_at: input.metadata.createdAt,
4854
+ updated_at: input.metadata.updatedAt,
4855
+ compiled_from: input.metadata.compiledFrom,
4856
+ managed_by: input.metadata.managedBy,
4857
+ backlinks: ["graph:report"],
4858
+ schema_hash: input.schemaHash,
4859
+ source_hashes: {},
4860
+ related_page_ids: uniqueStrings(["graph:report", ...communityPageIds]),
4861
+ related_node_ids: input.community.nodeIds,
4862
+ related_source_ids: relatedSourceIds
4863
+ };
4864
+ const body = [
4865
+ `# Community: ${input.community.label}`,
4866
+ "",
4867
+ "## Nodes",
4868
+ "",
4869
+ ...communityNodes.map((node) => `- ${graphNodeLink(node, pagesById)} (${nodeSummary(node)})`),
4870
+ "",
4871
+ "## Pages",
4872
+ "",
4873
+ ...communityPages.length ? communityPages.map((page) => `- ${pageLink(page)}`) : ["- No canonical pages linked."],
4874
+ "",
4875
+ "## External Links",
4876
+ "",
4877
+ ...externalEdges.length ? externalEdges.map((edge) => {
4878
+ const source = nodesById.get(edge.source);
4879
+ const target = nodesById.get(edge.target);
4880
+ return `- ${source ? graphNodeLink(source, pagesById) : `\`${edge.source}\``} ${edge.relation} ${target ? graphNodeLink(target, pagesById) : `\`${edge.target}\``} (${edge.evidenceClass})`;
4881
+ }) : ["- No external links detected."],
4882
+ ""
4883
+ ].join("\n");
4884
+ return {
4885
+ page: {
4886
+ id: pageId,
4887
+ path: pathValue,
4888
+ title: `Community: ${input.community.label}`,
4889
+ kind: "community_summary",
4890
+ sourceIds: relatedSourceIds,
4891
+ projectIds: [],
4892
+ nodeIds: input.community.nodeIds,
4893
+ freshness: "fresh",
4894
+ status: input.metadata.status,
4895
+ confidence: input.metadata.confidence,
4896
+ backlinks: ["graph:report"],
4897
+ schemaHash: input.schemaHash,
4898
+ sourceHashes: {},
4899
+ relatedPageIds: uniqueStrings(["graph:report", ...communityPageIds]),
4900
+ relatedNodeIds: input.community.nodeIds,
4901
+ relatedSourceIds,
4902
+ createdAt: input.metadata.createdAt,
4903
+ updatedAt: input.metadata.updatedAt,
4904
+ compiledFrom: input.metadata.compiledFrom,
4905
+ managedBy: input.metadata.managedBy
4906
+ },
4907
+ content: matter3.stringify(body, frontmatter)
4908
+ };
4909
+ }
4170
4910
  function buildProjectsIndex(projectPages, schemaHash, metadata) {
4171
4911
  return matter3.stringify(
4172
4912
  [
@@ -5016,7 +5756,7 @@ function toFtsQuery(query) {
5016
5756
  return tokens.join(" OR ");
5017
5757
  }
5018
5758
  function normalizeKind(value) {
5019
- return value === "index" || value === "source" || value === "module" || value === "concept" || value === "entity" || value === "output" || value === "insight" ? value : void 0;
5759
+ return value === "index" || value === "source" || value === "module" || value === "concept" || value === "entity" || value === "output" || value === "insight" || value === "graph_report" || value === "community_summary" ? value : void 0;
5020
5760
  }
5021
5761
  function normalizeStatus(value) {
5022
5762
  return value === "draft" || value === "candidate" || value === "active" || value === "archived" ? value : void 0;
@@ -5690,7 +6430,8 @@ function candidateActivePath(page) {
5690
6430
  return activeAggregatePath(page.kind, pageSlug(page));
5691
6431
  }
5692
6432
  function buildCommunityId(seed, index) {
5693
- return `community:${slugify(seed) || `cluster-${index + 1}`}`;
6433
+ const slug = slugify(seed) || "cluster";
6434
+ return `community:${slug}-${index + 1}`;
5694
6435
  }
5695
6436
  function pageHashes(pages) {
5696
6437
  return Object.fromEntries(pages.map((page) => [page.page.id, page.contentHash]));
@@ -5869,6 +6610,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
5869
6610
  const entityMap = /* @__PURE__ */ new Map();
5870
6611
  const moduleMap = /* @__PURE__ */ new Map();
5871
6612
  const symbolMap = /* @__PURE__ */ new Map();
6613
+ const rationaleMap = /* @__PURE__ */ new Map();
5872
6614
  const edges = [];
5873
6615
  const edgesById = /* @__PURE__ */ new Set();
5874
6616
  const pushEdge = (edge) => {
@@ -5900,6 +6642,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
5900
6642
  target: concept.id,
5901
6643
  relation: "mentions",
5902
6644
  status: "extracted",
6645
+ evidenceClass: "extracted",
5903
6646
  confidence: edgeConfidence(analysis.claims, concept.name),
5904
6647
  provenance: [analysis.sourceId]
5905
6648
  });
@@ -5923,6 +6666,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
5923
6666
  target: entity.id,
5924
6667
  relation: "mentions",
5925
6668
  status: "extracted",
6669
+ evidenceClass: "extracted",
5926
6670
  confidence: edgeConfidence(analysis.claims, entity.name),
5927
6671
  provenance: [analysis.sourceId]
5928
6672
  });
@@ -5951,6 +6695,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
5951
6695
  target: moduleId,
5952
6696
  relation: "contains_code",
5953
6697
  status: "extracted",
6698
+ evidenceClass: "extracted",
5954
6699
  confidence: 1,
5955
6700
  provenance: [analysis.sourceId]
5956
6701
  });
@@ -5974,6 +6719,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
5974
6719
  target: symbol.id,
5975
6720
  relation: "defines",
5976
6721
  status: "extracted",
6722
+ evidenceClass: "extracted",
5977
6723
  confidence: 1,
5978
6724
  provenance: [analysis.sourceId]
5979
6725
  });
@@ -5984,12 +6730,39 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
5984
6730
  target: symbol.id,
5985
6731
  relation: "exports",
5986
6732
  status: "extracted",
6733
+ evidenceClass: "extracted",
5987
6734
  confidence: 1,
5988
6735
  provenance: [analysis.sourceId]
5989
6736
  });
5990
6737
  }
5991
6738
  }
5992
6739
  const symbolIdsByName = new Map(analysis.code.symbols.map((symbol) => [symbol.name, symbol.id]));
6740
+ for (const rationale of analysis.rationales) {
6741
+ const targetSymbolId = rationale.symbolName ? symbolIdsByName.get(rationale.symbolName) : void 0;
6742
+ const targetId = targetSymbolId ?? moduleId;
6743
+ rationaleMap.set(rationale.id, {
6744
+ id: rationale.id,
6745
+ type: "rationale",
6746
+ label: truncate(rationale.text, 80),
6747
+ pageId: moduleId,
6748
+ freshness: "fresh",
6749
+ confidence: 1,
6750
+ sourceIds: [analysis.sourceId],
6751
+ projectIds: scopedProjectIdsFromSources([analysis.sourceId], sourceProjects),
6752
+ language: analysis.code.language,
6753
+ moduleId
6754
+ });
6755
+ pushEdge({
6756
+ id: `${rationale.id}->${targetId}:rationale_for`,
6757
+ source: rationale.id,
6758
+ target: targetId,
6759
+ relation: "rationale_for",
6760
+ status: "extracted",
6761
+ evidenceClass: "extracted",
6762
+ confidence: 1,
6763
+ provenance: [analysis.sourceId]
6764
+ });
6765
+ }
5993
6766
  const importedSymbolIdsByName = /* @__PURE__ */ new Map();
5994
6767
  for (const codeImport of analysis.code.imports.filter((item) => !item.isExternal)) {
5995
6768
  const targetSourceId = codeImport.resolvedSourceId;
@@ -6027,6 +6800,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6027
6800
  target: targetId,
6028
6801
  relation: "calls",
6029
6802
  status: "extracted",
6803
+ evidenceClass: "extracted",
6030
6804
  confidence: 1,
6031
6805
  provenance: [analysis.sourceId]
6032
6806
  });
@@ -6042,6 +6816,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6042
6816
  target: targetId,
6043
6817
  relation: "extends",
6044
6818
  status: "extracted",
6819
+ evidenceClass: "extracted",
6045
6820
  confidence: 1,
6046
6821
  provenance: [analysis.sourceId]
6047
6822
  });
@@ -6057,6 +6832,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6057
6832
  target: targetId,
6058
6833
  relation: "implements",
6059
6834
  status: "extracted",
6835
+ evidenceClass: "extracted",
6060
6836
  confidence: 1,
6061
6837
  provenance: [analysis.sourceId]
6062
6838
  });
@@ -6074,6 +6850,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6074
6850
  target: targetModuleId,
6075
6851
  relation: codeImport.reExport ? "exports" : "imports",
6076
6852
  status: "extracted",
6853
+ evidenceClass: "extracted",
6077
6854
  confidence: 1,
6078
6855
  provenance: [analysis.sourceId, targetSourceId]
6079
6856
  });
@@ -6113,13 +6890,21 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6113
6890
  target: `source:${negativeClaim.sourceId}`,
6114
6891
  relation: "conflicted_with",
6115
6892
  status: "conflicted",
6893
+ evidenceClass: "ambiguous",
6116
6894
  confidence: conflictConfidence(positiveClaim.claim, negativeClaim.claim),
6117
6895
  provenance: [positiveClaim.sourceId, negativeClaim.sourceId]
6118
6896
  });
6119
6897
  }
6120
6898
  }
6121
6899
  }
6122
- const graphNodes = [...sourceNodes, ...moduleMap.values(), ...symbolMap.values(), ...conceptMap.values(), ...entityMap.values()];
6900
+ const graphNodes = [
6901
+ ...sourceNodes,
6902
+ ...moduleMap.values(),
6903
+ ...symbolMap.values(),
6904
+ ...rationaleMap.values(),
6905
+ ...conceptMap.values(),
6906
+ ...entityMap.values()
6907
+ ];
6123
6908
  const metrics = deriveGraphMetrics(graphNodes, edges);
6124
6909
  return {
6125
6910
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6130,6 +6915,46 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6130
6915
  pages
6131
6916
  };
6132
6917
  }
6918
+ async function buildGraphOrientationPages(graph, paths, schemaHash) {
6919
+ const communityRecords = [];
6920
+ for (const community of graph.communities ?? []) {
6921
+ const absolutePath = path14.join(paths.wikiDir, "graph", "communities", `${community.id.replace(/^community:/, "")}.md`);
6922
+ communityRecords.push(
6923
+ await buildManagedGraphPage(
6924
+ absolutePath,
6925
+ {
6926
+ managedBy: "system",
6927
+ compiledFrom: uniqueStrings2(
6928
+ community.nodeIds.flatMap((nodeId) => graph.nodes.find((node) => node.id === nodeId)?.sourceIds ?? [])
6929
+ ),
6930
+ confidence: 1
6931
+ },
6932
+ (metadata) => buildCommunitySummaryPage({
6933
+ graph,
6934
+ community,
6935
+ schemaHash,
6936
+ metadata
6937
+ })
6938
+ )
6939
+ );
6940
+ }
6941
+ const reportAbsolutePath = path14.join(paths.wikiDir, "graph", "report.md");
6942
+ const reportRecord = await buildManagedGraphPage(
6943
+ reportAbsolutePath,
6944
+ {
6945
+ managedBy: "system",
6946
+ compiledFrom: uniqueStrings2(graph.pages.flatMap((page) => page.sourceIds)),
6947
+ confidence: 1
6948
+ },
6949
+ (metadata) => buildGraphReportPage({
6950
+ graph,
6951
+ schemaHash,
6952
+ metadata,
6953
+ communityPages: communityRecords.map((record) => record.page)
6954
+ })
6955
+ );
6956
+ return [reportRecord, ...communityRecords];
6957
+ }
6133
6958
  async function writePage(wikiDir, relativePath, content, changedPages) {
6134
6959
  const absolutePath = path14.resolve(wikiDir, relativePath);
6135
6960
  const changed = await writeFileIfChanged(absolutePath, content);
@@ -6494,8 +7319,15 @@ async function syncVaultArtifacts(rootDir, input) {
6494
7319
  }
6495
7320
  }
6496
7321
  const compiledPages = records.map((record) => record.page);
6497
- const allPages = [...compiledPages, ...input.outputPages, ...input.insightPages];
6498
- const graph = buildGraph(input.manifests, input.analyses, allPages, input.sourceProjects, input.codeIndex);
7322
+ const basePages = [...compiledPages, ...input.outputPages, ...input.insightPages];
7323
+ const baseGraph = buildGraph(input.manifests, input.analyses, basePages, input.sourceProjects, input.codeIndex);
7324
+ const graphOrientationRecords = await buildGraphOrientationPages(baseGraph, paths, globalSchemaHash);
7325
+ records.push(...graphOrientationRecords);
7326
+ const allPages = [...basePages, ...graphOrientationRecords.map((record) => record.page)];
7327
+ const graph = {
7328
+ ...baseGraph,
7329
+ pages: allPages
7330
+ };
6499
7331
  const activeConceptPages = allPages.filter((page) => page.kind === "concept" && page.status !== "candidate");
6500
7332
  const activeEntityPages = allPages.filter((page) => page.kind === "entity" && page.status !== "candidate");
6501
7333
  const modulePages = allPages.filter((page) => page.kind === "module");
@@ -6595,7 +7427,8 @@ async function syncVaultArtifacts(rootDir, input) {
6595
7427
  ["concepts/index.md", "concepts", activeConceptPages],
6596
7428
  ["entities/index.md", "entities", activeEntityPages],
6597
7429
  ["outputs/index.md", "outputs", allPages.filter((page) => page.kind === "output")],
6598
- ["candidates/index.md", "candidates", candidatePages]
7430
+ ["candidates/index.md", "candidates", candidatePages],
7431
+ ["graph/index.md", "graph", allPages.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
6599
7432
  ]) {
6600
7433
  records.push({
6601
7434
  page: emptyGraphPage({
@@ -6690,6 +7523,23 @@ async function refreshIndexesAndSearch(rootDir, pages) {
6690
7523
  const { config, paths } = await loadVaultConfig(rootDir);
6691
7524
  const schemas = await loadVaultSchemas(rootDir);
6692
7525
  const globalSchemaHash = schemas.effective.global.hash;
7526
+ const currentGraph = await readJsonFile(paths.graphPath);
7527
+ const basePages = pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary");
7528
+ const graphOrientationRecords = currentGraph ? await buildGraphOrientationPages(
7529
+ {
7530
+ ...currentGraph,
7531
+ pages: basePages
7532
+ },
7533
+ paths,
7534
+ globalSchemaHash
7535
+ ) : [];
7536
+ const pagesWithGraph = sortGraphPages([...basePages, ...graphOrientationRecords.map((record) => record.page)]);
7537
+ if (currentGraph) {
7538
+ await writeJsonFile(paths.graphPath, {
7539
+ ...currentGraph,
7540
+ pages: pagesWithGraph
7541
+ });
7542
+ }
6693
7543
  const configuredProjects = projectEntries(config);
6694
7544
  const projectIndexRefs = configuredProjects.map(
6695
7545
  (project) => emptyGraphPage({
@@ -6711,6 +7561,8 @@ async function refreshIndexesAndSearch(rootDir, pages) {
6711
7561
  ensureDir(path14.join(paths.wikiDir, "concepts")),
6712
7562
  ensureDir(path14.join(paths.wikiDir, "entities")),
6713
7563
  ensureDir(path14.join(paths.wikiDir, "outputs")),
7564
+ ensureDir(path14.join(paths.wikiDir, "graph")),
7565
+ ensureDir(path14.join(paths.wikiDir, "graph", "communities")),
6714
7566
  ensureDir(path14.join(paths.wikiDir, "projects")),
6715
7567
  ensureDir(path14.join(paths.wikiDir, "candidates"))
6716
7568
  ]);
@@ -6760,18 +7612,19 @@ async function refreshIndexesAndSearch(rootDir, pages) {
6760
7612
  rootIndexPath,
6761
7613
  {
6762
7614
  managedBy: "system",
6763
- compiledFrom: indexCompiledFrom(pages)
7615
+ compiledFrom: indexCompiledFrom(pagesWithGraph)
6764
7616
  },
6765
- (metadata) => buildIndexPage(pages, globalSchemaHash, metadata, projectIndexRefs)
7617
+ (metadata) => buildIndexPage(pagesWithGraph, globalSchemaHash, metadata, projectIndexRefs)
6766
7618
  )
6767
7619
  );
6768
7620
  for (const [relativePath, kind, sectionPages] of [
6769
- ["sources/index.md", "sources", pages.filter((page) => page.kind === "source")],
6770
- ["code/index.md", "code", pages.filter((page) => page.kind === "module")],
6771
- ["concepts/index.md", "concepts", pages.filter((page) => page.kind === "concept" && page.status !== "candidate")],
6772
- ["entities/index.md", "entities", pages.filter((page) => page.kind === "entity" && page.status !== "candidate")],
6773
- ["outputs/index.md", "outputs", pages.filter((page) => page.kind === "output")],
6774
- ["candidates/index.md", "candidates", pages.filter((page) => page.status === "candidate")]
7621
+ ["sources/index.md", "sources", pagesWithGraph.filter((page) => page.kind === "source")],
7622
+ ["code/index.md", "code", pagesWithGraph.filter((page) => page.kind === "module")],
7623
+ ["concepts/index.md", "concepts", pagesWithGraph.filter((page) => page.kind === "concept" && page.status !== "candidate")],
7624
+ ["entities/index.md", "entities", pagesWithGraph.filter((page) => page.kind === "entity" && page.status !== "candidate")],
7625
+ ["outputs/index.md", "outputs", pagesWithGraph.filter((page) => page.kind === "output")],
7626
+ ["candidates/index.md", "candidates", pagesWithGraph.filter((page) => page.status === "candidate")],
7627
+ ["graph/index.md", "graph", pagesWithGraph.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
6775
7628
  ]) {
6776
7629
  const absolutePath = path14.join(paths.wikiDir, relativePath);
6777
7630
  await writeFileIfChanged(
@@ -6786,6 +7639,9 @@ async function refreshIndexesAndSearch(rootDir, pages) {
6786
7639
  )
6787
7640
  );
6788
7641
  }
7642
+ for (const record of graphOrientationRecords) {
7643
+ await writeFileIfChanged(path14.join(paths.wikiDir, record.page.path), record.content);
7644
+ }
6789
7645
  const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path14.relative(paths.wikiDir, absolutePath)));
6790
7646
  const allowedProjectIndexPaths = /* @__PURE__ */ new Set([
6791
7647
  "projects/index.md",
@@ -6794,7 +7650,12 @@ async function refreshIndexesAndSearch(rootDir, pages) {
6794
7650
  await Promise.all(
6795
7651
  existingProjectIndexPaths.filter((relativePath) => !allowedProjectIndexPaths.has(relativePath)).map((relativePath) => fs11.rm(path14.join(paths.wikiDir, relativePath), { force: true }))
6796
7652
  );
6797
- await rebuildSearchIndex(paths.searchDbPath, pages, paths.wikiDir);
7653
+ const existingGraphPages = (await listFilesRecursive(path14.join(paths.wikiDir, "graph").replace(/\/$/, "")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path14.relative(paths.wikiDir, absolutePath)));
7654
+ const allowedGraphPages = /* @__PURE__ */ new Set(["graph/index.md", ...graphOrientationRecords.map((record) => record.page.path)]);
7655
+ await Promise.all(
7656
+ existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs11.rm(path14.join(paths.wikiDir, relativePath), { force: true }))
7657
+ );
7658
+ await rebuildSearchIndex(paths.searchDbPath, pagesWithGraph, paths.wikiDir);
6798
7659
  }
6799
7660
  async function prepareOutputPageSave(rootDir, input) {
6800
7661
  const { paths } = await loadVaultConfig(rootDir);
@@ -8104,6 +8965,35 @@ async function searchVault(rootDir, query, limit = 5) {
8104
8965
  }
8105
8966
  return searchPages(paths.searchDbPath, query, limit);
8106
8967
  }
8968
+ async function ensureCompiledGraph(rootDir) {
8969
+ const { paths } = await loadVaultConfig(rootDir);
8970
+ if (!await fileExists(paths.searchDbPath) || !await fileExists(paths.graphPath)) {
8971
+ await compileVault(rootDir, {});
8972
+ }
8973
+ const graph = await readJsonFile(paths.graphPath);
8974
+ if (!graph) {
8975
+ throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
8976
+ }
8977
+ return graph;
8978
+ }
8979
+ async function queryGraphVault(rootDir, question, options = {}) {
8980
+ const { paths } = await loadVaultConfig(rootDir);
8981
+ const graph = await ensureCompiledGraph(rootDir);
8982
+ const searchResults = searchPages(paths.searchDbPath, question, { limit: Math.max(5, options.budget ?? 10) });
8983
+ return queryGraph(graph, question, searchResults, options);
8984
+ }
8985
+ async function pathGraphVault(rootDir, from, to) {
8986
+ const graph = await ensureCompiledGraph(rootDir);
8987
+ return shortestGraphPath(graph, from, to);
8988
+ }
8989
+ async function explainGraphVault(rootDir, target) {
8990
+ const graph = await ensureCompiledGraph(rootDir);
8991
+ return explainGraphTarget(graph, target);
8992
+ }
8993
+ async function listGodNodes(rootDir, limit = 10) {
8994
+ const graph = await ensureCompiledGraph(rootDir);
8995
+ return topGodNodes(graph, limit);
8996
+ }
8107
8997
  async function listPages(rootDir) {
8108
8998
  const { paths } = await loadVaultConfig(rootDir);
8109
8999
  const graph = await readJsonFile(paths.graphPath);
@@ -8263,7 +9153,7 @@ async function bootstrapDemo(rootDir, input) {
8263
9153
  }
8264
9154
 
8265
9155
  // src/mcp.ts
8266
- var SERVER_VERSION = "0.1.18";
9156
+ var SERVER_VERSION = "0.1.19";
8267
9157
  async function createMcpServer(rootDir) {
8268
9158
  const server = new McpServer({
8269
9159
  name: "swarmvault",
@@ -8323,6 +9213,74 @@ async function createMcpServer(rootDir) {
8323
9213
  return asToolText(limit ? manifests.slice(0, limit) : manifests);
8324
9214
  }
8325
9215
  );
9216
+ server.registerTool(
9217
+ "query_graph",
9218
+ {
9219
+ description: "Traverse the local graph from search seeds without calling a model provider.",
9220
+ inputSchema: {
9221
+ question: z7.string().min(1).describe("Question or graph search seed"),
9222
+ traversal: z7.enum(["bfs", "dfs"]).optional().describe("Traversal strategy"),
9223
+ budget: z7.number().int().min(3).max(50).optional().describe("Maximum nodes to summarize")
9224
+ }
9225
+ },
9226
+ async ({ question, traversal, budget }) => {
9227
+ const result = await queryGraphVault(rootDir, question, {
9228
+ traversal,
9229
+ budget
9230
+ });
9231
+ return asToolText(result);
9232
+ }
9233
+ );
9234
+ server.registerTool(
9235
+ "get_node",
9236
+ {
9237
+ description: "Explain a graph node, its page, community, and neighbors.",
9238
+ inputSchema: {
9239
+ target: z7.string().min(1).describe("Node or page label/id")
9240
+ }
9241
+ },
9242
+ async ({ target }) => {
9243
+ return asToolText(await explainGraphVault(rootDir, target));
9244
+ }
9245
+ );
9246
+ server.registerTool(
9247
+ "get_neighbors",
9248
+ {
9249
+ description: "Return the neighbors of a graph node or page target.",
9250
+ inputSchema: {
9251
+ target: z7.string().min(1).describe("Node or page label/id")
9252
+ }
9253
+ },
9254
+ async ({ target }) => {
9255
+ const explanation = await explainGraphVault(rootDir, target);
9256
+ return asToolText(explanation.neighbors);
9257
+ }
9258
+ );
9259
+ server.registerTool(
9260
+ "shortest_path",
9261
+ {
9262
+ description: "Find the shortest graph path between two targets.",
9263
+ inputSchema: {
9264
+ from: z7.string().min(1).describe("Start node/page label or id"),
9265
+ to: z7.string().min(1).describe("End node/page label or id")
9266
+ }
9267
+ },
9268
+ async ({ from, to }) => {
9269
+ return asToolText(await pathGraphVault(rootDir, from, to));
9270
+ }
9271
+ );
9272
+ server.registerTool(
9273
+ "god_nodes",
9274
+ {
9275
+ description: "List the highest-connectivity graph nodes.",
9276
+ inputSchema: {
9277
+ limit: z7.number().int().min(1).max(25).optional().describe("Maximum nodes to return")
9278
+ }
9279
+ },
9280
+ async ({ limit }) => {
9281
+ return asToolText(await listGodNodes(rootDir, limit ?? 10));
9282
+ }
9283
+ );
8326
9284
  server.registerTool(
8327
9285
  "query_vault",
8328
9286
  {
@@ -8900,6 +9858,34 @@ async function startGraphServer(rootDir, port) {
8900
9858
  response.end(await fs14.readFile(paths.graphPath, "utf8"));
8901
9859
  return;
8902
9860
  }
9861
+ if (url.pathname === "/api/graph/query") {
9862
+ const question = url.searchParams.get("q") ?? "";
9863
+ const traversal = url.searchParams.get("traversal");
9864
+ const budget = Number.parseInt(url.searchParams.get("budget") ?? "12", 10);
9865
+ response.writeHead(200, { "content-type": "application/json" });
9866
+ response.end(
9867
+ JSON.stringify(
9868
+ await queryGraphVault(rootDir, question, {
9869
+ traversal: traversal === "dfs" ? "dfs" : "bfs",
9870
+ budget: Number.isFinite(budget) ? budget : 12
9871
+ })
9872
+ )
9873
+ );
9874
+ return;
9875
+ }
9876
+ if (url.pathname === "/api/graph/path") {
9877
+ const from = url.searchParams.get("from") ?? "";
9878
+ const to = url.searchParams.get("to") ?? "";
9879
+ response.writeHead(200, { "content-type": "application/json" });
9880
+ response.end(JSON.stringify(await pathGraphVault(rootDir, from, to)));
9881
+ return;
9882
+ }
9883
+ if (url.pathname === "/api/graph/explain") {
9884
+ const target2 = url.searchParams.get("target") ?? "";
9885
+ response.writeHead(200, { "content-type": "application/json" });
9886
+ response.end(JSON.stringify(await explainGraphVault(rootDir, target2)));
9887
+ return;
9888
+ }
8903
9889
  if (url.pathname === "/api/search") {
8904
9890
  if (!await fileExists(paths.searchDbPath)) {
8905
9891
  response.writeHead(404, { "content-type": "application/json" });
@@ -9248,6 +10234,7 @@ export {
9248
10234
  createWebSearchAdapter,
9249
10235
  defaultVaultConfig,
9250
10236
  defaultVaultSchema,
10237
+ explainGraphVault,
9251
10238
  exploreVault,
9252
10239
  exportGraphHtml,
9253
10240
  getProviderForTask,
@@ -9263,13 +10250,16 @@ export {
9263
10250
  lintVault,
9264
10251
  listApprovals,
9265
10252
  listCandidates,
10253
+ listGodNodes,
9266
10254
  listManifests,
9267
10255
  listPages,
9268
10256
  listSchedules,
9269
10257
  loadVaultConfig,
9270
10258
  loadVaultSchema,
9271
10259
  loadVaultSchemas,
10260
+ pathGraphVault,
9272
10261
  promoteCandidate,
10262
+ queryGraphVault,
9273
10263
  queryVault,
9274
10264
  readApproval,
9275
10265
  readExtractedText,