@swarmvaultai/engine 0.7.23 → 0.7.25

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
@@ -22,7 +22,12 @@ import {
22
22
  uniqueBy,
23
23
  writeFileIfChanged,
24
24
  writeJsonFile
25
- } from "./chunk-2CH2WWS4.js";
25
+ } from "./chunk-MB7HPUTR.js";
26
+ import {
27
+ estimatePageTokens,
28
+ estimateTokens,
29
+ trimToTokenBudget
30
+ } from "./chunk-NAIERP4C.js";
26
31
 
27
32
  // src/agents.ts
28
33
  import crypto from "crypto";
@@ -450,6 +455,43 @@ async function installConfiguredAgents(rootDir) {
450
455
  );
451
456
  }
452
457
 
458
+ // src/auto-commit.ts
459
+ import { execFile } from "child_process";
460
+ import { promisify } from "util";
461
+ var execFileAsync = promisify(execFile);
462
+ async function git(rootDir, ...args) {
463
+ const { stdout } = await execFileAsync("git", args, { cwd: rootDir });
464
+ return stdout.trim();
465
+ }
466
+ async function isGitRepo(rootDir) {
467
+ try {
468
+ await git(rootDir, "rev-parse", "--is-inside-work-tree");
469
+ return true;
470
+ } catch {
471
+ return false;
472
+ }
473
+ }
474
+ async function autoCommitWikiChanges(rootDir, operation, detail, options) {
475
+ const { config, paths } = await loadVaultConfig(rootDir);
476
+ if (!options?.force && !config.autoCommit) {
477
+ return null;
478
+ }
479
+ if (!await isGitRepo(rootDir)) {
480
+ return null;
481
+ }
482
+ const wikiRelative = paths.wikiDir.replace(`${rootDir}/`, "");
483
+ const stateRelative = paths.stateDir.replace(`${rootDir}/`, "");
484
+ await git(rootDir, "add", wikiRelative, stateRelative).catch(() => {
485
+ });
486
+ const status = await git(rootDir, "diff", "--cached", "--stat");
487
+ if (!status) {
488
+ return null;
489
+ }
490
+ const message = detail ? `vault ${operation}: ${detail}` : `vault ${operation}`;
491
+ await git(rootDir, "commit", "-m", message);
492
+ return message;
493
+ }
494
+
453
495
  // src/graph-export.ts
454
496
  import fs2 from "fs/promises";
455
497
  import path2 from "path";
@@ -587,6 +629,316 @@ function graphCounts(graph) {
587
629
  };
588
630
  }
589
631
 
