@swarmvaultai/engine 0.1.29 → 0.1.31

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
@@ -21,7 +21,7 @@ import {
21
21
  uniqueBy,
22
22
  writeFileIfChanged,
23
23
  writeJsonFile
24
- } from "./chunk-IHMJCCXR.js";
24
+ } from "./chunk-IAEYFTUS.js";
25
25
 
26
26
  // src/agents.ts
27
27
  import crypto from "crypto";
@@ -748,16 +748,14 @@ async function installConfiguredAgents(rootDir) {
748
748
  // src/graph-export.ts
749
749
  import fs2 from "fs/promises";
750
750
  import path2 from "path";
751
- var NODE_COLORS = {
752
- source: "#f59e0b",
753
- module: "#fb7185",
754
- symbol: "#8b5cf6",
755
- rationale: "#14b8a6",
756
- concept: "#0ea5e9",
757
- entity: "#22c55e"
758
- };
759
- function xmlEscape(value) {
760
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
751
+
752
+ // src/graph-interchange.ts
753
+ function exportHyperedgeNodeId(hyperedge) {
754
+ return `hyperedge:${hyperedge.id}`;
755
+ }
756
+ function relationType(relation) {
757
+ const normalized = relation.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
758
+ return normalized || "RELATED_TO";
761
759
  }
762
760
  function cypherStringLiteral(value) {
763
761
  let escaped = "";
@@ -792,18 +790,109 @@ function cypherStringLiteral(value) {
792
790
  }
793
791
  return `'${escaped}'`;
794
792
  }
795
- function relationType(relation) {
796
- const normalized = relation.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
797
- return normalized || "RELATED_TO";
798
- }
799
793
  function graphPageById(graph) {
800
794
  return new Map(graph.pages.map((page) => [page.id, page]));
801
795
  }
802
796
  function graphNodeById(graph) {
803
797
  return new Map(graph.nodes.map((node) => [node.id, node]));
804
798
  }
805
- function exportHyperedgeNodeId(hyperedge) {
806
- return `hyperedge:${hyperedge.id}`;
799
+ function normalizeSwarmNodeProps(node, page) {
800
+ return {
801
+ id: node.id,
802
+ label: node.label,
803
+ type: node.type,
804
+ sourceIds: JSON.stringify(node.sourceIds),
805
+ projectIds: JSON.stringify(node.projectIds),
806
+ ...node.pageId ? { pageId: node.pageId } : {},
807
+ ...page?.path ? { pagePath: page.path } : {},
808
+ ...node.sourceClass ? { sourceClass: node.sourceClass } : {},
809
+ ...node.language ? { language: node.language } : {},
810
+ ...node.moduleId ? { moduleId: node.moduleId } : {},
811
+ ...node.symbolKind ? { symbolKind: node.symbolKind } : {},
812
+ ...node.communityId ? { communityId: node.communityId } : {},
813
+ ...node.freshness ? { freshness: node.freshness } : {},
814
+ ...node.confidence !== void 0 ? { confidence: node.confidence } : {},
815
+ ...node.degree !== void 0 ? { degree: node.degree } : {},
816
+ ...node.bridgeScore !== void 0 ? { bridgeScore: node.bridgeScore } : {},
817
+ ...node.isGodNode !== void 0 ? { isGodNode: node.isGodNode } : {}
818
+ };
819
+ }
820
+ function normalizeHyperedgeNodeProps(hyperedge) {
821
+ return {
822
+ id: exportHyperedgeNodeId(hyperedge),
823
+ label: hyperedge.label,
824
+ type: "hyperedge",
825
+ relation: hyperedge.relation,
826
+ evidenceClass: hyperedge.evidenceClass,
827
+ confidence: hyperedge.confidence,
828
+ sourcePageIds: JSON.stringify(hyperedge.sourcePageIds),
829
+ why: hyperedge.why
830
+ };
831
+ }
832
+ function normalizeEdgeProps(edge) {
833
+ return {
834
+ id: edge.id,
835
+ relation: edge.relation,
836
+ status: edge.status,
837
+ evidenceClass: edge.evidenceClass,
838
+ confidence: edge.confidence,
839
+ provenance: JSON.stringify(edge.provenance),
840
+ ...edge.similarityReasons?.length ? { similarityReasons: JSON.stringify(edge.similarityReasons) } : {},
841
+ ...edge.similarityBasis ? { similarityBasis: edge.similarityBasis } : {}
842
+ };
843
+ }
844
+ function normalizeGroupMemberProps(hyperedge, nodeId) {
845
+ return {
846
+ id: `member:${hyperedge.id}:${nodeId}`,
847
+ relation: "group_member",
848
+ status: "inferred",
849
+ evidenceClass: hyperedge.evidenceClass,
850
+ confidence: hyperedge.confidence,
851
+ provenance: JSON.stringify(hyperedge.sourcePageIds)
852
+ };
853
+ }
854
+ function filterGraphBySourceClasses(graph, includeClasses) {
855
+ const allowed = new Set(includeClasses);
856
+ const nodeIds = new Set(graph.nodes.filter((node) => node.sourceClass && allowed.has(node.sourceClass)).map((node) => node.id));
857
+ const pageIds = new Set(graph.pages.filter((page) => page.sourceClass && allowed.has(page.sourceClass)).map((page) => page.id));
858
+ return {
859
+ ...graph,
860
+ nodes: graph.nodes.filter((node) => nodeIds.has(node.id)),
861
+ edges: graph.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)),
862
+ hyperedges: graph.hyperedges.map((hyperedge) => ({
863
+ ...hyperedge,
864
+ nodeIds: hyperedge.nodeIds.filter((nodeId) => nodeIds.has(nodeId))
865
+ })).filter((hyperedge) => hyperedge.nodeIds.length >= 2),
866
+ communities: (graph.communities ?? []).map((community) => ({
867
+ ...community,
868
+ nodeIds: community.nodeIds.filter((nodeId) => nodeIds.has(nodeId))
869
+ })).filter((community) => community.nodeIds.length > 0),
870
+ sources: graph.sources.filter((source) => source.sourceClass && allowed.has(source.sourceClass)),
871
+ pages: graph.pages.filter((page) => pageIds.has(page.id))
872
+ };
873
+ }
874
+ function graphCounts(graph) {
875
+ return {
876
+ sources: graph.sources.length,
877
+ pages: graph.pages.length,
878
+ nodes: graph.nodes.length,
879
+ relationships: graph.edges.length,
880
+ hyperedges: graph.hyperedges.length,
881
+ groupMembers: graph.hyperedges.reduce((total, hyperedge) => total + hyperedge.nodeIds.length, 0)
882
+ };
883
+ }
884
+
885
+ // src/graph-export.ts
886
+ var NODE_COLORS = {
887
+ source: "#f59e0b",
888
+ module: "#fb7185",
889
+ symbol: "#8b5cf6",
890
+ rationale: "#14b8a6",
891
+ concept: "#0ea5e9",
892
+ entity: "#22c55e"
893
+ };
894
+ function xmlEscape(value) {
895
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
807
896
  }
808
897
  function sortedCommunities(graph) {
809
898
  const known = (graph.communities ?? []).map((community) => ({
@@ -1065,35 +1154,14 @@ function renderCypher(graph) {
1065
1154
  const lines = ["// Neo4j Cypher import generated by SwarmVault", ""];
1066
1155
  for (const node of [...graph.nodes].sort((left, right) => left.id.localeCompare(right.id))) {
1067
1156
  const page = node.pageId ? pageById2.get(node.pageId) : void 0;
1068
- const props = [
1069
- `id: ${cypherStringLiteral(node.id)}`,
1070
- `label: ${cypherStringLiteral(node.label)}`,
1071
- `type: ${cypherStringLiteral(node.type)}`,
1072
- `sourceIds: ${cypherStringLiteral(JSON.stringify(node.sourceIds))}`,
1073
- `projectIds: ${cypherStringLiteral(JSON.stringify(node.projectIds))}`,
1074
- node.pageId ? `pageId: ${cypherStringLiteral(node.pageId)}` : "",
1075
- page?.path ? `pagePath: ${cypherStringLiteral(page.path)}` : "",
1076
- node.language ? `language: ${cypherStringLiteral(node.language)}` : "",
1077
- node.symbolKind ? `symbolKind: ${cypherStringLiteral(node.symbolKind)}` : "",
1078
- node.communityId ? `communityId: ${cypherStringLiteral(node.communityId)}` : "",
1079
- node.degree !== void 0 ? `degree: ${node.degree}` : "",
1080
- node.bridgeScore !== void 0 ? `bridgeScore: ${node.bridgeScore}` : "",
1081
- node.isGodNode !== void 0 ? `isGodNode: ${node.isGodNode}` : ""
1082
- ].filter(Boolean).join(", ");
1157
+ const props = Object.entries(normalizeSwarmNodeProps(node, page)).map(([key, value]) => `${key}: ${typeof value === "string" ? cypherStringLiteral(value) : value}`).filter(Boolean).join(", ");
1083
1158
  lines.push(`MERGE (n:SwarmNode {id: ${cypherStringLiteral(node.id)}}) SET n += { ${props} };`);
1084
1159
  }
1085
1160
  lines.push("");
1086
1161
  for (const hyperedge of [...graph.hyperedges ?? []].sort((left, right) => left.id.localeCompare(right.id))) {
1087
1162
  const hyperedgeNodeId = exportHyperedgeNodeId(hyperedge);
1088
- lines.push(
1089
- `MERGE (h:SwarmNode {id: ${cypherStringLiteral(hyperedgeNodeId)}}) SET h += { id: ${cypherStringLiteral(hyperedgeNodeId)}, label: ${cypherStringLiteral(
1090
- hyperedge.label
1091
- )}, type: ${cypherStringLiteral("hyperedge")}, relation: ${cypherStringLiteral(hyperedge.relation)}, evidenceClass: ${cypherStringLiteral(
1092
- hyperedge.evidenceClass
1093
- )}, confidence: ${hyperedge.confidence}, sourcePageIds: ${cypherStringLiteral(JSON.stringify(hyperedge.sourcePageIds))}, why: ${cypherStringLiteral(
1094
- hyperedge.why
1095
- )} };`
1096
- );
1163
+ const props = Object.entries(normalizeHyperedgeNodeProps(hyperedge)).map(([key, value]) => `${key}: ${typeof value === "string" ? cypherStringLiteral(value) : value}`).join(", ");
1164
+ lines.push(`MERGE (h:SwarmNode {id: ${cypherStringLiteral(hyperedgeNodeId)}}) SET h += { ${props} };`);
1097
1165
  }
1098
1166
  if ((graph.hyperedges ?? []).length) {
1099
1167
  lines.push("");
@@ -1101,23 +1169,21 @@ function renderCypher(graph) {
1101
1169
  for (const hyperedge of [...graph.hyperedges ?? []].sort((left, right) => left.id.localeCompare(right.id))) {
1102
1170
  const hyperedgeNodeId = exportHyperedgeNodeId(hyperedge);
1103
1171
  for (const nodeId of hyperedge.nodeIds) {
1172
+ const props = Object.entries(normalizeGroupMemberProps(hyperedge, nodeId)).map(([key, value]) => `${key}: ${typeof value === "string" ? cypherStringLiteral(value) : value}`).join(", ");
1104
1173
  lines.push(
1105
1174
  `MATCH (h:SwarmNode {id: ${cypherStringLiteral(hyperedgeNodeId)}}), (n:SwarmNode {id: ${cypherStringLiteral(nodeId)}})`,
1106
1175
  `MERGE (h)-[r:GROUP_MEMBER {id: ${cypherStringLiteral(`member:${hyperedge.id}:${nodeId}`)}}]->(n)`,
1107
- `SET r += { relation: ${cypherStringLiteral("group_member")}, status: ${cypherStringLiteral("inferred")}, evidenceClass: ${cypherStringLiteral(
1108
- hyperedge.evidenceClass
1109
- )}, confidence: ${hyperedge.confidence}, provenance: ${cypherStringLiteral(JSON.stringify(hyperedge.sourcePageIds))} };`
1176
+ `SET r += { ${props} };`
1110
1177
  );
1111
1178
  }
1112
1179
  }
1113
1180
  lines.push("");
1114
1181
  for (const edge of [...graph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
1182
+ const props = Object.entries(normalizeEdgeProps(edge)).map(([key, value]) => `${key}: ${typeof value === "string" ? cypherStringLiteral(value) : value}`).join(", ");
1115
1183
  lines.push(
1116
1184
  `MATCH (a:SwarmNode {id: ${cypherStringLiteral(edge.source)}}), (b:SwarmNode {id: ${cypherStringLiteral(edge.target)}})`,
1117
1185
  `MERGE (a)-[r:${relationType(edge.relation)} {id: ${cypherStringLiteral(edge.id)}}]->(b)`,
1118
- `SET r += { relation: ${cypherStringLiteral(edge.relation)}, status: ${cypherStringLiteral(edge.status)}, evidenceClass: ${cypherStringLiteral(
1119
- edge.evidenceClass
1120
- )}, confidence: ${edge.confidence}, provenance: ${cypherStringLiteral(JSON.stringify(edge.provenance))}${edge.similarityReasons?.length ? `, similarityReasons: ${cypherStringLiteral(JSON.stringify(edge.similarityReasons))}` : ""} };`
1186
+ `SET r += { ${props} };`
1121
1187
  );
1122
1188
  }
1123
1189
  lines.push("");
@@ -1143,27 +1209,400 @@ async function exportGraphFormat(rootDir, format, outputPath) {
1143
1209
  return { format, outputPath: resolvedPath };
1144
1210
  }
1145
1211
 
1146
- // src/hooks.ts
1212
+ // src/graph-push.ts
1147
1213
  import fs3 from "fs/promises";
1148
1214
  import path3 from "path";
1215
+ import neo4j from "neo4j-driver";
1216
+
1217
+ // src/benchmark.ts
1218
+ var CHARS_PER_TOKEN = 4;
1219
+ var DEFAULT_BENCHMARK_QUESTIONS = [
1220
+ "How does this vault connect the main concepts?",
1221
+ "Which pages bridge the biggest communities?",
1222
+ "What are the core abstractions in this vault?",
1223
+ "Where are the biggest knowledge gaps?",
1224
+ "What evidence should I read first?"
1225
+ ];
1226
+ var RESEARCH_BENCHMARK_QUESTION = "Which research sources should I read first, and why?";
1227
+ function nodeMap(graph) {
1228
+ return new Map(graph.nodes.map((node) => [node.id, node]));
1229
+ }
1230
+ function pageMap(graph) {
1231
+ return new Map(graph.pages.map((page) => [page.id, page]));
1232
+ }
1233
+ function estimateTokens(text) {
1234
+ return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
1235
+ }
1236
+ function estimateCorpusWords(texts) {
1237
+ return texts.reduce((total, text) => total + normalizeWhitespace(text).split(/\s+/).filter(Boolean).length, 0);
1238
+ }
1239
+ function benchmarkQueryTokens(graph, queryResult, pageContentsById) {
1240
+ const nodesById = nodeMap(graph);
1241
+ const pagesById = pageMap(graph);
1242
+ const edgeIds = new Set(queryResult.visitedEdgeIds);
1243
+ const lines = [];
1244
+ for (const pageId of queryResult.pageIds) {
1245
+ const page = pagesById.get(pageId);
1246
+ if (!page) {
1247
+ continue;
1248
+ }
1249
+ const content = normalizeWhitespace(pageContentsById.get(pageId) ?? "").slice(0, 280);
1250
+ lines.push(`PAGE ${page.title} path=${page.path} kind=${page.kind}`);
1251
+ if (content) {
1252
+ lines.push(`PAGE_BODY ${content}`);
1253
+ }
1254
+ }
1255
+ for (const nodeId of queryResult.visitedNodeIds) {
1256
+ const node = nodesById.get(nodeId);
1257
+ if (!node) {
1258
+ continue;
1259
+ }
1260
+ lines.push(`NODE ${node.label} type=${node.type} community=${node.communityId ?? "unassigned"} page=${node.pageId ?? "none"}`);
1261
+ }
1262
+ for (const edge of graph.edges) {
1263
+ if (!edgeIds.has(edge.id)) {
1264
+ continue;
1265
+ }
1266
+ const source = nodesById.get(edge.source)?.label ?? edge.source;
1267
+ const target = nodesById.get(edge.target)?.label ?? edge.target;
1268
+ lines.push(`EDGE ${source} --${edge.relation}/${edge.evidenceClass}/${edge.confidence.toFixed(2)}--> ${target}`);
1269
+ }
1270
+ const queryTokens = estimateTokens(lines.join("\n"));
1271
+ return {
1272
+ question: queryResult.question,
1273
+ queryTokens,
1274
+ reduction: 0,
1275
+ visitedNodeIds: queryResult.visitedNodeIds,
1276
+ visitedEdgeIds: queryResult.visitedEdgeIds,
1277
+ pageIds: queryResult.pageIds
1278
+ };
1279
+ }
1280
+ function graphHash(graph) {
1281
+ const hashedPages = graph.pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary");
1282
+ const normalized = JSON.stringify(
1283
+ {
1284
+ nodes: [...graph.nodes].map((node) => ({
1285
+ id: node.id,
1286
+ type: node.type,
1287
+ label: node.label,
1288
+ pageId: node.pageId ?? null,
1289
+ sourceClass: node.sourceClass ?? null,
1290
+ communityId: node.communityId ?? null,
1291
+ degree: node.degree ?? null,
1292
+ bridgeScore: node.bridgeScore ?? null,
1293
+ isGodNode: node.isGodNode ?? false,
1294
+ sourceIds: [...node.sourceIds].sort(),
1295
+ projectIds: [...node.projectIds].sort()
1296
+ })).sort((left, right) => left.id.localeCompare(right.id)),
1297
+ edges: [...graph.edges].map((edge) => ({
1298
+ id: edge.id,
1299
+ source: edge.source,
1300
+ target: edge.target,
1301
+ relation: edge.relation,
1302
+ status: edge.status,
1303
+ evidenceClass: edge.evidenceClass,
1304
+ similarityBasis: edge.similarityBasis ?? null,
1305
+ confidence: edge.confidence,
1306
+ provenance: [...edge.provenance].sort()
1307
+ })).sort((left, right) => left.id.localeCompare(right.id)),
1308
+ pages: [...hashedPages].map((page) => ({
1309
+ id: page.id,
1310
+ path: page.path,
1311
+ kind: page.kind,
1312
+ status: page.status,
1313
+ sourceType: page.sourceType ?? null,
1314
+ sourceClass: page.sourceClass ?? null,
1315
+ sourceIds: [...page.sourceIds].sort(),
1316
+ projectIds: [...page.projectIds].sort(),
1317
+ nodeIds: [...page.nodeIds].sort()
1318
+ })).sort((left, right) => left.id.localeCompare(right.id)),
1319
+ communities: [...graph.communities ?? []].map((community) => ({
1320
+ id: community.id,
1321
+ label: community.label,
1322
+ nodeIds: [...community.nodeIds].sort()
1323
+ })).sort((left, right) => left.id.localeCompare(right.id))
1324
+ },
1325
+ null,
1326
+ 0
1327
+ );
1328
+ return sha256(normalized);
1329
+ }
1330
+ function hasResearchSources(pages) {
1331
+ return pages.some((page) => page.kind === "source" && Boolean(page.sourceType) && page.sourceType !== "url");
1332
+ }
1333
+ function defaultBenchmarkQuestionsForGraph(graph, maxQuestions = 3) {
1334
+ const normalizedLimit = Math.max(1, Math.min(maxQuestions, DEFAULT_BENCHMARK_QUESTIONS.length));
1335
+ const questions = [...DEFAULT_BENCHMARK_QUESTIONS];
1336
+ if (hasResearchSources(graph.pages)) {
1337
+ questions.unshift(RESEARCH_BENCHMARK_QUESTION);
1338
+ }
1339
+ return uniqueBy(questions, (item) => item).slice(0, normalizedLimit);
1340
+ }
1341
+ function buildBenchmarkArtifact(input) {
1342
+ const corpusTokens = Math.max(1, Math.round(input.corpusWords * (100 / 75)));
1343
+ const perQuestion = input.perQuestion.filter((entry) => entry.queryTokens > 0).map((entry) => ({
1344
+ ...entry,
1345
+ reduction: Number(Math.max(0, 1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
1346
+ }));
1347
+ const avgQueryTokens = perQuestion.length ? Math.max(1, Math.round(perQuestion.reduce((total, entry) => total + entry.queryTokens, 0) / perQuestion.length)) : 0;
1348
+ const reductionRatio = avgQueryTokens ? Number(Math.max(0, 1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
1349
+ const uniqueVisitedNodes = new Set(perQuestion.flatMap((entry) => entry.visitedNodeIds)).size;
1350
+ const summary = {
1351
+ questionCount: input.questions.length,
1352
+ uniqueVisitedNodes,
1353
+ finalContextTokens: avgQueryTokens,
1354
+ naiveCorpusTokens: corpusTokens,
1355
+ avgReduction: reductionRatio,
1356
+ reductionRatio
1357
+ };
1358
+ return {
1359
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1360
+ graphHash: graphHash(input.graph),
1361
+ corpusWords: input.corpusWords,
1362
+ corpusTokens,
1363
+ nodes: input.graph.nodes.length,
1364
+ edges: input.graph.edges.length,
1365
+ avgQueryTokens,
1366
+ reductionRatio,
1367
+ sampleQuestions: input.questions,
1368
+ perQuestion,
1369
+ questionResults: perQuestion,
1370
+ summary
1371
+ };
1372
+ }
1373
+
1374
+ // src/graph-push.ts
1375
+ var DEFAULT_NEO4J_BATCH_SIZE = 500;
1376
+ var DEFAULT_NEO4J_DATABASE = "neo4j";
1377
+ function requireConfigValue(value, name) {
1378
+ if (typeof value === "string" && value.trim()) {
1379
+ return value.trim();
1380
+ }
1381
+ throw new Error(`Neo4j push requires ${name}. Configure \`graphSinks.neo4j.${name}\` or pass the matching CLI flag.`);
1382
+ }
1383
+ async function deriveVaultId(rootDir) {
1384
+ const realRoot = await fs3.realpath(rootDir).catch(() => path3.resolve(rootDir));
1385
+ const label = slugify(path3.basename(realRoot));
1386
+ return `${label}-${sha256(realRoot).slice(0, 12)}`;
1387
+ }
1388
+ async function resolveNeo4jPushConfig(rootDir, options) {
1389
+ const { config } = await loadVaultConfig(rootDir);
1390
+ const sink = config.graphSinks?.neo4j;
1391
+ const includeClasses = normalizeIncludedClasses(options.includeClasses ?? sink?.includeClasses ?? ["first_party"]);
1392
+ return {
1393
+ uri: requireConfigValue(options.uri ?? sink?.uri, "uri"),
1394
+ username: requireConfigValue(options.username ?? sink?.username, "username"),
1395
+ passwordEnv: requireConfigValue(options.passwordEnv ?? sink?.passwordEnv, "passwordEnv"),
1396
+ database: options.database?.trim() || sink?.database?.trim() || DEFAULT_NEO4J_DATABASE,
1397
+ vaultId: options.vaultId?.trim() || sink?.vaultId?.trim() || await deriveVaultId(rootDir),
1398
+ includeClasses,
1399
+ batchSize: normalizeBatchSize(options.batchSize ?? sink?.batchSize)
1400
+ };
1401
+ }
1402
+ function normalizeIncludedClasses(values) {
1403
+ const allowed = ["first_party", "third_party", "resource", "generated"];
1404
+ const unique = [...new Set(values)].filter((value) => allowed.includes(value));
1405
+ return unique.length ? unique : ["first_party"];
1406
+ }
1407
+ function normalizeBatchSize(value) {
1408
+ if (!Number.isFinite(value) || !value || value <= 0) {
1409
+ return DEFAULT_NEO4J_BATCH_SIZE;
1410
+ }
1411
+ return Math.max(1, Math.floor(value));
1412
+ }
1413
+ async function loadGraph2(rootDir) {
1414
+ const { paths } = await loadVaultConfig(rootDir);
1415
+ const raw = JSON.parse(await fs3.readFile(paths.graphPath, "utf8"));
1416
+ return raw;
1417
+ }
1418
+ function buildResult(input) {
1419
+ const counts = graphCounts(input.filteredGraph);
1420
+ const fullCounts = graphCounts(input.fullGraph);
1421
+ return {
1422
+ sink: "neo4j",
1423
+ uri: input.resolved.uri,
1424
+ database: input.resolved.database,
1425
+ vaultId: input.resolved.vaultId,
1426
+ dryRun: input.dryRun,
1427
+ graphHash: graphHash(input.fullGraph),
1428
+ includedSourceClasses: input.resolved.includeClasses,
1429
+ counts,
1430
+ skipped: {
1431
+ sources: Math.max(0, fullCounts.sources - counts.sources),
1432
+ pages: Math.max(0, fullCounts.pages - counts.pages),
1433
+ nodes: Math.max(0, fullCounts.nodes - counts.nodes),
1434
+ relationships: Math.max(0, fullCounts.relationships - counts.relationships),
1435
+ hyperedges: Math.max(0, fullCounts.hyperedges - counts.hyperedges),
1436
+ groupMembers: Math.max(0, fullCounts.groupMembers - counts.groupMembers)
1437
+ },
1438
+ warnings: input.warnings ?? []
1439
+ };
1440
+ }
1441
+ function createDriver(uri, username, password) {
1442
+ return neo4j.driver(uri, neo4j.auth.basic(username, password));
1443
+ }
1444
+ async function ensureNeo4jConstraints(session) {
1445
+ await session.run("CREATE CONSTRAINT swarmvault_node_identity IF NOT EXISTS FOR (n:SwarmNode) REQUIRE (n.vaultId, n.id) IS UNIQUE");
1446
+ await session.run("CREATE CONSTRAINT swarmvault_sync_identity IF NOT EXISTS FOR (s:SwarmVaultSync) REQUIRE s.vaultId IS UNIQUE");
1447
+ }
1448
+ function chunkRows(rows, batchSize) {
1449
+ const chunks = [];
1450
+ for (let index = 0; index < rows.length; index += batchSize) {
1451
+ chunks.push(rows.slice(index, index + batchSize));
1452
+ }
1453
+ return chunks;
1454
+ }
1455
+ async function writeNodeRows(session, vaultId, rows, batchSize) {
1456
+ for (const chunk of chunkRows(rows, batchSize)) {
1457
+ await session.executeWrite(
1458
+ (tx) => tx.run(["UNWIND $rows AS row", "MERGE (n:SwarmNode { vaultId: $vaultId, id: row.id })", "SET n += row.props"].join("\n"), {
1459
+ vaultId,
1460
+ rows: chunk
1461
+ })
1462
+ );
1463
+ }
1464
+ }
1465
+ async function writeEdgeRows(session, vaultId, rows, batchSize, relation) {
1466
+ const neoRelation = relationType(relation);
1467
+ const query = [
1468
+ "UNWIND $rows AS row",
1469
+ "MATCH (a:SwarmNode { vaultId: $vaultId, id: row.source })",
1470
+ "MATCH (b:SwarmNode { vaultId: $vaultId, id: row.target })",
1471
+ `MERGE (a)-[r:${neoRelation} { vaultId: $vaultId, id: row.id }]->(b)`,
1472
+ "SET r += row.props"
1473
+ ].join("\n");
1474
+ for (const chunk of chunkRows(rows, batchSize)) {
1475
+ await session.executeWrite((tx) => tx.run(query, { vaultId, rows: chunk }));
1476
+ }
1477
+ }
1478
+ async function writeSyncNode(session, input) {
1479
+ await session.executeWrite(
1480
+ (tx) => tx.run(
1481
+ [
1482
+ "MERGE (s:SwarmVaultSync { vaultId: $vaultId })",
1483
+ "SET s += {",
1484
+ " vaultId: $vaultId,",
1485
+ " rootDir: $rootDir,",
1486
+ " graphGeneratedAt: $graphGeneratedAt,",
1487
+ " graphHash: $graphHash,",
1488
+ " pushedAt: $pushedAt,",
1489
+ " includedSourceClasses: $includedSourceClasses,",
1490
+ " sources: $sources,",
1491
+ " pages: $pages,",
1492
+ " nodes: $nodes,",
1493
+ " relationships: $relationships,",
1494
+ " hyperedges: $hyperedges,",
1495
+ " groupMembers: $groupMembers",
1496
+ "}"
1497
+ ].join("\n"),
1498
+ {
1499
+ vaultId: input.vaultId,
1500
+ rootDir: path3.resolve(input.rootDir),
1501
+ graphGeneratedAt: input.graph.generatedAt,
1502
+ graphHash: graphHash(input.graph),
1503
+ pushedAt: input.pushedAt,
1504
+ includedSourceClasses: input.includedSourceClasses,
1505
+ ...input.counts
1506
+ }
1507
+ )
1508
+ );
1509
+ }
1510
+ async function pushGraphNeo4j(rootDir, options = {}) {
1511
+ const graph = await loadGraph2(rootDir);
1512
+ const resolved = await resolveNeo4jPushConfig(rootDir, options);
1513
+ const filteredGraph = filterGraphBySourceClasses(graph, resolved.includeClasses);
1514
+ const warnings = filteredGraph.nodes.length || filteredGraph.hyperedges.length || filteredGraph.edges.length ? [] : [`No graph records matched the included source classes: ${resolved.includeClasses.join(", ")}`];
1515
+ const result = buildResult({
1516
+ resolved,
1517
+ filteredGraph,
1518
+ fullGraph: graph,
1519
+ dryRun: options.dryRun ?? false,
1520
+ warnings
1521
+ });
1522
+ if (options.dryRun) {
1523
+ return result;
1524
+ }
1525
+ const password = process.env[resolved.passwordEnv];
1526
+ if (!password) {
1527
+ throw new Error(`Environment variable ${resolved.passwordEnv} is required for Neo4j push.`);
1528
+ }
1529
+ const driver = (options.driverFactory ?? createDriver)(resolved.uri, resolved.username, password);
1530
+ const session = driver.session({ database: resolved.database });
1531
+ try {
1532
+ await ensureNeo4jConstraints(session);
1533
+ const pageById2 = graphPageById(filteredGraph);
1534
+ const nodeRows = [
1535
+ ...filteredGraph.nodes.sort((left, right) => left.id.localeCompare(right.id)).map((node) => ({
1536
+ id: node.id,
1537
+ props: normalizeSwarmNodeProps(node, node.pageId ? pageById2.get(node.pageId) : void 0)
1538
+ })),
1539
+ ...filteredGraph.hyperedges.sort((left, right) => left.id.localeCompare(right.id)).map((hyperedge) => ({
1540
+ id: normalizeHyperedgeNodeProps(hyperedge).id,
1541
+ props: normalizeHyperedgeNodeProps(hyperedge)
1542
+ }))
1543
+ ];
1544
+ await writeNodeRows(session, resolved.vaultId, nodeRows, resolved.batchSize);
1545
+ const edgeGroups = /* @__PURE__ */ new Map();
1546
+ for (const edge of [...filteredGraph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
1547
+ const rows = edgeGroups.get(edge.relation) ?? [];
1548
+ rows.push({
1549
+ id: edge.id,
1550
+ source: edge.source,
1551
+ target: edge.target,
1552
+ props: normalizeEdgeProps(edge)
1553
+ });
1554
+ edgeGroups.set(edge.relation, rows);
1555
+ }
1556
+ for (const [relation, rows] of [...edgeGroups.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
1557
+ await writeEdgeRows(session, resolved.vaultId, rows, resolved.batchSize, relation);
1558
+ }
1559
+ const memberRows = filteredGraph.hyperedges.flatMap(
1560
+ (hyperedge) => hyperedge.nodeIds.map((nodeId) => ({
1561
+ id: `member:${hyperedge.id}:${nodeId}`,
1562
+ source: normalizeHyperedgeNodeProps(hyperedge).id,
1563
+ target: nodeId,
1564
+ props: normalizeGroupMemberProps(hyperedge, nodeId)
1565
+ }))
1566
+ );
1567
+ if (memberRows.length) {
1568
+ await writeEdgeRows(session, resolved.vaultId, memberRows, resolved.batchSize, "group_member");
1569
+ }
1570
+ await writeSyncNode(session, {
1571
+ vaultId: resolved.vaultId,
1572
+ rootDir,
1573
+ graph,
1574
+ pushedAt: (/* @__PURE__ */ new Date()).toISOString(),
1575
+ includedSourceClasses: resolved.includeClasses,
1576
+ counts: result.counts
1577
+ });
1578
+ return result;
1579
+ } finally {
1580
+ await session.close();
1581
+ await driver.close();
1582
+ }
1583
+ }
1584
+
1585
+ // src/hooks.ts
1586
+ import fs4 from "fs/promises";
1587
+ import path4 from "path";
1149
1588
  import process2 from "process";
1150
1589
  var hookStart = "# >>> swarmvault hook >>>";
1151
1590
  var hookEnd = "# <<< swarmvault hook <<<";
1152
1591
  async function findNearestGitRoot(startPath) {
1153
- let current = path3.resolve(startPath);
1592
+ let current = path4.resolve(startPath);
1154
1593
  try {
1155
- const stat = await fs3.stat(current);
1594
+ const stat = await fs4.stat(current);
1156
1595
  if (!stat.isDirectory()) {
1157
- current = path3.dirname(current);
1596
+ current = path4.dirname(current);
1158
1597
  }
1159
1598
  } catch {
1160
- current = path3.dirname(current);
1599
+ current = path4.dirname(current);
1161
1600
  }
1162
1601
  while (true) {
1163
- if (await fileExists(path3.join(current, ".git"))) {
1602
+ if (await fileExists(path4.join(current, ".git"))) {
1164
1603
  return current;
1165
1604
  }
1166
- const parent = path3.dirname(current);
1605
+ const parent = path4.dirname(current);
1167
1606
  if (parent === current) {
1168
1607
  return null;
1169
1608
  }
@@ -1175,8 +1614,8 @@ function shellQuote(value) {
1175
1614
  }
1176
1615
  function resolveSwarmvaultExecutableCandidate() {
1177
1616
  const argvPath = process2.argv[1];
1178
- if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path3.sep}@swarmvaultai${path3.sep}cli${path3.sep}`) || argvPath.includes(`${path3.sep}packages${path3.sep}cli${path3.sep}`))) {
1179
- return path3.resolve(argvPath);
1617
+ if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path4.sep}@swarmvaultai${path4.sep}cli${path4.sep}`) || argvPath.includes(`${path4.sep}packages${path4.sep}cli${path4.sep}`))) {
1618
+ return path4.resolve(argvPath);
1180
1619
  }
1181
1620
  return "swarmvault";
1182
1621
  }
@@ -1195,17 +1634,17 @@ function managedHookBlock(vaultRoot) {
1195
1634
  ].join("\n");
1196
1635
  }
1197
1636
  function hookPath(repoRoot, hookName) {
1198
- return path3.join(repoRoot, ".git", "hooks", hookName);
1637
+ return path4.join(repoRoot, ".git", "hooks", hookName);
1199
1638
  }
1200
1639
  async function readHookStatus(filePath) {
1201
1640
  if (!await fileExists(filePath)) {
1202
1641
  return "not_installed";
1203
1642
  }
1204
- const content = await fs3.readFile(filePath, "utf8");
1643
+ const content = await fs4.readFile(filePath, "utf8");
1205
1644
  return content.includes(hookStart) && content.includes(hookEnd) ? "installed" : "other_content";
1206
1645
  }
1207
1646
  async function upsertHookFile(filePath, block) {
1208
- const existing = await fileExists(filePath) ? await fs3.readFile(filePath, "utf8") : "";
1647
+ const existing = await fileExists(filePath) ? await fs4.readFile(filePath, "utf8") : "";
1209
1648
  let next;
1210
1649
  const startIndex = existing.indexOf(hookStart);
1211
1650
  const endIndex = existing.indexOf(hookEnd);
@@ -1219,16 +1658,16 @@ ${block}`.trimEnd();
1219
1658
  next = `#!/bin/sh
1220
1659
  ${block}`.trimEnd();
1221
1660
  }
1222
- await ensureDir(path3.dirname(filePath));
1223
- await fs3.writeFile(filePath, `${next}
1661
+ await ensureDir(path4.dirname(filePath));
1662
+ await fs4.writeFile(filePath, `${next}
1224
1663
  `, { mode: 493, encoding: "utf8" });
1225
- await fs3.chmod(filePath, 493);
1664
+ await fs4.chmod(filePath, 493);
1226
1665
  }
1227
1666
  async function removeHookBlock(filePath) {
1228
1667
  if (!await fileExists(filePath)) {
1229
1668
  return;
1230
1669
  }
1231
- const existing = await fs3.readFile(filePath, "utf8");
1670
+ const existing = await fs4.readFile(filePath, "utf8");
1232
1671
  const startIndex = existing.indexOf(hookStart);
1233
1672
  const endIndex = existing.indexOf(hookEnd);
1234
1673
  if (startIndex === -1 || endIndex === -1) {
@@ -1236,10 +1675,10 @@ async function removeHookBlock(filePath) {
1236
1675
  }
1237
1676
  const next = `${existing.slice(0, startIndex)}${existing.slice(endIndex + hookEnd.length)}`.trim();
1238
1677
  if (!next || next === "#!/bin/sh") {
1239
- await fs3.rm(filePath, { force: true });
1678
+ await fs4.rm(filePath, { force: true });
1240
1679
  return;
1241
1680
  }
1242
- await fs3.writeFile(filePath, `${next}
1681
+ await fs4.writeFile(filePath, `${next}
1243
1682
  `, "utf8");
1244
1683
  }
1245
1684
  async function getGitHookStatus(rootDir) {
@@ -1262,7 +1701,7 @@ async function installGitHooks(rootDir) {
1262
1701
  if (!repoRoot) {
1263
1702
  throw new Error("No git repository found above the current vault.");
1264
1703
  }
1265
- const block = managedHookBlock(path3.resolve(rootDir));
1704
+ const block = managedHookBlock(path4.resolve(rootDir));
1266
1705
  await upsertHookFile(hookPath(repoRoot, "post-commit"), block);
1267
1706
  await upsertHookFile(hookPath(repoRoot, "post-checkout"), block);
1268
1707
  return getGitHookStatus(rootDir);
@@ -1282,8 +1721,8 @@ async function uninstallGitHooks(rootDir) {
1282
1721
  }
1283
1722
 
1284
1723
  // src/ingest.ts
1285
- import fs9 from "fs/promises";
1286
- import path10 from "path";
1724
+ import fs10 from "fs/promises";
1725
+ import path11 from "path";
1287
1726
  import { pathToFileURL } from "url";
1288
1727
  import { Readability } from "@mozilla/readability";
1289
1728
  import matter3 from "gray-matter";
@@ -1293,16 +1732,16 @@ import mime from "mime-types";
1293
1732
  import TurndownService from "turndown";
1294
1733
 
1295
1734
  // src/code-analysis.ts
1296
- import fs5 from "fs/promises";
1297
- import path5 from "path";
1735
+ import fs6 from "fs/promises";
1736
+ import path6 from "path";
1298
1737
  import ts from "typescript";
1299
1738
 
1300
1739
  // src/code-tree-sitter.ts
1301
- import fs4 from "fs/promises";
1740
+ import fs5 from "fs/promises";
1302
1741
  import { createRequire } from "module";
1303
- import path4 from "path";
1742
+ import path5 from "path";
1304
1743
  var require2 = createRequire(import.meta.url);
1305
- var TREE_SITTER_PACKAGE_ROOT = path4.dirname(path4.dirname(require2.resolve("@vscode/tree-sitter-wasm")));
1744
+ var TREE_SITTER_PACKAGE_ROOT = path5.dirname(path5.dirname(require2.resolve("@vscode/tree-sitter-wasm")));
1306
1745
  var RATIONALE_MARKERS = ["NOTE:", "IMPORTANT:", "HACK:", "WHY:", "RATIONALE:"];
1307
1746
  function stripKnownCommentPrefix(line) {
1308
1747
  let next = line.trim();
@@ -1339,7 +1778,7 @@ async function getTreeSitterModule() {
1339
1778
  async function ensureTreeSitterInit(module) {
1340
1779
  if (!treeSitterInitPromise) {
1341
1780
  treeSitterInitPromise = module.Parser.init({
1342
- locateFile: () => path4.join(TREE_SITTER_PACKAGE_ROOT, "wasm", "tree-sitter.wasm")
1781
+ locateFile: () => path5.join(TREE_SITTER_PACKAGE_ROOT, "wasm", "tree-sitter.wasm")
1343
1782
  });
1344
1783
  }
1345
1784
  return treeSitterInitPromise;
@@ -1352,7 +1791,7 @@ async function loadLanguage(language) {
1352
1791
  const loader = (async () => {
1353
1792
  const module = await getTreeSitterModule();
1354
1793
  await ensureTreeSitterInit(module);
1355
- const bytes = await fs4.readFile(path4.join(TREE_SITTER_PACKAGE_ROOT, "wasm", grammarFileByLanguage[language]));
1794
+ const bytes = await fs5.readFile(path5.join(TREE_SITTER_PACKAGE_ROOT, "wasm", grammarFileByLanguage[language]));
1356
1795
  return module.Language.load(bytes);
1357
1796
  })();
1358
1797
  languageCache.set(language, loader);
@@ -1369,16 +1808,16 @@ function stripCodeExtension(filePath) {
1369
1808
  return filePath.replace(/\.(?:[cm]?jsx?|tsx?|mts|cts|py|go|rs|java|cs|php|c|cc|cpp|cxx|h|hh|hpp|hxx)$/i, "");
1370
1809
  }
1371
1810
  function manifestModuleName(manifest, language) {
1372
- const repoPath = manifest.repoRelativePath ?? path4.basename(manifest.originalPath ?? manifest.storedPath);
1811
+ const repoPath = manifest.repoRelativePath ?? path5.basename(manifest.originalPath ?? manifest.storedPath);
1373
1812
  const normalized = toPosix(stripCodeExtension(repoPath)).replace(/^\.\/+/, "");
1374
1813
  if (!normalized) {
1375
1814
  return void 0;
1376
1815
  }
1377
1816
  if (language === "python") {
1378
1817
  const dotted = normalized.replace(/\/__init__$/i, "").replace(/\//g, ".").replace(/^src\./, "");
1379
- return dotted || path4.posix.basename(normalized);
1818
+ return dotted || path5.posix.basename(normalized);
1380
1819
  }
1381
- return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path4.posix.basename(normalized) : normalized;
1820
+ return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path5.posix.basename(normalized) : normalized;
1382
1821
  }
1383
1822
  function singleLineSignature(value) {
1384
1823
  return truncate(
@@ -2639,11 +3078,11 @@ function isNodeExported(node) {
2639
3078
  )
2640
3079
  );
2641
3080
  }
2642
- function makeSymbolId2(sourceId, name, seen) {
3081
+ function makeSymbolId2(scope, name, seen) {
2643
3082
  const base = slugify(name);
2644
3083
  const count = (seen.get(base) ?? 0) + 1;
2645
3084
  seen.set(base, count);
2646
- return `symbol:${sourceId}:${count === 1 ? base : `${base}-${count}`}`;
3085
+ return `symbol:${scope}:${count === 1 ? base : `${base}-${count}`}`;
2647
3086
  }
2648
3087
  function summarizeModule(manifest, code) {
2649
3088
  const localImports = code.imports.filter((item) => !item.isExternal && !item.reExport).length;
@@ -2783,16 +3222,16 @@ function stripCodeExtension2(filePath) {
2783
3222
  return filePath.replace(/\.(?:[cm]?jsx?|tsx?|mts|cts|py|go|rs|java|cs|php|c|cc|cpp|cxx|h|hh|hpp|hxx)$/i, "");
2784
3223
  }
2785
3224
  function manifestModuleName2(manifest, language) {
2786
- const repoPath = manifest.repoRelativePath ?? path5.basename(manifest.originalPath ?? manifest.storedPath);
3225
+ const repoPath = manifest.repoRelativePath ?? path6.basename(manifest.originalPath ?? manifest.storedPath);
2787
3226
  const normalized = toPosix(stripCodeExtension2(repoPath)).replace(/^\.\/+/, "");
2788
3227
  if (!normalized) {
2789
3228
  return void 0;
2790
3229
  }
2791
3230
  if (language === "python") {
2792
3231
  const dotted = normalized.replace(/\/__init__$/i, "").replace(/\//g, ".").replace(/^src\./, "");
2793
- return dotted || path5.posix.basename(normalized);
3232
+ return dotted || path6.posix.basename(normalized);
2794
3233
  }
2795
- return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path5.posix.basename(normalized) : normalized;
3234
+ return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path6.posix.basename(normalized) : normalized;
2796
3235
  }
2797
3236
  function finalizeCodeAnalysis2(manifest, language, imports, draftSymbols, exportLabels, diagnostics, metadata) {
2798
3237
  const topLevelNames = new Set(draftSymbols.map((symbol) => symbol.name));
@@ -2802,8 +3241,9 @@ function finalizeCodeAnalysis2(manifest, language, imports, draftSymbols, export
2802
3241
  }
2803
3242
  }
2804
3243
  const seenSymbolIds = /* @__PURE__ */ new Map();
3244
+ const symbolScope = metadata?.namespace ? `ns:${slugify(metadata.namespace)}` : manifest.sourceId;
2805
3245
  const symbols = draftSymbols.map((symbol) => ({
2806
- id: makeSymbolId2(manifest.sourceId, symbol.name, seenSymbolIds),
3246
+ id: makeSymbolId2(symbolScope, symbol.name, seenSymbolIds),
2807
3247
  name: symbol.name,
2808
3248
  kind: symbol.kind,
2809
3249
  signature: symbol.signature,
@@ -3085,7 +3525,7 @@ function analyzeTypeScriptLikeCode(manifest, content) {
3085
3525
  };
3086
3526
  }
3087
3527
  function inferCodeLanguage(filePath, mimeType = "") {
3088
- const extension = path5.extname(filePath).toLowerCase();
3528
+ const extension = path6.extname(filePath).toLowerCase();
3089
3529
  if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
3090
3530
  return "typescript";
3091
3531
  }
@@ -3134,12 +3574,12 @@ function modulePageTitle(manifest) {
3134
3574
  return `${manifest.title} module`;
3135
3575
  }
3136
3576
  function importResolutionCandidates(basePath, specifier, extensions) {
3137
- const resolved = path5.posix.normalize(path5.posix.join(path5.posix.dirname(basePath), specifier));
3138
- if (path5.posix.extname(resolved)) {
3577
+ const resolved = path6.posix.normalize(path6.posix.join(path6.posix.dirname(basePath), specifier));
3578
+ if (path6.posix.extname(resolved)) {
3139
3579
  return [resolved];
3140
3580
  }
3141
- const direct = extensions.map((extension) => path5.posix.normalize(`${resolved}${extension}`));
3142
- const indexFiles = extensions.map((extension) => path5.posix.normalize(path5.posix.join(resolved, `index${extension}`)));
3581
+ const direct = extensions.map((extension) => path6.posix.normalize(`${resolved}${extension}`));
3582
+ const indexFiles = extensions.map((extension) => path6.posix.normalize(path6.posix.join(resolved, `index${extension}`)));
3143
3583
  return uniqueBy([resolved, ...direct, ...indexFiles], (candidate) => candidate);
3144
3584
  }
3145
3585
  function normalizeAlias(value) {
@@ -3158,32 +3598,32 @@ function recordAlias(target, value) {
3158
3598
  }
3159
3599
  function manifestBasenameWithoutExtension(manifest) {
3160
3600
  const target = manifest.repoRelativePath ?? manifest.originalPath ?? manifest.storedPath;
3161
- return path5.posix.basename(stripCodeExtension2(normalizeAlias(target)));
3601
+ return path6.posix.basename(stripCodeExtension2(normalizeAlias(target)));
3162
3602
  }
3163
3603
  async function readNearestGoModulePath(startPath, cache) {
3164
- let current = path5.resolve(startPath);
3604
+ let current = path6.resolve(startPath);
3165
3605
  try {
3166
- const stat = await fs5.stat(current);
3606
+ const stat = await fs6.stat(current);
3167
3607
  if (!stat.isDirectory()) {
3168
- current = path5.dirname(current);
3608
+ current = path6.dirname(current);
3169
3609
  }
3170
3610
  } catch {
3171
- current = path5.dirname(current);
3611
+ current = path6.dirname(current);
3172
3612
  }
3173
3613
  while (true) {
3174
3614
  if (cache.has(current)) {
3175
3615
  const cached = cache.get(current);
3176
3616
  return cached === null ? void 0 : cached;
3177
3617
  }
3178
- const goModPath = path5.join(current, "go.mod");
3179
- if (await fs5.access(goModPath).then(() => true).catch(() => false)) {
3180
- const content = await fs5.readFile(goModPath, "utf8");
3618
+ const goModPath = path6.join(current, "go.mod");
3619
+ if (await fs6.access(goModPath).then(() => true).catch(() => false)) {
3620
+ const content = await fs6.readFile(goModPath, "utf8");
3181
3621
  const match = content.match(/^\s*module\s+(\S+)/m);
3182
3622
  const modulePath = match?.[1]?.trim() ?? null;
3183
3623
  cache.set(current, modulePath);
3184
3624
  return modulePath ?? void 0;
3185
3625
  }
3186
- const parent = path5.dirname(current);
3626
+ const parent = path6.dirname(current);
3187
3627
  if (parent === current) {
3188
3628
  cache.set(current, null);
3189
3629
  return void 0;
@@ -3264,10 +3704,10 @@ async function buildCodeIndex(rootDir, manifests, analyses) {
3264
3704
  if (normalizedNamespace) {
3265
3705
  recordAlias(aliases, normalizedNamespace);
3266
3706
  }
3267
- const originalPath = manifest.originalPath ? path5.resolve(manifest.originalPath) : path5.resolve(rootDir, manifest.storedPath);
3707
+ const originalPath = manifest.originalPath ? path6.resolve(manifest.originalPath) : path6.resolve(rootDir, manifest.storedPath);
3268
3708
  const goModulePath = await readNearestGoModulePath(originalPath, goModuleCache);
3269
3709
  if (goModulePath && repoRelativePath) {
3270
- const dir = path5.posix.dirname(repoRelativePath);
3710
+ const dir = path6.posix.dirname(repoRelativePath);
3271
3711
  const packageAlias = dir === "." ? goModulePath : `${goModulePath}/${dir}`;
3272
3712
  recordAlias(aliases, packageAlias);
3273
3713
  }
@@ -3343,10 +3783,10 @@ function resolvePythonRelativeAliases(repoRelativePath, specifier) {
3343
3783
  const dotMatch = specifier.match(/^\.+/);
3344
3784
  const depth = dotMatch ? dotMatch[0].length : 0;
3345
3785
  const relativeModule = specifier.slice(depth).replace(/\./g, "/");
3346
- const baseDir = path5.posix.dirname(repoRelativePath);
3347
- const parentDir = path5.posix.normalize(path5.posix.join(baseDir, ...Array(Math.max(depth - 1, 0)).fill("..")));
3348
- const moduleBase = relativeModule ? path5.posix.join(parentDir, relativeModule) : parentDir;
3349
- return uniqueBy([`${moduleBase}.py`, path5.posix.join(moduleBase, "__init__.py")], (item) => item);
3786
+ const baseDir = path6.posix.dirname(repoRelativePath);
3787
+ const parentDir = path6.posix.normalize(path6.posix.join(baseDir, ...Array(Math.max(depth - 1, 0)).fill("..")));
3788
+ const moduleBase = relativeModule ? path6.posix.join(parentDir, relativeModule) : parentDir;
3789
+ return uniqueBy([`${moduleBase}.py`, path6.posix.join(moduleBase, "__init__.py")], (item) => item);
3350
3790
  }
3351
3791
  function resolveRustAliases(manifest, specifier) {
3352
3792
  const repoRelativePath = manifest.repoRelativePath ? normalizeAlias(manifest.repoRelativePath) : "";
@@ -3470,7 +3910,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
3470
3910
  const language = manifest.language ?? inferCodeLanguage(manifest.originalPath ?? manifest.storedPath, manifest.mimeType) ?? "typescript";
3471
3911
  const { code, rationales } = language === "javascript" || language === "jsx" || language === "typescript" || language === "tsx" ? analyzeTypeScriptLikeCode(manifest, extractedText) : await analyzeTreeSitterCode(manifest, extractedText, language);
3472
3912
  return {
3473
- analysisVersion: 5,
3913
+ analysisVersion: 6,
3474
3914
  sourceId: manifest.sourceId,
3475
3915
  sourceHash: manifest.contentHash,
3476
3916
  extractionHash: manifest.extractionHash,
@@ -3481,6 +3921,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
3481
3921
  entities: [],
3482
3922
  claims: codeClaims(manifest, code),
3483
3923
  questions: codeQuestions(manifest, code),
3924
+ tags: [],
3484
3925
  rationales,
3485
3926
  code,
3486
3927
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3488,9 +3929,9 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
3488
3929
  }
3489
3930
 
3490
3931
  // src/extraction.ts
3491
- import fs6 from "fs/promises";
3932
+ import fs7 from "fs/promises";
3492
3933
  import os from "os";
3493
- import path6 from "path";
3934
+ import path7 from "path";
3494
3935
  import { strFromU8, unzipSync } from "fflate";
3495
3936
  import { JSDOM } from "jsdom";
3496
3937
  import { z } from "zod";
@@ -3572,14 +4013,14 @@ async function materializeAttachmentPath(input) {
3572
4013
  if (!input.bytes) {
3573
4014
  throw new Error("Image extraction requires a file path or bytes.");
3574
4015
  }
3575
- const tempDir = await fs6.mkdtemp(path6.join(os.tmpdir(), "swarmvault-image-extract-"));
4016
+ const tempDir = await fs7.mkdtemp(path7.join(os.tmpdir(), "swarmvault-image-extract-"));
3576
4017
  const extension = input.mimeType.split("/")[1]?.split("+")[0] ?? "bin";
3577
- const tempPath = path6.join(tempDir, `source.${extension}`);
3578
- await fs6.writeFile(tempPath, input.bytes);
4018
+ const tempPath = path7.join(tempDir, `source.${extension}`);
4019
+ await fs7.writeFile(tempPath, input.bytes);
3579
4020
  return {
3580
4021
  filePath: tempPath,
3581
4022
  cleanup: async () => {
3582
- await fs6.rm(tempDir, { recursive: true, force: true });
4023
+ await fs7.rm(tempDir, { recursive: true, force: true });
3583
4024
  }
3584
4025
  };
3585
4026
  }
@@ -3790,18 +4231,18 @@ async function extractDocxText(input) {
3790
4231
  }
3791
4232
 
3792
4233
  // src/logs.ts
3793
- import fs7 from "fs/promises";
3794
- import path7 from "path";
4234
+ import fs8 from "fs/promises";
4235
+ import path8 from "path";
3795
4236
  import matter from "gray-matter";
3796
4237
  async function resolveUniqueSessionPath(rootDir, operation, title, startedAt) {
3797
4238
  const { paths } = await initWorkspace(rootDir);
3798
4239
  await ensureDir(paths.sessionsDir);
3799
4240
  const timestamp = startedAt.replace(/[:.]/g, "-");
3800
4241
  const baseName = `${timestamp}-${operation}-${slugify(title)}`;
3801
- let candidate = path7.join(paths.sessionsDir, `${baseName}.md`);
4242
+ let candidate = path8.join(paths.sessionsDir, `${baseName}.md`);
3802
4243
  let counter = 2;
3803
4244
  while (await fileExists(candidate)) {
3804
- candidate = path7.join(paths.sessionsDir, `${baseName}-${counter}.md`);
4245
+ candidate = path8.join(paths.sessionsDir, `${baseName}-${counter}.md`);
3805
4246
  counter++;
3806
4247
  }
3807
4248
  return candidate;
@@ -3809,11 +4250,11 @@ async function resolveUniqueSessionPath(rootDir, operation, title, startedAt) {
3809
4250
  async function appendLogEntry(rootDir, action, title, lines = []) {
3810
4251
  const { paths } = await initWorkspace(rootDir);
3811
4252
  await ensureDir(paths.wikiDir);
3812
- const logPath = path7.join(paths.wikiDir, "log.md");
4253
+ const logPath = path8.join(paths.wikiDir, "log.md");
3813
4254
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
3814
4255
  const entry = [`## [${timestamp}] ${action} | ${title}`, ...lines.map((line) => `- ${line}`), ""].join("\n");
3815
- const existing = await fileExists(logPath) ? await fs7.readFile(logPath, "utf8") : "# Log\n\n";
3816
- await fs7.writeFile(logPath, `${existing}${entry}
4256
+ const existing = await fileExists(logPath) ? await fs8.readFile(logPath, "utf8") : "# Log\n\n";
4257
+ await fs8.writeFile(logPath, `${existing}${entry}
3817
4258
  `, "utf8");
3818
4259
  }
3819
4260
  async function recordSession(rootDir, input) {
@@ -3823,8 +4264,8 @@ async function recordSession(rootDir, input) {
3823
4264
  const finishedAtIso = new Date(input.finishedAt ?? input.startedAt).toISOString();
3824
4265
  const durationMs = Math.max(0, new Date(finishedAtIso).getTime() - new Date(startedAtIso).getTime());
3825
4266
  const sessionPath = await resolveUniqueSessionPath(rootDir, input.operation, input.title, startedAtIso);
3826
- const sessionId = path7.basename(sessionPath, ".md");
3827
- const relativeSessionPath = path7.relative(rootDir, sessionPath).split(path7.sep).join(path7.posix.sep);
4267
+ const sessionId = path8.basename(sessionPath, ".md");
4268
+ const relativeSessionPath = path8.relative(rootDir, sessionPath).split(path8.sep).join(path8.posix.sep);
3828
4269
  const frontmatter = Object.fromEntries(
3829
4270
  Object.entries({
3830
4271
  session_id: sessionId,
@@ -3872,7 +4313,7 @@ async function recordSession(rootDir, input) {
3872
4313
  frontmatter
3873
4314
  );
3874
4315
  await writeFileIfChanged(sessionPath, content);
3875
- const logPath = path7.join(paths.wikiDir, "log.md");
4316
+ const logPath = path8.join(paths.wikiDir, "log.md");
3876
4317
  const timestamp = startedAtIso.slice(0, 19).replace("T", " ");
3877
4318
  const entry = [
3878
4319
  `## [${timestamp}] ${input.operation} | ${input.title}`,
@@ -3880,8 +4321,8 @@ async function recordSession(rootDir, input) {
3880
4321
  ...(input.lines ?? []).map((line) => `- ${line}`),
3881
4322
  ""
3882
4323
  ].join("\n");
3883
- const existing = await fileExists(logPath) ? await fs7.readFile(logPath, "utf8") : "# Log\n\n";
3884
- await fs7.writeFile(logPath, `${existing}${entry}
4324
+ const existing = await fileExists(logPath) ? await fs8.readFile(logPath, "utf8") : "# Log\n\n";
4325
+ await fs8.writeFile(logPath, `${existing}${entry}
3885
4326
  `, "utf8");
3886
4327
  return { sessionPath, sessionId };
3887
4328
  }
@@ -3891,13 +4332,13 @@ async function appendWatchRun(rootDir, run) {
3891
4332
  }
3892
4333
 
3893
4334
  // src/source-classification.ts
3894
- import path8 from "path";
4335
+ import path9 from "path";
3895
4336
  var ALL_SOURCE_CLASSES = ["first_party", "third_party", "resource", "generated"];
3896
4337
  var THIRD_PARTY_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", "vendor", "Pods"]);
3897
4338
  var GENERATED_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", ".next", "coverage", "DerivedData", "target"]);
3898
4339
  function matchesAnyGlob(relativePath, patterns) {
3899
4340
  return patterns.some(
3900
- (pattern) => path8.matchesGlob(relativePath, pattern) || path8.matchesGlob(path8.posix.basename(relativePath), pattern)
4341
+ (pattern) => path9.matchesGlob(relativePath, pattern) || path9.matchesGlob(path9.posix.basename(relativePath), pattern)
3901
4342
  );
3902
4343
  }
3903
4344
  function classifyRepoPath(relativePath, repoAnalysis) {
@@ -3950,8 +4391,8 @@ function aggregateManifestSourceClass(manifests, sourceIds) {
3950
4391
  }
3951
4392
 
3952
4393
  // src/watch-state.ts
3953
- import fs8 from "fs/promises";
3954
- import path9 from "path";
4394
+ import fs9 from "fs/promises";
4395
+ import path10 from "path";
3955
4396
  import matter2 from "gray-matter";
3956
4397
  function pendingEntryKey(entry) {
3957
4398
  return entry.path;
@@ -3965,7 +4406,7 @@ function normalizeRelativePath(rootDir, filePath) {
3965
4406
  if (!filePath) {
3966
4407
  return void 0;
3967
4408
  }
3968
- return toPosix(path9.relative(rootDir, path9.resolve(filePath)));
4409
+ return toPosix(path10.relative(rootDir, path10.resolve(filePath)));
3969
4410
  }
3970
4411
  async function readPendingSemanticRefresh(rootDir) {
3971
4412
  const { paths } = await initWorkspace(rootDir);
@@ -4059,11 +4500,11 @@ async function markPagesStaleForSources(rootDir, sourceIds) {
4059
4500
  if (page.freshness !== "stale" || !page.sourceIds.some((sourceId) => affectedSourceIds.has(sourceId))) {
4060
4501
  continue;
4061
4502
  }
4062
- const absolutePath = path9.join(paths.wikiDir, page.path);
4503
+ const absolutePath = path10.join(paths.wikiDir, page.path);
4063
4504
  if (!await fileExists(absolutePath)) {
4064
4505
  continue;
4065
4506
  }
4066
- const raw = await fs8.readFile(absolutePath, "utf8");
4507
+ const raw = await fs9.readFile(absolutePath, "utf8");
4067
4508
  const parsed = matter2(raw);
4068
4509
  if (parsed.data.freshness === "stale") {
4069
4510
  continue;
@@ -4117,7 +4558,7 @@ function normalizeIngestOptions(options) {
4117
4558
  return {
4118
4559
  includeAssets: options?.includeAssets ?? true,
4119
4560
  maxAssetSize: Math.max(0, Math.floor(options?.maxAssetSize ?? DEFAULT_MAX_ASSET_SIZE)),
4120
- repoRoot: options?.repoRoot ? path10.resolve(options.repoRoot) : void 0,
4561
+ repoRoot: options?.repoRoot ? path11.resolve(options.repoRoot) : void 0,
4121
4562
  include: (options?.include ?? []).map((pattern) => pattern.trim()).filter(Boolean),
4122
4563
  exclude: (options?.exclude ?? []).map((pattern) => pattern.trim()).filter(Boolean),
4123
4564
  maxFiles: Math.max(1, Math.floor(options?.maxFiles ?? DEFAULT_MAX_DIRECTORY_FILES)),
@@ -4137,27 +4578,27 @@ async function resolveRepoIngestOptions(rootDir, options) {
4137
4578
  }
4138
4579
  function matchesAnyGlob2(relativePath, patterns) {
4139
4580
  return patterns.some(
4140
- (pattern) => path10.matchesGlob(relativePath, pattern) || path10.matchesGlob(path10.posix.basename(relativePath), pattern)
4581
+ (pattern) => path11.matchesGlob(relativePath, pattern) || path11.matchesGlob(path11.posix.basename(relativePath), pattern)
4141
4582
  );
4142
4583
  }
4143
4584
  function supportedDirectoryKind(sourceKind) {
4144
4585
  return sourceKind !== "binary";
4145
4586
  }
4146
4587
  async function findNearestGitRoot2(startPath) {
4147
- let current = path10.resolve(startPath);
4588
+ let current = path11.resolve(startPath);
4148
4589
  try {
4149
- const stat = await fs9.stat(current);
4590
+ const stat = await fs10.stat(current);
4150
4591
  if (!stat.isDirectory()) {
4151
- current = path10.dirname(current);
4592
+ current = path11.dirname(current);
4152
4593
  }
4153
4594
  } catch {
4154
- current = path10.dirname(current);
4595
+ current = path11.dirname(current);
4155
4596
  }
4156
4597
  while (true) {
4157
- if (await fileExists(path10.join(current, ".git"))) {
4598
+ if (await fileExists(path11.join(current, ".git"))) {
4158
4599
  return current;
4159
4600
  }
4160
- const parent = path10.dirname(current);
4601
+ const parent = path11.dirname(current);
4161
4602
  if (parent === current) {
4162
4603
  return null;
4163
4604
  }
@@ -4165,26 +4606,26 @@ async function findNearestGitRoot2(startPath) {
4165
4606
  }
4166
4607
  }
4167
4608
  function withinRoot(rootPath, targetPath) {
4168
- const relative = path10.relative(rootPath, targetPath);
4169
- return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
4609
+ const relative = path11.relative(rootPath, targetPath);
4610
+ return relative === "" || !relative.startsWith("..") && !path11.isAbsolute(relative);
4170
4611
  }
4171
4612
  function repoRootFromManifest(manifest) {
4172
4613
  if (manifest.originType !== "file" || !manifest.originalPath || !manifest.repoRelativePath) {
4173
4614
  return null;
4174
4615
  }
4175
- const repoDir = path10.posix.dirname(manifest.repoRelativePath);
4176
- const fileDir = path10.dirname(path10.resolve(manifest.originalPath));
4616
+ const repoDir = path11.posix.dirname(manifest.repoRelativePath);
4617
+ const fileDir = path11.dirname(path11.resolve(manifest.originalPath));
4177
4618
  if (repoDir === "." || !repoDir) {
4178
4619
  return fileDir;
4179
4620
  }
4180
4621
  const segments = repoDir.split("/").filter(Boolean);
4181
- return path10.resolve(fileDir, ...segments.map(() => ".."));
4622
+ return path11.resolve(fileDir, ...segments.map(() => ".."));
4182
4623
  }
4183
4624
  function repoRelativePathFor(absolutePath, repoRoot) {
4184
4625
  if (!repoRoot || !withinRoot(repoRoot, absolutePath)) {
4185
4626
  return void 0;
4186
4627
  }
4187
- const relative = toPosix(path10.relative(repoRoot, absolutePath));
4628
+ const relative = toPosix(path11.relative(repoRoot, absolutePath));
4188
4629
  return relative && !relative.startsWith("..") ? relative : void 0;
4189
4630
  }
4190
4631
  function normalizeOriginUrl(input) {
@@ -4260,7 +4701,41 @@ function prepareCapturedMarkdownInput(input) {
4260
4701
  logDetails: input.logDetails
4261
4702
  };
4262
4703
  }
4704
+ function isPrivateIp(ip) {
4705
+ if (ip === "::1" || ip.startsWith("fc") || ip.startsWith("fd")) return true;
4706
+ const parts = ip.split(".").map(Number);
4707
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return false;
4708
+ return parts[0] === 0 || parts[0] === 127 || parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168 || parts[0] === 169 && parts[1] === 254;
4709
+ }
4710
+ function allowPrivateUrlsForProcess() {
4711
+ return process.env.SWARMVAULT_ALLOW_PRIVATE_URLS === "1";
4712
+ }
4713
+ function isReservedTestHostname(hostname) {
4714
+ const lower = hostname.toLowerCase();
4715
+ return lower.endsWith(".test") || lower.endsWith(".example") || lower.endsWith(".invalid");
4716
+ }
4717
+ async function validateUrlSafety(url) {
4718
+ const parsed = new URL(url);
4719
+ if (!["http:", "https:"].includes(parsed.protocol)) {
4720
+ throw new Error(`Blocked protocol: ${parsed.protocol}`);
4721
+ }
4722
+ if (allowPrivateUrlsForProcess() || isReservedTestHostname(parsed.hostname)) {
4723
+ return;
4724
+ }
4725
+ let address;
4726
+ try {
4727
+ const { lookup } = await import("dns/promises");
4728
+ const result = await lookup(parsed.hostname);
4729
+ address = result.address;
4730
+ } catch {
4731
+ return;
4732
+ }
4733
+ if (isPrivateIp(address)) {
4734
+ throw new Error(`Blocked private/reserved IP ${address} (resolved from ${parsed.hostname})`);
4735
+ }
4736
+ }
4263
4737
  async function fetchText(url) {
4738
+ await validateUrlSafety(url);
4264
4739
  const response = await fetch(url);
4265
4740
  if (!response.ok) {
4266
4741
  throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
@@ -4268,6 +4743,7 @@ async function fetchText(url) {
4268
4743
  return response.text();
4269
4744
  }
4270
4745
  async function fetchResolvedText(url) {
4746
+ await validateUrlSafety(url);
4271
4747
  const response = await fetch(url);
4272
4748
  if (!response.ok) {
4273
4749
  throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
@@ -4481,7 +4957,7 @@ function buildCompositeHash(payloadBytes, attachments = []) {
4481
4957
  return sha256(`${sha256(payloadBytes)}|${attachmentSignature}`);
4482
4958
  }
4483
4959
  function sanitizeAssetRelativePath(value) {
4484
- const normalized = path10.posix.normalize(value.replace(/\\/g, "/"));
4960
+ const normalized = path11.posix.normalize(value.replace(/\\/g, "/"));
4485
4961
  const segments = normalized.split("/").filter(Boolean).map((segment) => {
4486
4962
  if (segment === ".") {
4487
4963
  return "";
@@ -4501,7 +4977,7 @@ function normalizeLocalReference(value) {
4501
4977
  return null;
4502
4978
  }
4503
4979
  const lowered = candidate.toLowerCase();
4504
- if (lowered.startsWith("http://") || lowered.startsWith("https://") || lowered.startsWith("data:") || lowered.startsWith("mailto:") || lowered.startsWith("#") || path10.isAbsolute(candidate)) {
4980
+ if (lowered.startsWith("http://") || lowered.startsWith("https://") || lowered.startsWith("data:") || lowered.startsWith("mailto:") || lowered.startsWith("#") || path11.isAbsolute(candidate)) {
4505
4981
  return null;
4506
4982
  }
4507
4983
  return candidate.replace(/\\/g, "/");
@@ -4579,12 +5055,12 @@ async function convertHtmlToMarkdown(html, url) {
4579
5055
  };
4580
5056
  }
4581
5057
  async function readManifestByHash(manifestsDir, contentHash) {
4582
- const entries = await fs9.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
5058
+ const entries = await fs10.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
4583
5059
  for (const entry of entries) {
4584
5060
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
4585
5061
  continue;
4586
5062
  }
4587
- const manifest = await readJsonFile(path10.join(manifestsDir, entry.name));
5063
+ const manifest = await readJsonFile(path11.join(manifestsDir, entry.name));
4588
5064
  if (manifest?.contentHash === contentHash) {
4589
5065
  return manifest;
4590
5066
  }
@@ -4592,12 +5068,12 @@ async function readManifestByHash(manifestsDir, contentHash) {
4592
5068
  return null;
4593
5069
  }
4594
5070
  async function readManifestByOrigin(manifestsDir, prepared) {
4595
- const entries = await fs9.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
5071
+ const entries = await fs10.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
4596
5072
  for (const entry of entries) {
4597
5073
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
4598
5074
  continue;
4599
5075
  }
4600
- const manifest = await readJsonFile(path10.join(manifestsDir, entry.name));
5076
+ const manifest = await readJsonFile(path11.join(manifestsDir, entry.name));
4601
5077
  if (manifest && manifestMatchesOrigin(manifest, prepared)) {
4602
5078
  return manifest;
4603
5079
  }
@@ -4608,12 +5084,12 @@ async function loadGitignoreMatcher(repoRoot, enabled) {
4608
5084
  if (!enabled) {
4609
5085
  return null;
4610
5086
  }
4611
- const gitignorePath = path10.join(repoRoot, ".gitignore");
5087
+ const gitignorePath = path11.join(repoRoot, ".gitignore");
4612
5088
  if (!await fileExists(gitignorePath)) {
4613
5089
  return null;
4614
5090
  }
4615
5091
  const matcher = ignore();
4616
- matcher.add(await fs9.readFile(gitignorePath, "utf8"));
5092
+ matcher.add(await fs10.readFile(gitignorePath, "utf8"));
4617
5093
  return matcher;
4618
5094
  }
4619
5095
  function builtInIgnoreReason(relativePath) {
@@ -4637,23 +5113,23 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
4637
5113
  if (!currentDir) {
4638
5114
  continue;
4639
5115
  }
4640
- const entries = await fs9.readdir(currentDir, { withFileTypes: true });
5116
+ const entries = await fs10.readdir(currentDir, { withFileTypes: true });
4641
5117
  entries.sort((left, right) => left.name.localeCompare(right.name));
4642
5118
  for (const entry of entries) {
4643
- const absolutePath = path10.join(currentDir, entry.name);
4644
- const relativeToRepo = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path10.relative(inputDir, absolutePath));
5119
+ const absolutePath = path11.join(currentDir, entry.name);
5120
+ const relativeToRepo = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path11.relative(inputDir, absolutePath));
4645
5121
  const relativePath = relativeToRepo || entry.name;
4646
5122
  const builtInReason = builtInIgnoreReason(relativePath);
4647
5123
  if (builtInReason) {
4648
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: builtInReason });
5124
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: builtInReason });
4649
5125
  continue;
4650
5126
  }
4651
5127
  if (matcher?.ignores(relativePath)) {
4652
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "gitignore" });
5128
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "gitignore" });
4653
5129
  continue;
4654
5130
  }
4655
5131
  if (matchesAnyGlob2(relativePath, options.exclude)) {
4656
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "exclude_glob" });
5132
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "exclude_glob" });
4657
5133
  continue;
4658
5134
  }
4659
5135
  if (entry.isDirectory()) {
@@ -4661,26 +5137,26 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
4661
5137
  continue;
4662
5138
  }
4663
5139
  if (!entry.isFile()) {
4664
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "unsupported_entry" });
5140
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "unsupported_entry" });
4665
5141
  continue;
4666
5142
  }
4667
5143
  if (options.include.length > 0 && !matchesAnyGlob2(relativePath, options.include)) {
4668
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "include_glob" });
5144
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "include_glob" });
4669
5145
  continue;
4670
5146
  }
4671
5147
  const mimeType = guessMimeType(absolutePath);
4672
5148
  const sourceKind = inferKind(mimeType, absolutePath);
4673
5149
  const sourceClass = sourceClassForRelativePath(relativePath, options);
4674
5150
  if (!supportedDirectoryKind(sourceKind)) {
4675
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
5151
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
4676
5152
  continue;
4677
5153
  }
4678
5154
  if (!options.extractClasses.includes(sourceClass)) {
4679
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: `source_class:${sourceClass}` });
5155
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: `source_class:${sourceClass}` });
4680
5156
  continue;
4681
5157
  }
4682
5158
  if (files.length >= options.maxFiles) {
4683
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "max_files" });
5159
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "max_files" });
4684
5160
  continue;
4685
5161
  }
4686
5162
  files.push(absolutePath);
@@ -4702,12 +5178,12 @@ function resolveUrlMimeType(input, response) {
4702
5178
  function buildRemoteAssetRelativePath(assetUrl, mimeType) {
4703
5179
  const url = new URL(assetUrl);
4704
5180
  const normalized = sanitizeAssetRelativePath(`${url.hostname}${url.pathname || "/asset"}`);
4705
- const extension = path10.posix.extname(normalized);
4706
- const directory = path10.posix.dirname(normalized);
4707
- const basename = extension ? path10.posix.basename(normalized, extension) : path10.posix.basename(normalized);
5181
+ const extension = path11.posix.extname(normalized);
5182
+ const directory = path11.posix.dirname(normalized);
5183
+ const basename = extension ? path11.posix.basename(normalized, extension) : path11.posix.basename(normalized);
4708
5184
  const resolvedExtension = extension || `.${mime.extension(mimeType) || "bin"}`;
4709
5185
  const hashedName = `${basename || "asset"}-${sha256(assetUrl).slice(0, 8)}${resolvedExtension}`;
4710
- return directory === "." ? hashedName : path10.posix.join(directory, hashedName);
5186
+ return directory === "." ? hashedName : path11.posix.join(directory, hashedName);
4711
5187
  }
4712
5188
  async function readResponseBytesWithinLimit(response, maxBytes) {
4713
5189
  const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
@@ -4739,6 +5215,7 @@ async function readResponseBytesWithinLimit(response, maxBytes) {
4739
5215
  return Buffer.concat(chunks);
4740
5216
  }
4741
5217
  async function fetchRemoteImageAttachment(assetUrl, maxAssetSize) {
5218
+ await validateUrlSafety(assetUrl);
4742
5219
  const response = await fetch(assetUrl);
4743
5220
  if (!response.ok) {
4744
5221
  throw new Error(`failed with ${response.status} ${response.statusText}`);
@@ -4859,34 +5336,34 @@ async function persistPreparedInput(rootDir, prepared, paths) {
4859
5336
  const previous = existingByOrigin ?? void 0;
4860
5337
  const sourceId = previous?.sourceId ?? `${slugify(prepared.title)}-${contentHash.slice(0, 8)}`;
4861
5338
  const now = (/* @__PURE__ */ new Date()).toISOString();
4862
- const storedPath = path10.join(paths.rawSourcesDir, `${sourceId}${prepared.storedExtension}`);
4863
- const extractedTextPath = prepared.extractedText ? path10.join(paths.extractsDir, `${sourceId}.md`) : void 0;
4864
- const extractedMetadataPath = prepared.extractionArtifact ? path10.join(paths.extractsDir, `${sourceId}.json`) : void 0;
4865
- const attachmentsDir = path10.join(paths.rawAssetsDir, sourceId);
5339
+ const storedPath = path11.join(paths.rawSourcesDir, `${sourceId}${prepared.storedExtension}`);
5340
+ const extractedTextPath = prepared.extractedText ? path11.join(paths.extractsDir, `${sourceId}.md`) : void 0;
5341
+ const extractedMetadataPath = prepared.extractionArtifact ? path11.join(paths.extractsDir, `${sourceId}.json`) : void 0;
5342
+ const attachmentsDir = path11.join(paths.rawAssetsDir, sourceId);
4866
5343
  if (previous?.storedPath) {
4867
- await fs9.rm(path10.resolve(rootDir, previous.storedPath), { force: true });
5344
+ await fs10.rm(path11.resolve(rootDir, previous.storedPath), { force: true });
4868
5345
  }
4869
5346
  if (previous?.extractedTextPath) {
4870
- await fs9.rm(path10.resolve(rootDir, previous.extractedTextPath), { force: true });
5347
+ await fs10.rm(path11.resolve(rootDir, previous.extractedTextPath), { force: true });
4871
5348
  }
4872
5349
  if (previous?.extractedMetadataPath) {
4873
- await fs9.rm(path10.resolve(rootDir, previous.extractedMetadataPath), { force: true });
5350
+ await fs10.rm(path11.resolve(rootDir, previous.extractedMetadataPath), { force: true });
4874
5351
  }
4875
- await fs9.rm(attachmentsDir, { recursive: true, force: true });
4876
- await fs9.writeFile(storedPath, prepared.payloadBytes);
5352
+ await fs10.rm(attachmentsDir, { recursive: true, force: true });
5353
+ await fs10.writeFile(storedPath, prepared.payloadBytes);
4877
5354
  if (prepared.extractedText && extractedTextPath) {
4878
- await fs9.writeFile(extractedTextPath, prepared.extractedText, "utf8");
5355
+ await fs10.writeFile(extractedTextPath, prepared.extractedText, "utf8");
4879
5356
  }
4880
5357
  if (prepared.extractionArtifact && extractedMetadataPath) {
4881
5358
  await writeJsonFile(extractedMetadataPath, prepared.extractionArtifact);
4882
5359
  }
4883
5360
  const manifestAttachments = [];
4884
5361
  for (const attachment of attachments) {
4885
- const absoluteAttachmentPath = path10.join(attachmentsDir, attachment.relativePath);
4886
- await ensureDir(path10.dirname(absoluteAttachmentPath));
4887
- await fs9.writeFile(absoluteAttachmentPath, attachment.bytes);
5362
+ const absoluteAttachmentPath = path11.join(attachmentsDir, attachment.relativePath);
5363
+ await ensureDir(path11.dirname(absoluteAttachmentPath));
5364
+ await fs10.writeFile(absoluteAttachmentPath, attachment.bytes);
4888
5365
  manifestAttachments.push({
4889
- path: toPosix(path10.relative(rootDir, absoluteAttachmentPath)),
5366
+ path: toPosix(path11.relative(rootDir, absoluteAttachmentPath)),
4890
5367
  mimeType: attachment.mimeType,
4891
5368
  originalPath: attachment.originalPath
4892
5369
  });
@@ -4902,9 +5379,9 @@ async function persistPreparedInput(rootDir, prepared, paths) {
4902
5379
  originalPath: prepared.originalPath,
4903
5380
  repoRelativePath: prepared.repoRelativePath,
4904
5381
  url: prepared.url,
4905
- storedPath: toPosix(path10.relative(rootDir, storedPath)),
4906
- extractedTextPath: extractedTextPath ? toPosix(path10.relative(rootDir, extractedTextPath)) : void 0,
4907
- extractedMetadataPath: extractedMetadataPath ? toPosix(path10.relative(rootDir, extractedMetadataPath)) : void 0,
5382
+ storedPath: toPosix(path11.relative(rootDir, storedPath)),
5383
+ extractedTextPath: extractedTextPath ? toPosix(path11.relative(rootDir, extractedTextPath)) : void 0,
5384
+ extractedMetadataPath: extractedMetadataPath ? toPosix(path11.relative(rootDir, extractedMetadataPath)) : void 0,
4908
5385
  extractionHash,
4909
5386
  mimeType: prepared.mimeType,
4910
5387
  contentHash,
@@ -4912,7 +5389,7 @@ async function persistPreparedInput(rootDir, prepared, paths) {
4912
5389
  updatedAt: now,
4913
5390
  attachments: manifestAttachments.length ? manifestAttachments : void 0
4914
5391
  };
4915
- await writeJsonFile(path10.join(paths.manifestsDir, `${sourceId}.json`), manifest);
5392
+ await writeJsonFile(path11.join(paths.manifestsDir, `${sourceId}.json`), manifest);
4916
5393
  await appendLogEntry(rootDir, "ingest", prepared.title, [
4917
5394
  `source_id=${sourceId}`,
4918
5395
  `kind=${prepared.sourceKind}`,
@@ -4930,16 +5407,16 @@ async function persistPreparedInput(rootDir, prepared, paths) {
4930
5407
  return { manifest, isNew: !previous, wasUpdated: Boolean(previous) };
4931
5408
  }
4932
5409
  async function removeManifestArtifacts(rootDir, manifest, paths) {
4933
- await fs9.rm(path10.join(paths.manifestsDir, `${manifest.sourceId}.json`), { force: true });
4934
- await fs9.rm(path10.resolve(rootDir, manifest.storedPath), { force: true });
5410
+ await fs10.rm(path11.join(paths.manifestsDir, `${manifest.sourceId}.json`), { force: true });
5411
+ await fs10.rm(path11.resolve(rootDir, manifest.storedPath), { force: true });
4935
5412
  if (manifest.extractedTextPath) {
4936
- await fs9.rm(path10.resolve(rootDir, manifest.extractedTextPath), { force: true });
5413
+ await fs10.rm(path11.resolve(rootDir, manifest.extractedTextPath), { force: true });
4937
5414
  }
4938
5415
  if (manifest.extractedMetadataPath) {
4939
- await fs9.rm(path10.resolve(rootDir, manifest.extractedMetadataPath), { force: true });
5416
+ await fs10.rm(path11.resolve(rootDir, manifest.extractedMetadataPath), { force: true });
4940
5417
  }
4941
- await fs9.rm(path10.join(paths.rawAssetsDir, manifest.sourceId), { recursive: true, force: true });
4942
- await fs9.rm(path10.join(paths.analysesDir, `${manifest.sourceId}.json`), { force: true });
5418
+ await fs10.rm(path11.join(paths.rawAssetsDir, manifest.sourceId), { recursive: true, force: true });
5419
+ await fs10.rm(path11.join(paths.analysesDir, `${manifest.sourceId}.json`), { force: true });
4943
5420
  }
4944
5421
  function repoSyncWorkspaceIgnorePaths(rootDir, paths, repoRoot) {
4945
5422
  const candidates = [
@@ -4948,11 +5425,11 @@ function repoSyncWorkspaceIgnorePaths(rootDir, paths, repoRoot) {
4948
5425
  paths.stateDir,
4949
5426
  paths.agentDir,
4950
5427
  paths.inboxDir,
4951
- path10.join(rootDir, ".claude"),
4952
- path10.join(rootDir, ".cursor"),
4953
- path10.join(rootDir, ".obsidian")
5428
+ path11.join(rootDir, ".claude"),
5429
+ path11.join(rootDir, ".cursor"),
5430
+ path11.join(rootDir, ".obsidian")
4954
5431
  ];
4955
- return candidates.map((candidate) => path10.resolve(candidate)).filter((candidate, index, items) => items.indexOf(candidate) === index).filter((candidate) => withinRoot(repoRoot, candidate));
5432
+ return candidates.map((candidate) => path11.resolve(candidate)).filter((candidate, index, items) => items.indexOf(candidate) === index).filter((candidate) => withinRoot(repoRoot, candidate));
4956
5433
  }
4957
5434
  function preparedMatchesManifest(manifest, prepared, contentHash) {
4958
5435
  return manifest.contentHash === contentHash && manifest.extractionHash === (prepared.extractionHash ?? buildExtractionHash(prepared.extractedText, prepared.extractionArtifact)) && manifest.title === prepared.title && manifest.sourceKind === prepared.sourceKind && manifest.sourceType === prepared.sourceType && manifest.sourceClass === prepared.sourceClass && manifest.language === prepared.language && manifest.mimeType === prepared.mimeType && manifest.repoRelativePath === prepared.repoRelativePath;
@@ -4974,16 +5451,16 @@ async function syncTrackedRepos(rootDir, options, repoRoots) {
4974
5451
  const normalizedOptions = await resolveRepoIngestOptions(rootDir, options);
4975
5452
  const manifests = await listManifests(rootDir);
4976
5453
  const trackedRoots = (repoRoots && repoRoots.length > 0 ? repoRoots : await listTrackedRepoRoots(rootDir)).map(
4977
- (item) => path10.resolve(item)
5454
+ (item) => path11.resolve(item)
4978
5455
  );
4979
5456
  const uniqueRoots = [...new Set(trackedRoots)].sort((left, right) => left.localeCompare(right));
4980
5457
  const manifestsByRepoRoot = /* @__PURE__ */ new Map();
4981
5458
  for (const manifest of manifests) {
4982
5459
  const repoRoot = repoRootFromManifest(manifest);
4983
- if (!repoRoot || !uniqueRoots.includes(path10.resolve(repoRoot))) {
5460
+ if (!repoRoot || !uniqueRoots.includes(path11.resolve(repoRoot))) {
4984
5461
  continue;
4985
5462
  }
4986
- const key = path10.resolve(repoRoot);
5463
+ const key = path11.resolve(repoRoot);
4987
5464
  const bucket = manifestsByRepoRoot.get(key) ?? [];
4988
5465
  bucket.push(manifest);
4989
5466
  manifestsByRepoRoot.set(key, bucket);
@@ -5008,14 +5485,14 @@ async function syncTrackedRepos(rootDir, options, repoRoots) {
5008
5485
  skipped.push(
5009
5486
  ...collected.skipped,
5010
5487
  ...collected.files.filter((absolutePath) => ignoreRoots.some((ignoreRoot) => withinRoot(ignoreRoot, absolutePath))).map((absolutePath) => ({
5011
- path: toPosix(path10.relative(rootDir, absolutePath)),
5488
+ path: toPosix(path11.relative(rootDir, absolutePath)),
5012
5489
  reason: "workspace_generated"
5013
5490
  }))
5014
5491
  );
5015
5492
  scannedCount += files.length;
5016
- const currentPaths = new Set(files.map((absolutePath) => path10.resolve(absolutePath)));
5493
+ const currentPaths = new Set(files.map((absolutePath) => path11.resolve(absolutePath)));
5017
5494
  for (const absolutePath of files) {
5018
- const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path10.relative(repoRoot, absolutePath));
5495
+ const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path11.relative(repoRoot, absolutePath));
5019
5496
  const prepared = await prepareFileInput(rootDir, absolutePath, repoRoot, sourceClassForRelativePath(relativePath, normalizedOptions));
5020
5497
  const result = await persistPreparedInput(rootDir, prepared, paths);
5021
5498
  if (result.isNew) {
@@ -5025,7 +5502,7 @@ async function syncTrackedRepos(rootDir, options, repoRoots) {
5025
5502
  }
5026
5503
  }
5027
5504
  for (const manifest of repoManifests) {
5028
- const originalPath = manifest.originalPath ? path10.resolve(manifest.originalPath) : null;
5505
+ const originalPath = manifest.originalPath ? path11.resolve(manifest.originalPath) : null;
5029
5506
  if (originalPath && !currentPaths.has(originalPath)) {
5030
5507
  await removeManifestArtifacts(rootDir, manifest, paths);
5031
5508
  removed.push(manifest);
@@ -5033,7 +5510,7 @@ async function syncTrackedRepos(rootDir, options, repoRoots) {
5033
5510
  }
5034
5511
  }
5035
5512
  if (uniqueRoots.length > 0) {
5036
- await appendLogEntry(rootDir, "sync_repo", uniqueRoots.map((repoRoot) => toPosix(path10.relative(rootDir, repoRoot)) || ".").join(","), [
5513
+ await appendLogEntry(rootDir, "sync_repo", uniqueRoots.map((repoRoot) => toPosix(path11.relative(rootDir, repoRoot)) || ".").join(","), [
5037
5514
  `repo_roots=${uniqueRoots.length}`,
5038
5515
  `scanned=${scannedCount}`,
5039
5516
  `imported=${imported.length}`,
@@ -5056,16 +5533,16 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5056
5533
  const normalizedOptions = await resolveRepoIngestOptions(rootDir, options);
5057
5534
  const manifests = await listManifests(rootDir);
5058
5535
  const trackedRoots = (repoRoots && repoRoots.length > 0 ? repoRoots : await listTrackedRepoRoots(rootDir)).map(
5059
- (item) => path10.resolve(item)
5536
+ (item) => path11.resolve(item)
5060
5537
  );
5061
5538
  const uniqueRoots = [...new Set(trackedRoots)].sort((left, right) => left.localeCompare(right));
5062
5539
  const manifestsByRepoRoot = /* @__PURE__ */ new Map();
5063
5540
  for (const manifest of manifests) {
5064
5541
  const repoRoot = repoRootFromManifest(manifest);
5065
- if (!repoRoot || !uniqueRoots.includes(path10.resolve(repoRoot))) {
5542
+ if (!repoRoot || !uniqueRoots.includes(path11.resolve(repoRoot))) {
5066
5543
  continue;
5067
5544
  }
5068
- const key = path10.resolve(repoRoot);
5545
+ const key = path11.resolve(repoRoot);
5069
5546
  const bucket = manifestsByRepoRoot.get(key) ?? [];
5070
5547
  bucket.push(manifest);
5071
5548
  manifestsByRepoRoot.set(key, bucket);
@@ -5080,7 +5557,7 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5080
5557
  for (const repoRoot of uniqueRoots) {
5081
5558
  const repoManifests = manifestsByRepoRoot.get(repoRoot) ?? [];
5082
5559
  const manifestsByOriginalPath = new Map(
5083
- repoManifests.filter((manifest) => manifest.originalPath).map((manifest) => [path10.resolve(manifest.originalPath), manifest])
5560
+ repoManifests.filter((manifest) => manifest.originalPath).map((manifest) => [path11.resolve(manifest.originalPath), manifest])
5084
5561
  );
5085
5562
  if (!await fileExists(repoRoot)) {
5086
5563
  for (const manifest of repoManifests) {
@@ -5088,7 +5565,7 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5088
5565
  pendingSemanticRefresh.push({
5089
5566
  id: pendingSemanticRefreshId("removed", repoRoot, manifest.repoRelativePath ?? manifest.storedPath),
5090
5567
  repoRoot,
5091
- path: toPosix(path10.relative(rootDir, manifest.originalPath ?? manifest.storedPath)),
5568
+ path: toPosix(path11.relative(rootDir, manifest.originalPath ?? manifest.storedPath)),
5092
5569
  changeType: "removed",
5093
5570
  detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
5094
5571
  sourceId: manifest.sourceId,
@@ -5108,17 +5585,17 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5108
5585
  skipped.push(
5109
5586
  ...collected.skipped,
5110
5587
  ...collected.files.filter((absolutePath) => ignoreRoots.some((ignoreRoot) => withinRoot(ignoreRoot, absolutePath))).map((absolutePath) => ({
5111
- path: toPosix(path10.relative(rootDir, absolutePath)),
5588
+ path: toPosix(path11.relative(rootDir, absolutePath)),
5112
5589
  reason: "workspace_generated"
5113
5590
  }))
5114
5591
  );
5115
5592
  scannedCount += files.length;
5116
- const currentPaths = new Set(files.map((absolutePath) => path10.resolve(absolutePath)));
5593
+ const currentPaths = new Set(files.map((absolutePath) => path11.resolve(absolutePath)));
5117
5594
  for (const absolutePath of files) {
5118
- const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path10.relative(repoRoot, absolutePath));
5595
+ const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path11.relative(repoRoot, absolutePath));
5119
5596
  const prepared = await prepareFileInput(rootDir, absolutePath, repoRoot, sourceClassForRelativePath(relativePath, normalizedOptions));
5120
5597
  if (shouldDeferWatchSemanticRefresh(prepared.sourceKind)) {
5121
- const existing = manifestsByOriginalPath.get(path10.resolve(absolutePath));
5598
+ const existing = manifestsByOriginalPath.get(path11.resolve(absolutePath));
5122
5599
  const contentHash = buildCompositeHash(prepared.payloadBytes, prepared.attachments);
5123
5600
  const changed = !existing || !preparedMatchesManifest(existing, prepared, contentHash);
5124
5601
  if (changed) {
@@ -5126,10 +5603,10 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5126
5603
  id: pendingSemanticRefreshId(
5127
5604
  existing ? "modified" : "added",
5128
5605
  repoRoot,
5129
- prepared.repoRelativePath ?? toPosix(path10.relative(repoRoot, absolutePath))
5606
+ prepared.repoRelativePath ?? toPosix(path11.relative(repoRoot, absolutePath))
5130
5607
  ),
5131
5608
  repoRoot,
5132
- path: toPosix(path10.relative(rootDir, absolutePath)),
5609
+ path: toPosix(path11.relative(rootDir, absolutePath)),
5133
5610
  changeType: existing ? "modified" : "added",
5134
5611
  detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
5135
5612
  sourceId: existing?.sourceId,
@@ -5149,13 +5626,13 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5149
5626
  }
5150
5627
  }
5151
5628
  for (const manifest of repoManifests) {
5152
- const originalPath = manifest.originalPath ? path10.resolve(manifest.originalPath) : null;
5629
+ const originalPath = manifest.originalPath ? path11.resolve(manifest.originalPath) : null;
5153
5630
  if (originalPath && !currentPaths.has(originalPath)) {
5154
5631
  if (shouldDeferWatchSemanticRefresh(manifest.sourceKind)) {
5155
5632
  pendingSemanticRefresh.push({
5156
- id: pendingSemanticRefreshId("removed", repoRoot, manifest.repoRelativePath ?? toPosix(path10.relative(repoRoot, originalPath))),
5633
+ id: pendingSemanticRefreshId("removed", repoRoot, manifest.repoRelativePath ?? toPosix(path11.relative(repoRoot, originalPath))),
5157
5634
  repoRoot,
5158
- path: toPosix(path10.relative(rootDir, originalPath)),
5635
+ path: toPosix(path11.relative(rootDir, originalPath)),
5159
5636
  changeType: "removed",
5160
5637
  detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
5161
5638
  sourceId: manifest.sourceId,
@@ -5173,7 +5650,7 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5173
5650
  await appendLogEntry(
5174
5651
  rootDir,
5175
5652
  "sync_repo_watch",
5176
- uniqueRoots.map((repoRoot) => toPosix(path10.relative(rootDir, repoRoot)) || ".").join(","),
5653
+ uniqueRoots.map((repoRoot) => toPosix(path11.relative(rootDir, repoRoot)) || ".").join(","),
5177
5654
  [
5178
5655
  `repo_roots=${uniqueRoots.length}`,
5179
5656
  `scanned=${scannedCount}`,
@@ -5199,17 +5676,17 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
5199
5676
  };
5200
5677
  }
5201
5678
  async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
5202
- const payloadBytes = await fs9.readFile(absoluteInput);
5679
+ const payloadBytes = await fs10.readFile(absoluteInput);
5203
5680
  const mimeType = guessMimeType(absoluteInput);
5204
5681
  const sourceKind = inferKind(mimeType, absoluteInput);
5205
5682
  const language = inferCodeLanguage(absoluteInput, mimeType);
5206
- const storedExtension = path10.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
5683
+ const storedExtension = path11.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
5207
5684
  let title;
5208
5685
  let extractedText;
5209
5686
  let extractionArtifact;
5210
5687
  if (sourceKind === "markdown" || sourceKind === "text" || sourceKind === "code") {
5211
5688
  extractedText = payloadBytes.toString("utf8");
5212
- title = titleFromText(path10.basename(absoluteInput, path10.extname(absoluteInput)), extractedText);
5689
+ title = titleFromText(path11.basename(absoluteInput, path11.extname(absoluteInput)), extractedText);
5213
5690
  extractionArtifact = createPlainTextExtractionArtifact(sourceKind, mimeType);
5214
5691
  } else if (sourceKind === "html") {
5215
5692
  const html = payloadBytes.toString("utf8");
@@ -5218,18 +5695,18 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
5218
5695
  extractedText = converted.markdown;
5219
5696
  extractionArtifact = createHtmlReadabilityExtractionArtifact(sourceKind, mimeType);
5220
5697
  } else if (sourceKind === "pdf") {
5221
- title = path10.basename(absoluteInput, path10.extname(absoluteInput));
5698
+ title = path11.basename(absoluteInput, path11.extname(absoluteInput));
5222
5699
  const extracted = await extractPdfText({ mimeType, bytes: payloadBytes });
5223
5700
  extractedText = extracted.extractedText;
5224
5701
  extractionArtifact = extracted.artifact;
5225
5702
  } else if (sourceKind === "docx") {
5226
- title = path10.basename(absoluteInput, path10.extname(absoluteInput));
5703
+ title = path11.basename(absoluteInput, path11.extname(absoluteInput));
5227
5704
  const extracted = await extractDocxText({ mimeType, bytes: payloadBytes });
5228
5705
  title = extracted.artifact.metadata?.title?.trim() || title;
5229
5706
  extractedText = extracted.extractedText;
5230
5707
  extractionArtifact = extracted.artifact;
5231
5708
  } else if (sourceKind === "image") {
5232
- title = path10.basename(absoluteInput, path10.extname(absoluteInput));
5709
+ title = path11.basename(absoluteInput, path11.extname(absoluteInput));
5233
5710
  const extracted = await extractImageWithVision(rootDir, {
5234
5711
  title,
5235
5712
  mimeType,
@@ -5239,7 +5716,7 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
5239
5716
  extractedText = extracted.extractedText;
5240
5717
  extractionArtifact = extracted.artifact;
5241
5718
  } else {
5242
- title = path10.basename(absoluteInput, path10.extname(absoluteInput));
5719
+ title = path11.basename(absoluteInput, path11.extname(absoluteInput));
5243
5720
  }
5244
5721
  return {
5245
5722
  title,
@@ -5258,6 +5735,7 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
5258
5735
  };
5259
5736
  }
5260
5737
  async function prepareUrlInput(rootDir, input, options) {
5738
+ await validateUrlSafety(input);
5261
5739
  const response = await fetch(input);
5262
5740
  if (!response.ok) {
5263
5741
  throw new Error(`Failed to fetch ${input}: ${response.status} ${response.statusText}`);
@@ -5315,7 +5793,7 @@ async function prepareUrlInput(rootDir, input, options) {
5315
5793
  sourceKind = "markdown";
5316
5794
  storedExtension = ".md";
5317
5795
  } else {
5318
- const extension = path10.extname(inputUrl.pathname);
5796
+ const extension = path11.extname(inputUrl.pathname);
5319
5797
  storedExtension = extension || `.${mime.extension(mimeType) || "bin"}`;
5320
5798
  if (sourceKind === "markdown" || sourceKind === "text" || sourceKind === "code") {
5321
5799
  extractedText = payloadBytes.toString("utf8");
@@ -5386,14 +5864,14 @@ async function collectInboxAttachmentRefs(inputDir, files) {
5386
5864
  if (sourceKind !== "markdown" && sourceKind !== "html") {
5387
5865
  continue;
5388
5866
  }
5389
- const content = await fs9.readFile(absolutePath, "utf8");
5867
+ const content = await fs10.readFile(absolutePath, "utf8");
5390
5868
  const refs = sourceKind === "html" ? extractHtmlLocalReferences(content, pathToFileURL(absolutePath).toString()) : extractMarkdownReferences(content);
5391
5869
  if (!refs.length) {
5392
5870
  continue;
5393
5871
  }
5394
5872
  const sourceRefs = [];
5395
5873
  for (const ref of refs) {
5396
- const resolved = path10.resolve(path10.dirname(absolutePath), ref);
5874
+ const resolved = path11.resolve(path11.dirname(absolutePath), ref);
5397
5875
  if (!resolved.startsWith(inputDir) || !await fileExists(resolved)) {
5398
5876
  continue;
5399
5877
  }
@@ -5427,12 +5905,12 @@ function rewriteMarkdownReferences(content, replacements) {
5427
5905
  });
5428
5906
  }
5429
5907
  async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
5430
- const originalBytes = await fs9.readFile(absolutePath);
5908
+ const originalBytes = await fs10.readFile(absolutePath);
5431
5909
  const originalText = originalBytes.toString("utf8");
5432
- const title = titleFromText(path10.basename(absolutePath, path10.extname(absolutePath)), originalText);
5910
+ const title = titleFromText(path11.basename(absolutePath, path11.extname(absolutePath)), originalText);
5433
5911
  const attachments = [];
5434
5912
  for (const attachmentRef of attachmentRefs) {
5435
- const bytes = await fs9.readFile(attachmentRef.absolutePath);
5913
+ const bytes = await fs10.readFile(attachmentRef.absolutePath);
5436
5914
  attachments.push({
5437
5915
  relativePath: sanitizeAssetRelativePath(attachmentRef.relativeRef),
5438
5916
  mimeType: guessMimeType(attachmentRef.absolutePath),
@@ -5456,7 +5934,7 @@ async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
5456
5934
  sourceKind: "markdown",
5457
5935
  originalPath: toPosix(absolutePath),
5458
5936
  mimeType: "text/markdown",
5459
- storedExtension: path10.extname(absolutePath) || ".md",
5937
+ storedExtension: path11.extname(absolutePath) || ".md",
5460
5938
  payloadBytes: Buffer.from(rewrittenText, "utf8"),
5461
5939
  extractedText: rewrittenText,
5462
5940
  extractionArtifact,
@@ -5466,12 +5944,12 @@ async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
5466
5944
  };
5467
5945
  }
5468
5946
  async function prepareInboxHtmlInput(absolutePath, attachmentRefs) {
5469
- const originalBytes = await fs9.readFile(absolutePath);
5947
+ const originalBytes = await fs10.readFile(absolutePath);
5470
5948
  const originalHtml = originalBytes.toString("utf8");
5471
5949
  const initialConversion = await convertHtmlToMarkdown(originalHtml, pathToFileURL(absolutePath).toString());
5472
5950
  const attachments = [];
5473
5951
  for (const attachmentRef of attachmentRefs) {
5474
- const bytes = await fs9.readFile(attachmentRef.absolutePath);
5952
+ const bytes = await fs10.readFile(attachmentRef.absolutePath);
5475
5953
  attachments.push({
5476
5954
  relativePath: sanitizeAssetRelativePath(attachmentRef.relativeRef),
5477
5955
  mimeType: guessMimeType(attachmentRef.absolutePath),
@@ -5480,7 +5958,7 @@ async function prepareInboxHtmlInput(absolutePath, attachmentRefs) {
5480
5958
  });
5481
5959
  }
5482
5960
  const contentHash = buildCompositeHash(originalBytes, attachments);
5483
- const fallbackTitle = path10.basename(absolutePath, path10.extname(absolutePath));
5961
+ const fallbackTitle = path11.basename(absolutePath, path11.extname(absolutePath));
5484
5962
  const title = initialConversion.title || fallbackTitle;
5485
5963
  const sourceId = `${slugify(title)}-${contentHash.slice(0, 8)}`;
5486
5964
  const replacements = new Map(
@@ -5498,7 +5976,7 @@ async function prepareInboxHtmlInput(absolutePath, attachmentRefs) {
5498
5976
  sourceKind: "html",
5499
5977
  originalPath: toPosix(absolutePath),
5500
5978
  mimeType: "text/html",
5501
- storedExtension: path10.extname(absolutePath) || ".html",
5979
+ storedExtension: path11.extname(absolutePath) || ".html",
5502
5980
  payloadBytes: Buffer.from(rewrittenHtml, "utf8"),
5503
5981
  extractedText: converted.markdown,
5504
5982
  extractionArtifact,
@@ -5513,8 +5991,8 @@ function isSupportedInboxKind(sourceKind) {
5513
5991
  async function ingestInput(rootDir, input, options) {
5514
5992
  const { paths } = await initWorkspace(rootDir);
5515
5993
  const normalizedOptions = normalizeIngestOptions(options);
5516
- const absoluteInput = path10.resolve(rootDir, input);
5517
- const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await findNearestGitRoot2(absoluteInput).then((value) => value ?? path10.dirname(absoluteInput));
5994
+ const absoluteInput = path11.resolve(rootDir, input);
5995
+ const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await findNearestGitRoot2(absoluteInput).then((value) => value ?? path11.dirname(absoluteInput));
5518
5996
  const prepared = isHttpUrl(input) ? await prepareUrlInput(rootDir, input, normalizedOptions) : await prepareFileInput(rootDir, absoluteInput, repoRoot);
5519
5997
  const result = await persistPreparedInput(rootDir, prepared, paths);
5520
5998
  return result.manifest;
@@ -5604,7 +6082,7 @@ async function addInput(rootDir, input, options = {}) {
5604
6082
  async function ingestDirectory(rootDir, inputDir, options) {
5605
6083
  const { paths } = await initWorkspace(rootDir);
5606
6084
  const normalizedOptions = await resolveRepoIngestOptions(rootDir, options);
5607
- const absoluteInputDir = path10.resolve(rootDir, inputDir);
6085
+ const absoluteInputDir = path11.resolve(rootDir, inputDir);
5608
6086
  const repoRoot = normalizedOptions.repoRoot ?? await findNearestGitRoot2(absoluteInputDir) ?? absoluteInputDir;
5609
6087
  if (!await fileExists(absoluteInputDir)) {
5610
6088
  throw new Error(`Directory not found: ${absoluteInputDir}`);
@@ -5613,7 +6091,7 @@ async function ingestDirectory(rootDir, inputDir, options) {
5613
6091
  const imported = [];
5614
6092
  const updated = [];
5615
6093
  for (const absolutePath of files) {
5616
- const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path10.relative(repoRoot, absolutePath));
6094
+ const relativePath = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path11.relative(repoRoot, absolutePath));
5617
6095
  const prepared = await prepareFileInput(rootDir, absolutePath, repoRoot, sourceClassForRelativePath(relativePath, normalizedOptions));
5618
6096
  const result = await persistPreparedInput(rootDir, prepared, paths);
5619
6097
  if (result.isNew) {
@@ -5621,11 +6099,11 @@ async function ingestDirectory(rootDir, inputDir, options) {
5621
6099
  } else if (result.wasUpdated) {
5622
6100
  updated.push(result.manifest);
5623
6101
  } else {
5624
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "duplicate_content" });
6102
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "duplicate_content" });
5625
6103
  }
5626
6104
  }
5627
- await appendLogEntry(rootDir, "ingest_directory", toPosix(path10.relative(rootDir, absoluteInputDir)) || ".", [
5628
- `repo_root=${toPosix(path10.relative(rootDir, repoRoot)) || "."}`,
6105
+ await appendLogEntry(rootDir, "ingest_directory", toPosix(path11.relative(rootDir, absoluteInputDir)) || ".", [
6106
+ `repo_root=${toPosix(path11.relative(rootDir, repoRoot)) || "."}`,
5629
6107
  `scanned=${files.length}`,
5630
6108
  `imported=${imported.length}`,
5631
6109
  `updated=${updated.length}`,
@@ -5642,7 +6120,7 @@ async function ingestDirectory(rootDir, inputDir, options) {
5642
6120
  }
5643
6121
  async function importInbox(rootDir, inputDir) {
5644
6122
  const { paths } = await initWorkspace(rootDir);
5645
- const effectiveInputDir = path10.resolve(rootDir, inputDir ?? paths.inboxDir);
6123
+ const effectiveInputDir = path11.resolve(rootDir, inputDir ?? paths.inboxDir);
5646
6124
  if (!await fileExists(effectiveInputDir)) {
5647
6125
  throw new Error(`Inbox directory not found: ${effectiveInputDir}`);
5648
6126
  }
@@ -5653,31 +6131,31 @@ async function importInbox(rootDir, inputDir) {
5653
6131
  const skipped = [];
5654
6132
  let attachmentCount = 0;
5655
6133
  for (const absolutePath of files) {
5656
- const basename = path10.basename(absolutePath);
6134
+ const basename = path11.basename(absolutePath);
5657
6135
  if (basename.startsWith(".")) {
5658
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "hidden_file" });
6136
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "hidden_file" });
5659
6137
  continue;
5660
6138
  }
5661
6139
  if (claimedAttachments.has(absolutePath)) {
5662
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "referenced_attachment" });
6140
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "referenced_attachment" });
5663
6141
  continue;
5664
6142
  }
5665
6143
  const mimeType = guessMimeType(absolutePath);
5666
6144
  const sourceKind = inferKind(mimeType, absolutePath);
5667
6145
  if (!isSupportedInboxKind(sourceKind)) {
5668
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
6146
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
5669
6147
  continue;
5670
6148
  }
5671
6149
  const prepared = sourceKind === "markdown" && refsBySource.has(absolutePath) ? await prepareInboxMarkdownInput(absolutePath, refsBySource.get(absolutePath) ?? []) : sourceKind === "html" && refsBySource.has(absolutePath) ? await prepareInboxHtmlInput(absolutePath, refsBySource.get(absolutePath) ?? []) : await prepareFileInput(rootDir, absolutePath);
5672
6150
  const result = await persistPreparedInput(rootDir, prepared, paths);
5673
6151
  if (!result.isNew) {
5674
- skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "duplicate_content" });
6152
+ skipped.push({ path: toPosix(path11.relative(rootDir, absolutePath)), reason: "duplicate_content" });
5675
6153
  continue;
5676
6154
  }
5677
6155
  attachmentCount += result.manifest.attachments?.length ?? 0;
5678
6156
  imported.push(result.manifest);
5679
6157
  }
5680
- await appendLogEntry(rootDir, "inbox_import", toPosix(path10.relative(rootDir, effectiveInputDir)) || ".", [
6158
+ await appendLogEntry(rootDir, "inbox_import", toPosix(path11.relative(rootDir, effectiveInputDir)) || ".", [
5681
6159
  `scanned=${files.length}`,
5682
6160
  `imported=${imported.length}`,
5683
6161
  `attachments=${attachmentCount}`,
@@ -5696,9 +6174,9 @@ async function listManifests(rootDir) {
5696
6174
  if (!await fileExists(paths.manifestsDir)) {
5697
6175
  return [];
5698
6176
  }
5699
- const entries = await fs9.readdir(paths.manifestsDir);
6177
+ const entries = await fs10.readdir(paths.manifestsDir);
5700
6178
  const manifests = await Promise.all(
5701
- entries.filter((entry) => entry.endsWith(".json")).map((entry) => readJsonFile(path10.join(paths.manifestsDir, entry)))
6179
+ entries.filter((entry) => entry.endsWith(".json")).map((entry) => readJsonFile(path11.join(paths.manifestsDir, entry)))
5702
6180
  );
5703
6181
  return manifests.filter((manifest) => Boolean(manifest));
5704
6182
  }
@@ -5706,17 +6184,17 @@ async function readExtractedText(rootDir, manifest) {
5706
6184
  if (!manifest.extractedTextPath) {
5707
6185
  return void 0;
5708
6186
  }
5709
- const absolutePath = path10.resolve(rootDir, manifest.extractedTextPath);
6187
+ const absolutePath = path11.resolve(rootDir, manifest.extractedTextPath);
5710
6188
  if (!await fileExists(absolutePath)) {
5711
6189
  return void 0;
5712
6190
  }
5713
- return fs9.readFile(absolutePath, "utf8");
6191
+ return fs10.readFile(absolutePath, "utf8");
5714
6192
  }
5715
6193
  async function readExtractionArtifact(rootDir, manifest) {
5716
6194
  if (!manifest.extractedMetadataPath) {
5717
6195
  return void 0;
5718
6196
  }
5719
- const absolutePath = path10.resolve(rootDir, manifest.extractedMetadataPath);
6197
+ const absolutePath = path11.resolve(rootDir, manifest.extractedMetadataPath);
5720
6198
  if (!await fileExists(absolutePath)) {
5721
6199
  return void 0;
5722
6200
  }
@@ -5724,20 +6202,20 @@ async function readExtractionArtifact(rootDir, manifest) {
5724
6202
  }
5725
6203
 
5726
6204
  // src/mcp.ts
5727
- import fs17 from "fs/promises";
5728
- import path21 from "path";
6205
+ import fs18 from "fs/promises";
6206
+ import path22 from "path";
5729
6207
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5730
6208
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5731
6209
  import { z as z8 } from "zod";
5732
6210
 
5733
6211
  // src/schema.ts
5734
- import fs10 from "fs/promises";
5735
- import path11 from "path";
6212
+ import fs11 from "fs/promises";
6213
+ import path12 from "path";
5736
6214
  function normalizeSchemaContent(content) {
5737
6215
  return content.trim() ? content.trim() : defaultVaultSchema().trim();
5738
6216
  }
5739
6217
  async function readSchemaFile(schemaPath, fallback = defaultVaultSchema()) {
5740
- const content = await fileExists(schemaPath) ? await fs10.readFile(schemaPath, "utf8") : fallback;
6218
+ const content = await fileExists(schemaPath) ? await fs11.readFile(schemaPath, "utf8") : fallback;
5741
6219
  const normalized = normalizeSchemaContent(content);
5742
6220
  return {
5743
6221
  path: schemaPath,
@@ -5746,7 +6224,7 @@ async function readSchemaFile(schemaPath, fallback = defaultVaultSchema()) {
5746
6224
  };
5747
6225
  }
5748
6226
  function resolveProjectSchemaPath(rootDir, schemaPath) {
5749
- return path11.resolve(rootDir, schemaPath);
6227
+ return path12.resolve(rootDir, schemaPath);
5750
6228
  }
5751
6229
  function composeVaultSchema(root, projectSchemas = []) {
5752
6230
  if (!projectSchemas.length) {
@@ -5762,7 +6240,7 @@ function composeVaultSchema(root, projectSchemas = []) {
5762
6240
  (schema) => [
5763
6241
  `## Project Schema`,
5764
6242
  "",
5765
- `Path: ${toPosix(path11.relative(path11.dirname(root.path), schema.path) || schema.path)}`,
6243
+ `Path: ${toPosix(path12.relative(path12.dirname(root.path), schema.path) || schema.path)}`,
5766
6244
  "",
5767
6245
  schema.content
5768
6246
  ].join("\n")
@@ -5838,15 +6316,15 @@ function buildSchemaPrompt(schema, instruction) {
5838
6316
  }
5839
6317
 
5840
6318
  // src/vault.ts
5841
- import fs16 from "fs/promises";
5842
- import path20 from "path";
6319
+ import fs17 from "fs/promises";
6320
+ import path21 from "path";
5843
6321
  import matter9 from "gray-matter";
5844
6322
  import { z as z7 } from "zod";
5845
6323
 
5846
6324
  // src/analysis.ts
5847
- import path12 from "path";
6325
+ import path13 from "path";
5848
6326
  import { z as z2 } from "zod";
5849
- var ANALYSIS_FORMAT_VERSION = 5;
6327
+ var ANALYSIS_FORMAT_VERSION = 6;
5850
6328
  var sourceAnalysisSchema = z2.object({
5851
6329
  title: z2.string().min(1),
5852
6330
  summary: z2.string().min(1),
@@ -5861,7 +6339,8 @@ var sourceAnalysisSchema = z2.object({
5861
6339
  citation: z2.string().min(1)
5862
6340
  })
5863
6341
  ).max(8).default([]),
5864
- questions: z2.array(z2.string()).max(6).default([])
6342
+ questions: z2.array(z2.string()).max(6).default([]),
6343
+ tags: z2.array(z2.string()).max(5).default([])
5865
6344
  });
5866
6345
  var STOPWORDS = /* @__PURE__ */ new Set([
5867
6346
  "about",
@@ -5965,6 +6444,7 @@ function heuristicAnalysis(manifest, text, schemaHash) {
5965
6444
  citation: manifest.sourceId
5966
6445
  })),
5967
6446
  questions: concepts.slice(0, 3).map((term) => `How does ${term.name} relate to ${manifest.title}?`),
6447
+ tags: [],
5968
6448
  rationales: [],
5969
6449
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
5970
6450
  };
@@ -5977,6 +6457,8 @@ async function providerAnalysis(manifest, text, provider, schema) {
5977
6457
  "",
5978
6458
  "Follow the vault schema when choosing titles, categories, relationships, and summaries.",
5979
6459
  "",
6460
+ "Return up to 5 broad domain tags that categorize this source. Tags should be lowercase kebab-case (e.g., cryptography, distributed-systems, machine-learning). These are broader categories, not specific concepts or entity names.",
6461
+ "",
5980
6462
  `Vault schema path: ${schema.path}`,
5981
6463
  "",
5982
6464
  "Vault schema instructions:",
@@ -6020,6 +6502,7 @@ ${truncate(text, 18e3)}`
6020
6502
  citation: claim.citation
6021
6503
  })),
6022
6504
  questions: parsed.questions,
6505
+ tags: parsed.tags,
6023
6506
  rationales: [],
6024
6507
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6025
6508
  };
@@ -6055,6 +6538,7 @@ function analysisFromVisionExtraction(manifest, extraction, schemaHash) {
6055
6538
  citation: manifest.sourceId
6056
6539
  })),
6057
6540
  questions: extraction.vision.questions,
6541
+ tags: [],
6058
6542
  rationales: [],
6059
6543
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6060
6544
  };
@@ -6067,7 +6551,7 @@ function extractionWarningSummary(manifest, extraction) {
6067
6551
  return `Imported ${manifest.sourceKind} source. Text extraction is not yet available for this source.`;
6068
6552
  }
6069
6553
  async function analyzeSource(manifest, extractedText, provider, paths, schema) {
6070
- const cachePath = path12.join(paths.analysesDir, `${manifest.sourceId}.json`);
6554
+ const cachePath = path13.join(paths.analysesDir, `${manifest.sourceId}.json`);
6071
6555
  const cached = await readJsonFile(cachePath);
6072
6556
  if (cached && cached.analysisVersion === ANALYSIS_FORMAT_VERSION && cached.sourceHash === manifest.contentHash && cached.extractionHash === manifest.extractionHash && cached.schemaHash === schema.hash) {
6073
6557
  return cached;
@@ -6094,6 +6578,7 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
6094
6578
  entities: [],
6095
6579
  claims: [],
6096
6580
  questions: [],
6581
+ tags: [],
6097
6582
  rationales: [],
6098
6583
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6099
6584
  };
@@ -6119,6 +6604,7 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
6119
6604
  entities: [],
6120
6605
  claims: [],
6121
6606
  questions: [],
6607
+ tags: [],
6122
6608
  rationales: [],
6123
6609
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6124
6610
  };
@@ -6138,163 +6624,6 @@ function analysisSignature(analysis) {
6138
6624
  return sha256(JSON.stringify(analysis));
6139
6625
  }
6140
6626
 
6141
- // src/benchmark.ts
6142
- var CHARS_PER_TOKEN = 4;
6143
- var DEFAULT_BENCHMARK_QUESTIONS = [
6144
- "How does this vault connect the main concepts?",
6145
- "Which pages bridge the biggest communities?",
6146
- "What are the core abstractions in this vault?",
6147
- "Where are the biggest knowledge gaps?",
6148
- "What evidence should I read first?"
6149
- ];
6150
- var RESEARCH_BENCHMARK_QUESTION = "Which research sources should I read first, and why?";
6151
- function nodeMap(graph) {
6152
- return new Map(graph.nodes.map((node) => [node.id, node]));
6153
- }
6154
- function pageMap(graph) {
6155
- return new Map(graph.pages.map((page) => [page.id, page]));
6156
- }
6157
- function estimateTokens(text) {
6158
- return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
6159
- }
6160
- function estimateCorpusWords(texts) {
6161
- return texts.reduce((total, text) => total + normalizeWhitespace(text).split(/\s+/).filter(Boolean).length, 0);
6162
- }
6163
- function benchmarkQueryTokens(graph, queryResult, pageContentsById) {
6164
- const nodesById = nodeMap(graph);
6165
- const pagesById = pageMap(graph);
6166
- const edgeIds = new Set(queryResult.visitedEdgeIds);
6167
- const lines = [];
6168
- for (const pageId of queryResult.pageIds) {
6169
- const page = pagesById.get(pageId);
6170
- if (!page) {
6171
- continue;
6172
- }
6173
- const content = normalizeWhitespace(pageContentsById.get(pageId) ?? "").slice(0, 280);
6174
- lines.push(`PAGE ${page.title} path=${page.path} kind=${page.kind}`);
6175
- if (content) {
6176
- lines.push(`PAGE_BODY ${content}`);
6177
- }
6178
- }
6179
- for (const nodeId of queryResult.visitedNodeIds) {
6180
- const node = nodesById.get(nodeId);
6181
- if (!node) {
6182
- continue;
6183
- }
6184
- lines.push(`NODE ${node.label} type=${node.type} community=${node.communityId ?? "unassigned"} page=${node.pageId ?? "none"}`);
6185
- }
6186
- for (const edge of graph.edges) {
6187
- if (!edgeIds.has(edge.id)) {
6188
- continue;
6189
- }
6190
- const source = nodesById.get(edge.source)?.label ?? edge.source;
6191
- const target = nodesById.get(edge.target)?.label ?? edge.target;
6192
- lines.push(`EDGE ${source} --${edge.relation}/${edge.evidenceClass}/${edge.confidence.toFixed(2)}--> ${target}`);
6193
- }
6194
- const queryTokens = estimateTokens(lines.join("\n"));
6195
- return {
6196
- question: queryResult.question,
6197
- queryTokens,
6198
- reduction: 0,
6199
- visitedNodeIds: queryResult.visitedNodeIds,
6200
- visitedEdgeIds: queryResult.visitedEdgeIds,
6201
- pageIds: queryResult.pageIds
6202
- };
6203
- }
6204
- function graphHash(graph) {
6205
- const hashedPages = graph.pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary");
6206
- const normalized = JSON.stringify(
6207
- {
6208
- nodes: [...graph.nodes].map((node) => ({
6209
- id: node.id,
6210
- type: node.type,
6211
- label: node.label,
6212
- pageId: node.pageId ?? null,
6213
- sourceClass: node.sourceClass ?? null,
6214
- communityId: node.communityId ?? null,
6215
- degree: node.degree ?? null,
6216
- bridgeScore: node.bridgeScore ?? null,
6217
- isGodNode: node.isGodNode ?? false,
6218
- sourceIds: [...node.sourceIds].sort(),
6219
- projectIds: [...node.projectIds].sort()
6220
- })).sort((left, right) => left.id.localeCompare(right.id)),
6221
- edges: [...graph.edges].map((edge) => ({
6222
- id: edge.id,
6223
- source: edge.source,
6224
- target: edge.target,
6225
- relation: edge.relation,
6226
- status: edge.status,
6227
- evidenceClass: edge.evidenceClass,
6228
- similarityBasis: edge.similarityBasis ?? null,
6229
- confidence: edge.confidence,
6230
- provenance: [...edge.provenance].sort()
6231
- })).sort((left, right) => left.id.localeCompare(right.id)),
6232
- pages: [...hashedPages].map((page) => ({
6233
- id: page.id,
6234
- path: page.path,
6235
- kind: page.kind,
6236
- status: page.status,
6237
- sourceType: page.sourceType ?? null,
6238
- sourceClass: page.sourceClass ?? null,
6239
- sourceIds: [...page.sourceIds].sort(),
6240
- projectIds: [...page.projectIds].sort(),
6241
- nodeIds: [...page.nodeIds].sort()
6242
- })).sort((left, right) => left.id.localeCompare(right.id)),
6243
- communities: [...graph.communities ?? []].map((community) => ({
6244
- id: community.id,
6245
- label: community.label,
6246
- nodeIds: [...community.nodeIds].sort()
6247
- })).sort((left, right) => left.id.localeCompare(right.id))
6248
- },
6249
- null,
6250
- 0
6251
- );
6252
- return sha256(normalized);
6253
- }
6254
- function hasResearchSources(pages) {
6255
- return pages.some((page) => page.kind === "source" && Boolean(page.sourceType) && page.sourceType !== "url");
6256
- }
6257
- function defaultBenchmarkQuestionsForGraph(graph, maxQuestions = 3) {
6258
- const normalizedLimit = Math.max(1, Math.min(maxQuestions, DEFAULT_BENCHMARK_QUESTIONS.length));
6259
- const questions = [...DEFAULT_BENCHMARK_QUESTIONS];
6260
- if (hasResearchSources(graph.pages)) {
6261
- questions.unshift(RESEARCH_BENCHMARK_QUESTION);
6262
- }
6263
- return uniqueBy(questions, (item) => item).slice(0, normalizedLimit);
6264
- }
6265
- function buildBenchmarkArtifact(input) {
6266
- const corpusTokens = Math.max(1, Math.round(input.corpusWords * (100 / 75)));
6267
- const perQuestion = input.perQuestion.filter((entry) => entry.queryTokens > 0).map((entry) => ({
6268
- ...entry,
6269
- reduction: Number(Math.max(0, 1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
6270
- }));
6271
- const avgQueryTokens = perQuestion.length ? Math.max(1, Math.round(perQuestion.reduce((total, entry) => total + entry.queryTokens, 0) / perQuestion.length)) : 0;
6272
- const reductionRatio = avgQueryTokens ? Number(Math.max(0, 1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
6273
- const uniqueVisitedNodes = new Set(perQuestion.flatMap((entry) => entry.visitedNodeIds)).size;
6274
- const summary = {
6275
- questionCount: input.questions.length,
6276
- uniqueVisitedNodes,
6277
- finalContextTokens: avgQueryTokens,
6278
- naiveCorpusTokens: corpusTokens,
6279
- avgReduction: reductionRatio,
6280
- reductionRatio
6281
- };
6282
- return {
6283
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6284
- graphHash: graphHash(input.graph),
6285
- corpusWords: input.corpusWords,
6286
- corpusTokens,
6287
- nodes: input.graph.nodes.length,
6288
- edges: input.graph.edges.length,
6289
- avgQueryTokens,
6290
- reductionRatio,
6291
- sampleQuestions: input.questions,
6292
- perQuestion,
6293
- questionResults: perQuestion,
6294
- summary
6295
- };
6296
- }
6297
-
6298
6627
  // src/confidence.ts
6299
6628
  function nodeConfidence(sourceCount) {
6300
6629
  return Math.min(0.5 + sourceCount * 0.15, 0.95);
@@ -6312,8 +6641,8 @@ function conflictConfidence(claimA, claimB) {
6312
6641
  }
6313
6642
 
6314
6643
  // src/deep-lint.ts
6315
- import fs11 from "fs/promises";
6316
- import path15 from "path";
6644
+ import fs12 from "fs/promises";
6645
+ import path16 from "path";
6317
6646
  import matter4 from "gray-matter";
6318
6647
  import { z as z5 } from "zod";
6319
6648
 
@@ -6334,7 +6663,7 @@ function normalizeFindingSeverity(value) {
6334
6663
 
6335
6664
  // src/orchestration.ts
6336
6665
  import { spawn } from "child_process";
6337
- import path13 from "path";
6666
+ import path14 from "path";
6338
6667
  import { z as z3 } from "zod";
6339
6668
  var orchestrationRoleResultSchema = z3.object({
6340
6669
  summary: z3.string().optional(),
@@ -6427,7 +6756,7 @@ async function runProviderRole(rootDir, role, roleConfig, input) {
6427
6756
  }
6428
6757
  async function runCommandRole(rootDir, role, executor, input) {
6429
6758
  const [command, ...args] = executor.command;
6430
- const cwd = executor.cwd ? path13.resolve(rootDir, executor.cwd) : rootDir;
6759
+ const cwd = executor.cwd ? path14.resolve(rootDir, executor.cwd) : rootDir;
6431
6760
  const child = spawn(command, args, {
6432
6761
  cwd,
6433
6762
  env: {
@@ -6521,7 +6850,7 @@ function summarizeRoleQuestions(results) {
6521
6850
  }
6522
6851
 
6523
6852
  // src/web-search/registry.ts
6524
- import path14 from "path";
6853
+ import path15 from "path";
6525
6854
  import { pathToFileURL as pathToFileURL2 } from "url";
6526
6855
  import { z as z4 } from "zod";
6527
6856
 
@@ -6619,7 +6948,7 @@ async function createWebSearchAdapter(id, config, rootDir) {
6619
6948
  if (!config.module) {
6620
6949
  throw new Error(`Web search provider ${id} is type "custom" but no module path was configured.`);
6621
6950
  }
6622
- const resolvedModule = path14.isAbsolute(config.module) ? config.module : path14.resolve(rootDir, config.module);
6951
+ const resolvedModule = path15.isAbsolute(config.module) ? config.module : path15.resolve(rootDir, config.module);
6623
6952
  const loaded = await import(pathToFileURL2(resolvedModule).href);
6624
6953
  const parsed = customWebSearchModuleSchema.parse(loaded);
6625
6954
  return parsed.createAdapter(id, config, rootDir);
@@ -6647,7 +6976,14 @@ var deepLintResponseSchema = z5.object({
6647
6976
  findings: z5.array(
6648
6977
  z5.object({
6649
6978
  severity: z5.string().optional().default("info"),
6650
- code: z5.enum(["coverage_gap", "contradiction_candidate", "missing_citation", "candidate_page", "follow_up_question"]),
6979
+ code: z5.enum([
6980
+ "coverage_gap",
6981
+ "contradiction_candidate",
6982
+ "contradiction",
6983
+ "missing_citation",
6984
+ "candidate_page",
6985
+ "follow_up_question"
6986
+ ]),
6651
6987
  message: z5.string().min(1),
6652
6988
  relatedSourceIds: z5.array(z5.string()).default([]),
6653
6989
  relatedPageIds: z5.array(z5.string()).default([]),
@@ -6679,8 +7015,8 @@ async function loadContextPages(rootDir, graph) {
6679
7015
  );
6680
7016
  return Promise.all(
6681
7017
  contextPages.slice(0, 18).map(async (page) => {
6682
- const absolutePath = path15.join(paths.wikiDir, page.path);
6683
- const raw = await fs11.readFile(absolutePath, "utf8").catch(() => "");
7018
+ const absolutePath = path16.join(paths.wikiDir, page.path);
7019
+ const raw = await fs12.readFile(absolutePath, "utf8").catch(() => "");
6684
7020
  const parsed = matter4(raw);
6685
7021
  return {
6686
7022
  id: page.id,
@@ -6728,7 +7064,7 @@ function heuristicDeepFindings(contextPages, structuralFindings, graph) {
6728
7064
  code: "missing_citation",
6729
7065
  message: finding.message,
6730
7066
  pagePath: finding.pagePath,
6731
- suggestedQuery: finding.pagePath ? `Which sources support the claims in ${path15.basename(finding.pagePath, ".md")}?` : void 0
7067
+ suggestedQuery: finding.pagePath ? `Which sources support the claims in ${path16.basename(finding.pagePath, ".md")}?` : void 0
6732
7068
  });
6733
7069
  }
6734
7070
  for (const page of contextPages.filter((item) => item.kind === "source").slice(0, 3)) {
@@ -6780,6 +7116,7 @@ async function runDeepLint(rootDir, structuralFindings, options = {}) {
6780
7116
  system: "You are an auditor for a local-first LLM knowledge vault. Return advisory findings only. Do not propose direct file edits.",
6781
7117
  prompt: [
6782
7118
  "Review this SwarmVault state and return high-signal advisory findings.",
7119
+ "Look for claims that contradict each other across different sources. When you find a genuine contradiction, use code 'contradiction' and include both source IDs in relatedSourceIds.",
6783
7120
  "",
6784
7121
  "Schema:",
6785
7122
  schema.content,
@@ -6906,8 +7243,8 @@ async function runDeepLint(rootDir, structuralFindings, options = {}) {
6906
7243
  }
6907
7244
 
6908
7245
  // src/embeddings.ts
6909
- import fs12 from "fs/promises";
6910
- import path16 from "path";
7246
+ import fs13 from "fs/promises";
7247
+ import path17 from "path";
6911
7248
  var MAX_EMBEDDING_BATCH = 32;
6912
7249
  var MAX_SIMILARITY_NODES = 240;
6913
7250
  function cosineSimilarity(left, right) {
@@ -6941,8 +7278,8 @@ async function loadPageContents(rootDir, graph) {
6941
7278
  const contents = /* @__PURE__ */ new Map();
6942
7279
  await Promise.all(
6943
7280
  graph.pages.map(async (page) => {
6944
- const absolutePath = path16.join(paths.wikiDir, page.path);
6945
- const content = await fs12.readFile(absolutePath, "utf8").catch(() => {
7281
+ const absolutePath = path17.join(paths.wikiDir, page.path);
7282
+ const content = await fs13.readFile(absolutePath, "utf8").catch(() => {
6946
7283
  process.stderr.write(`[swarmvault] Warning: could not read page ${page.path} for embedding
6947
7284
  `);
6948
7285
  return "";
@@ -8444,15 +8781,15 @@ function sourceTypeForNode(node, pagesById) {
8444
8781
  return pagesById.get(node.pageId)?.sourceType;
8445
8782
  }
8446
8783
  function supportingPathDetails(graph, edge) {
8447
- const path25 = shortestGraphPath(graph, edge.source, edge.target);
8784
+ const path26 = shortestGraphPath(graph, edge.source, edge.target);
8448
8785
  const edgesById = new Map(graph.edges.map((item) => [item.id, item]));
8449
- const pathEdges = path25.edgeIds.map((edgeId) => edgesById.get(edgeId)).filter((item) => Boolean(item));
8786
+ const pathEdges = path26.edgeIds.map((edgeId) => edgesById.get(edgeId)).filter((item) => Boolean(item));
8450
8787
  return {
8451
- pathNodeIds: path25.nodeIds,
8452
- pathEdgeIds: path25.edgeIds,
8788
+ pathNodeIds: path26.nodeIds,
8789
+ pathEdgeIds: path26.edgeIds,
8453
8790
  pathRelations: pathEdges.map((item) => item.relation),
8454
8791
  pathEvidenceClasses: pathEdges.map((item) => item.evidenceClass),
8455
- pathSummary: path25.summary
8792
+ pathSummary: path26.summary
8456
8793
  };
8457
8794
  }
8458
8795
  function surpriseScore(edge, graph, pagesById, hyperedgesByNodeId) {
@@ -8521,7 +8858,7 @@ function topSurprisingConnections(graph, pagesById) {
8521
8858
  }).map((edge) => {
8522
8859
  const source = nodesById.get(edge.source);
8523
8860
  const target = nodesById.get(edge.target);
8524
- const path25 = supportingPathDetails(graph, edge);
8861
+ const path26 = supportingPathDetails(graph, edge);
8525
8862
  const scored = surpriseScore(edge, graph, pagesById, hyperedgesByNodeId);
8526
8863
  return {
8527
8864
  id: edge.id,
@@ -8532,11 +8869,11 @@ function topSurprisingConnections(graph, pagesById) {
8532
8869
  relation: edge.relation,
8533
8870
  evidenceClass: edge.evidenceClass,
8534
8871
  confidence: edge.confidence,
8535
- pathNodeIds: path25.pathNodeIds,
8536
- pathEdgeIds: path25.pathEdgeIds,
8537
- pathRelations: path25.pathRelations,
8538
- pathEvidenceClasses: path25.pathEvidenceClasses,
8539
- pathSummary: path25.pathSummary,
8872
+ pathNodeIds: path26.pathNodeIds,
8873
+ pathEdgeIds: path26.pathEdgeIds,
8874
+ pathRelations: path26.pathRelations,
8875
+ pathEvidenceClasses: path26.pathEvidenceClasses,
8876
+ pathSummary: path26.pathSummary,
8540
8877
  why: scored.why,
8541
8878
  explanation: scored.explanation,
8542
8879
  surpriseScore: scored.score
@@ -8642,6 +8979,13 @@ function buildGraphReportArtifact(input) {
8642
8979
  title: page.title,
8643
8980
  sourceType: page.sourceType,
8644
8981
  updatedAt: page.updatedAt
8982
+ })),
8983
+ contradictions: (input.contradictions ?? []).map((c) => ({
8984
+ sourceIdA: c.sourceIdA,
8985
+ sourceIdB: c.sourceIdB,
8986
+ claimA: c.claimA.text,
8987
+ claimB: c.claimB.text,
8988
+ confidenceDelta: Math.abs(c.claimA.confidence - c.claimB.confidence)
8645
8989
  }))
8646
8990
  };
8647
8991
  }
@@ -8761,6 +9105,12 @@ function buildGraphReportPage(input) {
8761
9105
  return `- ${sourceLabel} ${connection.relation} ${targetLabel} (${connection.evidenceClass}, ${connection.confidence.toFixed(2)}). Why: ${connection.why}. ${connection.explanation} Path: ${connection.pathSummary}.`;
8762
9106
  }) : ["- No cross-community links detected."],
8763
9107
  "",
9108
+ "## Contradictions",
9109
+ "",
9110
+ ...input.report.contradictions.length ? input.report.contradictions.map(
9111
+ (c) => `- **${c.claimA}** vs **${c.claimB}** (sources: \`${c.sourceIdA}\`, \`${c.sourceIdB}\`, confidence delta: ${c.confidenceDelta.toFixed(2)})`
9112
+ ) : ["- No contradictions detected."],
9113
+ "",
8764
9114
  "## Group Patterns",
8765
9115
  "",
8766
9116
  ...input.report.groupPatterns.length ? input.report.groupPatterns.map((hyperedge) => {
@@ -9421,13 +9771,13 @@ function buildOutputAssetManifest(input) {
9421
9771
  }
9422
9772
 
9423
9773
  // src/outputs.ts
9424
- import fs14 from "fs/promises";
9425
- import path18 from "path";
9774
+ import fs15 from "fs/promises";
9775
+ import path19 from "path";
9426
9776
  import matter7 from "gray-matter";
9427
9777
 
9428
9778
  // src/pages.ts
9429
- import fs13 from "fs/promises";
9430
- import path17 from "path";
9779
+ import fs14 from "fs/promises";
9780
+ import path18 from "path";
9431
9781
  import matter6 from "gray-matter";
9432
9782
  function normalizeStringArray(value) {
9433
9783
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
@@ -9505,7 +9855,7 @@ async function loadExistingManagedPageState(absolutePath, defaults = {}) {
9505
9855
  updatedAt: updatedFallback
9506
9856
  };
9507
9857
  }
9508
- const content = await fs13.readFile(absolutePath, "utf8");
9858
+ const content = await fs14.readFile(absolutePath, "utf8");
9509
9859
  const parsed = matter6(content);
9510
9860
  return {
9511
9861
  status: normalizePageStatus(parsed.data.status, defaults.status ?? "active"),
@@ -9544,7 +9894,7 @@ function parseStoredPage(relativePath, content, defaults = {}) {
9544
9894
  const now = (/* @__PURE__ */ new Date()).toISOString();
9545
9895
  const fallbackCreatedAt = defaults.createdAt ?? now;
9546
9896
  const fallbackUpdatedAt = defaults.updatedAt ?? fallbackCreatedAt;
9547
- const title = typeof parsed.data.title === "string" ? parsed.data.title : path17.basename(relativePath, ".md");
9897
+ const title = typeof parsed.data.title === "string" ? parsed.data.title : path18.basename(relativePath, ".md");
9548
9898
  const kind = inferPageKind(relativePath, parsed.data.kind);
9549
9899
  const sourceIds = normalizeStringArray(parsed.data.source_ids);
9550
9900
  const projectIds = normalizeProjectIds(parsed.data.project_ids);
@@ -9585,18 +9935,18 @@ function parseStoredPage(relativePath, content, defaults = {}) {
9585
9935
  };
9586
9936
  }
9587
9937
  async function loadInsightPages(wikiDir) {
9588
- const insightsDir = path17.join(wikiDir, "insights");
9938
+ const insightsDir = path18.join(wikiDir, "insights");
9589
9939
  if (!await fileExists(insightsDir)) {
9590
9940
  return [];
9591
9941
  }
9592
- const files = (await listFilesRecursive(insightsDir)).filter((filePath) => filePath.endsWith(".md")).filter((filePath) => path17.basename(filePath) !== "index.md").sort((left, right) => left.localeCompare(right));
9942
+ const files = (await listFilesRecursive(insightsDir)).filter((filePath) => filePath.endsWith(".md")).filter((filePath) => path18.basename(filePath) !== "index.md").sort((left, right) => left.localeCompare(right));
9593
9943
  const insights = [];
9594
9944
  for (const absolutePath of files) {
9595
- const relativePath = toPosix(path17.relative(wikiDir, absolutePath));
9596
- const content = await fs13.readFile(absolutePath, "utf8");
9945
+ const relativePath = toPosix(path18.relative(wikiDir, absolutePath));
9946
+ const content = await fs14.readFile(absolutePath, "utf8");
9597
9947
  const parsed = matter6(content);
9598
- const stats = await fs13.stat(absolutePath);
9599
- const title = typeof parsed.data.title === "string" ? parsed.data.title : path17.basename(absolutePath, ".md");
9948
+ const stats = await fs14.stat(absolutePath);
9949
+ const title = typeof parsed.data.title === "string" ? parsed.data.title : path18.basename(absolutePath, ".md");
9600
9950
  const sourceIds = normalizeStringArray(parsed.data.source_ids);
9601
9951
  const projectIds = normalizeProjectIds(parsed.data.project_ids);
9602
9952
  const nodeIds = normalizeStringArray(parsed.data.node_ids);
@@ -9659,27 +10009,27 @@ function relatedOutputsForPage(targetPage, outputPages) {
9659
10009
  return outputPages.map((page) => ({ page, rank: relationRank(page, targetPage) })).filter((item) => item.rank > 0).sort((left, right) => right.rank - left.rank || left.page.title.localeCompare(right.page.title)).map((item) => item.page);
9660
10010
  }
9661
10011
  async function resolveUniqueOutputSlug(wikiDir, baseSlug) {
9662
- const outputsDir = path18.join(wikiDir, "outputs");
10012
+ const outputsDir = path19.join(wikiDir, "outputs");
9663
10013
  const root = baseSlug || "output";
9664
10014
  let candidate = root;
9665
10015
  let counter = 2;
9666
- while (await fileExists(path18.join(outputsDir, `${candidate}.md`))) {
10016
+ while (await fileExists(path19.join(outputsDir, `${candidate}.md`))) {
9667
10017
  candidate = `${root}-${counter}`;
9668
10018
  counter++;
9669
10019
  }
9670
10020
  return candidate;
9671
10021
  }
9672
10022
  async function loadSavedOutputPages(wikiDir) {
9673
- const outputsDir = path18.join(wikiDir, "outputs");
9674
- const entries = await fs14.readdir(outputsDir, { withFileTypes: true }).catch(() => []);
10023
+ const outputsDir = path19.join(wikiDir, "outputs");
10024
+ const entries = await fs15.readdir(outputsDir, { withFileTypes: true }).catch(() => []);
9675
10025
  const outputs = [];
9676
10026
  for (const entry of entries) {
9677
10027
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "index.md") {
9678
10028
  continue;
9679
10029
  }
9680
- const relativePath = path18.posix.join("outputs", entry.name);
9681
- const absolutePath = path18.join(outputsDir, entry.name);
9682
- const content = await fs14.readFile(absolutePath, "utf8");
10030
+ const relativePath = path19.posix.join("outputs", entry.name);
10031
+ const absolutePath = path19.join(outputsDir, entry.name);
10032
+ const content = await fs15.readFile(absolutePath, "utf8");
9683
10033
  const parsed = matter7(content);
9684
10034
  const slug = entry.name.replace(/\.md$/, "");
9685
10035
  const title = typeof parsed.data.title === "string" ? parsed.data.title : slug;
@@ -9692,7 +10042,7 @@ async function loadSavedOutputPages(wikiDir) {
9692
10042
  const relatedSourceIds = normalizeStringArray(parsed.data.related_source_ids);
9693
10043
  const backlinks = normalizeStringArray(parsed.data.backlinks);
9694
10044
  const compiledFrom = normalizeStringArray(parsed.data.compiled_from);
9695
- const stats = await fs14.stat(absolutePath);
10045
+ const stats = await fs15.stat(absolutePath);
9696
10046
  const createdAt = typeof parsed.data.created_at === "string" ? parsed.data.created_at : stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : stats.mtime.toISOString();
9697
10047
  const updatedAt = typeof parsed.data.updated_at === "string" ? parsed.data.updated_at : stats.mtime.toISOString();
9698
10048
  outputs.push({
@@ -9730,8 +10080,8 @@ async function loadSavedOutputPages(wikiDir) {
9730
10080
  }
9731
10081
 
9732
10082
  // src/search.ts
9733
- import fs15 from "fs/promises";
9734
- import path19 from "path";
10083
+ import fs16 from "fs/promises";
10084
+ import path20 from "path";
9735
10085
  import matter8 from "gray-matter";
9736
10086
  function getDatabaseSync() {
9737
10087
  const builtin = process.getBuiltinModule?.("node:sqlite");
@@ -9757,7 +10107,7 @@ function normalizeSourceClass2(value) {
9757
10107
  return value === "first_party" || value === "third_party" || value === "resource" || value === "generated" ? value : void 0;
9758
10108
  }
9759
10109
  async function rebuildSearchIndex(dbPath, pages, wikiDir) {
9760
- await ensureDir(path19.dirname(dbPath));
10110
+ await ensureDir(path20.dirname(dbPath));
9761
10111
  const DatabaseSync = getDatabaseSync();
9762
10112
  const db = new DatabaseSync(dbPath);
9763
10113
  db.exec("PRAGMA journal_mode = WAL;");
@@ -9789,8 +10139,8 @@ async function rebuildSearchIndex(dbPath, pages, wikiDir) {
9789
10139
  "INSERT INTO pages (id, path, title, body, kind, status, source_type, source_class, project_ids, project_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
9790
10140
  );
9791
10141
  for (const page of pages) {
9792
- const absolutePath = path19.join(wikiDir, page.path);
9793
- const content = await fs15.readFile(absolutePath, "utf8");
10142
+ const absolutePath = path20.join(wikiDir, page.path);
10143
+ const content = await fs16.readFile(absolutePath, "utf8");
9794
10144
  const parsed = matter8(content);
9795
10145
  insertPage.run(
9796
10146
  page.id,
@@ -9907,7 +10257,7 @@ function outputFormatInstruction(format) {
9907
10257
  }
9908
10258
  }
9909
10259
  function outputAssetPath(slug, fileName) {
9910
- return toPosix(path20.join("outputs", "assets", slug, fileName));
10260
+ return toPosix(path21.join("outputs", "assets", slug, fileName));
9911
10261
  }
9912
10262
  function outputAssetId(slug, role) {
9913
10263
  return `output:${slug}:asset:${role}`;
@@ -10047,7 +10397,7 @@ async function resolveImageGenerationProvider(rootDir) {
10047
10397
  if (!providerConfig) {
10048
10398
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
10049
10399
  }
10050
- const { createProvider: createProvider2 } = await import("./registry-5SYH3Y3U.js");
10400
+ const { createProvider: createProvider2 } = await import("./registry-G7NSRYCO.js");
10051
10401
  return createProvider2(preferredProviderId, providerConfig, rootDir);
10052
10402
  }
10053
10403
  async function generateOutputArtifacts(rootDir, input) {
@@ -10245,7 +10595,7 @@ async function generateOutputArtifacts(rootDir, input) {
10245
10595
  };
10246
10596
  }
10247
10597
  function normalizeProjectRoot(root) {
10248
- const normalized = toPosix(path20.posix.normalize(root.replace(/\\/g, "/"))).replace(/^\.\/+/, "").replace(/\/+$/, "");
10598
+ const normalized = toPosix(path21.posix.normalize(root.replace(/\\/g, "/"))).replace(/^\.\/+/, "").replace(/\/+$/, "");
10249
10599
  return normalized;
10250
10600
  }
10251
10601
  function projectEntries(config) {
@@ -10271,10 +10621,10 @@ function manifestPathForProject(rootDir, manifest) {
10271
10621
  if (!rawPath) {
10272
10622
  return toPosix(manifest.storedPath);
10273
10623
  }
10274
- if (!path20.isAbsolute(rawPath)) {
10624
+ if (!path21.isAbsolute(rawPath)) {
10275
10625
  return normalizeProjectRoot(rawPath);
10276
10626
  }
10277
- const relative = toPosix(path20.relative(rootDir, rawPath));
10627
+ const relative = toPosix(path21.relative(rootDir, rawPath));
10278
10628
  return relative.startsWith("..") ? toPosix(rawPath) : normalizeProjectRoot(relative);
10279
10629
  }
10280
10630
  function prefixMatches(value, prefix) {
@@ -10448,7 +10798,7 @@ function pageHashes(pages) {
10448
10798
  return Object.fromEntries(pages.map((page) => [page.page.id, page.contentHash]));
10449
10799
  }
10450
10800
  async function buildManagedGraphPage(absolutePath, defaults, build) {
10451
- const existingContent = await fileExists(absolutePath) ? await fs16.readFile(absolutePath, "utf8") : null;
10801
+ const existingContent = await fileExists(absolutePath) ? await fs17.readFile(absolutePath, "utf8") : null;
10452
10802
  let existing = await loadExistingManagedPageState(absolutePath, {
10453
10803
  status: defaults.status ?? "active",
10454
10804
  managedBy: defaults.managedBy
@@ -10486,7 +10836,7 @@ async function buildManagedGraphPage(absolutePath, defaults, build) {
10486
10836
  return built;
10487
10837
  }
10488
10838
  async function buildManagedContent(absolutePath, defaults, build) {
10489
- const existingContent = await fileExists(absolutePath) ? await fs16.readFile(absolutePath, "utf8") : null;
10839
+ const existingContent = await fileExists(absolutePath) ? await fs17.readFile(absolutePath, "utf8") : null;
10490
10840
  let existing = await loadExistingManagedPageState(absolutePath, {
10491
10841
  status: defaults.status ?? "active",
10492
10842
  managedBy: defaults.managedBy
@@ -10609,7 +10959,7 @@ function resetGraphNodeMetrics(nodes) {
10609
10959
  return nodes.map(({ communityId: _communityId, degree: _degree, bridgeScore: _bridgeScore, isGodNode: _isGodNode, ...node }) => node);
10610
10960
  }
10611
10961
  function manifestRepoPath(manifest) {
10612
- return toPosix(manifest.repoRelativePath ?? path20.basename(manifest.originalPath ?? manifest.storedPath));
10962
+ return toPosix(manifest.repoRelativePath ?? path21.basename(manifest.originalPath ?? manifest.storedPath));
10613
10963
  }
10614
10964
  function goPackageScopeKey(manifest, analysis) {
10615
10965
  if (analysis.code?.language !== "go") {
@@ -10619,7 +10969,7 @@ function goPackageScopeKey(manifest, analysis) {
10619
10969
  if (!packageName) {
10620
10970
  return null;
10621
10971
  }
10622
- return `${packageName}:${path20.posix.dirname(manifestRepoPath(manifest))}`;
10972
+ return `${packageName}:${path21.posix.dirname(manifestRepoPath(manifest))}`;
10623
10973
  }
10624
10974
  function buildGoPackageSymbolLookups(analyses, manifestsById) {
10625
10975
  const lookups = /* @__PURE__ */ new Map();
@@ -10663,21 +11013,64 @@ function buildGoPackageSymbolLookups(analyses, manifestsById) {
10663
11013
  ])
10664
11014
  );
10665
11015
  }
11016
+ function claimTokens(text) {
11017
+ return new Set(
11018
+ text.toLowerCase().match(/[a-z][a-z0-9-]{2,}/g)?.filter((t) => !(/* @__PURE__ */ new Set(["the", "and", "for", "that", "this", "with", "are", "was", "from", "has", "not", "all", "but"])).has(t)) ?? []
11019
+ );
11020
+ }
11021
+ function claimJaccardSimilarity(a, b) {
11022
+ if (a.size === 0 && b.size === 0) return 0;
11023
+ let intersection = 0;
11024
+ for (const token of a) {
11025
+ if (b.has(token)) intersection++;
11026
+ }
11027
+ return intersection / (a.size + b.size - intersection);
11028
+ }
11029
+ function detectContradictions(analyses) {
11030
+ const contradictions = [];
11031
+ const claimsWithTokens = analyses.flatMap(
11032
+ (analysis) => analysis.claims.filter((c) => c.polarity === "positive" || c.polarity === "negative").map((c) => ({ sourceId: analysis.sourceId, claim: c, tokens: claimTokens(c.text) }))
11033
+ );
11034
+ for (let i = 0; i < claimsWithTokens.length; i++) {
11035
+ for (let j = i + 1; j < claimsWithTokens.length; j++) {
11036
+ const a = claimsWithTokens[i];
11037
+ const b = claimsWithTokens[j];
11038
+ if (a.sourceId === b.sourceId) continue;
11039
+ if (a.claim.polarity === b.claim.polarity) continue;
11040
+ const similarity = claimJaccardSimilarity(a.tokens, b.tokens);
11041
+ if (similarity >= 0.3) {
11042
+ contradictions.push({
11043
+ sourceIdA: a.sourceId,
11044
+ sourceIdB: b.sourceId,
11045
+ claimA: { text: a.claim.text, confidence: a.claim.confidence },
11046
+ claimB: { text: b.claim.text, confidence: b.claim.confidence },
11047
+ similarity
11048
+ });
11049
+ }
11050
+ }
11051
+ }
11052
+ return contradictions;
11053
+ }
10666
11054
  function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10667
11055
  const manifestsById = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
10668
11056
  const goPackageSymbolLookups = buildGoPackageSymbolLookups(analyses, manifestsById);
10669
- const sourceNodes = manifests.map((manifest) => ({
10670
- id: `source:${manifest.sourceId}`,
10671
- type: "source",
10672
- label: manifest.title,
10673
- pageId: `source:${manifest.sourceId}`,
10674
- freshness: "fresh",
10675
- confidence: 1,
10676
- sourceIds: [manifest.sourceId],
10677
- projectIds: scopedProjectIdsFromSources([manifest.sourceId], sourceProjects),
10678
- sourceClass: manifest.sourceClass,
10679
- language: manifest.language
10680
- }));
11057
+ const analysesBySourceId = new Map(analyses.map((analysis) => [analysis.sourceId, analysis]));
11058
+ const sourceNodes = manifests.map((manifest) => {
11059
+ const analysis = analysesBySourceId.get(manifest.sourceId);
11060
+ return {
11061
+ id: `source:${manifest.sourceId}`,
11062
+ type: "source",
11063
+ label: manifest.title,
11064
+ pageId: `source:${manifest.sourceId}`,
11065
+ freshness: "fresh",
11066
+ confidence: 1,
11067
+ sourceIds: [manifest.sourceId],
11068
+ projectIds: scopedProjectIdsFromSources([manifest.sourceId], sourceProjects),
11069
+ sourceClass: manifest.sourceClass,
11070
+ language: manifest.language,
11071
+ tags: analysis?.tags ?? []
11072
+ };
11073
+ });
10681
11074
  const conceptMap = /* @__PURE__ */ new Map();
10682
11075
  const entityMap = /* @__PURE__ */ new Map();
10683
11076
  const moduleMap = /* @__PURE__ */ new Map();
@@ -10692,7 +11085,6 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10692
11085
  edgesById.add(edge.id);
10693
11086
  edges.push(edge);
10694
11087
  };
10695
- const analysesBySourceId = new Map(analyses.map((analysis) => [analysis.sourceId, analysis]));
10696
11088
  for (const analysis of analyses) {
10697
11089
  for (const concept of analysis.concepts) {
10698
11090
  const existing = conceptMap.get(concept.id);
@@ -11042,11 +11434,11 @@ function recentResearchSourcePages(graph, previousCompiledAt) {
11042
11434
  sourceType: page.sourceType
11043
11435
  }));
11044
11436
  }
11045
- async function buildGraphOrientationPages(graph, paths, schemaHash, previousCompiledAt) {
11437
+ async function buildGraphOrientationPages(graph, paths, schemaHash, previousCompiledAt, contradictions = []) {
11046
11438
  const benchmark = await readJsonFile(paths.benchmarkPath);
11047
11439
  const communityRecords = [];
11048
11440
  for (const community of graph.communities ?? []) {
11049
- const absolutePath = path20.join(paths.wikiDir, "graph", "communities", `${community.id.replace(/^community:/, "")}.md`);
11441
+ const absolutePath = path21.join(paths.wikiDir, "graph", "communities", `${community.id.replace(/^community:/, "")}.md`);
11050
11442
  communityRecords.push(
11051
11443
  await buildManagedGraphPage(
11052
11444
  absolutePath,
@@ -11072,9 +11464,10 @@ async function buildGraphOrientationPages(graph, paths, schemaHash, previousComp
11072
11464
  benchmark,
11073
11465
  benchmarkStale: benchmark ? benchmark.graphHash !== graphHash(graph) : false,
11074
11466
  recentResearchSources: recentResearchSourcePages(graph, previousCompiledAt),
11075
- graphHash: graphHash(graph)
11467
+ graphHash: graphHash(graph),
11468
+ contradictions
11076
11469
  });
11077
- const reportAbsolutePath = path20.join(paths.wikiDir, "graph", "report.md");
11470
+ const reportAbsolutePath = path21.join(paths.wikiDir, "graph", "report.md");
11078
11471
  const reportRecord = await buildManagedGraphPage(
11079
11472
  reportAbsolutePath,
11080
11473
  {
@@ -11095,7 +11488,7 @@ async function buildGraphOrientationPages(graph, paths, schemaHash, previousComp
11095
11488
  };
11096
11489
  }
11097
11490
  async function writePage(wikiDir, relativePath, content, changedPages) {
11098
- const absolutePath = path20.resolve(wikiDir, relativePath);
11491
+ const absolutePath = path21.resolve(wikiDir, relativePath);
11099
11492
  const changed = await writeFileIfChanged(absolutePath, content);
11100
11493
  if (changed) {
11101
11494
  changedPages.push(relativePath);
@@ -11158,29 +11551,29 @@ async function requiredCompileArtifactsExist(paths) {
11158
11551
  paths.graphPath,
11159
11552
  paths.codeIndexPath,
11160
11553
  paths.searchDbPath,
11161
- path20.join(paths.wikiDir, "index.md"),
11162
- path20.join(paths.wikiDir, "sources", "index.md"),
11163
- path20.join(paths.wikiDir, "code", "index.md"),
11164
- path20.join(paths.wikiDir, "concepts", "index.md"),
11165
- path20.join(paths.wikiDir, "entities", "index.md"),
11166
- path20.join(paths.wikiDir, "outputs", "index.md"),
11167
- path20.join(paths.wikiDir, "projects", "index.md"),
11168
- path20.join(paths.wikiDir, "candidates", "index.md")
11554
+ path21.join(paths.wikiDir, "index.md"),
11555
+ path21.join(paths.wikiDir, "sources", "index.md"),
11556
+ path21.join(paths.wikiDir, "code", "index.md"),
11557
+ path21.join(paths.wikiDir, "concepts", "index.md"),
11558
+ path21.join(paths.wikiDir, "entities", "index.md"),
11559
+ path21.join(paths.wikiDir, "outputs", "index.md"),
11560
+ path21.join(paths.wikiDir, "projects", "index.md"),
11561
+ path21.join(paths.wikiDir, "candidates", "index.md")
11169
11562
  ];
11170
11563
  const checks = await Promise.all(requiredPaths.map((filePath) => fileExists(filePath)));
11171
11564
  return checks.every(Boolean);
11172
11565
  }
11173
11566
  async function loadAvailableCachedAnalyses(paths, manifests) {
11174
11567
  const analyses = await Promise.all(
11175
- manifests.map(async (manifest) => readJsonFile(path20.join(paths.analysesDir, `${manifest.sourceId}.json`)))
11568
+ manifests.map(async (manifest) => readJsonFile(path21.join(paths.analysesDir, `${manifest.sourceId}.json`)))
11176
11569
  );
11177
11570
  return analyses.filter((analysis) => Boolean(analysis));
11178
11571
  }
11179
11572
  function approvalManifestPath(paths, approvalId) {
11180
- return path20.join(paths.approvalsDir, approvalId, "manifest.json");
11573
+ return path21.join(paths.approvalsDir, approvalId, "manifest.json");
11181
11574
  }
11182
11575
  function approvalGraphPath(paths, approvalId) {
11183
- return path20.join(paths.approvalsDir, approvalId, "state", "graph.json");
11576
+ return path21.join(paths.approvalsDir, approvalId, "state", "graph.json");
11184
11577
  }
11185
11578
  async function readApprovalManifest(paths, approvalId) {
11186
11579
  const manifest = await readJsonFile(approvalManifestPath(paths, approvalId));
@@ -11190,7 +11583,7 @@ async function readApprovalManifest(paths, approvalId) {
11190
11583
  return manifest;
11191
11584
  }
11192
11585
  async function writeApprovalManifest(paths, manifest) {
11193
- await fs16.writeFile(approvalManifestPath(paths, manifest.approvalId), `${JSON.stringify(manifest, null, 2)}
11586
+ await fs17.writeFile(approvalManifestPath(paths, manifest.approvalId), `${JSON.stringify(manifest, null, 2)}
11194
11587
  `, "utf8");
11195
11588
  }
11196
11589
  async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousGraph, graph) {
@@ -11205,7 +11598,7 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
11205
11598
  continue;
11206
11599
  }
11207
11600
  const previousPage = previousPagesById.get(nextPage.id);
11208
- const currentExists = await fileExists(path20.join(paths.wikiDir, file.relativePath));
11601
+ const currentExists = await fileExists(path21.join(paths.wikiDir, file.relativePath));
11209
11602
  if (previousPage && previousPage.path !== nextPage.path) {
11210
11603
  entries.push({
11211
11604
  pageId: nextPage.id,
@@ -11238,7 +11631,7 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
11238
11631
  const previousPage = previousPagesByPath.get(deletedPath);
11239
11632
  entries.push({
11240
11633
  pageId: previousPage?.id ?? `page:${slugify(deletedPath)}`,
11241
- title: previousPage?.title ?? path20.basename(deletedPath, ".md"),
11634
+ title: previousPage?.title ?? path21.basename(deletedPath, ".md"),
11242
11635
  kind: previousPage?.kind ?? "index",
11243
11636
  changeType: "delete",
11244
11637
  status: "pending",
@@ -11250,16 +11643,16 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
11250
11643
  }
11251
11644
  async function stageApprovalBundle(paths, changedFiles, deletedPaths, previousGraph, graph) {
11252
11645
  const approvalId = `compile-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
11253
- const approvalDir = path20.join(paths.approvalsDir, approvalId);
11646
+ const approvalDir = path21.join(paths.approvalsDir, approvalId);
11254
11647
  await ensureDir(approvalDir);
11255
- await ensureDir(path20.join(approvalDir, "wiki"));
11256
- await ensureDir(path20.join(approvalDir, "state"));
11648
+ await ensureDir(path21.join(approvalDir, "wiki"));
11649
+ await ensureDir(path21.join(approvalDir, "state"));
11257
11650
  for (const file of changedFiles) {
11258
- const targetPath = path20.join(approvalDir, "wiki", file.relativePath);
11259
- await ensureDir(path20.dirname(targetPath));
11260
- await fs16.writeFile(targetPath, file.content, "utf8");
11651
+ const targetPath = path21.join(approvalDir, "wiki", file.relativePath);
11652
+ await ensureDir(path21.dirname(targetPath));
11653
+ await fs17.writeFile(targetPath, file.content, "utf8");
11261
11654
  }
11262
- await fs16.writeFile(path20.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
11655
+ await fs17.writeFile(path21.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
11263
11656
  await writeApprovalManifest(paths, {
11264
11657
  approvalId,
11265
11658
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11321,7 +11714,7 @@ async function syncVaultArtifacts(rootDir, input) {
11321
11714
  confidence: 1
11322
11715
  });
11323
11716
  const sourceRecord = await buildManagedGraphPage(
11324
- path20.join(paths.wikiDir, preview.path),
11717
+ path21.join(paths.wikiDir, preview.path),
11325
11718
  {
11326
11719
  managedBy: "system",
11327
11720
  confidence: 1,
@@ -11336,7 +11729,7 @@ async function syncVaultArtifacts(rootDir, input) {
11336
11729
  modulePreview ?? void 0,
11337
11730
  {
11338
11731
  projectIds: sourceProjectIds,
11339
- extraTags: sourceCategoryTags,
11732
+ extraTags: [...sourceCategoryTags, ...analysis.tags ?? []],
11340
11733
  sourceClass: manifest.sourceClass
11341
11734
  }
11342
11735
  )
@@ -11367,7 +11760,7 @@ async function syncVaultArtifacts(rootDir, input) {
11367
11760
  );
11368
11761
  records.push(
11369
11762
  await buildManagedGraphPage(
11370
- path20.join(paths.wikiDir, modulePreview.path),
11763
+ path21.join(paths.wikiDir, modulePreview.path),
11371
11764
  {
11372
11765
  managedBy: "system",
11373
11766
  confidence: 1,
@@ -11382,7 +11775,7 @@ async function syncVaultArtifacts(rootDir, input) {
11382
11775
  localModules,
11383
11776
  relatedOutputs: relatedOutputsForPage(modulePreview, input.outputPages),
11384
11777
  projectIds: sourceProjectIds,
11385
- extraTags: sourceCategoryTags
11778
+ extraTags: [...sourceCategoryTags, ...analysis.tags ?? []]
11386
11779
  })
11387
11780
  )
11388
11781
  );
@@ -11401,8 +11794,8 @@ async function syncVaultArtifacts(rootDir, input) {
11401
11794
  const relativePath = promoted ? activeAggregatePath(itemKind, slug) : candidatePagePathFor(itemKind, slug);
11402
11795
  const aggregateSourceClass2 = aggregateManifestSourceClass(input.manifests, sourceIds);
11403
11796
  const fallbackPaths = [
11404
- path20.join(paths.wikiDir, activeAggregatePath(itemKind, slug)),
11405
- path20.join(paths.wikiDir, candidatePagePathFor(itemKind, slug))
11797
+ path21.join(paths.wikiDir, activeAggregatePath(itemKind, slug)),
11798
+ path21.join(paths.wikiDir, candidatePagePathFor(itemKind, slug))
11406
11799
  ];
11407
11800
  const confidence = nodeConfidence(aggregate.sourceAnalyses.length);
11408
11801
  const preview = emptyGraphPage({
@@ -11420,7 +11813,7 @@ async function syncVaultArtifacts(rootDir, input) {
11420
11813
  status: promoted ? "active" : "candidate"
11421
11814
  });
11422
11815
  const pageRecord = await buildManagedGraphPage(
11423
- path20.join(paths.wikiDir, relativePath),
11816
+ path21.join(paths.wikiDir, relativePath),
11424
11817
  {
11425
11818
  status: promoted ? "active" : "candidate",
11426
11819
  managedBy: "system",
@@ -11462,6 +11855,22 @@ async function syncVaultArtifacts(rootDir, input) {
11462
11855
  const compiledPages = records.map((record) => record.page);
11463
11856
  const basePages = [...compiledPages, ...input.outputPages, ...input.insightPages];
11464
11857
  const structuralGraph = buildGraph(input.manifests, input.analyses, basePages, input.sourceProjects, input.codeIndex);
11858
+ const contradictions = detectContradictions(input.analyses);
11859
+ for (const contradiction of contradictions) {
11860
+ const edgeId = `contradiction:${contradiction.sourceIdA}->${contradiction.sourceIdB}`;
11861
+ if (!structuralGraph.edges.some((e) => e.id === edgeId)) {
11862
+ structuralGraph.edges.push({
11863
+ id: edgeId,
11864
+ source: `source:${contradiction.sourceIdA}`,
11865
+ target: `source:${contradiction.sourceIdB}`,
11866
+ relation: "contradicts",
11867
+ status: "conflicted",
11868
+ evidenceClass: "ambiguous",
11869
+ confidence: Math.abs(contradiction.claimA.confidence - contradiction.claimB.confidence),
11870
+ provenance: [contradiction.sourceIdA, contradiction.sourceIdB]
11871
+ });
11872
+ }
11873
+ }
11465
11874
  const embeddingEdges = await embeddingSimilarityEdges(rootDir, structuralGraph).catch(() => []);
11466
11875
  const baseGraph = embeddingEdges.length > 0 ? (() => {
11467
11876
  const edges = uniqueBy([...structuralGraph.edges, ...embeddingEdges], (edge) => edge.id).sort(
@@ -11475,7 +11884,13 @@ async function syncVaultArtifacts(rootDir, input) {
11475
11884
  communities: metrics.communities
11476
11885
  };
11477
11886
  })() : structuralGraph;
11478
- const graphOrientation = await buildGraphOrientationPages(baseGraph, paths, globalSchemaHash, input.previousState?.generatedAt);
11887
+ const graphOrientation = await buildGraphOrientationPages(
11888
+ baseGraph,
11889
+ paths,
11890
+ globalSchemaHash,
11891
+ input.previousState?.generatedAt,
11892
+ contradictions
11893
+ );
11479
11894
  records.push(...graphOrientation.records);
11480
11895
  const allPages = [...basePages, ...graphOrientation.records.map((record) => record.page)];
11481
11896
  const graph = {
@@ -11515,7 +11930,7 @@ async function syncVaultArtifacts(rootDir, input) {
11515
11930
  confidence: 1
11516
11931
  }),
11517
11932
  content: await buildManagedContent(
11518
- path20.join(paths.wikiDir, "projects", "index.md"),
11933
+ path21.join(paths.wikiDir, "projects", "index.md"),
11519
11934
  {
11520
11935
  managedBy: "system",
11521
11936
  compiledFrom: indexCompiledFrom(projectIndexRefs)
@@ -11539,7 +11954,7 @@ async function syncVaultArtifacts(rootDir, input) {
11539
11954
  records.push({
11540
11955
  page: projectIndexRef,
11541
11956
  content: await buildManagedContent(
11542
- path20.join(paths.wikiDir, projectIndexRef.path),
11957
+ path21.join(paths.wikiDir, projectIndexRef.path),
11543
11958
  {
11544
11959
  managedBy: "system",
11545
11960
  compiledFrom: indexCompiledFrom(Object.values(sections).flat())
@@ -11567,7 +11982,7 @@ async function syncVaultArtifacts(rootDir, input) {
11567
11982
  confidence: 1
11568
11983
  }),
11569
11984
  content: await buildManagedContent(
11570
- path20.join(paths.wikiDir, "index.md"),
11985
+ path21.join(paths.wikiDir, "index.md"),
11571
11986
  {
11572
11987
  managedBy: "system",
11573
11988
  compiledFrom: indexCompiledFrom(allPages)
@@ -11598,7 +12013,7 @@ async function syncVaultArtifacts(rootDir, input) {
11598
12013
  confidence: 1
11599
12014
  }),
11600
12015
  content: await buildManagedContent(
11601
- path20.join(paths.wikiDir, relativePath),
12016
+ path21.join(paths.wikiDir, relativePath),
11602
12017
  {
11603
12018
  managedBy: "system",
11604
12019
  compiledFrom: indexCompiledFrom(pages)
@@ -11609,12 +12024,12 @@ async function syncVaultArtifacts(rootDir, input) {
11609
12024
  }
11610
12025
  const nextPagePaths = new Set(records.map((record) => record.page.path));
11611
12026
  const obsoleteGraphPaths = (previousGraph?.pages ?? []).filter((page) => page.kind !== "output" && page.kind !== "insight").map((page) => page.path).filter((relativePath) => !nextPagePaths.has(relativePath));
11612
- const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path20.relative(paths.wikiDir, absolutePath))).filter((relativePath) => !nextPagePaths.has(relativePath));
12027
+ const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path21.relative(paths.wikiDir, absolutePath))).filter((relativePath) => !nextPagePaths.has(relativePath));
11613
12028
  const obsoletePaths = uniqueStrings3([...obsoleteGraphPaths, ...existingProjectIndexPaths]);
11614
12029
  const changedFiles = [];
11615
12030
  for (const record of records) {
11616
- const absolutePath = path20.join(paths.wikiDir, record.page.path);
11617
- const current = await fileExists(absolutePath) ? await fs16.readFile(absolutePath, "utf8") : null;
12031
+ const absolutePath = path21.join(paths.wikiDir, record.page.path);
12032
+ const current = await fileExists(absolutePath) ? await fs17.readFile(absolutePath, "utf8") : null;
11618
12033
  if (current !== record.content) {
11619
12034
  changedPages.push(record.page.path);
11620
12035
  changedFiles.push({ relativePath: record.page.path, content: record.content });
@@ -11639,10 +12054,10 @@ async function syncVaultArtifacts(rootDir, input) {
11639
12054
  await writePage(paths.wikiDir, record.page.path, record.content, writeChanges);
11640
12055
  }
11641
12056
  for (const relativePath of obsoletePaths) {
11642
- await fs16.rm(path20.join(paths.wikiDir, relativePath), { force: true });
12057
+ await fs17.rm(path21.join(paths.wikiDir, relativePath), { force: true });
11643
12058
  }
11644
12059
  await writeJsonFile(paths.graphPath, graph);
11645
- await writeJsonFile(path20.join(paths.wikiDir, "graph", "report.json"), graphOrientation.report);
12060
+ await writeJsonFile(path21.join(paths.wikiDir, "graph", "report.json"), graphOrientation.report);
11646
12061
  await writeJsonFile(paths.codeIndexPath, input.codeIndex);
11647
12062
  await writeJsonFile(paths.compileStatePath, {
11648
12063
  generatedAt: graph.generatedAt,
@@ -11713,17 +12128,17 @@ async function refreshIndexesAndSearch(rootDir, pages) {
11713
12128
  })
11714
12129
  );
11715
12130
  await Promise.all([
11716
- ensureDir(path20.join(paths.wikiDir, "sources")),
11717
- ensureDir(path20.join(paths.wikiDir, "code")),
11718
- ensureDir(path20.join(paths.wikiDir, "concepts")),
11719
- ensureDir(path20.join(paths.wikiDir, "entities")),
11720
- ensureDir(path20.join(paths.wikiDir, "outputs")),
11721
- ensureDir(path20.join(paths.wikiDir, "graph")),
11722
- ensureDir(path20.join(paths.wikiDir, "graph", "communities")),
11723
- ensureDir(path20.join(paths.wikiDir, "projects")),
11724
- ensureDir(path20.join(paths.wikiDir, "candidates"))
12131
+ ensureDir(path21.join(paths.wikiDir, "sources")),
12132
+ ensureDir(path21.join(paths.wikiDir, "code")),
12133
+ ensureDir(path21.join(paths.wikiDir, "concepts")),
12134
+ ensureDir(path21.join(paths.wikiDir, "entities")),
12135
+ ensureDir(path21.join(paths.wikiDir, "outputs")),
12136
+ ensureDir(path21.join(paths.wikiDir, "graph")),
12137
+ ensureDir(path21.join(paths.wikiDir, "graph", "communities")),
12138
+ ensureDir(path21.join(paths.wikiDir, "projects")),
12139
+ ensureDir(path21.join(paths.wikiDir, "candidates"))
11725
12140
  ]);
11726
- const projectsIndexPath = path20.join(paths.wikiDir, "projects", "index.md");
12141
+ const projectsIndexPath = path21.join(paths.wikiDir, "projects", "index.md");
11727
12142
  await writeFileIfChanged(
11728
12143
  projectsIndexPath,
11729
12144
  await buildManagedContent(
@@ -11744,7 +12159,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
11744
12159
  outputs: pages.filter((page) => page.kind === "output" && page.projectIds.includes(project.id)),
11745
12160
  candidates: pages.filter((page) => page.status === "candidate" && page.projectIds.includes(project.id))
11746
12161
  };
11747
- const absolutePath = path20.join(paths.wikiDir, "projects", project.id, "index.md");
12162
+ const absolutePath = path21.join(paths.wikiDir, "projects", project.id, "index.md");
11748
12163
  await writeFileIfChanged(
11749
12164
  absolutePath,
11750
12165
  await buildManagedContent(
@@ -11762,7 +12177,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
11762
12177
  )
11763
12178
  );
11764
12179
  }
11765
- const rootIndexPath = path20.join(paths.wikiDir, "index.md");
12180
+ const rootIndexPath = path21.join(paths.wikiDir, "index.md");
11766
12181
  await writeFileIfChanged(
11767
12182
  rootIndexPath,
11768
12183
  await buildManagedContent(
@@ -11783,7 +12198,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
11783
12198
  ["candidates/index.md", "candidates", pagesWithGraph.filter((page) => page.status === "candidate")],
11784
12199
  ["graph/index.md", "graph", pagesWithGraph.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
11785
12200
  ]) {
11786
- const absolutePath = path20.join(paths.wikiDir, relativePath);
12201
+ const absolutePath = path21.join(paths.wikiDir, relativePath);
11787
12202
  await writeFileIfChanged(
11788
12203
  absolutePath,
11789
12204
  await buildManagedContent(
@@ -11797,23 +12212,23 @@ async function refreshIndexesAndSearch(rootDir, pages) {
11797
12212
  );
11798
12213
  }
11799
12214
  for (const record of graphOrientation.records) {
11800
- await writeFileIfChanged(path20.join(paths.wikiDir, record.page.path), record.content);
12215
+ await writeFileIfChanged(path21.join(paths.wikiDir, record.page.path), record.content);
11801
12216
  }
11802
12217
  if (graphOrientation.report) {
11803
- await writeJsonFile(path20.join(paths.wikiDir, "graph", "report.json"), graphOrientation.report);
12218
+ await writeJsonFile(path21.join(paths.wikiDir, "graph", "report.json"), graphOrientation.report);
11804
12219
  }
11805
- const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path20.relative(paths.wikiDir, absolutePath)));
12220
+ const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path21.relative(paths.wikiDir, absolutePath)));
11806
12221
  const allowedProjectIndexPaths = /* @__PURE__ */ new Set([
11807
12222
  "projects/index.md",
11808
12223
  ...configuredProjects.map((project) => `projects/${project.id}/index.md`)
11809
12224
  ]);
11810
12225
  await Promise.all(
11811
- existingProjectIndexPaths.filter((relativePath) => !allowedProjectIndexPaths.has(relativePath)).map((relativePath) => fs16.rm(path20.join(paths.wikiDir, relativePath), { force: true }))
12226
+ existingProjectIndexPaths.filter((relativePath) => !allowedProjectIndexPaths.has(relativePath)).map((relativePath) => fs17.rm(path21.join(paths.wikiDir, relativePath), { force: true }))
11812
12227
  );
11813
- const existingGraphPages = (await listFilesRecursive(path20.join(paths.wikiDir, "graph").replace(/\/$/, "")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path20.relative(paths.wikiDir, absolutePath)));
12228
+ const existingGraphPages = (await listFilesRecursive(path21.join(paths.wikiDir, "graph").replace(/\/$/, "")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path21.relative(paths.wikiDir, absolutePath)));
11814
12229
  const allowedGraphPages = /* @__PURE__ */ new Set(["graph/index.md", ...graphOrientation.records.map((record) => record.page.path)]);
11815
12230
  await Promise.all(
11816
- existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs16.rm(path20.join(paths.wikiDir, relativePath), { force: true }))
12231
+ existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs17.rm(path21.join(paths.wikiDir, relativePath), { force: true }))
11817
12232
  );
11818
12233
  await rebuildSearchIndex(paths.searchDbPath, pagesWithGraph, paths.wikiDir);
11819
12234
  }
@@ -11833,7 +12248,7 @@ async function prepareOutputPageSave(rootDir, input) {
11833
12248
  confidence: 0.74
11834
12249
  }
11835
12250
  });
11836
- const absolutePath = path20.join(paths.wikiDir, output.page.path);
12251
+ const absolutePath = path21.join(paths.wikiDir, output.page.path);
11837
12252
  return {
11838
12253
  page: output.page,
11839
12254
  savedPath: absolutePath,
@@ -11845,15 +12260,15 @@ async function prepareOutputPageSave(rootDir, input) {
11845
12260
  async function persistOutputPage(rootDir, input) {
11846
12261
  const { paths } = await loadVaultConfig(rootDir);
11847
12262
  const prepared = await prepareOutputPageSave(rootDir, input);
11848
- await ensureDir(path20.dirname(prepared.savedPath));
11849
- await fs16.writeFile(prepared.savedPath, prepared.content, "utf8");
12263
+ await ensureDir(path21.dirname(prepared.savedPath));
12264
+ await fs17.writeFile(prepared.savedPath, prepared.content, "utf8");
11850
12265
  for (const assetFile of prepared.assetFiles) {
11851
- const assetPath = path20.join(paths.wikiDir, assetFile.relativePath);
11852
- await ensureDir(path20.dirname(assetPath));
12266
+ const assetPath = path21.join(paths.wikiDir, assetFile.relativePath);
12267
+ await ensureDir(path21.dirname(assetPath));
11853
12268
  if (typeof assetFile.content === "string") {
11854
- await fs16.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
12269
+ await fs17.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
11855
12270
  } else {
11856
- await fs16.writeFile(assetPath, assetFile.content);
12271
+ await fs17.writeFile(assetPath, assetFile.content);
11857
12272
  }
11858
12273
  }
11859
12274
  return { page: prepared.page, savedPath: prepared.savedPath, outputAssets: prepared.outputAssets };
@@ -11874,7 +12289,7 @@ async function prepareExploreHubSave(rootDir, input) {
11874
12289
  confidence: 0.76
11875
12290
  }
11876
12291
  });
11877
- const absolutePath = path20.join(paths.wikiDir, hub.page.path);
12292
+ const absolutePath = path21.join(paths.wikiDir, hub.page.path);
11878
12293
  return {
11879
12294
  page: hub.page,
11880
12295
  savedPath: absolutePath,
@@ -11886,15 +12301,15 @@ async function prepareExploreHubSave(rootDir, input) {
11886
12301
  async function persistExploreHub(rootDir, input) {
11887
12302
  const { paths } = await loadVaultConfig(rootDir);
11888
12303
  const prepared = await prepareExploreHubSave(rootDir, input);
11889
- await ensureDir(path20.dirname(prepared.savedPath));
11890
- await fs16.writeFile(prepared.savedPath, prepared.content, "utf8");
12304
+ await ensureDir(path21.dirname(prepared.savedPath));
12305
+ await fs17.writeFile(prepared.savedPath, prepared.content, "utf8");
11891
12306
  for (const assetFile of prepared.assetFiles) {
11892
- const assetPath = path20.join(paths.wikiDir, assetFile.relativePath);
11893
- await ensureDir(path20.dirname(assetPath));
12307
+ const assetPath = path21.join(paths.wikiDir, assetFile.relativePath);
12308
+ await ensureDir(path21.dirname(assetPath));
11894
12309
  if (typeof assetFile.content === "string") {
11895
- await fs16.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
12310
+ await fs17.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
11896
12311
  } else {
11897
- await fs16.writeFile(assetPath, assetFile.content);
12312
+ await fs17.writeFile(assetPath, assetFile.content);
11898
12313
  }
11899
12314
  }
11900
12315
  return { page: prepared.page, savedPath: prepared.savedPath, outputAssets: prepared.outputAssets };
@@ -11911,17 +12326,17 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
11911
12326
  }))
11912
12327
  ]);
11913
12328
  const approvalId = `schedule-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
11914
- const approvalDir = path20.join(paths.approvalsDir, approvalId);
12329
+ const approvalDir = path21.join(paths.approvalsDir, approvalId);
11915
12330
  await ensureDir(approvalDir);
11916
- await ensureDir(path20.join(approvalDir, "wiki"));
11917
- await ensureDir(path20.join(approvalDir, "state"));
12331
+ await ensureDir(path21.join(approvalDir, "wiki"));
12332
+ await ensureDir(path21.join(approvalDir, "state"));
11918
12333
  for (const file of changedFiles) {
11919
- const targetPath = path20.join(approvalDir, "wiki", file.relativePath);
11920
- await ensureDir(path20.dirname(targetPath));
12334
+ const targetPath = path21.join(approvalDir, "wiki", file.relativePath);
12335
+ await ensureDir(path21.dirname(targetPath));
11921
12336
  if ("binary" in file && file.binary) {
11922
- await fs16.writeFile(targetPath, Buffer.from(file.content, "base64"));
12337
+ await fs17.writeFile(targetPath, Buffer.from(file.content, "base64"));
11923
12338
  } else {
11924
- await fs16.writeFile(targetPath, file.content, "utf8");
12339
+ await fs17.writeFile(targetPath, file.content, "utf8");
11925
12340
  }
11926
12341
  }
11927
12342
  const nextPages = sortGraphPages([
@@ -11936,7 +12351,7 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
11936
12351
  sources: previousGraph?.sources ?? [],
11937
12352
  pages: nextPages
11938
12353
  };
11939
- await fs16.writeFile(path20.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
12354
+ await fs17.writeFile(path21.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
11940
12355
  await writeApprovalManifest(paths, {
11941
12356
  approvalId,
11942
12357
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11965,9 +12380,9 @@ async function executeQuery(rootDir, question, format) {
11965
12380
  const searchResults = searchPages(paths.searchDbPath, question, 5);
11966
12381
  const excerpts = await Promise.all(
11967
12382
  searchResults.map(async (result) => {
11968
- const absolutePath = path20.join(paths.wikiDir, result.path);
12383
+ const absolutePath = path21.join(paths.wikiDir, result.path);
11969
12384
  try {
11970
- const content = await fs16.readFile(absolutePath, "utf8");
12385
+ const content = await fs17.readFile(absolutePath, "utf8");
11971
12386
  const parsed = matter9(content);
11972
12387
  return `# ${result.title}
11973
12388
  ${truncate(normalizeWhitespace(parsed.content), 1200)}`;
@@ -12146,10 +12561,62 @@ function updateCandidateHistory(compileState, page, deleted = false) {
12146
12561
  function sortGraphPages(pages) {
12147
12562
  return [...pages].sort((left, right) => left.path.localeCompare(right.path) || left.title.localeCompare(right.title));
12148
12563
  }
12564
+ function computeUnifiedDiff(current, staged, label) {
12565
+ const currentLines = current.split("\n");
12566
+ const stagedLines = staged.split("\n");
12567
+ const output = [`--- a/${label}`, `+++ b/${label}`];
12568
+ const n = currentLines.length;
12569
+ const m = stagedLines.length;
12570
+ const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
12571
+ for (let i2 = n - 1; i2 >= 0; i2--) {
12572
+ for (let j2 = m - 1; j2 >= 0; j2--) {
12573
+ dp[i2][j2] = currentLines[i2] === stagedLines[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
12574
+ }
12575
+ }
12576
+ let i = 0;
12577
+ let j = 0;
12578
+ while (i < n || j < m) {
12579
+ if (i < n && j < m && currentLines[i] === stagedLines[j]) {
12580
+ output.push(` ${currentLines[i]}`);
12581
+ i++;
12582
+ j++;
12583
+ } else if (j < m && (i >= n || dp[i][j + 1] >= dp[i + 1][j])) {
12584
+ output.push(`+${stagedLines[j]}`);
12585
+ j++;
12586
+ } else {
12587
+ output.push(`-${currentLines[i]}`);
12588
+ i++;
12589
+ }
12590
+ }
12591
+ return output.join("\n");
12592
+ }
12593
+ function computeChangeSummary(current, staged, changeType) {
12594
+ if (changeType === "create") return "New page";
12595
+ if (changeType === "delete") return "Removed page";
12596
+ if (changeType === "promote") return "Promoted from candidate";
12597
+ if (!current || !staged) return "Updated page";
12598
+ const currentParsed = matter9(current);
12599
+ const stagedParsed = matter9(staged);
12600
+ const changes = [];
12601
+ const currentTags = currentParsed.data.tags ?? [];
12602
+ const stagedTags = stagedParsed.data.tags ?? [];
12603
+ const addedTags = stagedTags.filter((t) => !currentTags.includes(t));
12604
+ const removedTags = currentTags.filter((t) => !stagedTags.includes(t));
12605
+ if (addedTags.length) changes.push(`added ${addedTags.length} tag(s)`);
12606
+ if (removedTags.length) changes.push(`removed ${removedTags.length} tag(s)`);
12607
+ if (currentParsed.data.title !== stagedParsed.data.title) changes.push("updated title");
12608
+ const currentLines = currentParsed.content.trim().split("\n").length;
12609
+ const stagedLines = stagedParsed.content.trim().split("\n").length;
12610
+ const lineDelta = stagedLines - currentLines;
12611
+ if (lineDelta > 0) changes.push(`added ${lineDelta} line(s)`);
12612
+ else if (lineDelta < 0) changes.push(`removed ${Math.abs(lineDelta)} line(s)`);
12613
+ else if (currentParsed.content !== stagedParsed.content) changes.push("modified content");
12614
+ return changes.length ? changes.join(", ") : "no visible changes";
12615
+ }
12149
12616
  async function listApprovals(rootDir) {
12150
12617
  const { paths } = await loadVaultConfig(rootDir);
12151
12618
  const manifests = await Promise.all(
12152
- (await fs16.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => {
12619
+ (await fs17.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => {
12153
12620
  try {
12154
12621
  return await readApprovalManifest(paths, entry.name);
12155
12622
  } catch {
@@ -12159,19 +12626,24 @@ async function listApprovals(rootDir) {
12159
12626
  );
12160
12627
  return manifests.filter((manifest) => Boolean(manifest)).map(approvalSummary).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
12161
12628
  }
12162
- async function readApproval(rootDir, approvalId) {
12629
+ async function readApproval(rootDir, approvalId, options) {
12163
12630
  const { paths } = await loadVaultConfig(rootDir);
12164
12631
  const manifest = await readApprovalManifest(paths, approvalId);
12165
12632
  const details = await Promise.all(
12166
12633
  manifest.entries.map(async (entry) => {
12167
12634
  const currentPath = entry.previousPath ?? entry.nextPath;
12168
- const currentContent = currentPath ? await fs16.readFile(path20.join(paths.wikiDir, currentPath), "utf8").catch(() => void 0) : void 0;
12169
- const stagedContent = entry.nextPath ? await fs16.readFile(path20.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath), "utf8").catch(() => void 0) : void 0;
12170
- return {
12635
+ const currentContent = currentPath ? await fs17.readFile(path21.join(paths.wikiDir, currentPath), "utf8").catch(() => void 0) : void 0;
12636
+ const stagedContent = entry.nextPath ? await fs17.readFile(path21.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath), "utf8").catch(() => void 0) : void 0;
12637
+ const detail = {
12171
12638
  ...entry,
12172
12639
  currentContent,
12173
12640
  stagedContent
12174
12641
  };
12642
+ detail.changeSummary = computeChangeSummary(detail.currentContent, detail.stagedContent, detail.changeType);
12643
+ if (options?.diff && detail.currentContent && detail.stagedContent) {
12644
+ detail.diff = computeUnifiedDiff(detail.currentContent, detail.stagedContent, detail.nextPath ?? detail.pageId);
12645
+ }
12646
+ return detail;
12175
12647
  })
12176
12648
  );
12177
12649
  return {
@@ -12194,26 +12666,26 @@ async function acceptApproval(rootDir, approvalId, targets = []) {
12194
12666
  if (!entry.nextPath) {
12195
12667
  throw new Error(`Approval entry ${entry.pageId} is missing a staged path.`);
12196
12668
  }
12197
- const stagedAbsolutePath = path20.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath);
12198
- const stagedContent = await fs16.readFile(stagedAbsolutePath, "utf8");
12199
- const targetAbsolutePath = path20.join(paths.wikiDir, entry.nextPath);
12200
- await ensureDir(path20.dirname(targetAbsolutePath));
12201
- await fs16.writeFile(targetAbsolutePath, stagedContent, "utf8");
12669
+ const stagedAbsolutePath = path21.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath);
12670
+ const stagedContent = await fs17.readFile(stagedAbsolutePath, "utf8");
12671
+ const targetAbsolutePath = path21.join(paths.wikiDir, entry.nextPath);
12672
+ await ensureDir(path21.dirname(targetAbsolutePath));
12673
+ await fs17.writeFile(targetAbsolutePath, stagedContent, "utf8");
12202
12674
  if (entry.changeType === "promote" && entry.previousPath) {
12203
- await fs16.rm(path20.join(paths.wikiDir, entry.previousPath), { force: true });
12675
+ await fs17.rm(path21.join(paths.wikiDir, entry.previousPath), { force: true });
12204
12676
  }
12205
12677
  const nextPage = bundleGraph?.pages.find((page) => page.id === entry.pageId && page.path === entry.nextPath) ?? parseStoredPage(entry.nextPath, stagedContent);
12206
12678
  if (nextPage.kind === "output" && nextPage.outputAssets?.length) {
12207
- const outputAssetDir = path20.join(paths.wikiDir, "outputs", "assets", path20.basename(nextPage.path, ".md"));
12208
- await fs16.rm(outputAssetDir, { recursive: true, force: true });
12679
+ const outputAssetDir = path21.join(paths.wikiDir, "outputs", "assets", path21.basename(nextPage.path, ".md"));
12680
+ await fs17.rm(outputAssetDir, { recursive: true, force: true });
12209
12681
  for (const asset of nextPage.outputAssets) {
12210
- const stagedAssetPath = path20.join(paths.approvalsDir, approvalId, "wiki", asset.path);
12682
+ const stagedAssetPath = path21.join(paths.approvalsDir, approvalId, "wiki", asset.path);
12211
12683
  if (!await fileExists(stagedAssetPath)) {
12212
12684
  continue;
12213
12685
  }
12214
- const targetAssetPath = path20.join(paths.wikiDir, asset.path);
12215
- await ensureDir(path20.dirname(targetAssetPath));
12216
- await fs16.copyFile(stagedAssetPath, targetAssetPath);
12686
+ const targetAssetPath = path21.join(paths.wikiDir, asset.path);
12687
+ await ensureDir(path21.dirname(targetAssetPath));
12688
+ await fs17.copyFile(stagedAssetPath, targetAssetPath);
12217
12689
  }
12218
12690
  }
12219
12691
  nextPages = nextPages.filter(
@@ -12224,10 +12696,10 @@ async function acceptApproval(rootDir, approvalId, targets = []) {
12224
12696
  } else {
12225
12697
  const deletedPage = nextPages.find((page) => page.id === entry.pageId || page.path === entry.previousPath) ?? bundleGraph?.pages.find((page) => page.id === entry.pageId || page.path === entry.previousPath) ?? null;
12226
12698
  if (entry.previousPath) {
12227
- await fs16.rm(path20.join(paths.wikiDir, entry.previousPath), { force: true });
12699
+ await fs17.rm(path21.join(paths.wikiDir, entry.previousPath), { force: true });
12228
12700
  }
12229
12701
  if (deletedPage?.kind === "output") {
12230
- await fs16.rm(path20.join(paths.wikiDir, "outputs", "assets", path20.basename(deletedPage.path, ".md")), {
12702
+ await fs17.rm(path21.join(paths.wikiDir, "outputs", "assets", path21.basename(deletedPage.path, ".md")), {
12231
12703
  recursive: true,
12232
12704
  force: true
12233
12705
  });
@@ -12318,7 +12790,7 @@ async function promoteCandidate(rootDir, target) {
12318
12790
  const { paths } = await loadVaultConfig(rootDir);
12319
12791
  const graph = await readJsonFile(paths.graphPath);
12320
12792
  const candidate = resolveCandidateTarget(graph?.pages ?? [], target);
12321
- const raw = await fs16.readFile(path20.join(paths.wikiDir, candidate.path), "utf8");
12793
+ const raw = await fs17.readFile(path21.join(paths.wikiDir, candidate.path), "utf8");
12322
12794
  const parsed = matter9(raw);
12323
12795
  const nextUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
12324
12796
  const nextContent = matter9.stringify(parsed.content, {
@@ -12330,10 +12802,10 @@ async function promoteCandidate(rootDir, target) {
12330
12802
  )
12331
12803
  });
12332
12804
  const nextPath = candidateActivePath(candidate);
12333
- const nextAbsolutePath = path20.join(paths.wikiDir, nextPath);
12334
- await ensureDir(path20.dirname(nextAbsolutePath));
12335
- await fs16.writeFile(nextAbsolutePath, nextContent, "utf8");
12336
- await fs16.rm(path20.join(paths.wikiDir, candidate.path), { force: true });
12805
+ const nextAbsolutePath = path21.join(paths.wikiDir, nextPath);
12806
+ await ensureDir(path21.dirname(nextAbsolutePath));
12807
+ await fs17.writeFile(nextAbsolutePath, nextContent, "utf8");
12808
+ await fs17.rm(path21.join(paths.wikiDir, candidate.path), { force: true });
12337
12809
  const nextPage = parseStoredPage(nextPath, nextContent, { createdAt: candidate.createdAt, updatedAt: nextUpdatedAt });
12338
12810
  const nextPages = sortGraphPages(
12339
12811
  (graph?.pages ?? []).filter((page) => page.id !== candidate.id && page.path !== candidate.path).concat(nextPage)
@@ -12378,7 +12850,7 @@ async function archiveCandidate(rootDir, target) {
12378
12850
  const { paths } = await loadVaultConfig(rootDir);
12379
12851
  const graph = await readJsonFile(paths.graphPath);
12380
12852
  const candidate = resolveCandidateTarget(graph?.pages ?? [], target);
12381
- await fs16.rm(path20.join(paths.wikiDir, candidate.path), { force: true });
12853
+ await fs17.rm(path21.join(paths.wikiDir, candidate.path), { force: true });
12382
12854
  const nextPages = sortGraphPages((graph?.pages ?? []).filter((page) => page.id !== candidate.id && page.path !== candidate.path));
12383
12855
  const nextGraph = {
12384
12856
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12417,18 +12889,18 @@ async function archiveCandidate(rootDir, target) {
12417
12889
  }
12418
12890
  async function ensureObsidianWorkspace(rootDir) {
12419
12891
  const { config } = await loadVaultConfig(rootDir);
12420
- const obsidianDir = path20.join(rootDir, ".obsidian");
12892
+ const obsidianDir = path21.join(rootDir, ".obsidian");
12421
12893
  const projectIds = projectEntries(config).map((project) => project.id);
12422
12894
  await ensureDir(obsidianDir);
12423
12895
  await Promise.all([
12424
- writeJsonFile(path20.join(obsidianDir, "app.json"), {
12896
+ writeJsonFile(path21.join(obsidianDir, "app.json"), {
12425
12897
  alwaysUpdateLinks: true,
12426
12898
  newFileLocation: "folder",
12427
12899
  newFileFolderPath: "wiki/insights",
12428
12900
  useMarkdownLinks: false,
12429
12901
  attachmentFolderPath: "raw/assets"
12430
12902
  }),
12431
- writeJsonFile(path20.join(obsidianDir, "core-plugins.json"), [
12903
+ writeJsonFile(path21.join(obsidianDir, "core-plugins.json"), [
12432
12904
  "file-explorer",
12433
12905
  "global-search",
12434
12906
  "switcher",
@@ -12438,7 +12910,7 @@ async function ensureObsidianWorkspace(rootDir) {
12438
12910
  "tag-pane",
12439
12911
  "page-preview"
12440
12912
  ]),
12441
- writeJsonFile(path20.join(obsidianDir, "graph.json"), {
12913
+ writeJsonFile(path21.join(obsidianDir, "graph.json"), {
12442
12914
  "collapse-filter": false,
12443
12915
  search: "",
12444
12916
  showTags: true,
@@ -12450,7 +12922,7 @@ async function ensureObsidianWorkspace(rootDir) {
12450
12922
  })),
12451
12923
  localJumps: false
12452
12924
  }),
12453
- writeJsonFile(path20.join(obsidianDir, "workspace.json"), {
12925
+ writeJsonFile(path21.join(obsidianDir, "workspace.json"), {
12454
12926
  active: "root",
12455
12927
  lastOpenFiles: ["wiki/index.md", "wiki/projects/index.md", "wiki/candidates/index.md", "wiki/insights/index.md"],
12456
12928
  left: {
@@ -12465,7 +12937,7 @@ async function ensureObsidianWorkspace(rootDir) {
12465
12937
  async function initVault(rootDir, options = {}) {
12466
12938
  const { paths } = await initWorkspace(rootDir);
12467
12939
  await installConfiguredAgents(rootDir);
12468
- const insightsIndexPath = path20.join(paths.wikiDir, "insights", "index.md");
12940
+ const insightsIndexPath = path21.join(paths.wikiDir, "insights", "index.md");
12469
12941
  const now = (/* @__PURE__ */ new Date()).toISOString();
12470
12942
  await writeFileIfChanged(
12471
12943
  insightsIndexPath,
@@ -12501,7 +12973,7 @@ async function initVault(rootDir, options = {}) {
12501
12973
  )
12502
12974
  );
12503
12975
  await writeFileIfChanged(
12504
- path20.join(paths.wikiDir, "projects", "index.md"),
12976
+ path21.join(paths.wikiDir, "projects", "index.md"),
12505
12977
  matter9.stringify(["# Projects", "", "- Run `swarmvault compile` to build project rollups.", ""].join("\n"), {
12506
12978
  page_id: "projects:index",
12507
12979
  kind: "index",
@@ -12523,7 +12995,7 @@ async function initVault(rootDir, options = {}) {
12523
12995
  })
12524
12996
  );
12525
12997
  await writeFileIfChanged(
12526
- path20.join(paths.wikiDir, "candidates", "index.md"),
12998
+ path21.join(paths.wikiDir, "candidates", "index.md"),
12527
12999
  matter9.stringify(["# Candidates", "", "- Run `swarmvault compile` to stage candidate pages.", ""].join("\n"), {
12528
13000
  page_id: "candidates:index",
12529
13001
  kind: "index",
@@ -12659,7 +13131,7 @@ async function compileVault(rootDir, options = {}) {
12659
13131
  ),
12660
13132
  Promise.all(
12661
13133
  clean.map(async (manifest) => {
12662
- const cached = await readJsonFile(path20.join(paths.analysesDir, `${manifest.sourceId}.json`));
13134
+ const cached = await readJsonFile(path21.join(paths.analysesDir, `${manifest.sourceId}.json`));
12663
13135
  if (cached) {
12664
13136
  return cached;
12665
13137
  }
@@ -12683,22 +13155,22 @@ async function compileVault(rootDir, options = {}) {
12683
13155
  }
12684
13156
  const enriched = enrichResolvedCodeImports(manifest, analysis, codeIndex);
12685
13157
  if (analysisSignature(enriched) !== analysisSignature(analysis)) {
12686
- await writeJsonFile(path20.join(paths.analysesDir, `${analysis.sourceId}.json`), enriched);
13158
+ await writeJsonFile(path21.join(paths.analysesDir, `${analysis.sourceId}.json`), enriched);
12687
13159
  }
12688
13160
  return enriched;
12689
13161
  })
12690
13162
  );
12691
13163
  await Promise.all([
12692
- ensureDir(path20.join(paths.wikiDir, "sources")),
12693
- ensureDir(path20.join(paths.wikiDir, "code")),
12694
- ensureDir(path20.join(paths.wikiDir, "concepts")),
12695
- ensureDir(path20.join(paths.wikiDir, "entities")),
12696
- ensureDir(path20.join(paths.wikiDir, "outputs")),
12697
- ensureDir(path20.join(paths.wikiDir, "projects")),
12698
- ensureDir(path20.join(paths.wikiDir, "insights")),
12699
- ensureDir(path20.join(paths.wikiDir, "candidates")),
12700
- ensureDir(path20.join(paths.wikiDir, "candidates", "concepts")),
12701
- ensureDir(path20.join(paths.wikiDir, "candidates", "entities"))
13164
+ ensureDir(path21.join(paths.wikiDir, "sources")),
13165
+ ensureDir(path21.join(paths.wikiDir, "code")),
13166
+ ensureDir(path21.join(paths.wikiDir, "concepts")),
13167
+ ensureDir(path21.join(paths.wikiDir, "entities")),
13168
+ ensureDir(path21.join(paths.wikiDir, "outputs")),
13169
+ ensureDir(path21.join(paths.wikiDir, "projects")),
13170
+ ensureDir(path21.join(paths.wikiDir, "insights")),
13171
+ ensureDir(path21.join(paths.wikiDir, "candidates")),
13172
+ ensureDir(path21.join(paths.wikiDir, "candidates", "concepts")),
13173
+ ensureDir(path21.join(paths.wikiDir, "candidates", "entities"))
12702
13174
  ]);
12703
13175
  const sync = await syncVaultArtifacts(rootDir, {
12704
13176
  schemas,
@@ -12845,7 +13317,7 @@ async function queryVault(rootDir, options) {
12845
13317
  assetFiles: staged.assetFiles
12846
13318
  }
12847
13319
  ]);
12848
- stagedPath = path20.join(approval.approvalDir, "wiki", staged.page.path);
13320
+ stagedPath = path21.join(approval.approvalDir, "wiki", staged.page.path);
12849
13321
  savedPageId = staged.page.id;
12850
13322
  approvalId = approval.approvalId;
12851
13323
  approvalDir = approval.approvalDir;
@@ -13101,9 +13573,9 @@ ${orchestrationNotes.join("\n")}
13101
13573
  approvalId = approval.approvalId;
13102
13574
  approvalDir = approval.approvalDir;
13103
13575
  stepResults.forEach((result, index) => {
13104
- result.stagedPath = path20.join(approval.approvalDir, "wiki", stagedStepPages[index]?.page.path ?? "");
13576
+ result.stagedPath = path21.join(approval.approvalDir, "wiki", stagedStepPages[index]?.page.path ?? "");
13105
13577
  });
13106
- stagedHubPath = path20.join(approval.approvalDir, "wiki", hubPage.path);
13578
+ stagedHubPath = path21.join(approval.approvalDir, "wiki", hubPage.path);
13107
13579
  } else {
13108
13580
  await refreshVaultAfterOutputSave(rootDir);
13109
13581
  }
@@ -13190,11 +13662,11 @@ async function benchmarkVault(rootDir, options = {}) {
13190
13662
  }
13191
13663
  }
13192
13664
  for (const page of graph.pages) {
13193
- const absolutePath = path20.join(paths.wikiDir, page.path);
13665
+ const absolutePath = path21.join(paths.wikiDir, page.path);
13194
13666
  if (!await fileExists(absolutePath)) {
13195
13667
  continue;
13196
13668
  }
13197
- const parsed = matter9(await fs16.readFile(absolutePath, "utf8"));
13669
+ const parsed = matter9(await fs17.readFile(absolutePath, "utf8"));
13198
13670
  pageContentsById.set(page.id, parsed.content);
13199
13671
  }
13200
13672
  const configuredQuestions = (config.benchmark?.questions ?? []).map((question) => normalizeWhitespace(question)).filter(Boolean);
@@ -13239,7 +13711,7 @@ async function listGraphHyperedges(rootDir, target, limit = 25) {
13239
13711
  }
13240
13712
  async function readGraphReport(rootDir) {
13241
13713
  const { paths } = await loadVaultConfig(rootDir);
13242
- return readJsonFile(path20.join(paths.wikiDir, "graph", "report.json"));
13714
+ return readJsonFile(path21.join(paths.wikiDir, "graph", "report.json"));
13243
13715
  }
13244
13716
  async function listGodNodes(rootDir, limit = 10) {
13245
13717
  const graph = await ensureCompiledGraph(rootDir);
@@ -13252,15 +13724,15 @@ async function listPages(rootDir) {
13252
13724
  }
13253
13725
  async function readPage(rootDir, relativePath) {
13254
13726
  const { paths } = await loadVaultConfig(rootDir);
13255
- const absolutePath = path20.resolve(paths.wikiDir, relativePath);
13727
+ const absolutePath = path21.resolve(paths.wikiDir, relativePath);
13256
13728
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
13257
13729
  return null;
13258
13730
  }
13259
- const raw = await fs16.readFile(absolutePath, "utf8");
13731
+ const raw = await fs17.readFile(absolutePath, "utf8");
13260
13732
  const parsed = matter9(raw);
13261
13733
  return {
13262
13734
  path: relativePath,
13263
- title: typeof parsed.data.title === "string" ? parsed.data.title : path20.basename(relativePath, path20.extname(relativePath)),
13735
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path21.basename(relativePath, path21.extname(relativePath)),
13264
13736
  frontmatter: parsed.data,
13265
13737
  content: parsed.content
13266
13738
  };
@@ -13296,7 +13768,7 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
13296
13768
  severity: "warning",
13297
13769
  code: "stale_page",
13298
13770
  message: `Page ${page.title} is stale because the vault schema changed.`,
13299
- pagePath: path20.join(paths.wikiDir, page.path),
13771
+ pagePath: path21.join(paths.wikiDir, page.path),
13300
13772
  relatedPageIds: [page.id]
13301
13773
  });
13302
13774
  }
@@ -13307,7 +13779,7 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
13307
13779
  severity: "warning",
13308
13780
  code: "stale_page",
13309
13781
  message: `Page ${page.title} is stale because source ${sourceId} changed.`,
13310
- pagePath: path20.join(paths.wikiDir, page.path),
13782
+ pagePath: path21.join(paths.wikiDir, page.path),
13311
13783
  relatedSourceIds: [sourceId],
13312
13784
  relatedPageIds: [page.id]
13313
13785
  });
@@ -13318,13 +13790,13 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
13318
13790
  severity: "info",
13319
13791
  code: "orphan_page",
13320
13792
  message: `Page ${page.title} has no backlinks.`,
13321
- pagePath: path20.join(paths.wikiDir, page.path),
13793
+ pagePath: path21.join(paths.wikiDir, page.path),
13322
13794
  relatedPageIds: [page.id]
13323
13795
  });
13324
13796
  }
13325
- const absolutePath = path20.join(paths.wikiDir, page.path);
13797
+ const absolutePath = path21.join(paths.wikiDir, page.path);
13326
13798
  if (await fileExists(absolutePath)) {
13327
- const content = await fs16.readFile(absolutePath, "utf8");
13799
+ const content = await fs17.readFile(absolutePath, "utf8");
13328
13800
  if (content.includes("## Claims")) {
13329
13801
  const uncited = content.split("\n").filter((line) => line.startsWith("- ") && !line.includes("[source:"));
13330
13802
  if (uncited.length) {
@@ -13367,11 +13839,43 @@ async function lintVault(rootDir, options = {}) {
13367
13839
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13368
13840
  success: true,
13369
13841
  lintFindingCount: findings2.length,
13370
- lines: [`findings=${findings2.length}`, `deep=${Boolean(options.deep)}`, `web=${Boolean(options.web)}`]
13842
+ lines: [
13843
+ `findings=${findings2.length}`,
13844
+ `deep=${Boolean(options.deep)}`,
13845
+ `web=${Boolean(options.web)}`,
13846
+ `conflicts=${Boolean(options.conflicts)}`
13847
+ ]
13371
13848
  });
13372
13849
  return findings2;
13373
13850
  }
13851
+ const contradictionFindings = options.conflicts ? graph.edges.filter((edge) => edge.relation === "contradicts").map((edge) => {
13852
+ const sourceIdA = edge.provenance[0] ?? edge.source.replace(/^source:/, "");
13853
+ const sourceIdB = edge.provenance[1] ?? edge.target.replace(/^source:/, "");
13854
+ return {
13855
+ severity: "warning",
13856
+ code: "contradiction",
13857
+ message: `Contradicting claims detected between source "${sourceIdA}" and source "${sourceIdB}".`,
13858
+ relatedSourceIds: [sourceIdA, sourceIdB]
13859
+ };
13860
+ }) : [];
13861
+ if (options.conflicts && !options.deep) {
13862
+ await recordSession(rootDir, {
13863
+ operation: "lint",
13864
+ title: `Linted ${graph.pages.length} page(s)`,
13865
+ startedAt,
13866
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13867
+ success: true,
13868
+ relatedPageIds: graph.pages.map((page) => page.id),
13869
+ relatedSourceIds: uniqueStrings3(graph.pages.flatMap((page) => page.sourceIds)),
13870
+ lintFindingCount: contradictionFindings.length,
13871
+ lines: [`findings=${contradictionFindings.length}`, `deep=false`, `web=false`, `conflicts=true`]
13872
+ });
13873
+ return contradictionFindings;
13874
+ }
13374
13875
  const findings = await structuralLintFindings(rootDir, paths, graph, schemas, manifests, sourceProjects);
13876
+ if (options.conflicts) {
13877
+ findings.push(...contradictionFindings);
13878
+ }
13375
13879
  if (options.deep) {
13376
13880
  findings.push(...await runDeepLint(rootDir, findings, { web: options.web }));
13377
13881
  }
@@ -13386,7 +13890,12 @@ async function lintVault(rootDir, options = {}) {
13386
13890
  relatedPageIds: graph.pages.map((page) => page.id),
13387
13891
  relatedSourceIds: uniqueStrings3(graph.pages.flatMap((page) => page.sourceIds)),
13388
13892
  lintFindingCount: findings.length,
13389
- lines: [`findings=${findings.length}`, `deep=${Boolean(options.deep)}`, `web=${Boolean(options.web)}`]
13893
+ lines: [
13894
+ `findings=${findings.length}`,
13895
+ `deep=${Boolean(options.deep)}`,
13896
+ `web=${Boolean(options.web)}`,
13897
+ `conflicts=${Boolean(options.conflicts)}`
13898
+ ]
13390
13899
  });
13391
13900
  return findings;
13392
13901
  }
@@ -13404,7 +13913,7 @@ async function bootstrapDemo(rootDir, input) {
13404
13913
  }
13405
13914
 
13406
13915
  // src/mcp.ts
13407
- var SERVER_VERSION = "0.1.29";
13916
+ var SERVER_VERSION = "0.1.31";
13408
13917
  async function createMcpServer(rootDir) {
13409
13918
  const server = new McpServer({
13410
13919
  name: "swarmvault",
@@ -13675,7 +14184,7 @@ async function createMcpServer(rootDir) {
13675
14184
  },
13676
14185
  async () => {
13677
14186
  const { paths } = await loadVaultConfig(rootDir);
13678
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path21.relative(paths.sessionsDir, filePath))).sort();
14187
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path22.relative(paths.sessionsDir, filePath))).sort();
13679
14188
  return asTextResource("swarmvault://sessions", JSON.stringify(files, null, 2));
13680
14189
  }
13681
14190
  );
@@ -13708,8 +14217,8 @@ async function createMcpServer(rootDir) {
13708
14217
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
13709
14218
  }
13710
14219
  const { paths } = await loadVaultConfig(rootDir);
13711
- const absolutePath = path21.resolve(paths.wikiDir, relativePath);
13712
- return asTextResource(`swarmvault://pages/${encodedPath}`, await fs17.readFile(absolutePath, "utf8"));
14220
+ const absolutePath = path22.resolve(paths.wikiDir, relativePath);
14221
+ return asTextResource(`swarmvault://pages/${encodedPath}`, await fs18.readFile(absolutePath, "utf8"));
13713
14222
  }
13714
14223
  );
13715
14224
  server.registerResource(
@@ -13717,11 +14226,11 @@ async function createMcpServer(rootDir) {
13717
14226
  new ResourceTemplate("swarmvault://sessions/{path}", {
13718
14227
  list: async () => {
13719
14228
  const { paths } = await loadVaultConfig(rootDir);
13720
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path21.relative(paths.sessionsDir, filePath))).sort();
14229
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path22.relative(paths.sessionsDir, filePath))).sort();
13721
14230
  return {
13722
14231
  resources: files.map((relativePath) => ({
13723
14232
  uri: `swarmvault://sessions/${encodeURIComponent(relativePath)}`,
13724
- name: path21.basename(relativePath, ".md"),
14233
+ name: path22.basename(relativePath, ".md"),
13725
14234
  title: relativePath,
13726
14235
  description: "SwarmVault session artifact",
13727
14236
  mimeType: "text/markdown"
@@ -13738,11 +14247,11 @@ async function createMcpServer(rootDir) {
13738
14247
  const { paths } = await loadVaultConfig(rootDir);
13739
14248
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
13740
14249
  const relativePath = decodeURIComponent(encodedPath);
13741
- const absolutePath = path21.resolve(paths.sessionsDir, relativePath);
14250
+ const absolutePath = path22.resolve(paths.sessionsDir, relativePath);
13742
14251
  if (!absolutePath.startsWith(paths.sessionsDir) || !await fileExists(absolutePath)) {
13743
14252
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
13744
14253
  }
13745
- return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs17.readFile(absolutePath, "utf8"));
14254
+ return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs18.readFile(absolutePath, "utf8"));
13746
14255
  }
13747
14256
  );
13748
14257
  return server;
@@ -13790,13 +14299,13 @@ function asTextResource(uri, text) {
13790
14299
  }
13791
14300
 
13792
14301
  // src/schedule.ts
13793
- import fs18 from "fs/promises";
13794
- import path22 from "path";
14302
+ import fs19 from "fs/promises";
14303
+ import path23 from "path";
13795
14304
  function scheduleStatePath(schedulesDir, jobId) {
13796
- return path22.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
14305
+ return path23.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
13797
14306
  }
13798
14307
  function scheduleLockPath(schedulesDir, jobId) {
13799
- return path22.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
14308
+ return path23.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
13800
14309
  }
13801
14310
  function parseEveryDuration(value) {
13802
14311
  const match = value.trim().match(/^(\d+)(m|h|d)$/i);
@@ -13899,13 +14408,13 @@ async function acquireJobLease(rootDir, jobId) {
13899
14408
  const { paths } = await loadVaultConfig(rootDir);
13900
14409
  const leasePath = scheduleLockPath(paths.schedulesDir, jobId);
13901
14410
  await ensureDir(paths.schedulesDir);
13902
- const handle = await fs18.open(leasePath, "wx");
14411
+ const handle = await fs19.open(leasePath, "wx");
13903
14412
  await handle.writeFile(`${process.pid}
13904
14413
  ${(/* @__PURE__ */ new Date()).toISOString()}
13905
14414
  `);
13906
14415
  await handle.close();
13907
14416
  return async () => {
13908
- await fs18.rm(leasePath, { force: true });
14417
+ await fs19.rm(leasePath, { force: true });
13909
14418
  };
13910
14419
  }
13911
14420
  async function listSchedules(rootDir) {
@@ -14053,15 +14562,15 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
14053
14562
 
14054
14563
  // src/viewer.ts
14055
14564
  import { execFile } from "child_process";
14056
- import fs19 from "fs/promises";
14565
+ import fs20 from "fs/promises";
14057
14566
  import http from "http";
14058
- import path24 from "path";
14567
+ import path25 from "path";
14059
14568
  import { promisify } from "util";
14060
14569
  import matter10 from "gray-matter";
14061
14570
  import mime2 from "mime-types";
14062
14571
 
14063
14572
  // src/watch.ts
14064
- import path23 from "path";
14573
+ import path24 from "path";
14065
14574
  import process3 from "process";
14066
14575
  import chokidar from "chokidar";
14067
14576
  var MAX_BACKOFF_MS = 3e4;
@@ -14069,15 +14578,15 @@ var BACKOFF_THRESHOLD = 3;
14069
14578
  var CRITICAL_THRESHOLD = 10;
14070
14579
  var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
14071
14580
  function withinRoot2(rootPath, targetPath) {
14072
- const relative = path23.relative(rootPath, targetPath);
14073
- return relative === "" || !relative.startsWith("..") && !path23.isAbsolute(relative);
14581
+ const relative = path24.relative(rootPath, targetPath);
14582
+ return relative === "" || !relative.startsWith("..") && !path24.isAbsolute(relative);
14074
14583
  }
14075
14584
  function hasIgnoredRepoSegment(baseDir, targetPath) {
14076
- const relativePath = path23.relative(baseDir, targetPath);
14585
+ const relativePath = path24.relative(baseDir, targetPath);
14077
14586
  if (!relativePath || relativePath.startsWith("..")) {
14078
14587
  return false;
14079
14588
  }
14080
- return relativePath.split(path23.sep).some((segment) => REPO_WATCH_IGNORES.has(segment));
14589
+ return relativePath.split(path24.sep).some((segment) => REPO_WATCH_IGNORES.has(segment));
14081
14590
  }
14082
14591
  function workspaceIgnoreRoots(rootDir, paths) {
14083
14592
  return [
@@ -14086,16 +14595,16 @@ function workspaceIgnoreRoots(rootDir, paths) {
14086
14595
  paths.stateDir,
14087
14596
  paths.agentDir,
14088
14597
  paths.inboxDir,
14089
- path23.join(rootDir, ".claude"),
14090
- path23.join(rootDir, ".cursor"),
14091
- path23.join(rootDir, ".obsidian")
14092
- ].map((candidate) => path23.resolve(candidate));
14598
+ path24.join(rootDir, ".claude"),
14599
+ path24.join(rootDir, ".cursor"),
14600
+ path24.join(rootDir, ".obsidian")
14601
+ ].map((candidate) => path24.resolve(candidate));
14093
14602
  }
14094
14603
  async function resolveWatchTargets(rootDir, paths, options) {
14095
- const targets = /* @__PURE__ */ new Set([path23.resolve(paths.inboxDir)]);
14604
+ const targets = /* @__PURE__ */ new Set([path24.resolve(paths.inboxDir)]);
14096
14605
  if (options.repo) {
14097
14606
  for (const repoRoot of await listTrackedRepoRoots(rootDir)) {
14098
- targets.add(path23.resolve(repoRoot));
14607
+ targets.add(path24.resolve(repoRoot));
14099
14608
  }
14100
14609
  }
14101
14610
  return [...targets].sort((left, right) => left.localeCompare(right));
@@ -14225,7 +14734,7 @@ async function watchVault(rootDir, options = {}) {
14225
14734
  const { paths } = await initWorkspace(rootDir);
14226
14735
  const baseDebounceMs = options.debounceMs ?? 900;
14227
14736
  const ignoredRoots = workspaceIgnoreRoots(rootDir, paths);
14228
- const inboxWatchRoot = path23.resolve(paths.inboxDir);
14737
+ const inboxWatchRoot = path24.resolve(paths.inboxDir);
14229
14738
  let watchTargets = await resolveWatchTargets(rootDir, paths, options);
14230
14739
  let timer;
14231
14740
  let running = false;
@@ -14239,7 +14748,7 @@ async function watchVault(rootDir, options = {}) {
14239
14748
  usePolling: true,
14240
14749
  interval: 100,
14241
14750
  ignored: (targetPath) => {
14242
- const absolutePath = path23.resolve(targetPath);
14751
+ const absolutePath = path24.resolve(targetPath);
14243
14752
  const primaryTarget = watchTargets.filter((watchTarget) => withinRoot2(watchTarget, absolutePath)).sort((left, right) => right.length - left.length)[0] ?? null;
14244
14753
  if (!primaryTarget) {
14245
14754
  return false;
@@ -14423,8 +14932,8 @@ async function watchVault(rootDir, options = {}) {
14423
14932
  }
14424
14933
  };
14425
14934
  const reasonForPath = (targetPath) => {
14426
- const baseDir = watchTargets.filter((watchTarget) => withinRoot2(watchTarget, path23.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
14427
- return path23.relative(baseDir, targetPath) || ".";
14935
+ const baseDir = watchTargets.filter((watchTarget) => withinRoot2(watchTarget, path24.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
14936
+ return path24.relative(baseDir, targetPath) || ".";
14428
14937
  };
14429
14938
  watcher.on("add", (filePath) => schedule(`add:${reasonForPath(filePath)}`)).on("change", (filePath) => schedule(`change:${reasonForPath(filePath)}`)).on("unlink", (filePath) => schedule(`unlink:${reasonForPath(filePath)}`)).on("addDir", (dirPath) => schedule(`addDir:${reasonForPath(dirPath)}`)).on("unlinkDir", (dirPath) => schedule(`unlinkDir:${reasonForPath(dirPath)}`)).on("error", (caught) => schedule(`error:${caught instanceof Error ? caught.message : String(caught)}`));
14430
14939
  await new Promise((resolve, reject) => {
@@ -14465,15 +14974,15 @@ async function getWatchStatus(rootDir) {
14465
14974
  var execFileAsync = promisify(execFile);
14466
14975
  async function readViewerPage(rootDir, relativePath) {
14467
14976
  const { paths } = await loadVaultConfig(rootDir);
14468
- const absolutePath = path24.resolve(paths.wikiDir, relativePath);
14977
+ const absolutePath = path25.resolve(paths.wikiDir, relativePath);
14469
14978
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
14470
14979
  return null;
14471
14980
  }
14472
- const raw = await fs19.readFile(absolutePath, "utf8");
14981
+ const raw = await fs20.readFile(absolutePath, "utf8");
14473
14982
  const parsed = matter10(raw);
14474
14983
  return {
14475
14984
  path: relativePath,
14476
- title: typeof parsed.data.title === "string" ? parsed.data.title : path24.basename(relativePath, path24.extname(relativePath)),
14985
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path25.basename(relativePath, path25.extname(relativePath)),
14477
14986
  frontmatter: parsed.data,
14478
14987
  content: parsed.content,
14479
14988
  assets: normalizeOutputAssets(parsed.data.output_assets)
@@ -14481,12 +14990,12 @@ async function readViewerPage(rootDir, relativePath) {
14481
14990
  }
14482
14991
  async function readViewerAsset(rootDir, relativePath) {
14483
14992
  const { paths } = await loadVaultConfig(rootDir);
14484
- const absolutePath = path24.resolve(paths.wikiDir, relativePath);
14993
+ const absolutePath = path25.resolve(paths.wikiDir, relativePath);
14485
14994
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
14486
14995
  return null;
14487
14996
  }
14488
14997
  return {
14489
- buffer: await fs19.readFile(absolutePath),
14998
+ buffer: await fs20.readFile(absolutePath),
14490
14999
  mimeType: mime2.lookup(absolutePath) || "application/octet-stream"
14491
15000
  };
14492
15001
  }
@@ -14509,12 +15018,12 @@ async function readJsonBody(request) {
14509
15018
  return JSON.parse(raw);
14510
15019
  }
14511
15020
  async function ensureViewerDist(viewerDistDir) {
14512
- const indexPath = path24.join(viewerDistDir, "index.html");
15021
+ const indexPath = path25.join(viewerDistDir, "index.html");
14513
15022
  if (await fileExists(indexPath)) {
14514
15023
  return;
14515
15024
  }
14516
- const viewerProjectDir = path24.dirname(viewerDistDir);
14517
- if (await fileExists(path24.join(viewerProjectDir, "package.json"))) {
15025
+ const viewerProjectDir = path25.dirname(viewerDistDir);
15026
+ if (await fileExists(path25.join(viewerProjectDir, "package.json"))) {
14518
15027
  await execFileAsync("pnpm", ["build"], { cwd: viewerProjectDir });
14519
15028
  }
14520
15029
  }
@@ -14531,7 +15040,7 @@ async function startGraphServer(rootDir, port) {
14531
15040
  return;
14532
15041
  }
14533
15042
  response.writeHead(200, { "content-type": "application/json" });
14534
- response.end(await fs19.readFile(paths.graphPath, "utf8"));
15043
+ response.end(await fs20.readFile(paths.graphPath, "utf8"));
14535
15044
  return;
14536
15045
  }
14537
15046
  if (url.pathname === "/api/graph/query") {
@@ -14588,14 +15097,14 @@ async function startGraphServer(rootDir, port) {
14588
15097
  return;
14589
15098
  }
14590
15099
  if (url.pathname === "/api/graph-report") {
14591
- const reportPath = path24.join(paths.wikiDir, "graph", "report.json");
15100
+ const reportPath = path25.join(paths.wikiDir, "graph", "report.json");
14592
15101
  if (!await fileExists(reportPath)) {
14593
15102
  response.writeHead(404, { "content-type": "application/json" });
14594
15103
  response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
14595
15104
  return;
14596
15105
  }
14597
15106
  response.writeHead(200, { "content-type": "application/json" });
14598
- response.end(await fs19.readFile(reportPath, "utf8"));
15107
+ response.end(await fs20.readFile(reportPath, "utf8"));
14599
15108
  return;
14600
15109
  }
14601
15110
  if (url.pathname === "/api/watch-status") {
@@ -14678,8 +15187,8 @@ async function startGraphServer(rootDir, port) {
14678
15187
  return;
14679
15188
  }
14680
15189
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
14681
- const target = path24.join(paths.viewerDistDir, relativePath);
14682
- const fallback = path24.join(paths.viewerDistDir, "index.html");
15190
+ const target = path25.join(paths.viewerDistDir, relativePath);
15191
+ const fallback = path25.join(paths.viewerDistDir, "index.html");
14683
15192
  const filePath = await fileExists(target) ? target : fallback;
14684
15193
  if (!await fileExists(filePath)) {
14685
15194
  response.writeHead(503, { "content-type": "text/plain" });
@@ -14687,7 +15196,7 @@ async function startGraphServer(rootDir, port) {
14687
15196
  return;
14688
15197
  }
14689
15198
  response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
14690
- response.end(await fs19.readFile(filePath));
15199
+ response.end(await fs20.readFile(filePath));
14691
15200
  });
14692
15201
  await new Promise((resolve) => {
14693
15202
  server.listen(effectivePort, resolve);
@@ -14714,7 +15223,7 @@ async function exportGraphHtml(rootDir, outputPath) {
14714
15223
  throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
14715
15224
  }
14716
15225
  await ensureViewerDist(paths.viewerDistDir);
14717
- const indexPath = path24.join(paths.viewerDistDir, "index.html");
15226
+ const indexPath = path25.join(paths.viewerDistDir, "index.html");
14718
15227
  if (!await fileExists(indexPath)) {
14719
15228
  throw new Error("Viewer build not found. Run `pnpm build` first.");
14720
15229
  }
@@ -14740,17 +15249,17 @@ async function exportGraphHtml(rootDir, outputPath) {
14740
15249
  } : null;
14741
15250
  })
14742
15251
  );
14743
- const rawHtml = await fs19.readFile(indexPath, "utf8");
15252
+ const rawHtml = await fs20.readFile(indexPath, "utf8");
14744
15253
  const scriptMatch = rawHtml.match(/<script type="module" crossorigin src="([^"]+)"><\/script>/);
14745
15254
  const styleMatch = rawHtml.match(/<link rel="stylesheet" crossorigin href="([^"]+)">/);
14746
- const scriptPath = scriptMatch?.[1] ? path24.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
14747
- const stylePath = styleMatch?.[1] ? path24.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
15255
+ const scriptPath = scriptMatch?.[1] ? path25.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
15256
+ const stylePath = styleMatch?.[1] ? path25.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
14748
15257
  if (!scriptPath || !await fileExists(scriptPath)) {
14749
15258
  throw new Error("Viewer script bundle not found. Run `pnpm build` first.");
14750
15259
  }
14751
- const script = await fs19.readFile(scriptPath, "utf8");
14752
- const style = stylePath && await fileExists(stylePath) ? await fs19.readFile(stylePath, "utf8") : "";
14753
- const report = await readJsonFile(path24.join(paths.wikiDir, "graph", "report.json"));
15260
+ const script = await fs20.readFile(scriptPath, "utf8");
15261
+ const style = stylePath && await fileExists(stylePath) ? await fs20.readFile(stylePath, "utf8") : "";
15262
+ const report = await readJsonFile(path25.join(paths.wikiDir, "graph", "report.json"));
14754
15263
  const embeddedData = JSON.stringify({ graph, pages: pages.filter(Boolean), report }, null, 2).replace(/</g, "\\u003c");
14755
15264
  const html = [
14756
15265
  "<!doctype html>",
@@ -14769,9 +15278,9 @@ async function exportGraphHtml(rootDir, outputPath) {
14769
15278
  "</html>",
14770
15279
  ""
14771
15280
  ].filter(Boolean).join("\n");
14772
- await fs19.mkdir(path24.dirname(outputPath), { recursive: true });
14773
- await fs19.writeFile(outputPath, html, "utf8");
14774
- return path24.resolve(outputPath);
15281
+ await fs20.mkdir(path25.dirname(outputPath), { recursive: true });
15282
+ await fs20.writeFile(outputPath, html, "utf8");
15283
+ return path25.resolve(outputPath);
14775
15284
  }
14776
15285
  export {
14777
15286
  acceptApproval,
@@ -14817,6 +15326,7 @@ export {
14817
15326
  loadVaultSchemas,
14818
15327
  pathGraphVault,
14819
15328
  promoteCandidate,
15329
+ pushGraphNeo4j,
14820
15330
  queryGraphVault,
14821
15331
  queryVault,
14822
15332
  readApproval,