@swarmvaultai/engine 3.6.0 → 3.7.1

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
@@ -125,7 +125,7 @@ import {
125
125
  writeGuidedSourceSession,
126
126
  writeRetrievalManifest,
127
127
  writeWatchStatusArtifact
128
- } from "./chunk-5GEPTIZE.js";
128
+ } from "./chunk-JTRE7C7P.js";
129
129
  import {
130
130
  LocalWhisperProviderAdapter,
131
131
  SWARMVAULT_OUT_ENV,
@@ -563,6 +563,7 @@ import chokidar from "chokidar";
563
563
  var MAX_BACKOFF_MS = 3e4;
564
564
  var BACKOFF_THRESHOLD = 3;
565
565
  var CRITICAL_THRESHOLD = 10;
566
+ var DEFAULT_MAX_GRAPH_SHRINK_RATIO = 0.25;
566
567
  var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
567
568
  var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
568
569
  ".ts",
@@ -602,7 +603,12 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
602
603
  ".psm1",
603
604
  ".ex",
604
605
  ".exs",
606
+ ".svelte",
605
607
  ".jl",
608
+ ".v",
609
+ ".vh",
610
+ ".sv",
611
+ ".svh",
606
612
  ".r",
607
613
  ".R"
608
614
  ]);
@@ -638,6 +644,62 @@ function collectNonCodePaths(reasons) {
638
644
  }
639
645
  return result;
640
646
  }