632
+ // src/graph-report-html.ts
633
+ function htmlEscape(text) {
634
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
635
+ }
636
+ function nodeTypeColor(type) {
637
+ const colors = {
638
+ source: "#f59e0b",
639
+ module: "#fb7185",
640
+ symbol: "#8b5cf6",
641
+ rationale: "#14b8a6",
642
+ concept: "#0ea5e9",
643
+ entity: "#22c55e"
644
+ };
645
+ return colors[type] ?? "#94a3b8";
646
+ }
647
+ function renderGraphReportHtml(graph, report) {
648
+ const nodesByType = /* @__PURE__ */ new Map();
649
+ for (const node of graph.nodes) {
650
+ nodesByType.set(node.type, (nodesByType.get(node.type) ?? 0) + 1);
651
+ }
652
+ const edgesByRelation = /* @__PURE__ */ new Map();
653
+ for (const edge of graph.edges) {
654
+ edgesByRelation.set(edge.relation, (edgesByRelation.get(edge.relation) ?? 0) + 1);
655
+ }
656
+ const pagesByKind = /* @__PURE__ */ new Map();
657
+ for (const page of graph.pages) {
658
+ const list = pagesByKind.get(page.kind) ?? [];
659
+ list.push(page);
660
+ pagesByKind.set(page.kind, list);
661
+ }
662
+ const godNodes = (report?.godNodes ?? []).slice(0, 15);
663
+ const bridgeNodes = (report?.bridgeNodes ?? []).slice(0, 10);
664
+ const communities = graph.communities ?? [];
665
+ const warnings = report?.warnings ?? [];
666
+ const overview = report?.overview ?? {
667
+ nodes: graph.nodes.length,
668
+ edges: graph.edges.length,
669
+ pages: graph.pages.length,
670
+ communities: communities.length
671
+ };
672
+ const sortedEdgeRelations = [...edgesByRelation.entries()].sort((a, b) => b[1] - a[1]);
673
+ const sortedNodeTypes = [...nodesByType.entries()].sort((a, b) => b[1] - a[1]);
674
+ const sortedCommunities2 = [...communities].sort((a, b) => b.nodeIds.length - a.nodeIds.length).slice(0, 20);
675
+ return `<!DOCTYPE html>
676
+ <html lang="en">
677
+ <head>
678
+ <meta charset="UTF-8">
679
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
680
+ <title>SwarmVault Graph Report</title>
681
+ <style>
682
+ :root {
683
+ --bg: #0f172a;
684
+ --surface: #1e293b;
685
+ --surface2: #334155;
686
+ --text: #e2e8f0;
687
+ --muted: #94a3b8;
688
+ --accent: #0ea5e9;
689
+ --accent2: #8b5cf6;
690
+ --border: #475569;
691
+ --success: #22c55e;
692
+ --warning: #f59e0b;
693
+ --danger: #ef4444;
694
+ }
695
+ * { box-sizing: border-box; margin: 0; padding: 0; }
696
+ body {
697
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
698
+ background: var(--bg);
699
+ color: var(--text);
700
+ line-height: 1.6;
701
+ padding: 2rem;
702
+ max-width: 1200px;
703
+ margin: 0 auto;
704
+ }
705
+ h1 {
706
+ font-size: 1.75rem;
707
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
708
+ -webkit-background-clip: text;
709
+ -webkit-text-fill-color: transparent;
710
+ margin-bottom: 0.25rem;
711
+ }
712
+ .subtitle { color: var(--muted); font-size: 0.85rem; margin-bottom: 2rem; }
713
+ h2 {
714
+ font-size: 1.15rem;
715
+ color: var(--text);
716
+ border-bottom: 1px solid var(--border);
717
+ padding-bottom: 0.5rem;
718
+ margin: 2rem 0 1rem;
719
+ }
720
+ .stats-grid {
721
+ display: grid;
722
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
723
+ gap: 1rem;
724
+ margin-bottom: 1.5rem;
725
+ }
726
+ .stat-card {
727
+ background: var(--surface);
728
+ border: 1px solid var(--border);
729
+ border-radius: 8px;
730
+ padding: 1rem;
731
+ text-align: center;
732
+ }
733
+ .stat-card .value {
734
+ font-size: 1.75rem;
735
+ font-weight: 700;
736
+ color: var(--accent);
737
+ }
738
+ .stat-card .label {
739
+ font-size: 0.8rem;
740
+ color: var(--muted);
741
+ text-transform: uppercase;
742
+ letter-spacing: 0.05em;
743
+ }
744
+ .badge {
745
+ display: inline-block;
746
+ padding: 0.15rem 0.5rem;
747
+ border-radius: 9999px;
748
+ font-size: 0.7rem;
749
+ font-weight: 600;
750
+ color: #fff;
751
+ }
752
+ table {
753
+ width: 100%;
754
+ border-collapse: collapse;
755
+ margin-bottom: 1rem;
756
+ }
757
+ th, td {
758
+ text-align: left;
759
+ padding: 0.5rem 0.75rem;
760
+ border-bottom: 1px solid var(--border);
761
+ font-size: 0.85rem;
762
+ }
763
+ th {
764
+ color: var(--muted);
765
+ font-weight: 600;
766
+ font-size: 0.75rem;
767
+ text-transform: uppercase;
768
+ letter-spacing: 0.05em;
769
+ }
770
+ tr:hover { background: var(--surface); }
771
+ .bar-container {
772
+ display: flex;
773
+ align-items: center;
774
+ gap: 0.5rem;
775
+ }
776
+ .bar {
777
+ height: 8px;
778
+ border-radius: 4px;
779
+ background: var(--accent);
780
+ min-width: 2px;
781
+ }
782
+ .warning-list {
783
+ list-style: none;
784
+ padding: 0;
785
+ }
786
+ .warning-list li {
787
+ padding: 0.5rem 0.75rem;
788
+ margin-bottom: 0.5rem;
789
+ background: var(--surface);
790
+ border-left: 3px solid var(--warning);
791
+ border-radius: 0 4px 4px 0;
792
+ font-size: 0.85rem;
793
+ }
794
+ .page-group { margin-bottom: 1.5rem; }
795
+ .page-group-title {
796
+ font-size: 0.85rem;
797
+ font-weight: 600;
798
+ color: var(--accent);
799
+ text-transform: capitalize;
800
+ margin-bottom: 0.5rem;
801
+ }
802
+ .page-list {
803
+ display: grid;
804
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
805
+ gap: 0.5rem;
806
+ }
807
+ .page-item {
808
+ background: var(--surface);
809
+ border: 1px solid var(--border);
810
+ border-radius: 6px;
811
+ padding: 0.5rem 0.75rem;
812
+ font-size: 0.8rem;
813
+ overflow: hidden;
814
+ text-overflow: ellipsis;
815
+ white-space: nowrap;
816
+ }
817
+ .page-item .path { color: var(--muted); font-size: 0.7rem; }
818
+ .empty { color: var(--muted); font-style: italic; font-size: 0.85rem; }
819
+ input[type="text"] {
820
+ width: 100%;
821
+ padding: 0.5rem 0.75rem;
822
+ background: var(--surface);
823
+ border: 1px solid var(--border);
824
+ border-radius: 6px;
825
+ color: var(--text);
826
+ font-size: 0.85rem;
827
+ margin-bottom: 1rem;
828
+ outline: none;
829
+ }
830
+ input[type="text"]:focus { border-color: var(--accent); }
831
+ .section { margin-bottom: 1rem; }
832
+ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.75rem; text-align: center; }
833
+ </style>
834
+ </head>
835
+ <body>
836
+ <h1>SwarmVault Graph Report</h1>
837
+ <p class="subtitle">Generated ${htmlEscape(report?.generatedAt ?? graph.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString())}</p>
838
+
839
+ <div class="stats-grid">
840
+ <div class="stat-card"><div class="value">${overview.nodes}</div><div class="label">Nodes</div></div>
841
+ <div class="stat-card"><div class="value">${overview.edges}</div><div class="label">Edges</div></div>
842
+ <div class="stat-card"><div class="value">${overview.pages}</div><div class="label">Pages</div></div>
843
+ <div class="stat-card"><div class="value">${overview.communities}</div><div class="label">Communities</div></div>
844
+ <div class="stat-card"><div class="value">${graph.sources.length}</div><div class="label">Sources</div></div>
845
+ <div class="stat-card"><div class="value">${(graph.hyperedges ?? []).length}</div><div class="label">Hyperedges</div></div>
846
+ </div>
847
+
848
+ <h2>Node Types</h2>
849
+ <table>
850
+ <thead><tr><th>Type</th><th>Count</th><th></th></tr></thead>
851
+ <tbody>
852
+ ${sortedNodeTypes.map(([type, count]) => {
853
+ const maxCount = sortedNodeTypes[0]?.[1] ?? 1;
854
+ const pct = Math.round(count / maxCount * 100);
855
+ return `<tr><td><span class="badge" style="background:${nodeTypeColor(type)}">${htmlEscape(type)}</span></td><td>${count}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%;background:${nodeTypeColor(type)}"></div></div></td></tr>`;
856
+ }).join("\n")}
857
+ </tbody>
858
+ </table>
859
+
860
+ <h2>Edge Relations</h2>
861
+ <table>
862
+ <thead><tr><th>Relation</th><th>Count</th><th></th></tr></thead>
863
+ <tbody>
864
+ ${sortedEdgeRelations.map(([relation, count]) => {
865
+ const maxCount = sortedEdgeRelations[0]?.[1] ?? 1;
866
+ const pct = Math.round(count / maxCount * 100);
867
+ return `<tr><td>${htmlEscape(relation)}</td><td>${count}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%"></div></div></td></tr>`;
868
+ }).join("\n")}
869
+ </tbody>
870
+ </table>
871
+
872
+ ${godNodes.length ? `<h2>God Nodes (Highest Connectivity)</h2>
873
+ <table>
874
+ <thead><tr><th>Label</th><th>Degree</th><th>Bridge Score</th><th></th></tr></thead>
875
+ <tbody>
876
+ ${godNodes.map((node) => {
877
+ const maxDegree = godNodes[0]?.degree ?? 1;
878
+ const pct = Math.round((node.degree ?? 0) / maxDegree * 100);
879
+ return `<tr><td>${htmlEscape(node.label)}</td><td>${node.degree ?? 0}</td><td>${(node.bridgeScore ?? 0).toFixed(2)}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%;background:var(--accent2)"></div></div></td></tr>`;
880
+ }).join("\n")}
881
+ </tbody>
882
+ </table>` : ""}
883
+
884
+ ${bridgeNodes.length ? `<h2>Bridge Nodes</h2>
885
+ <table>
886
+ <thead><tr><th>Label</th><th>Degree</th><th>Bridge Score</th></tr></thead>
887
+ <tbody>
888
+ ${bridgeNodes.map((node) => `<tr><td>${htmlEscape(node.label)}</td><td>${node.degree ?? 0}</td><td>${(node.bridgeScore ?? 0).toFixed(2)}</td></tr>`).join("\n")}
889
+ </tbody>
890
+ </table>` : ""}
891
+
892
+ ${sortedCommunities2.length ? `<h2>Communities</h2>
893
+ <table>
894
+ <thead><tr><th>Label</th><th>Nodes</th><th></th></tr></thead>
895
+ <tbody>
896
+ ${sortedCommunities2.map((c) => {
897
+ const maxSize = sortedCommunities2[0]?.nodeIds.length ?? 1;
898
+ const pct = Math.round(c.nodeIds.length / maxSize * 100);
899
+ return `<tr><td>${htmlEscape(c.label)}</td><td>${c.nodeIds.length}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%;background:var(--success)"></div></div></td></tr>`;
900
+ }).join("\n")}
901
+ </tbody>
902
+ </table>` : ""}
903
+
904
+ ${warnings.length ? `<h2>Warnings</h2>
905
+ <ul class="warning-list">
906
+ ${warnings.map((w) => `<li>${htmlEscape(w)}</li>`).join("\n")}
907
+ </ul>` : ""}
908
+
909
+ <h2>Pages</h2>
910
+ <input type="text" id="page-filter" placeholder="Filter pages..." />
911
+ <div id="pages-container">
912
+ ${[...pagesByKind.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(
913
+ ([kind, pages]) => `<div class="page-group" data-kind="${htmlEscape(kind)}">
914
+ <div class="page-group-title">${htmlEscape(kind)} (${pages.length})</div>
915
+ <div class="page-list">
916
+ ${pages.sort((a, b) => a.title.localeCompare(b.title)).map(
917
+ (p) => `<div class="page-item" data-title="${htmlEscape(p.title.toLowerCase())}"><strong>${htmlEscape(p.title)}</strong><div class="path">${htmlEscape(p.path)}</div></div>`
918
+ ).join("\n ")}
919
+ </div>
920
+ </div>`
921
+ ).join("\n")}
922
+ </div>
923
+
924
+ <footer>Generated by SwarmVault &middot; ${graph.nodes.length} nodes &middot; ${graph.edges.length} edges &middot; ${graph.pages.length} pages</footer>
925
+
926
+ <script>
927
+ document.getElementById("page-filter").addEventListener("input", function(e) {
928
+ var query = e.target.value.toLowerCase();
929
+ document.querySelectorAll(".page-item").forEach(function(el) {
930
+ el.style.display = el.getAttribute("data-title").includes(query) ? "" : "none";
931
+ });
932
+ document.querySelectorAll(".page-group").forEach(function(group) {
933
+ var visible = group.querySelectorAll('.page-item[style=""], .page-item:not([style])').length;
934
+ group.style.display = visible > 0 || !query ? "" : "none";
935
+ });
936
+ });
937
+ </script>
938
+ </body>
939
+ </html>`;
940
+ }
941
+
590
942
  // src/graph-export.ts
591
943
  var NODE_COLORS = {
592
944
  source: "#f59e0b",
@@ -1220,6 +1572,14 @@ async function exportGraphFormat(rootDir, format, outputPath) {
1220
1572
  const resolvedPath = await writeGraphExport(outputPath, rendered);
1221
1573
  return { format, outputPath: resolvedPath };
1222
1574
  }
1575
+ async function exportGraphReportHtml(rootDir, outputPath) {
1576
+ const { paths } = await loadVaultConfig(rootDir);
1577
+ const graph = await loadGraph(rootDir);
1578
+ const report = await readJsonFile(path2.join(paths.wikiDir, "graph", "report.json"));
1579
+ const html = renderGraphReportHtml(graph, report);
1580
+ const resolvedPath = await writeGraphExport(outputPath, html);
1581
+ return { format: "report", outputPath: resolvedPath };
1582
+ }
1223
1583
  function safeFileName(label) {
1224
1584
  return label.replace(/[\\/*?:"<>|#^[\]]/g, "").replace(/\s+/g, " ").trim().slice(0, 200) || "unnamed";
1225
1585
  }
@@ -1463,7 +1823,7 @@ function nodeMap(graph) {
1463
1823
  function pageMap(graph) {
1464
1824
  return new Map(graph.pages.map((page) => [page.id, page]));
1465
1825
  }
1466
- function estimateTokens(text) {
1826
+ function estimateTokens2(text) {
1467
1827
  return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
1468
1828
  }
1469
1829
  function estimateCorpusWords(texts) {
@@ -1500,7 +1860,7 @@ function benchmarkQueryTokens(graph, queryResult, pageContentsById) {
1500
1860
  const target = nodesById.get(edge.target)?.label ?? edge.target;
1501
1861
  lines.push(`EDGE ${source} --${edge.relation}/${edge.evidenceClass}/${edge.confidence.toFixed(2)}--> ${target}`);
1502
1862
  }
1503
- const queryTokens = estimateTokens(lines.join("\n"));
1863
+ const queryTokens = estimateTokens2(lines.join("\n"));
1504
1864
  return {
1505
1865
  question: queryResult.question,
1506
1866
  queryTokens,
@@ -2217,6 +2577,67 @@ function graphDiff(oldGraph, newGraph) {
2217
2577
  const summary = parts.length ? parts.join("; ") : "No changes";
2218
2578
  return { addedNodes, removedNodes, addedEdges, removedEdges, addedPages, removedPages, summary };
2219
2579
  }
2580
+ function blastRadius(graph, target, options) {
2581
+ const maxDepth = Math.max(1, Math.min(options?.maxDepth ?? 3, 10));
2582
+ const resolved = resolveNode(graph, target);
2583
+ const moduleNode = resolved?.type === "module" ? resolved : resolved?.moduleId ? graph.nodes.find((n) => n.id === resolved.moduleId) : void 0;
2584
+ if (!moduleNode) {
2585
+ const normalizedTarget = normalizeTarget(target);
2586
+ const candidate = graph.nodes.filter((n) => n.type === "module").find((n) => normalizeTarget(n.label).includes(normalizedTarget) || normalizeTarget(n.id).includes(normalizedTarget));
2587
+ if (!candidate) {
2588
+ return {
2589
+ target,
2590
+ totalAffected: 0,
2591
+ maxDepth,
2592
+ affectedModules: [],
2593
+ summary: `No module found matching "${target}".`
2594
+ };
2595
+ }
2596
+ return blastRadius(graph, candidate.id, options);
2597
+ }
2598
+ const reverseImports = /* @__PURE__ */ new Map();
2599
+ for (const edge of graph.edges) {
2600
+ if (edge.relation === "imports") {
2601
+ const dependents = reverseImports.get(edge.target) ?? [];
2602
+ dependents.push(edge.source);
2603
+ reverseImports.set(edge.target, dependents);
2604
+ }
2605
+ }
2606
+ const affected = [];
2607
+ const seen = /* @__PURE__ */ new Set([moduleNode.id]);
2608
+ const frontier = [{ id: moduleNode.id, depth: 0 }];
2609
+ const nodes = nodeById(graph);
2610
+ while (frontier.length > 0) {
2611
+ const current = frontier.shift();
2612
+ if (current.depth >= maxDepth) {
2613
+ continue;
2614
+ }
2615
+ for (const dependentId of reverseImports.get(current.id) ?? []) {
2616
+ if (seen.has(dependentId)) {
2617
+ continue;
2618
+ }
2619
+ seen.add(dependentId);
2620
+ const dependentNode = nodes.get(dependentId);
2621
+ const nextDepth = current.depth + 1;
2622
+ affected.push({
2623
+ moduleId: dependentId,
2624
+ label: dependentNode?.label ?? dependentId,
2625
+ depth: nextDepth
2626
+ });
2627
+ frontier.push({ id: dependentId, depth: nextDepth });
2628
+ }
2629
+ }
2630
+ affected.sort((a, b) => a.depth - b.depth || a.label.localeCompare(b.label));
2631
+ const summary = affected.length ? `Changing "${moduleNode.label}" affects ${affected.length} module${affected.length === 1 ? "" : "s"} (max depth ${maxDepth}).` : `No modules depend on "${moduleNode.label}".`;
2632
+ return {
2633
+ target,
2634
+ resolvedModuleId: moduleNode.id,
2635
+ affectedModules: affected,
2636
+ totalAffected: affected.length,
2637
+ maxDepth,
2638
+ summary
2639
+ };
2640
+ }
2220
2641
 
2221
2642
  // src/hooks.ts
2222
2643
  import fs4 from "fs/promises";
@@ -14024,6 +14445,33 @@ async function semanticGraphMatches(rootDir, graph, question, limit = 12) {
14024
14445
  score: Math.max(0, Number((cosineSimilarity(queryVector, vectors.get(`${item.kind}:${item.id}`) ?? []) * 100).toFixed(2)))
14025
14446
  })).filter((match) => match.score >= 18).sort((left, right) => right.score - left.score || left.label.localeCompare(right.label)).slice(0, limit);
14026
14447
  }