647
+ function shrinkDimension(before, after) {
648
+ const dropped = Math.max(0, before - after);
649
+ return {
650
+ before,
651
+ after,
652
+ dropped,
653
+ dropRatio: before > 0 ? dropped / before : 0
654
+ };
655
+ }
656
+ function evaluateGraphShrinkGuard(previousGraph, nextGraph, options = {}) {
657
+ const threshold = options.threshold ?? DEFAULT_MAX_GRAPH_SHRINK_RATIO;
658
+ const nodes = shrinkDimension(previousGraph?.nodes.length ?? 0, nextGraph?.nodes.length ?? 0);
659
+ const edges = shrinkDimension(previousGraph?.edges.length ?? 0, nextGraph?.edges.length ?? 0);
660
+ const blocked = nodes.dropRatio > threshold || edges.dropRatio > threshold;
661
+ return {
662
+ blocked,
663
+ threshold,
664
+ nodes,
665
+ edges,
666
+ message: blocked ? `Graph update aborted: node count changed ${nodes.before} -> ${nodes.after} (${Math.round(
667
+ nodes.dropRatio * 100
668
+ )}% drop) and edge count changed ${edges.before} -> ${edges.after} (${Math.round(
669
+ edges.dropRatio * 100
670
+ )}% drop). Re-run with --force or SWARMVAULT_FORCE_UPDATE=1 if this shrink is expected.` : void 0
671
+ };
672
+ }
673
+ function forceGraphUpdateEnabled(options) {
674
+ return options.force === true || process2.env.SWARMVAULT_FORCE_UPDATE === "1" || process2.env.SWARMVAULT_FORCE_UPDATE === "true";
675
+ }
676
+ function projectGraphAfterRemovals(graph, removedSourceIds) {
677
+ if (removedSourceIds.length === 0) {
678
+ return graph;
679
+ }
680
+ const removed = new Set(removedSourceIds);
681
+ const droppedNodeIds = new Set(
682
+ graph.nodes.filter((node) => node.sourceIds.length > 0 && node.sourceIds.every((sourceId) => removed.has(sourceId))).map((node) => node.id)
683
+ );
684
+ if (droppedNodeIds.size === 0) {
685
+ return graph;
686
+ }
687
+ const remainingNodes = graph.nodes.filter((node) => !droppedNodeIds.has(node.id));
688
+ const remainingEdges = graph.edges.filter((edge) => !droppedNodeIds.has(edge.source) && !droppedNodeIds.has(edge.target));
689
+ return { ...graph, nodes: remainingNodes, edges: remainingEdges };
690
+ }
691
+ async function predictRemovedSourceIdsFromRepoSync(rootDir, options) {
692
+ if (!options.repo) {
693
+ return [];
694
+ }
695
+ const overrideRoots = options.overrideRoots && options.overrideRoots.length > 0 ? options.overrideRoots : void 0;
696
+ const changes = await checkTrackedRepoChanges(rootDir, overrideRoots);
697
+ return [
698
+ ...new Set(
699
+ changes.filter((change) => change.changeType === "removed").map((change) => change.sourceId).filter((sourceId) => Boolean(sourceId))
700
+ )
701
+ ];
702
+ }
641
703
  function hasIgnoredRepoSegment(baseDir, targetPath) {
642
704
  const relativePath = path2.relative(baseDir, targetPath);
643
705
  if (!relativePath || relativePath.startsWith("..")) {
@@ -774,6 +836,7 @@ async function performWatchCycle(rootDir, paths, options, codeOnly = false) {
774
836
  }
775
837
  async function runWatchCycle(rootDir, options = {}) {
776
838
  const { paths } = await initWorkspace(rootDir);
839
+ const previousGraph = await readJsonFile(paths.graphPath);
777
840
  const startedAt = /* @__PURE__ */ new Date();
778
841
  let success = true;
779
842
  let error;
@@ -791,6 +854,16 @@ async function runWatchCycle(rootDir, options = {}) {
791
854
  changedPages: []
792
855
  };
793
856
  try {
857
+ if (previousGraph && options.repo && !forceGraphUpdateEnabled(options)) {
858
+ const removedSourceIds = await predictRemovedSourceIdsFromRepoSync(rootDir, options);
859
+ if (removedSourceIds.length > 0) {
860
+ const projectedGraph = projectGraphAfterRemovals(previousGraph, removedSourceIds);
861
+ const guard = evaluateGraphShrinkGuard(previousGraph, projectedGraph, { threshold: options.maxGraphShrinkRatio });
862
+ if (guard.blocked) {
863
+ throw new Error(guard.message ?? "Graph update aborted because the graph shrank unexpectedly.");
864
+ }
865
+ }
866
+ }
794
867
  result = await performWatchCycle(rootDir, paths, options, options.codeOnly ?? false);
795
868
  return result;
796
869
  } catch (caught) {
@@ -3710,9 +3783,350 @@ Community: ${communityLabel}`,
3710
3783
  return { format: "canvas", outputPath: resolvedPath };
3711
3784
  }
3712
3785
 
3713
- // src/graph-push.ts
3786
+ // src/graph-merge.ts
3714
3787
  import fs5 from "fs/promises";
3715
3788
  import path5 from "path";
3789
+ function isRecord(value) {
3790
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3791
+ }
3792
+ function isSwarmVaultGraph(value) {
3793
+ return isRecord(value) && Array.isArray(value.nodes) && Array.isArray(value.edges) && Array.isArray(value.sources) && Array.isArray(value.pages);
3794
+ }
3795
+ function stringField(record, ...fields) {
3796
+ for (const field of fields) {
3797
+ const value = record[field];
3798
+ if (typeof value === "string" && value.trim()) {
3799
+ return value.trim();
3800
+ }
3801
+ if (typeof value === "number" && Number.isFinite(value)) {
3802
+ return String(value);
3803
+ }
3804
+ }
3805
+ return void 0;
3806
+ }
3807
+ function arrayStringField(record, field) {
3808
+ const value = record[field];
3809
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3810
+ }
3811
+ function numberField(record, field, fallback) {
3812
+ const value = record[field];
3813
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3814
+ }
3815
+ function safePrefix(inputPath, index) {
3816
+ return slugify(path5.basename(inputPath, path5.extname(inputPath)) || `graph-${index + 1}`);
3817
+ }
3818
+ function prefixed(prefix, id) {
3819
+ return `${prefix}:${id}`;
3820
+ }
3821
+ function ensureUniquePrefix(base, used) {
3822
+ let candidate = base || "graph";
3823
+ let suffix = 2;
3824
+ while (used.has(candidate)) {
3825
+ candidate = `${base}-${suffix}`;
3826
+ suffix += 1;
3827
+ }
3828
+ used.add(candidate);
3829
+ return candidate;
3830
+ }
3831
+ function mapEvidenceClass(value) {
3832
+ const normalized = typeof value === "string" ? value.toLowerCase() : "";
3833
+ if (normalized === "extracted") return "extracted";
3834
+ if (normalized === "ambiguous") return "ambiguous";
3835
+ return "inferred";
3836
+ }
3837
+ function mapNodeType(value) {
3838
+ const normalized = typeof value === "string" ? value.toLowerCase() : "";
3839
+ if (["source", "file", "document", "paper", "image", "video"].includes(normalized)) return "source";
3840
+ if (["module", "code"].includes(normalized)) return "module";
3841
+ if (["function", "class", "symbol", "method", "component"].includes(normalized)) return "symbol";
3842
+ if (["entity", "person", "org", "organization"].includes(normalized)) return "entity";
3843
+ if (["rationale", "comment", "docstring", "why"].includes(normalized)) return "rationale";
3844
+ if (["decision", "adr"].includes(normalized)) return "decision";
3845
+ return "concept";
3846
+ }
3847
+ function remapSwarmVaultGraph(inputPath, graph, prefix) {
3848
+ const sourceMap = new Map(graph.sources.map((source) => [source.sourceId, prefixed(prefix, source.sourceId)]));
3849
+ const pageMap = new Map(graph.pages.map((page) => [page.id, prefixed(prefix, page.id)]));
3850
+ const nodeMap = new Map(graph.nodes.map((node) => [node.id, prefixed(prefix, node.id)]));
3851
+ const communityMap = new Map((graph.communities ?? []).map((community) => [community.id, prefixed(prefix, community.id)]));
3852
+ const sources = graph.sources.map((source) => ({
3853
+ ...source,
3854
+ sourceId: sourceMap.get(source.sourceId) ?? prefixed(prefix, source.sourceId),
3855
+ title: `[${prefix}] ${source.title}`,
3856
+ sourceGroupId: source.sourceGroupId ? prefixed(prefix, source.sourceGroupId) : void 0,
3857
+ details: {
3858
+ ...source.details ?? {},
3859
+ mergedInput: inputPath,
3860
+ mergedPrefix: prefix
3861
+ }
3862
+ }));
3863
+ const pages = graph.pages.map((page) => ({
3864
+ ...page,
3865
+ id: pageMap.get(page.id) ?? prefixed(prefix, page.id),
3866
+ path: toPosix(path5.posix.join("merged", prefix, page.path)),
3867
+ sourceIds: page.sourceIds.map((sourceId) => sourceMap.get(sourceId) ?? prefixed(prefix, sourceId)),
3868
+ nodeIds: page.nodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId)),
3869
+ relatedPageIds: page.relatedPageIds.map((pageId) => pageMap.get(pageId) ?? prefixed(prefix, pageId)),
3870
+ relatedNodeIds: page.relatedNodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId)),
3871
+ relatedSourceIds: page.relatedSourceIds.map((sourceId) => sourceMap.get(sourceId) ?? prefixed(prefix, sourceId)),
3872
+ backlinks: page.backlinks.map((pageId) => pageMap.get(pageId) ?? prefixed(prefix, pageId)),
3873
+ supersededBy: page.supersededBy ? pageMap.get(page.supersededBy) ?? prefixed(prefix, page.supersededBy) : void 0
3874
+ }));
3875
+ const nodes = graph.nodes.map((node) => ({
3876
+ ...node,
3877
+ id: nodeMap.get(node.id) ?? prefixed(prefix, node.id),
3878
+ pageId: node.pageId ? pageMap.get(node.pageId) ?? prefixed(prefix, node.pageId) : void 0,
3879
+ sourceIds: node.sourceIds.map((sourceId) => sourceMap.get(sourceId) ?? prefixed(prefix, sourceId)),
3880
+ moduleId: node.moduleId ? nodeMap.get(node.moduleId) ?? prefixed(prefix, node.moduleId) : void 0,
3881
+ communityId: node.communityId ? communityMap.get(node.communityId) ?? prefixed(prefix, node.communityId) : void 0
3882
+ }));
3883
+ const edges = graph.edges.map((edge) => ({
3884
+ ...edge,
3885
+ id: prefixed(prefix, edge.id),
3886
+ source: nodeMap.get(edge.source) ?? prefixed(prefix, edge.source),
3887
+ target: nodeMap.get(edge.target) ?? prefixed(prefix, edge.target),
3888
+ provenance: edge.provenance.map((id) => nodeMap.get(id) ?? pageMap.get(id) ?? sourceMap.get(id) ?? prefixed(prefix, id))
3889
+ }));
3890
+ const hyperedges = graph.hyperedges.map((hyperedge) => ({
3891
+ ...hyperedge,
3892
+ id: prefixed(prefix, hyperedge.id),
3893
+ nodeIds: hyperedge.nodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId)),
3894
+ sourcePageIds: hyperedge.sourcePageIds.map((pageId) => pageMap.get(pageId) ?? prefixed(prefix, pageId))
3895
+ }));
3896
+ return {
3897
+ generatedAt: graph.generatedAt,
3898
+ nodes,
3899
+ edges,
3900
+ hyperedges,
3901
+ communities: (graph.communities ?? []).map((community) => ({
3902
+ ...community,
3903
+ id: communityMap.get(community.id) ?? prefixed(prefix, community.id),
3904
+ label: `[${prefix}] ${community.label}`,
3905
+ nodeIds: community.nodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId))
3906
+ })),
3907
+ sources,
3908
+ pages
3909
+ };
3910
+ }
3911
+ function nodeLinkArrays(raw) {
3912
+ const nodes = raw.nodes;
3913
+ const edges = Array.isArray(raw.links) ? raw.links : raw.edges;
3914
+ if (!Array.isArray(nodes) || !Array.isArray(edges)) {
3915
+ return null;
3916
+ }
3917
+ return {
3918
+ nodes,
3919
+ edges: edges.filter(isRecord)
3920
+ };
3921
+ }
3922
+ function nodeLinkNodeId(node, index) {
3923
+ if (typeof node === "string" || typeof node === "number") {
3924
+ return String(node);
3925
+ }
3926
+ return stringField(node, "id", "key", "name", "label") ?? `node-${index + 1}`;
3927
+ }
3928
+ function endpointId(value) {
3929
+ if (typeof value === "string" || typeof value === "number") {
3930
+ return String(value);
3931
+ }
3932
+ if (isRecord(value)) {
3933
+ return stringField(value, "id", "key", "name", "label");
3934
+ }
3935
+ return void 0;
3936
+ }
3937
+ function remapNodeLinkGraph(inputPath, raw, prefix, now) {
3938
+ const arrays = nodeLinkArrays(raw);
3939
+ if (!arrays) {
3940
+ throw new Error(`${inputPath} is not a SwarmVault graph or node-link graph.`);
3941
+ }
3942
+ const syntheticSourceId = prefixed(prefix, "source");
3943
+ const source = {
3944
+ sourceId: syntheticSourceId,
3945
+ title: `${prefix} merged graph`,
3946
+ originType: "file",
3947
+ sourceKind: "data",
3948
+ sourceClass: "generated",
3949
+ originalPath: inputPath,
3950
+ storedPath: inputPath,
3951
+ mimeType: "application/json",
3952
+ contentHash: `sha256:${sha256(JSON.stringify(raw)).slice(0, 24)}`,
3953
+ semanticHash: `sha256:${sha256(`${inputPath}:${arrays.nodes.length}:${arrays.edges.length}`).slice(0, 24)}`,
3954
+ details: {
3955
+ mergedInput: inputPath,
3956
+ mergedFormat: "node-link"
3957
+ },
3958
+ createdAt: now,
3959
+ updatedAt: now
3960
+ };
3961
+ const idMap = /* @__PURE__ */ new Map();
3962
+ const nodes = arrays.nodes.map((node, index) => {
3963
+ const originalId = nodeLinkNodeId(node, index);
3964
+ const mappedId = prefixed(prefix, originalId);
3965
+ idMap.set(originalId, mappedId);
3966
+ const record = isRecord(node) ? node : {};
3967
+ const label = stringField(record, "label", "name", "title", "path", "id") ?? originalId;
3968
+ const type = mapNodeType(record.type ?? record.file_type ?? record.kind ?? record.category);
3969
+ return {
3970
+ id: mappedId,
3971
+ type,
3972
+ label,
3973
+ sourceIds: [syntheticSourceId],
3974
+ projectIds: [],
3975
+ sourceClass: "generated",
3976
+ confidence: numberField(record, "confidence", numberField(record, "confidence_score", 1)),
3977
+ tags: arrayStringField(record, "tags")
3978
+ };
3979
+ });
3980
+ const nodeIds = new Set(nodes.map((node) => node.id));
3981
+ const edges = arrays.edges.flatMap((edge, index) => {
3982
+ const source2 = endpointId(edge.source ?? edge.from);
3983
+ const target = endpointId(edge.target ?? edge.to);
3984
+ if (!source2 || !target) {
3985
+ return [];
3986
+ }
3987
+ const mappedSource = idMap.get(source2) ?? prefixed(prefix, source2);
3988
+ const mappedTarget = idMap.get(target) ?? prefixed(prefix, target);
3989
+ if (!nodeIds.has(mappedSource) || !nodeIds.has(mappedTarget)) {
3990
+ return [];
3991
+ }
3992
+ const evidenceClass = mapEvidenceClass(edge.evidenceClass ?? edge.evidence_class ?? edge.status ?? edge.confidence);
3993
+ return [
3994
+ {
3995
+ id: prefixed(prefix, stringField(edge, "id", "key") ?? `edge-${index + 1}`),
3996
+ source: mappedSource,
3997
+ target: mappedTarget,
3998
+ relation: stringField(edge, "relation", "type", "label") ?? "related_to",
3999
+ status: evidenceClass === "extracted" ? "extracted" : "inferred",
4000
+ evidenceClass,
4001
+ confidence: numberField(edge, "confidence", numberField(edge, "confidence_score", evidenceClass === "ambiguous" ? 0.5 : 0.75)),
4002
+ provenance: [syntheticSourceId]
4003
+ }
4004
+ ];
4005
+ });
4006
+ const page = {
4007
+ id: prefixed(prefix, "page"),
4008
+ path: toPosix(path5.posix.join("merged", prefix, "index.md")),
4009
+ title: `${prefix} merged graph`,
4010
+ kind: "source",
4011
+ sourceClass: "generated",
4012
+ sourceIds: [syntheticSourceId],
4013
+ projectIds: [],
4014
+ nodeIds: nodes.map((node) => node.id),
4015
+ freshness: "fresh",
4016
+ status: "active",
4017
+ confidence: 1,
4018
+ backlinks: [],
4019
+ schemaHash: "merged-node-link",
4020
+ sourceHashes: { [syntheticSourceId]: source.contentHash },
4021
+ sourceSemanticHashes: { [syntheticSourceId]: source.semanticHash },
4022
+ relatedPageIds: [],
4023
+ relatedNodeIds: nodes.map((node) => node.id),
4024
+ relatedSourceIds: [syntheticSourceId],
4025
+ createdAt: now,
4026
+ updatedAt: now,
4027
+ compiledFrom: [inputPath],
4028
+ managedBy: "system"
4029
+ };
4030
+ return {
4031
+ generatedAt: now,
4032
+ nodes,
4033
+ edges,
4034
+ hyperedges: [],
4035
+ communities: [],
4036
+ sources: [source],
4037
+ pages: [page]
4038
+ };
4039
+ }
4040
+ function mergeGraphs(graphs, now) {
4041
+ return {
4042
+ generatedAt: now,
4043
+ nodes: uniqueBy(
4044
+ graphs.flatMap((graph) => graph.nodes),
4045
+ (node) => node.id
4046
+ ),
4047
+ edges: uniqueBy(
4048
+ graphs.flatMap((graph) => graph.edges),
4049
+ (edge) => edge.id
4050
+ ),
4051
+ hyperedges: uniqueBy(
4052
+ graphs.flatMap((graph) => graph.hyperedges),
4053
+ (hyperedge) => hyperedge.id
4054
+ ),
4055
+ communities: uniqueBy(
4056
+ graphs.flatMap((graph) => graph.communities ?? []),
4057
+ (community) => community.id
4058
+ ),
4059
+ sources: uniqueBy(
4060
+ graphs.flatMap((graph) => graph.sources),
4061
+ (source) => source.sourceId
4062
+ ),
4063
+ pages: uniqueBy(
4064
+ graphs.flatMap((graph) => graph.pages),
4065
+ (page) => page.id
4066
+ )
4067
+ };
4068
+ }
4069
+ async function mergeGraphFiles(inputPaths, outputPath, options = {}) {
4070
+ if (inputPaths.length === 0) {
4071
+ throw new Error("At least one graph JSON path is required.");
4072
+ }
4073
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4074
+ const usedPrefixes = /* @__PURE__ */ new Set();
4075
+ const graphs = [];
4076
+ const inputGraphs = [];
4077
+ const warnings = [];
4078
+ for (const [index, inputPath] of inputPaths.entries()) {
4079
+ const resolvedInputPath = path5.resolve(inputPath);
4080
+ const raw = JSON.parse(await fs5.readFile(resolvedInputPath, "utf8"));
4081
+ const prefix = ensureUniquePrefix(
4082
+ inputPaths.length === 1 && options.label ? slugify(options.label) : safePrefix(resolvedInputPath, index),
4083
+ usedPrefixes
4084
+ );
4085
+ if (isSwarmVaultGraph(raw)) {
4086
+ const graph2 = remapSwarmVaultGraph(resolvedInputPath, raw, prefix);
4087
+ graphs.push(graph2);
4088
+ inputGraphs.push({
4089
+ path: resolvedInputPath,
4090
+ label: prefix,
4091
+ format: "swarmvault",
4092
+ nodeCount: raw.nodes.length,
4093
+ edgeCount: raw.edges.length
4094
+ });
4095
+ continue;
4096
+ }
4097
+ if (isRecord(raw) && nodeLinkArrays(raw)) {
4098
+ const graph2 = remapNodeLinkGraph(resolvedInputPath, raw, prefix, now);
4099
+ graphs.push(graph2);
4100
+ inputGraphs.push({
4101
+ path: resolvedInputPath,
4102
+ label: prefix,
4103
+ format: "node-link",
4104
+ nodeCount: graph2.nodes.length,
4105
+ edgeCount: graph2.edges.length
4106
+ });
4107
+ continue;
4108
+ }
4109
+ warnings.push(`${resolvedInputPath} was skipped because it is not a supported graph JSON shape.`);
4110
+ }
4111
+ if (graphs.length === 0) {
4112
+ throw new Error("No supported graph inputs were found.");
4113
+ }
4114
+ const graph = mergeGraphs(graphs, now);
4115
+ const resolvedOutputPath = path5.resolve(outputPath);
4116
+ await ensureDir(path5.dirname(resolvedOutputPath));
4117
+ await fs5.writeFile(resolvedOutputPath, `${JSON.stringify(graph, null, 2)}
4118
+ `, "utf8");
4119
+ return {
4120
+ outputPath: resolvedOutputPath,
4121
+ graph,
4122
+ inputGraphs,
4123
+ warnings
4124
+ };
4125
+ }
4126
+
4127
+ // src/graph-push.ts
4128
+ import fs6 from "fs/promises";
4129
+ import path6 from "path";
3716
4130
  import neo4j from "neo4j-driver";
3717
4131
  var DEFAULT_NEO4J_BATCH_SIZE = 500;
3718
4132
  var DEFAULT_NEO4J_DATABASE = "neo4j";
@@ -3723,8 +4137,8 @@ function requireConfigValue(value, name) {
3723
4137
  throw new Error(`Neo4j push requires ${name}. Configure \`graphSinks.neo4j.${name}\` or pass the matching CLI flag.`);
3724
4138
  }
3725
4139
  async function deriveVaultId(rootDir) {
3726
- const realRoot = await fs5.realpath(rootDir).catch(() => path5.resolve(rootDir));
3727
- const label = slugify(path5.basename(realRoot));
4140
+ const realRoot = await fs6.realpath(rootDir).catch(() => path6.resolve(rootDir));
4141
+ const label = slugify(path6.basename(realRoot));
3728
4142
  return `${label}-${sha256(realRoot).slice(0, 12)}`;
3729
4143
  }
3730
4144
  async function resolveNeo4jPushConfig(rootDir, options) {
@@ -3754,7 +4168,7 @@ function normalizeBatchSize(value) {
3754
4168
  }
3755
4169
  async function loadGraph2(rootDir) {
3756
4170
  const { paths } = await loadVaultConfig(rootDir);
3757
- const raw = JSON.parse(await fs5.readFile(paths.graphPath, "utf8"));
4171
+ const raw = JSON.parse(await fs6.readFile(paths.graphPath, "utf8"));
3758
4172
  return raw;
3759
4173
  }
3760
4174
  function buildResult(input) {
@@ -3839,7 +4253,7 @@ async function writeSyncNode(session, input) {
3839
4253
  ].join("\n"),
3840
4254
  {
3841
4255
  vaultId: input.vaultId,
3842
- rootDir: path5.resolve(input.rootDir),
4256
+ rootDir: path6.resolve(input.rootDir),
3843
4257
  graphGeneratedAt: input.graph.generatedAt,
3844
4258
  graphHash: graphHash(input.graph),
3845
4259
  pushedAt: input.pushedAt,
@@ -3925,7 +4339,7 @@ async function pushGraphNeo4j(rootDir, options = {}) {
3925
4339
  }
3926
4340
 
3927
4341
  // src/graph-status.ts
3928
- import path6 from "path";
4342
+ import path7 from "path";
3929
4343
  function recommendedCommand(input) {
3930
4344
  if (!input.graphExists || !input.reportExists) {
3931
4345
  return "swarmvault compile";
@@ -3941,8 +4355,8 @@ function recommendedCommand(input) {
3941
4355
  async function getGraphStatus(rootDir, options = {}) {
3942
4356
  const { paths } = await loadVaultConfig(rootDir);
3943
4357
  const graphPath = paths.graphPath;
3944
- const reportPath = path6.join(paths.wikiDir, "graph", "report.md");
3945
- const resolvedOverrideRoots = options.repoRoots?.map((repoRoot) => path6.resolve(rootDir, repoRoot));
4358
+ const reportPath = path7.join(paths.wikiDir, "graph", "report.md");
4359
+ const resolvedOverrideRoots = options.repoRoots?.map((repoRoot) => path7.resolve(rootDir, repoRoot));
3946
4360
  const [graphExists, reportExists, trackedRepoRoots, changes, pendingSemanticRefresh] = await Promise.all([
3947
4361
  fileExists(graphPath),
3948
4362
  fileExists(reportPath),
@@ -3975,27 +4389,268 @@ async function getGraphStatus(rootDir, options = {}) {
3975
4389
  };
3976
4390
  }
3977
4391
 
4392
+ // src/graph-tree.ts
4393
+ import path8 from "path";
4394
+ var DEFAULT_MAX_CHILDREN = 250;
4395
+ function compareTreeNodes(left, right) {
4396
+ const kindOrder = /* @__PURE__ */ new Map([
4397
+ ["directory", 0],
4398
+ ["source", 1],
4399
+ ["module", 2],
4400
+ ["symbol", 3],
4401
+ ["rationale", 4],
4402
+ ["node", 5],
4403
+ ["more", 6],
4404
+ ["root", 7]
4405
+ ]);
4406
+ const leftKind = kindOrder.get(left.kind) ?? 99;
4407
+ const rightKind = kindOrder.get(right.kind) ?? 99;
4408
+ return leftKind - rightKind || left.label.localeCompare(right.label) || left.id.localeCompare(right.id);
4409
+ }
4410
+ function escapeHtml(value) {
4411
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4412
+ }
4413
+ function normalizeSourcePath(rootDir, source) {
4414
+ const candidate = source.repoRelativePath ?? source.originalPath ?? source.storedPath ?? source.title;
4415
+ if (source.repoRelativePath) {
4416
+ return toPosix(source.repoRelativePath);
4417
+ }
4418
+ if (rootDir && path8.isAbsolute(candidate) && isPathWithin(rootDir, candidate)) {
4419
+ return toPosix(path8.relative(rootDir, candidate));
4420
+ }
4421
+ if (path8.isAbsolute(candidate)) {
4422
+ return toPosix(path8.basename(candidate));
4423
+ }
4424
+ return toPosix(candidate).replace(/^\/+/, "") || source.title || source.sourceId;
4425
+ }
4426
+ function makeDirectoryNode(parentId, segment) {
4427
+ return {
4428
+ id: `${parentId}/${segment}`,
4429
+ label: segment,
4430
+ kind: "directory",
4431
+ count: 0,
4432
+ children: []
4433
+ };
4434
+ }
4435
+ function ensureDirectory(parent, segment) {
4436
+ const existing = parent.children.find((child) => child.kind === "directory" && child.label === segment);
4437
+ if (existing) {
4438
+ existing.count += 1;
4439
+ return existing;
4440
+ }
4441
+ const created = makeDirectoryNode(parent.id, segment);
4442
+ created.count = 1;
4443
+ parent.children.push(created);
4444
+ return created;
4445
+ }
4446
+ function nodeChildrenForSource(sourceId, nodes) {
4447
+ const sourceNodes = nodes.filter((node) => node.sourceIds.includes(sourceId));
4448
+ const modules = sourceNodes.filter((node) => node.type === "module").sort((left, right) => left.label.localeCompare(right.label));
4449
+ const moduleIds = new Set(modules.map((node) => node.id));
4450
+ const symbols = sourceNodes.filter((node) => node.type === "symbol");
4451
+ const rationales = sourceNodes.filter((node) => node.type === "rationale");
4452
+ const children = [];
4453
+ for (const moduleNode of modules) {
4454
+ const moduleChildren = symbols.filter((symbol) => symbol.moduleId === moduleNode.id).sort((left, right) => left.label.localeCompare(right.label)).map((symbol) => graphNodeToTreeNode(symbol, "symbol"));
4455
+ children.push({
4456
+ id: `tree:${moduleNode.id}`,
4457
+ label: moduleNode.label,
4458
+ kind: "module",
4459
+ count: moduleChildren.length,
4460
+ children: moduleChildren,
4461
+ nodeId: moduleNode.id,
4462
+ sourceId,
4463
+ language: moduleNode.language
4464
+ });
4465
+ }
4466
+ for (const symbol of symbols.filter((node) => !node.moduleId || !moduleIds.has(node.moduleId))) {
4467
+ children.push(graphNodeToTreeNode(symbol, "symbol"));
4468
+ }
4469
+ for (const rationale of rationales) {
4470
+ children.push(graphNodeToTreeNode(rationale, "rationale"));
4471
+ }
4472
+ return children.sort(compareTreeNodes);
4473
+ }
4474
+ function graphNodeToTreeNode(node, kind) {
4475
+ return {
4476
+ id: `tree:${node.id}`,
4477
+ label: node.label,
4478
+ kind,
4479
+ count: 0,
4480
+ children: [],
4481
+ nodeId: node.id,
4482
+ language: node.language,
4483
+ symbolKind: node.symbolKind
4484
+ };
4485
+ }
4486
+ function sortAndCapTree(node, maxChildren) {
4487
+ const sortedChildren = node.children.map((child) => sortAndCapTree(child, maxChildren)).sort(compareTreeNodes);
4488
+ if (sortedChildren.length <= maxChildren) {
4489
+ return { ...node, children: sortedChildren };
4490
+ }
4491
+ const visible = sortedChildren.slice(0, maxChildren);
4492
+ const hidden = sortedChildren.length - visible.length;
4493
+ return {
4494
+ ...node,
4495
+ hiddenChildren: hidden,
4496
+ children: [
4497
+ ...visible,
4498
+ {
4499
+ id: `${node.id}:more`,
4500
+ label: `+${hidden} more`,
4501
+ kind: "more",
4502
+ count: hidden,
4503
+ children: []
4504
+ }
4505
+ ]
4506
+ };
4507
+ }
4508
+ function buildGraphTree(graph, options = {}) {
4509
+ const root = {
4510
+ id: "tree:root",
4511
+ label: options.label ?? "SwarmVault Graph Tree",
4512
+ kind: "root",
4513
+ count: graph.sources.length,
4514
+ children: []
4515
+ };
4516
+ const nodes = [...graph.nodes];
4517
+ for (const source of [...graph.sources].sort(
4518
+ (left, right) => normalizeSourcePath(options.rootDir, left).localeCompare(normalizeSourcePath(options.rootDir, right))
4519
+ )) {
4520
+ const normalizedPath = normalizeSourcePath(options.rootDir, source);
4521
+ const segments = normalizedPath.split("/").filter(Boolean);
4522
+ const fileLabel = segments.pop() ?? source.title ?? source.sourceId;
4523
+ let parent = root;
4524
+ for (const segment of segments) {
4525
+ parent = ensureDirectory(parent, segment);
4526
+ }
4527
+ const children = nodeChildrenForSource(source.sourceId, nodes);
4528
+ parent.children.push({
4529
+ id: `tree:source:${source.sourceId}`,
4530
+ label: fileLabel,
4531
+ kind: "source",
4532
+ count: children.length,
4533
+ children,
4534
+ path: normalizedPath,
4535
+ sourceId: source.sourceId,
4536
+ language: source.language
4537
+ });
4538
+ }
4539
+ return sortAndCapTree(root, Math.max(1, options.maxChildren ?? DEFAULT_MAX_CHILDREN));
4540
+ }
4541
+ function renderNode(node) {
4542
+ const meta = [
4543
+ node.kind,
4544
+ node.language,
4545
+ node.symbolKind,
4546
+ node.path,
4547
+ node.nodeId,
4548
+ node.sourceId,
4549
+ node.count ? `${node.count} item${node.count === 1 ? "" : "s"}` : void 0
4550
+ ].filter(Boolean);
4551
+ const content = [
4552
+ `<span class="label">${escapeHtml(node.label)}</span>`,
4553
+ meta.length ? `<span class="meta">${escapeHtml(meta.join(" \xB7 "))}</span>` : ""
4554
+ ].join("");
4555
+ if (node.children.length === 0) {
4556
+ return `<li class="tree-node kind-${escapeHtml(node.kind)}">${content}</li>`;
4557
+ }
4558
+ return `<li class="tree-node kind-${escapeHtml(node.kind)}"><details open><summary>${content}</summary><ul>${node.children.map(renderNode).join("")}</ul></details></li>`;
4559
+ }
4560
+ function renderGraphTreeHtml(tree, graph) {
4561
+ return `<!doctype html>
4562
+ <html lang="en">
4563
+ <head>
4564
+ <meta charset="utf-8">
4565
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4566
+ <title>${escapeHtml(tree.label)}</title>
4567
+ <style>
4568
+ :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
4569
+ body { margin: 0; background: #f7f7f5; color: #171717; }
4570
+ main { max-width: 1120px; margin: 0 auto; padding: 32px 20px 56px; }
4571
+ h1 { font-size: 28px; line-height: 1.2; margin: 0 0 8px; }
4572
+ .subtitle { color: #5d5d5d; margin: 0 0 20px; }
4573
+ .toolbar { display: flex; gap: 12px; align-items: center; margin: 0 0 18px; }
4574
+ input { flex: 1; min-width: 0; border: 1px solid #c9c9c9; border-radius: 6px; padding: 10px 12px; font: inherit; background: #fff; color: inherit; }
4575
+ .tree { background: #fff; border: 1px solid #deded9; border-radius: 8px; padding: 16px 18px; }
4576
+ ul { list-style: none; margin: 0; padding-left: 20px; }
4577
+ .tree > ul { padding-left: 0; }
4578
+ li { margin: 4px 0; }
4579
+ summary { cursor: pointer; }
4580
+ .label { font-weight: 600; }
4581
+ .meta { color: #666; font-size: 12px; margin-left: 8px; }
4582
+ .kind-directory > details > summary .label { color: #245b78; }
4583
+ .kind-source > details > summary .label, .kind-source > .label { color: #22543d; }
4584
+ .kind-module > details > summary .label { color: #6b3f12; }
4585
+ .kind-rationale > .label { color: #6d2f46; }
4586
+ .hidden { display: none !important; }
4587
+ @media (prefers-color-scheme: dark) {
4588
+ body { background: #161616; color: #efefef; }
4589
+ .subtitle, .meta { color: #ababab; }
4590
+ input, .tree { background: #202020; border-color: #3a3a3a; }
4591
+ }
4592
+ </style>
4593
+ </head>
4594
+ <body>
4595
+ <main>
4596
+ <h1>${escapeHtml(tree.label)}</h1>
4597
+ <p class="subtitle">${graph.sources.length} sources \xB7 ${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 generated ${escapeHtml(graph.generatedAt)}</p>
4598
+ <div class="toolbar"><input id="filter" type="search" placeholder="Filter files, modules, symbols, or ids" aria-label="Filter graph tree"></div>
4599
+ <section class="tree"><ul>${renderNode(tree)}</ul></section>
4600
+ </main>
4601
+ <script>
4602
+ const input = document.getElementById('filter');
4603
+ input.addEventListener('input', () => {
4604
+ const query = input.value.trim().toLowerCase();
4605
+ for (const node of document.querySelectorAll('.tree-node')) {
4606
+ const text = node.textContent.toLowerCase();
4607
+ node.classList.toggle('hidden', query.length > 0 && !text.includes(query));
4608
+ }
4609
+ });
4610
+ </script>
4611
+ </body>
4612
+ </html>
4613
+ `;
4614
+ }
4615
+ async function exportGraphTree(rootDir, outputPath, options = {}) {
4616
+ const { paths } = await loadVaultConfig(rootDir);
4617
+ const graph = await readJsonFile(paths.graphPath);
4618
+ if (!graph) {
4619
+ throw new Error(`Graph artifact not found at ${paths.graphPath}. Run swarmvault compile first.`);
4620
+ }
4621
+ const tree = buildGraphTree(graph, { ...options, rootDir });
4622
+ const resolvedOutputPath = path8.resolve(rootDir, outputPath ?? path8.join(paths.wikiDir, "graph", "tree.html"));
4623
+ await ensureDir(path8.dirname(resolvedOutputPath));
4624
+ await import("fs/promises").then((fs13) => fs13.writeFile(resolvedOutputPath, renderGraphTreeHtml(tree, graph), "utf8"));
4625
+ return {
4626
+ outputPath: resolvedOutputPath,
4627
+ sourceCount: graph.sources.length,
4628
+ nodeCount: graph.nodes.length,
4629
+ tree
4630
+ };
4631
+ }
4632
+
3978
4633
  // src/hooks.ts
3979
- import fs6 from "fs/promises";
3980
- import path7 from "path";
4634
+ import fs7 from "fs/promises";
4635
+ import path9 from "path";
3981
4636
  import process3 from "process";
3982
4637
  var hookStart = "# >>> swarmvault hook >>>";
3983
4638
  var hookEnd = "# <<< swarmvault hook <<<";
3984
4639
  async function findNearestGitRoot(startPath) {
3985
- let current = path7.resolve(startPath);
4640
+ let current = path9.resolve(startPath);
3986
4641
  try {
3987
- const stat = await fs6.stat(current);
4642
+ const stat = await fs7.stat(current);
3988
4643
  if (!stat.isDirectory()) {
3989
- current = path7.dirname(current);
4644
+ current = path9.dirname(current);
3990
4645
  }
3991
4646
  } catch {
3992
- current = path7.dirname(current);
4647
+ current = path9.dirname(current);
3993
4648
  }
3994
4649
  while (true) {
3995
- if (await fileExists(path7.join(current, ".git"))) {
4650
+ if (await fileExists(path9.join(current, ".git"))) {
3996
4651
  return current;
3997
4652
  }
3998
- const parent = path7.dirname(current);
4653
+ const parent = path9.dirname(current);
3999
4654
  if (parent === current) {
4000
4655
  return null;
4001
4656
  }
@@ -4007,8 +4662,8 @@ function shellQuote(value) {
4007
4662
  }
4008
4663
  function resolveSwarmvaultExecutableCandidate() {
4009
4664
  const argvPath = process3.argv[1];
4010
- if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path7.sep}@swarmvaultai${path7.sep}cli${path7.sep}`) || argvPath.includes(`${path7.sep}packages${path7.sep}cli${path7.sep}`))) {
4011
- return path7.resolve(argvPath);
4665
+ if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path9.sep}@swarmvaultai${path9.sep}cli${path9.sep}`) || argvPath.includes(`${path9.sep}packages${path9.sep}cli${path9.sep}`))) {
4666
+ return path9.resolve(argvPath);
4012
4667
  }
4013
4668
  return "swarmvault";
4014
4669
  }
@@ -4027,17 +4682,17 @@ function managedHookBlock(vaultRoot) {
4027
4682
  ].join("\n");
4028
4683
  }
4029
4684
  function hookPath(repoRoot, hookName) {
4030
- return path7.join(repoRoot, ".git", "hooks", hookName);
4685
+ return path9.join(repoRoot, ".git", "hooks", hookName);
4031
4686
  }
4032
4687
  async function readHookStatus(filePath) {
4033
4688
  if (!await fileExists(filePath)) {
4034
4689
  return "not_installed";
4035
4690
  }
4036
- const content = await fs6.readFile(filePath, "utf8");
4691
+ const content = await fs7.readFile(filePath, "utf8");
4037
4692
  return content.includes(hookStart) && content.includes(hookEnd) ? "installed" : "other_content";
4038
4693
  }
4039
4694
  async function upsertHookFile(filePath, block) {
4040
- const existing = await fileExists(filePath) ? await fs6.readFile(filePath, "utf8") : "";
4695
+ const existing = await fileExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
4041
4696
  let next;
4042
4697
  const startIndex = existing.indexOf(hookStart);
4043
4698
  const endIndex = existing.indexOf(hookEnd);
@@ -4051,16 +4706,16 @@ ${block}`.trimEnd();
4051
4706
  next = `#!/bin/sh
4052
4707
  ${block}`.trimEnd();
4053
4708
  }
4054
- await ensureDir(path7.dirname(filePath));
4055
- await fs6.writeFile(filePath, `${next}
4709
+ await ensureDir(path9.dirname(filePath));
4710
+ await fs7.writeFile(filePath, `${next}
4056
4711
  `, { mode: 493, encoding: "utf8" });
4057
- await fs6.chmod(filePath, 493);
4712
+ await fs7.chmod(filePath, 493);
4058
4713
  }
4059
4714
  async function removeHookBlock(filePath) {
4060
4715
  if (!await fileExists(filePath)) {
4061
4716
  return;
4062
4717
  }
4063
- const existing = await fs6.readFile(filePath, "utf8");
4718
+ const existing = await fs7.readFile(filePath, "utf8");
4064
4719
  const startIndex = existing.indexOf(hookStart);
4065
4720
  const endIndex = existing.indexOf(hookEnd);
4066
4721
  if (startIndex === -1 || endIndex === -1) {
@@ -4068,10 +4723,10 @@ async function removeHookBlock(filePath) {
4068
4723
  }
4069
4724
  const next = `${existing.slice(0, startIndex)}${existing.slice(endIndex + hookEnd.length)}`.trim();
4070
4725
  if (!next || next === "#!/bin/sh") {
4071
- await fs6.rm(filePath, { force: true });
4726
+ await fs7.rm(filePath, { force: true });
4072
4727
  return;
4073
4728
  }
4074
- await fs6.writeFile(filePath, `${next}
4729
+ await fs7.writeFile(filePath, `${next}
4075
4730
  `, "utf8");
4076
4731
  }
4077
4732
  async function getGitHookStatus(rootDir) {
@@ -4094,7 +4749,7 @@ async function installGitHooks(rootDir) {
4094
4749
  if (!repoRoot) {
4095
4750
  throw new Error("No git repository found above the current vault.");
4096
4751
  }
4097
- const block = managedHookBlock(path7.resolve(rootDir));
4752
+ const block = managedHookBlock(path9.resolve(rootDir));
4098
4753
  await upsertHookFile(hookPath(repoRoot, "post-commit"), block);
4099
4754
  await upsertHookFile(hookPath(repoRoot, "post-checkout"), block);
4100
4755
  return getGitHookStatus(rootDir);
@@ -4114,12 +4769,12 @@ async function uninstallGitHooks(rootDir) {
4114
4769
  }
4115
4770
 
4116
4771
  // src/mcp.ts
4117
- import fs7 from "fs/promises";
4118
- import path8 from "path";
4772
+ import fs8 from "fs/promises";
4773
+ import path10 from "path";
4119
4774
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4120
4775
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4121
4776
  import { z } from "zod";
4122
- var SERVER_VERSION = "3.6.0";
4777
+ var SERVER_VERSION = "3.7.1";
4123
4778
  async function createMcpServer(rootDir) {
4124
4779
  const server = new McpServer({
4125
4780
  name: "swarmvault",
@@ -4827,7 +5482,7 @@ async function createMcpServer(rootDir) {
4827
5482
  },
4828
5483
  async () => {
4829
5484
  const { paths } = await loadVaultConfig(rootDir);
4830
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path8.relative(paths.sessionsDir, filePath))).sort();
5485
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path10.relative(paths.sessionsDir, filePath))).sort();
4831
5486
  return asTextResource("swarmvault://sessions", JSON.stringify(files, null, 2));
4832
5487
  }
4833
5488
  );
@@ -4896,8 +5551,8 @@ async function createMcpServer(rootDir) {
4896
5551
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
4897
5552
  }
4898
5553
  const { paths } = await loadVaultConfig(rootDir);
4899
- const absolutePath = path8.resolve(paths.wikiDir, relativePath);
4900
- return asTextResource(`swarmvault://pages/${encodedPath}`, await fs7.readFile(absolutePath, "utf8"));
5554
+ const absolutePath = path10.resolve(paths.wikiDir, relativePath);
5555
+ return asTextResource(`swarmvault://pages/${encodedPath}`, await fs8.readFile(absolutePath, "utf8"));
4901
5556
  }
4902
5557
  );
4903
5558
  server.registerResource(
@@ -4905,11 +5560,11 @@ async function createMcpServer(rootDir) {
4905
5560
  new ResourceTemplate("swarmvault://sessions/{path}", {
4906
5561
  list: async () => {
4907
5562
  const { paths } = await loadVaultConfig(rootDir);
4908
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path8.relative(paths.sessionsDir, filePath))).sort();
5563
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path10.relative(paths.sessionsDir, filePath))).sort();
4909
5564
  return {
4910
5565
  resources: files.map((relativePath) => ({
4911
5566
  uri: `swarmvault://sessions/${encodeURIComponent(relativePath)}`,
4912
- name: path8.basename(relativePath, ".md"),
5567
+ name: path10.basename(relativePath, ".md"),
4913
5568
  title: relativePath,
4914
5569
  description: "SwarmVault session artifact",
4915
5570
  mimeType: "text/markdown"
@@ -4926,11 +5581,11 @@ async function createMcpServer(rootDir) {
4926
5581
  const { paths } = await loadVaultConfig(rootDir);
4927
5582
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
4928
5583
  const relativePath = decodeURIComponent(encodedPath);
4929
- const absolutePath = path8.resolve(paths.sessionsDir, relativePath);
5584
+ const absolutePath = path10.resolve(paths.sessionsDir, relativePath);
4930
5585
  if (!isPathWithin(paths.sessionsDir, absolutePath) || !await fileExists(absolutePath)) {
4931
5586
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
4932
5587
  }
4933
- return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs7.readFile(absolutePath, "utf8"));
5588
+ return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs8.readFile(absolutePath, "utf8"));
4934
5589
  }
4935
5590
  );
4936
5591
  return server;
@@ -4990,9 +5645,9 @@ function asTextResource(uri, text) {
4990
5645
 
4991
5646
  // src/providers/local-whisper-setup.ts
4992
5647
  import { createWriteStream, constants as fsConstants } from "fs";
4993
- import fs8 from "fs/promises";
5648
+ import fs9 from "fs/promises";
4994
5649
  import os from "os";
4995
- import path9 from "path";
5650
+ import path11 from "path";
4996
5651
  import { Readable } from "stream";
4997
5652
  import { pipeline } from "stream/promises";
4998
5653
  var BINARY_CANDIDATES = ["whisper-cli", "whisper-cpp", "whisper"];
@@ -5020,10 +5675,10 @@ async function discoverLocalWhisperBinary(options = {}) {
5020
5675
  }
5021
5676
  const pathValue = env.PATH ?? "";
5022
5677
  const candidates = [];
5023
- for (const dir of pathValue.split(path9.delimiter)) {
5678
+ for (const dir of pathValue.split(path11.delimiter)) {
5024
5679
  if (!dir) continue;
5025
5680
  for (const name of BINARY_CANDIDATES) {
5026
- const full = path9.join(dir, name);
5681
+ const full = path11.join(dir, name);
5027
5682
  candidates.push(full);
5028
5683
  if (await isExecutable(full)) {
5029
5684
  return { binaryPath: full, candidates, source: "path" };
@@ -5034,14 +5689,14 @@ async function discoverLocalWhisperBinary(options = {}) {
5034
5689
  }
5035
5690
  function expectedModelPath(modelName, homeDir) {
5036
5691
  const home = homeDir ?? os.homedir();
5037
- return path9.join(home, ".swarmvault", "models", `ggml-${modelName}.bin`);
5692
+ return path11.join(home, ".swarmvault", "models", `ggml-${modelName}.bin`);
5038
5693
  }
5039
5694
  function modelDownloadUrl(modelName) {
5040
5695
  return `${HUGGINGFACE_BASE}/ggml-${modelName}.bin`;
5041
5696
  }
5042
5697
  async function downloadWhisperModel(options) {
5043
5698
  const destPath = expectedModelPath(options.modelName, options.homeDir);
5044
- await ensureDir(path9.dirname(destPath));
5699
+ await ensureDir(path11.dirname(destPath));
5045
5700
  const doFetch = options.fetchImpl ?? fetch;
5046
5701
  const url = modelDownloadUrl(options.modelName);
5047
5702
  const response = await doFetch(url);
@@ -5062,8 +5717,8 @@ async function downloadWhisperModel(options) {
5062
5717
  });
5063
5718
  const tmpPath = `${destPath}.part`;
5064
5719
  await pipeline(source, createWriteStream(tmpPath));
5065
- await fs8.rename(tmpPath, destPath);
5066
- const stat = await fs8.stat(destPath);
5720
+ await fs9.rename(tmpPath, destPath);
5721
+ const stat = await fs9.stat(destPath);
5067
5722
  return { path: destPath, bytes: stat.size };
5068
5723
  }
5069
5724
  async function registerLocalWhisperProvider(options) {
@@ -5130,7 +5785,7 @@ async function summarizeLocalWhisperSetup(options) {
5130
5785
  }
5131
5786
  async function isExecutable(p) {
5132
5787
  try {
5133
- await fs8.access(p, fsConstants.X_OK);
5788
+ await fs9.access(p, fsConstants.X_OK);
5134
5789
  return true;
5135
5790
  } catch {
5136
5791
  return false;
@@ -5200,13 +5855,13 @@ async function withCapabilityFallback(provider, capability, run, fallback) {
5200
5855
  }
5201
5856
 
5202
5857
  // src/schedule.ts
5203
- import fs9 from "fs/promises";
5204
- import path10 from "path";
5858
+ import fs10 from "fs/promises";
5859
+ import path12 from "path";
5205
5860
  function scheduleStatePath(schedulesDir, jobId) {
5206
- return path10.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
5861
+ return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
5207
5862
  }
5208
5863
  function scheduleLockPath(schedulesDir, jobId) {
5209
- return path10.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
5864
+ return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
5210
5865
  }
5211
5866
  function parseEveryDuration(value) {
5212
5867
  const match = value.trim().match(/^(\d+)(m|h|d)$/i);
@@ -5309,13 +5964,13 @@ async function acquireJobLease(rootDir, jobId) {
5309
5964
  const { paths } = await loadVaultConfig(rootDir);
5310
5965
  const leasePath = scheduleLockPath(paths.schedulesDir, jobId);
5311
5966
  await ensureDir(paths.schedulesDir);
5312
- const handle = await fs9.open(leasePath, "wx");
5967
+ const handle = await fs10.open(leasePath, "wx");
5313
5968
  await handle.writeFile(`${process.pid}
5314
5969
  ${(/* @__PURE__ */ new Date()).toISOString()}
5315
5970
  `);
5316
5971
  await handle.close();
5317
5972
  return async () => {
5318
- await fs9.rm(leasePath, { force: true });
5973
+ await fs10.rm(leasePath, { force: true });
5319
5974
  };
5320
5975
  }
5321
5976
  async function listSchedules(rootDir) {
@@ -5474,8 +6129,8 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
5474
6129
 
5475
6130
  // src/sources.ts
5476
6131
  import { spawn } from "child_process";
5477
- import fs10 from "fs/promises";
5478
- import path11 from "path";
6132
+ import fs11 from "fs/promises";
6133
+ import path13 from "path";
5479
6134
  import matter3 from "gray-matter";
5480
6135
  import { JSDOM } from "jsdom";
5481
6136
  var DEFAULT_CRAWL_MAX_PAGES = 12;
@@ -5521,24 +6176,24 @@ function emptyManagedSourceSyncCounts() {
5521
6176
  };
5522
6177
  }
5523
6178
  function withinRoot(rootPath, targetPath) {
5524
- const relative = path11.relative(rootPath, targetPath);
5525
- return relative === "" || !relative.startsWith("..") && !path11.isAbsolute(relative);
6179
+ const relative = path13.relative(rootPath, targetPath);
6180
+ return relative === "" || !relative.startsWith("..") && !path13.isAbsolute(relative);
5526
6181
  }
5527
6182
  async function findNearestGitRoot2(startPath) {
5528
- let current = path11.resolve(startPath);
6183
+ let current = path13.resolve(startPath);
5529
6184
  try {
5530
- const stat = await fs10.stat(current);
6185
+ const stat = await fs11.stat(current);
5531
6186
  if (!stat.isDirectory()) {
5532
- current = path11.dirname(current);
6187
+ current = path13.dirname(current);
5533
6188
  }
5534
6189
  } catch {
5535
- current = path11.dirname(current);
6190
+ current = path13.dirname(current);
5536
6191
  }
5537
6192
  while (true) {
5538
- if (await fileExists(path11.join(current, ".git"))) {
6193
+ if (await fileExists(path13.join(current, ".git"))) {
5539
6194
  return current;
5540
6195
  }
5541
- const parent = path11.dirname(current);
6196
+ const parent = path13.dirname(current);
5542
6197
  if (parent === current) {
5543
6198
  return null;
5544
6199
  }
@@ -5612,7 +6267,7 @@ function isAllowedDocsCandidate(candidate, startUrl) {
5612
6267
  if (candidate.origin !== startUrl.origin) {
5613
6268
  return false;
5614
6269
  }
5615
- const extension = path11.extname(candidate.pathname).toLowerCase();
6270
+ const extension = path13.extname(candidate.pathname).toLowerCase();
5616
6271
  if (extension && extension !== ".html" && extension !== ".htm" && extension !== ".md") {
5617
6272
  return false;
5618
6273
  }
@@ -5701,14 +6356,40 @@ function matchesManagedSourceSpec(existing, input) {
5701
6356
  return false;
5702
6357
  }
5703
6358
  if (input.kind === "directory" || input.kind === "file") {
5704
- return path11.resolve(existing.path ?? "") === path11.resolve(input.path);
6359
+ return path13.resolve(existing.path ?? "") === path13.resolve(input.path);
6360
+ }
6361
+ if (input.kind === "github_repo") {
6362
+ return (existing.url ?? "") === input.url && (existing.branch ?? "") === (input.branch ?? "") && (existing.ref ?? "") === (input.ref ?? "");
5705
6363
  }
5706
6364
  return (existing.url ?? "") === input.url;
5707
6365
  }
5708
- async function resolveManagedSourceInput(rootDir, input) {
5709
- const absoluteInput = path11.resolve(rootDir, input);
6366
+ function normalizeGitSelector(value, label) {
6367
+ const trimmed = value?.trim();
6368
+ if (!trimmed) {
6369
+ return void 0;
6370
+ }
6371
+ if (trimmed.startsWith("-")) {
6372
+ throw new Error(`Git ${label} must not start with "-".`);
6373
+ }
6374
+ if (/[\s\0]/.test(trimmed)) {
6375
+ throw new Error(`Git ${label} must not contain whitespace or NUL bytes.`);
6376
+ }
6377
+ return trimmed;
6378
+ }
6379
+ function normalizeCheckoutDir(rootDir, value) {
6380
+ const trimmed = value?.trim();
6381
+ if (!trimmed) {
6382
+ return void 0;
6383
+ }
6384
+ return path13.isAbsolute(trimmed) ? path13.resolve(trimmed) : path13.resolve(rootDir, trimmed);
6385
+ }
6386
+ async function resolveManagedSourceInput(rootDir, input, options = {}) {
6387
+ const absoluteInput = path13.resolve(rootDir, input);
5710
6388
  if (!(input.startsWith("http://") || input.startsWith("https://"))) {
5711
- const stat = await fs10.stat(absoluteInput).catch(() => null);
6389
+ if (options.branch || options.ref || options.checkoutDir) {
6390
+ throw new Error("Git branch/ref/checkout options are only supported for public GitHub repo root URLs.");
6391
+ }
6392
+ const stat = await fs11.stat(absoluteInput).catch(() => null);
5712
6393
  if (!stat) {
5713
6394
  throw new Error(`Source not found: ${input}`);
5714
6395
  }
@@ -5716,7 +6397,7 @@ async function resolveManagedSourceInput(rootDir, input) {
5716
6397
  return {
5717
6398
  kind: "file",
5718
6399
  path: absoluteInput,
5719
- title: path11.basename(absoluteInput, path11.extname(absoluteInput)) || absoluteInput
6400
+ title: path13.basename(absoluteInput, path13.extname(absoluteInput)) || absoluteInput
5720
6401
  };
5721
6402
  }
5722
6403
  if (!stat.isDirectory()) {
@@ -5728,16 +6409,22 @@ async function resolveManagedSourceInput(rootDir, input) {
5728
6409
  kind: "directory",
5729
6410
  path: absoluteInput,
5730
6411
  repoRoot,
5731
- title: path11.basename(absoluteInput) || absoluteInput
6412
+ title: path13.basename(absoluteInput) || absoluteInput
5732
6413
  };
5733
6414
  }
5734
6415
  const github = normalizeGitHubRepoRootUrl(input);
5735
6416
  if (github) {
5736
6417
  return {
5737
6418
  kind: "github_repo",
5738
- ...github
6419
+ ...github,
6420
+ branch: normalizeGitSelector(options.branch, "branch"),
6421
+ ref: normalizeGitSelector(options.ref, "ref"),
6422
+ checkoutDir: normalizeCheckoutDir(rootDir, options.checkoutDir)
5739
6423
  };
5740
6424
  }
6425
+ if (options.branch || options.ref || options.checkoutDir) {
6426
+ throw new Error("Git branch/ref/checkout options are only supported for public GitHub repo root URLs.");
6427
+ }
5741
6428
  const parsed = new URL(input);
5742
6429
  if (parsed.hostname.toLowerCase().includes("github.com")) {
5743
6430
  throw new Error(
@@ -5751,16 +6438,16 @@ async function resolveManagedSourceInput(rootDir, input) {
5751
6438
  };
5752
6439
  }
5753
6440
  function directorySourceIdsFor(manifests, inputPath) {
5754
- return manifests.filter((manifest) => manifest.originalPath && withinRoot(path11.resolve(inputPath), path11.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
6441
+ return manifests.filter((manifest) => manifest.originalPath && withinRoot(path13.resolve(inputPath), path13.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
5755
6442
  }
5756
6443
  function fileSourceIdsFor(manifests, inputPath) {
5757
- const absoluteInput = path11.resolve(inputPath);
5758
- return manifests.filter((manifest) => manifest.originalPath && path11.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
6444
+ const absoluteInput = path13.resolve(inputPath);
6445
+ return manifests.filter((manifest) => manifest.originalPath && path13.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
5759
6446
  }
5760
6447
  async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5761
6448
  const manifestsBefore = await listManifests(rootDir);
5762
6449
  const previousInScope = manifestsBefore.filter(
5763
- (manifest) => manifest.originalPath && withinRoot(path11.resolve(inputPath), path11.resolve(manifest.originalPath))
6450
+ (manifest) => manifest.originalPath && withinRoot(path13.resolve(inputPath), path13.resolve(manifest.originalPath))
5764
6451
  );
5765
6452
  const result = await ingestDirectory(rootDir, inputPath, { repoRoot });
5766
6453
  const removed = [];
@@ -5768,7 +6455,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5768
6455
  if (!manifest.originalPath) {
5769
6456
  continue;
5770
6457
  }
5771
- if (await fileExists(path11.resolve(manifest.originalPath))) {
6458
+ if (await fileExists(path13.resolve(manifest.originalPath))) {
5772
6459
  continue;
5773
6460
  }
5774
6461
  const removedManifest = await removeManifestBySourceId(rootDir, manifest.sourceId);
@@ -5778,7 +6465,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5778
6465
  }
5779
6466
  const manifestsAfter = await listManifests(rootDir);
5780
6467
  return {
5781
- title: path11.basename(inputPath) || inputPath,
6468
+ title: path13.basename(inputPath) || inputPath,
5782
6469
  sourceIds: directorySourceIdsFor(manifestsAfter, inputPath),
5783
6470
  counts: {
5784
6471
  scannedCount: result.scannedCount,
@@ -5794,7 +6481,7 @@ async function syncFileSource(rootDir, inputPath) {
5794
6481
  const result = await ingestInputDetailed(rootDir, inputPath);
5795
6482
  const manifestsAfter = await listManifests(rootDir);
5796
6483
  return {
5797
- title: path11.basename(inputPath, path11.extname(inputPath)) || inputPath,
6484
+ title: path13.basename(inputPath, path13.extname(inputPath)) || inputPath,
5798
6485
  sourceIds: fileSourceIdsFor(manifestsAfter, inputPath),
5799
6486
  counts: {
5800
6487
  scannedCount: result.scannedCount,
@@ -5828,8 +6515,11 @@ async function runGitCommand(cwd, args) {
5828
6515
  }
5829
6516
  async function syncGitHubRepoSource(rootDir, entry) {
5830
6517
  const workingDir = await managedSourceWorkingDir(rootDir, entry.id);
5831
- const checkoutDir = path11.join(workingDir, "checkout");
5832
- await fs10.rm(checkoutDir, { recursive: true, force: true });
6518
+ const externalCheckoutDir = entry.checkoutDir ? path13.resolve(entry.checkoutDir) : void 0;
6519
+ const checkoutDir = externalCheckoutDir ?? path13.join(workingDir, "checkout");
6520
+ if (!externalCheckoutDir) {
6521
+ await fs11.rm(checkoutDir, { recursive: true, force: true });
6522
+ }
5833
6523
  await ensureDir(workingDir);
5834
6524
  if (!entry.url) {
5835
6525
  throw new Error(`Managed source ${entry.id} is missing its repository URL.`);
@@ -5838,7 +6528,35 @@ async function syncGitHubRepoSource(rootDir, entry) {
5838
6528
  if (!github) {
5839
6529
  throw new Error(`Managed source ${entry.id} has an invalid GitHub repo URL.`);
5840
6530
  }
5841
- await runGitCommand(workingDir, ["clone", "--depth", "1", github.cloneUrl, "checkout"]);
6531
+ const branch = normalizeGitSelector(entry.branch, "branch");
6532
+ const ref = normalizeGitSelector(entry.ref, "ref");
6533
+ const cloneArgs = ["clone", "--depth", "1"];
6534
+ if (branch) {
6535
+ cloneArgs.push("--branch", branch);
6536
+ }
6537
+ cloneArgs.push(github.cloneUrl, checkoutDir);
6538
+ if (await fileExists(path13.join(checkoutDir, ".git"))) {
6539
+ await runGitCommand(checkoutDir, ["remote", "set-url", "origin", github.cloneUrl]);
6540
+ if (branch) {
6541
+ await runGitCommand(checkoutDir, ["fetch", "--depth", "1", "origin", branch]);
6542
+ } else {
6543
+ await runGitCommand(checkoutDir, ["fetch", "--depth", "1", "origin"]);
6544
+ }
6545
+ if (!ref) {
6546
+ await runGitCommand(checkoutDir, ["checkout", "--detach", "FETCH_HEAD"]);
6547
+ }
6548
+ } else {
6549
+ const existingEntries = await fs11.readdir(checkoutDir).catch(() => []);
6550
+ if (externalCheckoutDir && existingEntries.length > 0) {
6551
+ throw new Error(`Checkout directory exists but is not a Git repository: ${checkoutDir}`);
6552
+ }
6553
+ await ensureDir(path13.dirname(checkoutDir));
6554
+ await runGitCommand(workingDir, cloneArgs);
6555
+ }
6556
+ if (ref) {
6557
+ await runGitCommand(checkoutDir, ["fetch", "--depth", "1", "origin", ref]);
6558
+ await runGitCommand(checkoutDir, ["checkout", "--detach", "FETCH_HEAD"]);
6559
+ }
5842
6560
  return await syncDirectorySource(rootDir, checkoutDir, checkoutDir);
5843
6561
  }
5844
6562
  async function syncCrawlSource(rootDir, entry, options) {
@@ -5959,7 +6677,7 @@ function scopedNodeIds(graph, sourceIds) {
5959
6677
  async function loadSourceAnalyses(rootDir, sourceIds) {
5960
6678
  const { paths } = await loadVaultConfig(rootDir);
5961
6679
  const analyses = await Promise.all(
5962
- sourceIds.map(async (sourceId) => await readJsonFile(path11.join(paths.analysesDir, `${sourceId}.json`)))
6680
+ sourceIds.map(async (sourceId) => await readJsonFile(path13.join(paths.analysesDir, `${sourceId}.json`)))
5963
6681
  );
5964
6682
  return analyses.filter((analysis) => Boolean(analysis?.sourceId));
5965
6683
  }
@@ -6120,9 +6838,9 @@ async function writeSourceBriefForScope(rootDir, source) {
6120
6838
  confidence: 0.82
6121
6839
  }
6122
6840
  });
6123
- const absolutePath = path11.join(paths.wikiDir, output.page.path);
6124
- await ensureDir(path11.dirname(absolutePath));
6125
- await fs10.writeFile(absolutePath, output.content, "utf8");
6841
+ const absolutePath = path13.join(paths.wikiDir, output.page.path);
6842
+ await ensureDir(path13.dirname(absolutePath));
6843
+ await fs11.writeFile(absolutePath, output.content, "utf8");
6126
6844
  return absolutePath;
6127
6845
  }
6128
6846
  async function writeSourceBrief(rootDir, source) {
@@ -6410,7 +7128,7 @@ function selectGuidedTargetPages(scope, sourcePages, questions) {
6410
7128
  return (matchedTargets.length ? matchedTargets : canonicalPages).slice(0, 6);
6411
7129
  }
6412
7130
  function insightRelativePathForTarget(page, scope) {
6413
- const basename = path11.basename(page.path);
7131
+ const basename = path13.basename(page.path);
6414
7132
  if (page.kind === "concept") {
6415
7133
  return `insights/concepts/${basename}`;
6416
7134
  }
@@ -6637,7 +7355,7 @@ async function stageSourceReviewForScope(rootDir, scope) {
6637
7355
  return {
6638
7356
  sourceId: scope.id,
6639
7357
  pageId: output.page.id,
6640
- reviewPath: path11.join(approval.approvalDir, "wiki", output.page.path),
7358
+ reviewPath: path13.join(approval.approvalDir, "wiki", output.page.path),
6641
7359
  staged: true,
6642
7360
  approvalId: approval.approvalId,
6643
7361
  approvalDir: approval.approvalDir
@@ -6704,7 +7422,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
6704
7422
  const evidenceState = contradictions.length > 0 ? "conflicting" : session.targetedPagePaths.some(
6705
7423
  (targetPath) => sourcePages.some((page) => page.path === targetPath && page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId)))
6706
7424
  ) ? "reinforcing" : session.targetedPagePaths.length ? "new" : "needs_judgment";
6707
- const relativeBriefPath = session.briefPath && path11.isAbsolute(session.briefPath) ? path11.relative(paths.wikiDir, session.briefPath) : session.briefPath;
7425
+ const relativeBriefPath = session.briefPath && path13.isAbsolute(session.briefPath) ? path13.relative(paths.wikiDir, session.briefPath) : session.briefPath;
6708
7426
  const sessionMarkdown = [
6709
7427
  `# Guided Session: ${scope.title}`,
6710
7428
  "",
@@ -6787,9 +7505,9 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
6787
7505
  async function persistSourceSessionPage(rootDir, scope, session) {
6788
7506
  const { paths } = await loadVaultConfig(rootDir);
6789
7507
  const output = await buildSourceSessionSavedPage(rootDir, scope, session);
6790
- const absolutePath = path11.join(paths.wikiDir, output.page.path);
6791
- await ensureDir(path11.dirname(absolutePath));
6792
- await fs10.writeFile(absolutePath, output.content, "utf8");
7508
+ const absolutePath = path13.join(paths.wikiDir, output.page.path);
7509
+ await ensureDir(path13.dirname(absolutePath));
7510
+ await fs11.writeFile(absolutePath, output.content, "utf8");
6793
7511
  return { pageId: output.page.id, sessionPath: absolutePath };
6794
7512
  }
6795
7513
  async function buildGuidedUpdatePages(rootDir, scope, session) {
@@ -6817,8 +7535,8 @@ async function buildGuidedUpdatePages(rootDir, scope, session) {
6817
7535
  targetPages.map(async (targetPage) => {
6818
7536
  const evidenceState = classifyGuidedEvidenceState(scope, targetPage, contradictions);
6819
7537
  const relativePath = useCanonicalTargets && targetPage ? targetPage.path : targetPage ? insightRelativePathForTarget(targetPage, scope) : `insights/topics/${slugify(scope.title)}.md`;
6820
- const absolutePath = path11.join(paths.wikiDir, relativePath);
6821
- const existingContent = await fileExists(absolutePath) ? await fs10.readFile(absolutePath, "utf8") : "";
7538
+ const absolutePath = path13.join(paths.wikiDir, relativePath);
7539
+ const existingContent = await fileExists(absolutePath) ? await fs11.readFile(absolutePath, "utf8") : "";
6822
7540
  const parsed = existingContent ? matter3(existingContent) : { data: {}, content: "" };
6823
7541
  const existingData = parsed.data;
6824
7542
  const existingSourceIds = Array.isArray(existingData.source_ids) ? existingData.source_ids.filter((value) => typeof value === "string") : [];
@@ -6989,8 +7707,8 @@ async function stageSourceGuideForScope(rootDir, scope, options = {}) {
6989
7707
  }
6990
7708
  );
6991
7709
  session.status = "staged";
6992
- session.reviewPath = path11.join(approval.approvalDir, "wiki", reviewOutput.page.path);
6993
- session.guidePath = path11.join(approval.approvalDir, "wiki", guideOutput.page.path);
7710
+ session.reviewPath = path13.join(approval.approvalDir, "wiki", reviewOutput.page.path);
7711
+ session.guidePath = path13.join(approval.approvalDir, "wiki", guideOutput.page.path);
6994
7712
  session.approvalId = approval.approvalId;
6995
7713
  session.approvalDir = approval.approvalDir;
6996
7714
  const persisted = await persistSourceSessionPage(rootDir, scope, session);
@@ -7124,16 +7842,28 @@ async function addManagedSource(rootDir, input, options = {}) {
7124
7842
  const briefRequested = guideRequested ? true : options.brief ?? true;
7125
7843
  const reviewRequested = guideRequested ? false : options.review ?? false;
7126
7844
  const sources = await loadManagedSources(rootDir);
7127
- const resolved = await resolveManagedSourceInput(rootDir, input);
7845
+ const resolved = await resolveManagedSourceInput(rootDir, input, options);
7128
7846
  const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
7129
7847
  const now = (/* @__PURE__ */ new Date()).toISOString();
7130
- const source = existing ?? {
7131
- id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path11.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
7848
+ const source = existing ? {
7849
+ ...existing,
7850
+ branch: resolved.kind === "github_repo" ? resolved.branch : existing.branch,
7851
+ ref: resolved.kind === "github_repo" ? resolved.ref : existing.ref,
7852
+ checkoutDir: resolved.kind === "github_repo" ? resolved.checkoutDir ?? existing.checkoutDir : existing.checkoutDir
7853
+ } : {
7854
+ id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path13.resolve(resolved.path), resolved.title) : stableManagedSourceId(
7855
+ resolved.kind,
7856
+ resolved.kind === "github_repo" ? `${resolved.url}#branch=${resolved.branch ?? ""}#ref=${resolved.ref ?? ""}` : resolved.url,
7857
+ resolved.title
7858
+ ),
7132
7859
  kind: resolved.kind,
7133
7860
  title: resolved.title,
7134
7861
  path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
7135
7862
  repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
7136
7863
  url: resolved.kind === "directory" || resolved.kind === "file" ? void 0 : resolved.url,
7864
+ branch: resolved.kind === "github_repo" ? resolved.branch : void 0,
7865
+ ref: resolved.kind === "github_repo" ? resolved.ref : void 0,
7866
+ checkoutDir: resolved.kind === "github_repo" ? resolved.checkoutDir : void 0,
7137
7867
  createdAt: now,
7138
7868
  updatedAt: now,
7139
7869
  status: "ready",
@@ -7256,7 +7986,7 @@ async function deleteManagedSource(rootDir, id) {
7256
7986
  sources.filter((source) => source.id !== id)
7257
7987
  );
7258
7988
  const workingDir = await managedSourceWorkingDir(rootDir, id);
7259
- await fs10.rm(workingDir, { recursive: true, force: true });
7989
+ await fs11.rm(workingDir, { recursive: true, force: true });
7260
7990
  return { removed: target };
7261
7991
  }
7262
7992
 
@@ -7264,9 +7994,9 @@ async function deleteManagedSource(rootDir, id) {
7264
7994
  import { execFile as execFile2 } from "child_process";
7265
7995
  import { randomUUID } from "crypto";
7266
7996
  import { EventEmitter } from "events";
7267
- import fs11 from "fs/promises";
7997
+ import fs12 from "fs/promises";
7268
7998
  import http from "http";
7269
- import path12 from "path";
7999
+ import path14 from "path";
7270
8000
  import { promisify as promisify2 } from "util";
7271
8001
  import matter4 from "gray-matter";
7272
8002
  import mime from "mime-types";
@@ -7420,7 +8150,7 @@ function toViewerLintFindings(findings) {
7420
8150
  var execFileAsync2 = promisify2(execFile2);
7421
8151
  async function isReadableFile(absolutePath) {
7422
8152
  try {
7423
- const stats = await fs11.stat(absolutePath);
8153
+ const stats = await fs12.stat(absolutePath);
7424
8154
  return stats.isFile();
7425
8155
  } catch {
7426
8156
  return false;
@@ -7431,15 +8161,15 @@ async function readViewerPage(rootDir, relativePath) {
7431
8161
  return null;
7432
8162
  }
7433
8163
  const { paths } = await loadVaultConfig(rootDir);
7434
- const absolutePath = path12.resolve(paths.wikiDir, relativePath);
8164
+ const absolutePath = path14.resolve(paths.wikiDir, relativePath);
7435
8165
  if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
7436
8166
  return null;
7437
8167
  }
7438
- const raw = await fs11.readFile(absolutePath, "utf8");
8168
+ const raw = await fs12.readFile(absolutePath, "utf8");
7439
8169
  const parsed = matter4(raw);
7440
8170
  return {
7441
8171
  path: relativePath,
7442
- title: typeof parsed.data.title === "string" ? parsed.data.title : path12.basename(relativePath, path12.extname(relativePath)),
8172
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path14.basename(relativePath, path14.extname(relativePath)),
7443
8173
  frontmatter: parsed.data,
7444
8174
  content: parsed.content,
7445
8175
  assets: normalizeOutputAssets(parsed.data.output_assets)
@@ -7450,12 +8180,12 @@ async function readViewerAsset(rootDir, relativePath) {
7450
8180
  return null;
7451
8181
  }
7452
8182
  const { paths } = await loadVaultConfig(rootDir);
7453
- const absolutePath = path12.resolve(paths.wikiDir, relativePath);
8183
+ const absolutePath = path14.resolve(paths.wikiDir, relativePath);
7454
8184
  if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
7455
8185
  return null;
7456
8186
  }
7457
8187
  return {
7458
- buffer: await fs11.readFile(absolutePath),
8188
+ buffer: await fs12.readFile(absolutePath),
7459
8189
  mimeType: mime.lookup(absolutePath) || "application/octet-stream"
7460
8190
  };
7461
8191
  }
@@ -7491,8 +8221,8 @@ async function writeInboxClip(rootDir, body) {
7491
8221
  const tags = Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
7492
8222
  const now = (/* @__PURE__ */ new Date()).toISOString();
7493
8223
  const fileName = `${now.replace(/[:.]/g, "-")}-${slugForClip(title)}.md`;
7494
- const inboxPath = path12.join(paths.inboxDir, fileName);
7495
- await fs11.mkdir(paths.inboxDir, { recursive: true });
8224
+ const inboxPath = path14.join(paths.inboxDir, fileName);
8225
+ await fs12.mkdir(paths.inboxDir, { recursive: true });
7496
8226
  const lines = [
7497
8227
  "---",
7498
8228
  `title: ${JSON.stringify(title)}`,
@@ -7509,17 +8239,17 @@ async function writeInboxClip(rootDir, body) {
7509
8239
  selectionHtml && !markdown ? ["", "## Original HTML", "", "```html", selectionHtml, "```"].join("\n") : void 0,
7510
8240
  ""
7511
8241
  ].filter((line) => line !== void 0);
7512
- await fs11.writeFile(inboxPath, lines.join("\n"), "utf8");
8242
+ await fs12.writeFile(inboxPath, lines.join("\n"), "utf8");
7513
8243
  const result = await importInbox(rootDir, paths.inboxDir);
7514
8244
  return { mode: "inbox", inboxPath, result };
7515
8245
  }
7516
8246
  async function ensureViewerDist(viewerDistDir) {
7517
- const indexPath = path12.join(viewerDistDir, "index.html");
8247
+ const indexPath = path14.join(viewerDistDir, "index.html");
7518
8248
  if (await fileExists(indexPath)) {
7519
8249
  return;
7520
8250
  }
7521
- const viewerProjectDir = path12.dirname(viewerDistDir);
7522
- if (await fileExists(path12.join(viewerProjectDir, "package.json"))) {
8251
+ const viewerProjectDir = path14.dirname(viewerDistDir);
8252
+ if (await fileExists(path14.join(viewerProjectDir, "package.json"))) {
7523
8253
  await execFileAsync2("pnpm", ["build"], { cwd: viewerProjectDir });
7524
8254
  }
7525
8255
  }
@@ -7542,7 +8272,7 @@ async function startGraphServer(rootDir, port, options = {}) {
7542
8272
  response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
7543
8273
  return;
7544
8274
  }
7545
- const reportPath = path12.join(paths.wikiDir, "graph", "report.json");
8275
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7546
8276
  const report = await readJsonFile(reportPath) ?? null;
7547
8277
  response.writeHead(200, { "content-type": "application/json" });
7548
8278
  response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
@@ -7606,13 +8336,13 @@ async function startGraphServer(rootDir, port, options = {}) {
7606
8336
  return;
7607
8337
  }
7608
8338
  if (url.pathname === "/api/graph-report") {
7609
- const reportPath = path12.join(paths.wikiDir, "graph", "report.json");
8339
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7610
8340
  if (!await fileExists(reportPath)) {
7611
8341
  response.writeHead(404, { "content-type": "application/json" });
7612
8342
  response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
7613
8343
  return;
7614
8344
  }
7615
- const body = await fs11.readFile(reportPath, "utf8");
8345
+ const body = await fs12.readFile(reportPath, "utf8");
7616
8346
  response.writeHead(200, { "content-type": "application/json" });
7617
8347
  response.end(body);
7618
8348
  return;
@@ -7850,7 +8580,7 @@ async function startGraphServer(rootDir, port, options = {}) {
7850
8580
  return;
7851
8581
  }
7852
8582
  if (url.pathname === "/api/workspace") {
7853
- const reportPath = path12.join(paths.wikiDir, "graph", "report.json");
8583
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7854
8584
  const [graphRaw, reportRaw, approvalsRaw, candidatesRaw, memoryTasksRaw, watchStatusRaw, lintRaw, doctorRaw] = await Promise.all([
7855
8585
  readJsonFile(paths.graphPath).catch(() => null),
7856
8586
  readJsonFile(reportPath).catch(() => null),
@@ -7975,15 +8705,15 @@ async function startGraphServer(rootDir, port, options = {}) {
7975
8705
  return;
7976
8706
  }
7977
8707
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
7978
- const target = path12.join(paths.viewerDistDir, relativePath);
7979
- const fallback = path12.join(paths.viewerDistDir, "index.html");
8708
+ const target = path14.join(paths.viewerDistDir, relativePath);
8709
+ const fallback = path14.join(paths.viewerDistDir, "index.html");
7980
8710
  const filePath = await fileExists(target) ? target : fallback;
7981
8711
  if (!await fileExists(filePath)) {
7982
8712
  response.writeHead(503, { "content-type": "text/plain" });
7983
8713
  response.end("Viewer build not found. Run `pnpm build` first.");
7984
8714
  return;
7985
8715
  }
7986
- const staticBody = await fs11.readFile(filePath);
8716
+ const staticBody = await fs12.readFile(filePath);
7987
8717
  response.writeHead(200, { "content-type": mime.lookup(filePath) || "text/plain" });
7988
8718
  response.end(staticBody);
7989
8719
  } catch (error) {
@@ -8023,7 +8753,7 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8023
8753
  throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
8024
8754
  }
8025
8755
  await ensureViewerDist(paths.viewerDistDir);
8026
- const indexPath = path12.join(paths.viewerDistDir, "index.html");
8756
+ const indexPath = path14.join(paths.viewerDistDir, "index.html");
8027
8757
  if (!await fileExists(indexPath)) {
8028
8758
  throw new Error("Viewer build not found. Run `pnpm build` first.");
8029
8759
  }
@@ -8049,17 +8779,17 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8049
8779
  } : null;
8050
8780
  })
8051
8781
  );
8052
- const rawHtml = await fs11.readFile(indexPath, "utf8");
8782
+ const rawHtml = await fs12.readFile(indexPath, "utf8");
8053
8783
  const scriptMatch = rawHtml.match(/<script type="module" crossorigin src="([^"]+)"><\/script>/);
8054
8784
  const styleMatch = rawHtml.match(/<link rel="stylesheet" crossorigin href="([^"]+)">/);
8055
- const scriptPath = scriptMatch?.[1] ? path12.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
8056
- const stylePath = styleMatch?.[1] ? path12.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
8785
+ const scriptPath = scriptMatch?.[1] ? path14.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
8786
+ const stylePath = styleMatch?.[1] ? path14.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
8057
8787
  if (!scriptPath || !await fileExists(scriptPath)) {
8058
8788
  throw new Error("Viewer script bundle not found. Run `pnpm build` first.");
8059
8789
  }
8060
- const script = await fs11.readFile(scriptPath, "utf8");
8061
- const style = stylePath && await fileExists(stylePath) ? await fs11.readFile(stylePath, "utf8") : "";
8062
- const report = await readJsonFile(path12.join(paths.wikiDir, "graph", "report.json"));
8790
+ const script = await fs12.readFile(scriptPath, "utf8");
8791
+ const style = stylePath && await fileExists(stylePath) ? await fs12.readFile(stylePath, "utf8") : "";
8792
+ const report = await readJsonFile(path14.join(paths.wikiDir, "graph", "report.json"));
8063
8793
  const embeddedData = JSON.stringify(
8064
8794
  { graph: buildViewerGraphArtifact(graph, { report, full: options.full ?? false }), pages: pages.filter(Boolean), report },
8065
8795
  null,
@@ -8082,9 +8812,9 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8082
8812
  "</html>",
8083
8813
  ""
8084
8814
  ].filter(Boolean).join("\n");
8085
- await fs11.mkdir(path12.dirname(outputPath), { recursive: true });
8086
- await fs11.writeFile(outputPath, html, "utf8");
8087
- return path12.resolve(outputPath);
8815
+ await fs12.mkdir(path14.dirname(outputPath), { recursive: true });
8816
+ await fs12.writeFile(outputPath, html, "utf8");
8817
+ return path14.resolve(outputPath);
8088
8818
  }
8089
8819
  export {
8090
8820
  ALL_MIGRATIONS,
@@ -8114,6 +8844,7 @@ export {
8114
8844
  buildConfiguredRedactor,
8115
8845
  buildContextPack,
8116
8846
  buildGraphShareArtifact,
8847
+ buildGraphTree,
8117
8848
  buildMemoryGraphElements,
8118
8849
  buildRedactor,
8119
8850
  checkTrackedRepoChanges,
@@ -8137,12 +8868,14 @@ export {
8137
8868
  estimatePageTokens,
8138
8869
  estimateTokens,
8139
8870
  evaluateCandidateForPromotion,
8871
+ evaluateGraphShrinkGuard,
8140
8872
  expectedModelPath,
8141
8873
  explainGraphVault,
8142
8874
  exploreVault,
8143
8875
  exportGraphFormat,
8144
8876
  exportGraphHtml,
8145
8877
  exportGraphReportHtml,
8878
+ exportGraphTree,
8146
8879
  exportObsidianCanvas,
8147
8880
  exportObsidianVault,
8148
8881
  finishMemoryTask,
@@ -8187,11 +8920,13 @@ export {
8187
8920
  lookupPresetCapabilities,
8188
8921
  markSuperseded,
8189
8922
  memoryTaskHashes,
8923
+ mergeGraphFiles,
8190
8924
  modelDownloadUrl,
8191
8925
  pathGraphVault,
8192
8926
  persistDecayFrontmatter,
8193
8927
  planMigration,
8194
8928
  previewCandidatePromotions,
8929
+ projectGraphAfterRemovals,
8195
8930
  promoteCandidate,
8196
8931
  pushGraphNeo4j,
8197
8932
  queryGraphVault,
@@ -8214,6 +8949,7 @@ export {
8214
8949
  renderGraphShareMarkdown,
8215
8950
  renderGraphSharePreviewHtml,
8216
8951
  renderGraphShareSvg,
8952
+ renderGraphTreeHtml,
8217
8953
  renderMemoryTaskMarkdown,
8218
8954
  resetDecay,
8219
8955
  resolveArtifactRootDir,