14448
+ async function semanticPageSearch(rootDir, graph, query, limit = 10) {
14449
+ const items = await buildEmbeddableItems(rootDir, graph);
14450
+ const pageItems = items.filter((item) => item.kind === "page");
14451
+ if (!pageItems.length) {
14452
+ return [];
14453
+ }
14454
+ const { provider, vectors } = await resolveVectorsForItems(rootDir, graph.generatedAt, items);
14455
+ if (!provider) {
14456
+ return [];
14457
+ }
14458
+ const [queryVector] = await provider.embedTexts([query]);
14459
+ if (!Array.isArray(queryVector) || queryVector.length === 0) {
14460
+ return [];
14461
+ }
14462
+ const pageMap2 = new Map(graph.pages.map((page) => [page.id, page]));
14463
+ return pageItems.map((item) => {
14464
+ const page = pageMap2.get(item.id);
14465
+ return {
14466
+ pageId: item.id,
14467
+ path: page?.path ?? "",
14468
+ title: item.label,
14469
+ kind: page?.kind ?? "",
14470
+ status: page?.status ?? "",
14471
+ score: cosineSimilarity(queryVector, vectors.get(`page:${item.id}`) ?? [])
14472
+ };
14473
+ }).filter((result) => result.score >= 0.25 && result.path).sort((left, right) => right.score - left.score).slice(0, limit);
14474
+ }
14027
14475
  function distinctScope(left, right) {
14028
14476
  const leftSources = new Set(left.sourceIds);
14029
14477
  const rightSources = new Set(right.sourceIds);
@@ -16633,6 +17081,38 @@ ${excerpt.trim()}`.trim();
16633
17081
  db.exec("INSERT INTO page_search (rowid, title, body) SELECT rowid, title, body FROM pages;");
16634
17082
  db.close();
16635
17083
  }
17084
+ function mergeSearchResults(ftsResults, semanticHits, limit) {
17085
+ const k = 60;
17086
+ const scores = /* @__PURE__ */ new Map();
17087
+ const resultMap = /* @__PURE__ */ new Map();
17088
+ for (let i = 0; i < ftsResults.length; i++) {
17089
+ const r = ftsResults[i];
17090
+ scores.set(r.pageId, (scores.get(r.pageId) ?? 0) + 1 / (k + i + 1));
17091
+ resultMap.set(r.pageId, r);
17092
+ }
17093
+ for (let i = 0; i < semanticHits.length; i++) {
17094
+ const hit = semanticHits[i];
17095
+ scores.set(hit.pageId, (scores.get(hit.pageId) ?? 0) + 1 / (k + i + 1));
17096
+ if (!resultMap.has(hit.pageId)) {
17097
+ resultMap.set(hit.pageId, {
17098
+ pageId: hit.pageId,
17099
+ path: hit.path,
17100
+ title: hit.title,
17101
+ snippet: "",
17102
+ rank: -hit.score,
17103
+ kind: hit.kind,
17104
+ status: hit.status,
17105
+ projectIds: [],
17106
+ sourceType: void 0,
17107
+ sourceClass: void 0
17108
+ });
17109
+ }
17110
+ }
17111
+ return [...scores.entries()].sort(([, a], [, b]) => b - a).slice(0, limit).map(([pageId, rrfScore]) => {
17112
+ const result = resultMap.get(pageId);
17113
+ return { ...result, rank: -rrfScore };
17114
+ });
17115
+ }
16636
17116
  function searchPages(dbPath, query, limitOrOptions = 5) {
16637
17117
  const options = typeof limitOrOptions === "number" ? { limit: limitOrOptions } : limitOrOptions;
16638
17118
  const ftsQuery = toFtsQuery(query);
@@ -16974,7 +17454,7 @@ async function resolveImageGenerationProvider(rootDir) {
16974
17454
  if (!providerConfig) {
16975
17455
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
16976
17456
  }
16977
- const { createProvider: createProvider2 } = await import("./registry-MYJX6AEE.js");
17457
+ const { createProvider: createProvider2 } = await import("./registry-UA42LQUQ.js");
16978
17458
  return createProvider2(preferredProviderId, providerConfig, rootDir);
16979
17459
  }
16980
17460
  async function generateOutputArtifacts(rootDir, input) {
@@ -17485,8 +17965,8 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17485
17965
  ).slice(0, 20);
17486
17966
  const sourceSessions = await listGuidedSourceSessions(paths.rootDir);
17487
17967
  const stagedGuideBundles = (await Promise.all(
17488
- (await fs19.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => await readJsonFile(approvalManifestPath(paths, entry.name)))
17489
- )).filter((manifest) => Boolean(manifest)).filter((manifest) => manifest.bundleType === "guided_source" || manifest.bundleType === "guided_session").sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, 12);
17968
+ (await fs19.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => await readApprovalManifest(paths, entry.name).catch(() => null))
17969
+ )).filter((manifest) => Boolean(manifest)).filter((manifest) => manifest.bundleType === "guided-source" || manifest.bundleType === "guided-session").sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, 12);
17490
17970
  const readerFocusPages = uniqueBy([...guidePages, ...briefPages, ...conceptPages, ...entityPages], (page) => page.id).slice(0, 8);
17491
17971
  const diligenceSessions = sourceSessions.filter((session) => session.status === "staged" || session.status === "awaiting_input").slice(0, 8);
17492
17972
  const dashboards = [
@@ -18713,11 +19193,22 @@ function approvalManifestPath(paths, approvalId) {
18713
19193
  function approvalGraphPath(paths, approvalId) {
18714
19194
  return path23.join(paths.approvalsDir, approvalId, "state", "graph.json");
18715
19195
  }
19196
+ function normalizeApprovalBundleType(raw) {
19197
+ if (!raw) return void 0;
19198
+ const legacy = {
19199
+ generated_output: "generated-output",
19200
+ source_review: "source-review",
19201
+ guided_source: "guided-source",
19202
+ guided_session: "guided-session"
19203
+ };
19204
+ return legacy[raw] ?? raw;
19205
+ }
18716
19206
  async function readApprovalManifest(paths, approvalId) {
18717
19207
  const manifest = await readJsonFile(approvalManifestPath(paths, approvalId));
18718
19208
  if (!manifest) {
18719
19209
  throw new Error(`Approval bundle not found: ${approvalId}`);
18720
19210
  }
19211
+ manifest.bundleType = normalizeApprovalBundleType(manifest.bundleType);
18721
19212
  return manifest;
18722
19213
  }
18723
19214
  async function writeApprovalManifest(paths, manifest) {
@@ -19560,7 +20051,7 @@ async function stageOutputApprovalBundle(rootDir, stagedPages, options = {}) {
19560
20051
  await writeApprovalManifest(paths, {
19561
20052
  approvalId,
19562
20053
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
19563
- bundleType: options.bundleType ?? "generated_output",
20054
+ bundleType: options.bundleType ?? "generated-output",
19564
20055
  title: options.title,
19565
20056
  sourceSessionId: options.sourceSessionId,
19566
20057
  entries: await buildApprovalEntries(
@@ -20542,6 +21033,45 @@ async function compileVault(rootDir, options = {}) {
20542
21033
  `benchmark=${benchmark.ok ? "ok" : `error:${benchmark.error}`}`
20543
21034
  ]
20544
21035
  });
21036
+ let tokenStats;
21037
+ if (options.maxTokens && options.maxTokens > 0) {
21038
+ const { estimatePageTokens: estimatePageTokens2, trimToTokenBudget: trimToTokenBudget2 } = await import("./token-estimation-TTONKT4O.js");
21039
+ const nodeDegreeLookup = /* @__PURE__ */ new Map();
21040
+ const graph = await readJsonFile(paths.graphPath);
21041
+ if (graph) {
21042
+ for (const node of graph.nodes) {
21043
+ if (node.pageId && node.degree) {
21044
+ const existing = nodeDegreeLookup.get(node.pageId) ?? 0;
21045
+ nodeDegreeLookup.set(node.pageId, Math.max(existing, node.degree));
21046
+ }
21047
+ }
21048
+ }
21049
+ const estimates = await Promise.all(
21050
+ sync.allPages.map(async (page) => {
21051
+ const fullPath = path23.join(paths.wikiDir, page.path);
21052
+ let content = "";
21053
+ try {
21054
+ content = await fs19.readFile(fullPath, "utf8");
21055
+ } catch {
21056
+ }
21057
+ return estimatePageTokens2(page.id, page.path, page.kind, content, nodeDegreeLookup.get(page.id), page.confidence);
21058
+ })
21059
+ );
21060
+ const budgetResult = trimToTokenBudget2(estimates, options.maxTokens);
21061
+ for (const dropped of budgetResult.dropped) {
21062
+ const fullPath = path23.join(paths.wikiDir, dropped.path);
21063
+ try {
21064
+ await fs19.unlink(fullPath);
21065
+ } catch {
21066
+ }
21067
+ }
21068
+ tokenStats = {
21069
+ estimatedTokens: budgetResult.totalTokens,
21070
+ maxTokens: options.maxTokens,
21071
+ pagesKept: budgetResult.kept.length,
21072
+ pagesDropped: budgetResult.dropped.length
21073
+ };
21074
+ }
20545
21075
  return {
20546
21076
  graphPath: paths.graphPath,
20547
21077
  pageCount: sync.allPages.length,
@@ -20553,7 +21083,8 @@ async function compileVault(rootDir, options = {}) {
20553
21083
  postPassApprovalId,
20554
21084
  postPassApprovalDir,
20555
21085
  promotedPageIds: sync.promotedPageIds,
20556
- candidatePageCount: sync.candidatePageCount
21086
+ candidatePageCount: sync.candidatePageCount,
21087
+ tokenStats
20557
21088
  };
20558
21089
  }
20559
21090
  async function queryVault(rootDir, options) {
@@ -20910,11 +21441,59 @@ ${orchestrationNotes.join("\n")}
20910
21441
  };
20911
21442
  }
20912
21443
  async function searchVault(rootDir, query, limit = 5) {
20913
- const { paths } = await loadVaultConfig(rootDir);
21444
+ const { paths, config } = await loadVaultConfig(rootDir);
20914
21445
  if (!await fileExists(paths.searchDbPath)) {
20915
21446
  await compileVault(rootDir, {});
20916
21447
  }
20917
- return searchPages(paths.searchDbPath, query, limit);
21448
+ const hybrid = config.search?.hybrid !== false;
21449
+ const ftsResults = searchPages(paths.searchDbPath, query, hybrid ? limit * 3 : limit);
21450
+ if (!hybrid || !await fileExists(paths.graphPath)) {
21451
+ return ftsResults.slice(0, limit);
21452
+ }
21453
+ const graph = await readJsonFile(paths.graphPath);
21454
+ if (!graph) {
21455
+ return ftsResults.slice(0, limit);
21456
+ }
21457
+ const semanticHits = await semanticPageSearch(rootDir, graph, query, limit * 3).catch(() => []);
21458
+ if (!semanticHits.length) {
21459
+ return ftsResults.slice(0, limit);
21460
+ }
21461
+ const merged = mergeSearchResults(ftsResults, semanticHits, limit);
21462
+ if (config.search?.rerank && merged.length > 1) {
21463
+ return rerankSearchResults(rootDir, query, merged, limit);
21464
+ }
21465
+ return merged;
21466
+ }
21467
+ async function rerankSearchResults(rootDir, query, results, limit) {
21468
+ const provider = await getProviderForTask(rootDir, "queryProvider");
21469
+ const candidates = results.slice(0, Math.min(results.length, 20)).map((r, i) => `[${i}] ${r.title} \u2014 ${r.snippet || r.path}`).join("\n");
21470
+ const prompt = `Given the search query: "${query}"
21471
+
21472
+ Rank these results by relevance (most relevant first).
21473
+
21474
+ ${candidates}`;
21475
+ try {
21476
+ const indices = await provider.generateStructured(
21477
+ { prompt, system: "You are a search result ranker." },
21478
+ z7.array(z7.number().int().nonnegative())
21479
+ );
21480
+ const reranked = [];
21481
+ const seen = /* @__PURE__ */ new Set();
21482
+ for (const idx of indices) {
21483
+ if (idx >= 0 && idx < results.length && !seen.has(idx)) {
21484
+ seen.add(idx);
21485
+ reranked.push(results[idx]);
21486
+ }
21487
+ }
21488
+ for (let i = 0; i < results.length && reranked.length < limit; i++) {
21489
+ if (!seen.has(i)) {
21490
+ reranked.push(results[i]);
21491
+ }
21492
+ }
21493
+ return reranked.slice(0, limit);
21494
+ } catch {
21495
+ return results.slice(0, limit);
21496
+ }
20918
21497
  }
20919
21498
  async function ensureCompiledGraph(rootDir) {
20920
21499
  const { paths } = await loadVaultConfig(rootDir);
@@ -21018,6 +21597,10 @@ async function listGodNodes(rootDir, limit = 10) {
21018
21597
  const graph = await ensureCompiledGraph(rootDir);
21019
21598
  return topGodNodes(graph, limit);
21020
21599
  }
21600
+ async function blastRadiusVault(rootDir, target, options) {
21601
+ const graph = await ensureCompiledGraph(rootDir);
21602
+ return blastRadius(graph, target, options);
21603
+ }
21021
21604
  async function listPages(rootDir) {
21022
21605
  const { paths } = await loadVaultConfig(rootDir);
21023
21606
  const graph = await readJsonFile(paths.graphPath);
@@ -21252,7 +21835,7 @@ async function bootstrapDemo(rootDir, input) {
21252
21835
  }
21253
21836
 
21254
21837
  // src/mcp.ts
21255
- var SERVER_VERSION = "0.7.23";
21838
+ var SERVER_VERSION = "0.7.25";
21256
21839
  async function createMcpServer(rootDir) {
21257
21840
  const server = new McpServer({
21258
21841
  name: "swarmvault",
@@ -21402,6 +21985,19 @@ async function createMcpServer(rootDir) {
21402
21985
  return asToolText(await listGodNodes(rootDir, limit ?? 10));
21403
21986
  })
21404
21987
  );
21988
+ server.registerTool(
21989
+ "blast_radius",
21990
+ {
21991
+ description: "Analyze the impact of changing a file or module by tracing reverse import edges.",
21992
+ inputSchema: {
21993
+ target: z8.string().min(1).describe("File path, module label, or module id"),
21994
+ maxDepth: z8.number().int().min(1).max(10).optional().describe("Maximum traversal depth (default 3)")
21995
+ }
21996
+ },
21997
+ safeHandler(async ({ target, maxDepth }) => {
21998
+ return asToolText(await blastRadiusVault(rootDir, target, { maxDepth: maxDepth ?? 3 }));
21999
+ })
22000
+ );
21405
22001
  server.registerTool(
21406
22002
  "query_vault",
21407
22003
  {
@@ -21439,11 +22035,12 @@ async function createMcpServer(rootDir) {
21439
22035
  {
21440
22036
  description: "Compile source manifests into wiki pages, graph data, and search index.",
21441
22037
  inputSchema: {
21442
- approve: z8.boolean().optional().describe("Stage a review bundle without applying active page changes")
22038
+ approve: z8.boolean().optional().describe("Stage a review bundle without applying active page changes"),
22039
+ maxTokens: z8.number().int().min(1e3).optional().describe("Maximum token budget for wiki output")
21443
22040
  }
21444
22041
  },
21445
- safeHandler(async ({ approve }) => {
21446
- const result = await compileVault(rootDir, { approve: approve ?? false });
22042
+ safeHandler(async ({ approve, maxTokens }) => {
22043
+ const result = await compileVault(rootDir, { approve: approve ?? false, maxTokens });
21447
22044
  return asToolText(result);
21448
22045
  })
21449
22046
  );
@@ -23078,7 +23675,7 @@ async function buildSourceGuideStagedPage(rootDir, scope) {
23078
23675
  async function stageSourceReviewForScope(rootDir, scope) {
23079
23676
  const output = await buildSourceReviewStagedPage(rootDir, scope);
23080
23677
  const approval = await stageGeneratedOutputPages(rootDir, [{ page: output.page, content: output.content, label: "source-review" }], {
23081
- bundleType: "source_review",
23678
+ bundleType: "source-review",
23082
23679
  title: `Source Review: ${scope.title}`
23083
23680
  });
23084
23681
  return {
@@ -23430,7 +24027,7 @@ async function stageSourceGuideForScope(rootDir, scope, options = {}) {
23430
24027
  ...guidedUpdates
23431
24028
  ],
23432
24029
  {
23433
- bundleType: "guided_session",
24030
+ bundleType: "guided-session",
23434
24031
  title: `Guided Session: ${scope.title}`,
23435
24032
  sourceSessionId: session.sessionId
23436
24033
  }
@@ -23708,11 +24305,11 @@ async function deleteManagedSource(rootDir, id) {
23708
24305
  }
23709
24306
 
23710
24307
  // src/viewer.ts
23711
- import { execFile } from "child_process";
24308
+ import { execFile as execFile2 } from "child_process";
23712
24309
  import fs23 from "fs/promises";
23713
24310
  import http from "http";
23714
24311
  import path28 from "path";
23715
- import { promisify } from "util";
24312
+ import { promisify as promisify2 } from "util";
23716
24313
  import matter11 from "gray-matter";
23717
24314
  import mime2 from "mime-types";
23718
24315
 
@@ -24326,7 +24923,7 @@ async function getWatchStatus(rootDir) {
24326
24923
  }
24327
24924
 
24328
24925
  // src/viewer.ts
24329
- var execFileAsync = promisify(execFile);
24926
+ var execFileAsync2 = promisify2(execFile2);
24330
24927
  async function isReadableFile(absolutePath) {
24331
24928
  try {
24332
24929
  const stats = await fs23.stat(absolutePath);
@@ -24393,7 +24990,7 @@ async function ensureViewerDist(viewerDistDir) {
24393
24990
  }
24394
24991
  const viewerProjectDir = path28.dirname(viewerDistDir);
24395
24992
  if (await fileExists(path28.join(viewerProjectDir, "package.json"))) {
24396
- await execFileAsync("pnpm", ["build"], { cwd: viewerProjectDir });
24993
+ await execFileAsync2("pnpm", ["build"], { cwd: viewerProjectDir });
24397
24994
  }
24398
24995
  }
24399
24996
  async function startGraphServer(rootDir, port, options = {}) {
@@ -24573,6 +25170,44 @@ async function startGraphServer(rootDir, port, options = {}) {
24573
25170
  response.end(JSON.stringify(result));
24574
25171
  return;
24575
25172
  }
25173
+ if (url.pathname === "/api/clip" && request.method === "POST") {
25174
+ const body = await readJsonBody(request);
25175
+ const clipUrl = typeof body.url === "string" ? body.url.trim() : "";
25176
+ if (!clipUrl) {
25177
+ response.writeHead(400, { "content-type": "application/json" });
25178
+ response.end(JSON.stringify({ error: "Missing url field." }));
25179
+ return;
25180
+ }
25181
+ const manifest = await ingestInput(rootDir, clipUrl);
25182
+ response.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*" });
25183
+ response.end(JSON.stringify({ ok: true, sourceId: manifest.sourceId, title: manifest.title }));
25184
+ return;
25185
+ }
25186
+ if (url.pathname === "/api/clip" && request.method === "OPTIONS") {
25187
+ response.writeHead(204, {
25188
+ "access-control-allow-origin": "*",
25189
+ "access-control-allow-methods": "POST, OPTIONS",
25190
+ "access-control-allow-headers": "content-type"
25191
+ });
25192
+ response.end();
25193
+ return;
25194
+ }
25195
+ if (url.pathname === "/api/bookmarklet") {
25196
+ const script = `javascript:void(fetch('http://localhost:${effectivePort}/api/clip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:location.href})}).then(r=>r.json()).then(d=>alert('Clipped: '+(d.title||d.sourceId))).catch(e=>alert('Clip failed: '+e.message)))`;
25197
+ response.writeHead(200, { "content-type": "text/html" });
25198
+ response.end(
25199
+ [
25200
+ "<!doctype html><html><head><title>SwarmVault Clipper</title></head><body>",
25201
+ "<h1>SwarmVault Clipper</h1>",
25202
+ `<p>Drag this link to your bookmarks bar:</p>`,
25203
+ `<p style="font-size:1.5em"><a href="${script.replace(/&/g, "&amp;").replace(/"/g, "&quot;")}">Clip to SwarmVault</a></p>`,
25204
+ `<p>When clicked on any page, it sends the URL to your running SwarmVault instance for ingestion.</p>`,
25205
+ `<p>Server: <code>http://localhost:${effectivePort}</code></p>`,
25206
+ "</body></html>"
25207
+ ].join("\n")
25208
+ );
25209
+ return;
25210
+ }
24576
25211
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
24577
25212
  const target = path28.join(paths.viewerDistDir, relativePath);
24578
25213
  const fallback = path28.join(paths.viewerDistDir, "index.html");
@@ -24691,7 +25326,10 @@ export {
24691
25326
  addManagedSource,
24692
25327
  archiveCandidate,
24693
25328
  assertProviderCapability,
25329
+ autoCommitWikiChanges,
24694
25330
  benchmarkVault,
25331
+ blastRadius,
25332
+ blastRadiusVault,
24695
25333
  bootstrapDemo,
24696
25334
  compileVault,
24697
25335
  createMcpServer,
@@ -24700,10 +25338,13 @@ export {
24700
25338
  defaultVaultConfig,
24701
25339
  defaultVaultSchema,
24702
25340
  deleteManagedSource,
25341
+ estimatePageTokens,
25342
+ estimateTokens,
24703
25343
  explainGraphVault,
24704
25344
  exploreVault,
24705
25345
  exportGraphFormat,
24706
25346
  exportGraphHtml,
25347
+ exportGraphReportHtml,
24707
25348
  exportObsidianCanvas,
24708
25349
  exportObsidianVault,
24709
25350
  getGitHookStatus,
@@ -24760,6 +25401,7 @@ export {
24760
25401
  startMcpServer,
24761
25402
  syncTrackedRepos,
24762
25403
  syncTrackedReposForWatch,
25404
+ trimToTokenBudget,
24763
25405
  uninstallGitHooks,
24764
25406
  watchVault
24765
25407
  };