@tekmidian/pai 0.8.3 → 0.8.5

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.
@@ -1,5 +1,6 @@
1
1
  import { a as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DGRAPAYb.mjs";
2
2
  import { t as zettelThemes } from "./themes-BvYF0W8T.mjs";
3
+ import { n as saveQueryResult } from "./query-feedback-Dv43XKHM.mjs";
3
4
  import { basename, dirname } from "node:path";
4
5
 
5
6
  //#region src/zettelkasten/explore.ts
@@ -563,6 +564,30 @@ async function zettelHealth(backend, opts) {
563
564
  const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
564
565
  lowConnectivity = await backend.getLowConnectivityAfter(cutoff);
565
566
  }
567
+ let linkConfidence;
568
+ if (scope === "full") try {
569
+ const samplePaths = await backend.getAllVaultFilePaths();
570
+ const sampleSize = Math.min(samplePaths.length, 200);
571
+ const sampled = samplePaths.slice(0, sampleSize);
572
+ let extracted = 0;
573
+ let inferred = 0;
574
+ let ambiguous = 0;
575
+ for (const path of sampled) {
576
+ const links = await backend.getLinksFromSource(path);
577
+ for (const link of links) {
578
+ const c = link.confidence ?? "EXTRACTED";
579
+ if (c === "EXTRACTED") extracted++;
580
+ else if (c === "INFERRED") inferred++;
581
+ else ambiguous++;
582
+ }
583
+ }
584
+ const factor = samplePaths.length > 0 ? samplePaths.length / sampleSize : 1;
585
+ linkConfidence = {
586
+ extracted: Math.round(extracted * factor),
587
+ inferred: Math.round(inferred * factor),
588
+ ambiguous: Math.round(ambiguous * factor)
589
+ };
590
+ } catch {}
566
591
  const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;
567
592
  const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;
568
593
  const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;
@@ -575,7 +600,8 @@ async function zettelHealth(backend, opts) {
575
600
  disconnectedClusters,
576
601
  lowConnectivity,
577
602
  healthScore,
578
- computedAt
603
+ computedAt,
604
+ linkConfidence
579
605
  };
580
606
  }
581
607
 
@@ -704,5 +730,334 @@ async function zettelSuggest(backend, opts) {
704
730
  }
705
731
 
706
732
  //#endregion
707
- export { zettelConverse, zettelExplore, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
708
- //# sourceMappingURL=zettelkasten-cdajbnPr.mjs.map
733
+ //#region src/zettelkasten/god-notes.ts
734
+ /** Patterns that identify structural/meta pages rather than concept notes. */
735
+ const STRUCTURAL_PATTERNS = [
736
+ /\bindex\b/i,
737
+ /\bMOC\b/,
738
+ /\bmaster\b/i,
739
+ /\btag\s*page/i,
740
+ /\bhome\b/i,
741
+ /\bdashboard\b/i,
742
+ /\btemplate/i,
743
+ /^_/
744
+ ];
745
+ function isStructuralNote(title, path) {
746
+ const text = title ?? path;
747
+ return STRUCTURAL_PATTERNS.some((pattern) => pattern.test(text));
748
+ }
749
+ /**
750
+ * Find hub/"god" notes in the vault — notes with the highest inbound link counts,
751
+ * excluding structural pages.
752
+ */
753
+ async function zettelGodNotes(backend, opts) {
754
+ const limit = opts?.limit ?? 20;
755
+ const minInbound = opts?.minInbound ?? 3;
756
+ const linkGraph = await backend.getVaultLinkGraph();
757
+ const inboundCounts = /* @__PURE__ */ new Map();
758
+ const outboundCounts = /* @__PURE__ */ new Map();
759
+ for (const { source_path, target_path } of linkGraph) {
760
+ inboundCounts.set(target_path, (inboundCounts.get(target_path) ?? 0) + 1);
761
+ outboundCounts.set(source_path, (outboundCounts.get(source_path) ?? 0) + 1);
762
+ }
763
+ const allFiles = await backend.getAllVaultFiles();
764
+ const titleMap = /* @__PURE__ */ new Map();
765
+ for (const f of allFiles) titleMap.set(f.vaultPath, f.title);
766
+ const totalVaultFiles = allFiles.length;
767
+ const allInbounds = allFiles.map((f) => inboundCounts.get(f.vaultPath) ?? 0);
768
+ allInbounds.sort((a, b) => a - b);
769
+ const medianInbound = allInbounds.length > 0 ? allInbounds[Math.floor(allInbounds.length / 2)] : 0;
770
+ const candidates = [];
771
+ for (const [path, inbound] of inboundCounts) {
772
+ if (inbound < minInbound) continue;
773
+ const title = titleMap.get(path) ?? null;
774
+ if (isStructuralNote(title, path)) continue;
775
+ const outbound = outboundCounts.get(path) ?? 0;
776
+ const totalDegree = inbound + outbound;
777
+ const inboundRatio = totalDegree > 0 ? inbound / totalDegree : 0;
778
+ candidates.push({
779
+ path,
780
+ title,
781
+ inboundCount: inbound,
782
+ outboundCount: outbound,
783
+ inboundRatio: Math.round(inboundRatio * 1e3) / 1e3
784
+ });
785
+ }
786
+ candidates.sort((a, b) => b.inboundCount - a.inboundCount);
787
+ return {
788
+ godNotes: candidates.slice(0, limit),
789
+ totalVaultFiles,
790
+ medianInbound
791
+ };
792
+ }
793
+
794
+ //#endregion
795
+ //#region src/zettelkasten/communities.ts
796
+ function buildUndirectedGraph(edges) {
797
+ const adj = /* @__PURE__ */ new Map();
798
+ const nodeSet = /* @__PURE__ */ new Set();
799
+ function getOrCreate(node) {
800
+ let m = adj.get(node);
801
+ if (!m) {
802
+ m = /* @__PURE__ */ new Map();
803
+ adj.set(node, m);
804
+ }
805
+ nodeSet.add(node);
806
+ return m;
807
+ }
808
+ let totalWeight = 0;
809
+ for (const { source_path, target_path } of edges) {
810
+ if (source_path === target_path) continue;
811
+ const aMap = getOrCreate(source_path);
812
+ const bMap = getOrCreate(target_path);
813
+ aMap.set(target_path, (aMap.get(target_path) ?? 0) + 1);
814
+ bMap.set(source_path, (bMap.get(source_path) ?? 0) + 1);
815
+ totalWeight += 1;
816
+ }
817
+ const degree = /* @__PURE__ */ new Map();
818
+ for (const [node, neighbors] of adj) {
819
+ let d = 0;
820
+ for (const w of neighbors.values()) d += w;
821
+ degree.set(node, d);
822
+ }
823
+ return {
824
+ nodes: Array.from(nodeSet),
825
+ adj,
826
+ totalWeight,
827
+ degree
828
+ };
829
+ }
830
+ /**
831
+ * Run one pass of Phase 1: local node movement.
832
+ * Returns true if any node changed community.
833
+ */
834
+ function louvainPhase1(graph, community, resolution) {
835
+ const m2 = 2 * graph.totalWeight;
836
+ if (m2 === 0) return false;
837
+ const communityDegreeSum = /* @__PURE__ */ new Map();
838
+ const communityInternalWeight = /* @__PURE__ */ new Map();
839
+ for (const node of graph.nodes) {
840
+ const c = community.get(node);
841
+ communityDegreeSum.set(c, (communityDegreeSum.get(c) ?? 0) + (graph.degree.get(node) ?? 0));
842
+ }
843
+ for (const [node, neighbors] of graph.adj) {
844
+ const nc = community.get(node);
845
+ for (const [neighbor, weight] of neighbors) if (community.get(neighbor) === nc) communityInternalWeight.set(nc, (communityInternalWeight.get(nc) ?? 0) + weight);
846
+ }
847
+ for (const [c, w] of communityInternalWeight) communityInternalWeight.set(c, w / 2);
848
+ let improved = false;
849
+ const shuffled = [...graph.nodes];
850
+ for (let i = shuffled.length - 1; i > 0; i--) {
851
+ const j = Math.floor(Math.random() * (i + 1));
852
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
853
+ }
854
+ for (const node of shuffled) {
855
+ const currentComm = community.get(node);
856
+ const ki = graph.degree.get(node) ?? 0;
857
+ const neighbors = graph.adj.get(node) ?? /* @__PURE__ */ new Map();
858
+ const weightToComm = /* @__PURE__ */ new Map();
859
+ for (const [neighbor, weight] of neighbors) {
860
+ const nc = community.get(neighbor);
861
+ weightToComm.set(nc, (weightToComm.get(nc) ?? 0) + weight);
862
+ }
863
+ communityDegreeSum.set(currentComm, (communityDegreeSum.get(currentComm) ?? 0) - ki);
864
+ const weightToCurrent = weightToComm.get(currentComm) ?? 0;
865
+ communityInternalWeight.set(currentComm, (communityInternalWeight.get(currentComm) ?? 0) - weightToCurrent);
866
+ let bestComm = currentComm;
867
+ let bestDelta = 0;
868
+ for (const [candidateComm, weightToCandidate] of weightToComm) {
869
+ const delta = weightToCandidate - resolution * (ki * (communityDegreeSum.get(candidateComm) ?? 0)) / m2;
870
+ if (delta > bestDelta) {
871
+ bestDelta = delta;
872
+ bestComm = candidateComm;
873
+ }
874
+ }
875
+ community.set(node, bestComm);
876
+ communityDegreeSum.set(bestComm, (communityDegreeSum.get(bestComm) ?? 0) + ki);
877
+ const weightToBest = weightToComm.get(bestComm) ?? 0;
878
+ communityInternalWeight.set(bestComm, (communityInternalWeight.get(bestComm) ?? 0) + weightToBest);
879
+ if (bestComm !== currentComm) improved = true;
880
+ }
881
+ return improved;
882
+ }
883
+ /**
884
+ * Compute modularity for a given partition.
885
+ */
886
+ function computeModularity(graph, community, resolution) {
887
+ const m2 = 2 * graph.totalWeight;
888
+ if (m2 === 0) return 0;
889
+ let q = 0;
890
+ for (const [node, neighbors] of graph.adj) {
891
+ const ci = community.get(node);
892
+ const ki = graph.degree.get(node) ?? 0;
893
+ for (const [neighbor, weight] of neighbors) if (community.get(neighbor) === ci) q += weight - resolution * (ki * (graph.degree.get(neighbor) ?? 0)) / m2;
894
+ }
895
+ return q / m2;
896
+ }
897
+ /**
898
+ * Run Louvain community detection on the vault link graph.
899
+ */
900
+ function runLouvain(graph, resolution) {
901
+ const community = /* @__PURE__ */ new Map();
902
+ let nextComm = 0;
903
+ for (const node of graph.nodes) community.set(node, nextComm++);
904
+ const MAX_ITERATIONS = 20;
905
+ for (let iter = 0; iter < MAX_ITERATIONS; iter++) if (!louvainPhase1(graph, community, resolution)) break;
906
+ const commRemap = /* @__PURE__ */ new Map();
907
+ let remapIdx = 0;
908
+ for (const [, c] of community) if (!commRemap.has(c)) commRemap.set(c, remapIdx++);
909
+ for (const [node, c] of community) community.set(node, commRemap.get(c));
910
+ return community;
911
+ }
912
+ const STOP_WORDS = new Set([
913
+ "the",
914
+ "and",
915
+ "for",
916
+ "are",
917
+ "but",
918
+ "not",
919
+ "you",
920
+ "all",
921
+ "can",
922
+ "her",
923
+ "was",
924
+ "one",
925
+ "our",
926
+ "out",
927
+ "has",
928
+ "had",
929
+ "how",
930
+ "its",
931
+ "may",
932
+ "new",
933
+ "now",
934
+ "old",
935
+ "see",
936
+ "way",
937
+ "who",
938
+ "did",
939
+ "get",
940
+ "let",
941
+ "say",
942
+ "she",
943
+ "too",
944
+ "use",
945
+ "from",
946
+ "with",
947
+ "this",
948
+ "that",
949
+ "will",
950
+ "been",
951
+ "have",
952
+ "each",
953
+ "make",
954
+ "like",
955
+ "long",
956
+ "look",
957
+ "many",
958
+ "them",
959
+ "then",
960
+ "what",
961
+ "when",
962
+ "some",
963
+ "time",
964
+ "very",
965
+ "your",
966
+ "about",
967
+ "could",
968
+ "into",
969
+ "just",
970
+ "more",
971
+ "note",
972
+ "notes",
973
+ "than",
974
+ "over"
975
+ ]);
976
+ function generateCommunityLabel(titles) {
977
+ const wordCounts = /* @__PURE__ */ new Map();
978
+ for (const title of titles) {
979
+ if (!title) continue;
980
+ const words = title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
981
+ for (const word of words) wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
982
+ }
983
+ return [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([w]) => w).join(" / ") || "unnamed";
984
+ }
985
+ function getTopFolder(path) {
986
+ const slash = path.indexOf("/");
987
+ return slash === -1 ? path : path.slice(0, slash);
988
+ }
989
+ /**
990
+ * Detect communities in the vault link graph using the Louvain algorithm.
991
+ * Returns clusters of densely connected notes with labels and cohesion scores.
992
+ */
993
+ async function zettelCommunities(backend, opts) {
994
+ const minSize = opts?.minSize ?? 3;
995
+ const maxCommunities = opts?.maxCommunities ?? 20;
996
+ const resolution = opts?.resolution ?? 1;
997
+ const graph = buildUndirectedGraph(await backend.getVaultLinkGraph());
998
+ if (graph.nodes.length === 0) return {
999
+ communities: [],
1000
+ totalNodes: 0,
1001
+ totalEdges: 0,
1002
+ modularity: 0
1003
+ };
1004
+ const communityMap = runLouvain(graph, resolution);
1005
+ const modularity = computeModularity(graph, communityMap, resolution);
1006
+ const groups = /* @__PURE__ */ new Map();
1007
+ for (const [node, comm] of communityMap) {
1008
+ const arr = groups.get(comm);
1009
+ if (arr) arr.push(node);
1010
+ else groups.set(comm, [node]);
1011
+ }
1012
+ const allFiles = await backend.getAllVaultFiles();
1013
+ const titleMap = /* @__PURE__ */ new Map();
1014
+ for (const f of allFiles) titleMap.set(f.vaultPath, f.title);
1015
+ const communities = [];
1016
+ let commId = 0;
1017
+ for (const [, members] of groups) {
1018
+ if (members.length < minSize) continue;
1019
+ const memberSet = new Set(members);
1020
+ const label = generateCommunityLabel(members.map((p) => titleMap.get(p) ?? null));
1021
+ const nodes = members.map((path) => {
1022
+ const neighbors = graph.adj.get(path) ?? /* @__PURE__ */ new Map();
1023
+ let internalDeg = 0;
1024
+ for (const [neighbor, weight] of neighbors) if (memberSet.has(neighbor)) internalDeg += weight;
1025
+ return {
1026
+ path,
1027
+ title: titleMap.get(path) ?? null,
1028
+ internalDegree: internalDeg
1029
+ };
1030
+ });
1031
+ nodes.sort((a, b) => b.internalDegree - a.internalDegree);
1032
+ let internalEdges = 0;
1033
+ for (const node of nodes) internalEdges += node.internalDegree;
1034
+ internalEdges /= 2;
1035
+ const possibleEdges = members.length * (members.length - 1) / 2;
1036
+ const cohesion = possibleEdges > 0 ? Math.round(internalEdges / possibleEdges * 1e3) / 1e3 : 0;
1037
+ const folderCounts = /* @__PURE__ */ new Map();
1038
+ for (const path of members) {
1039
+ const folder = getTopFolder(path);
1040
+ folderCounts.set(folder, (folderCounts.get(folder) ?? 0) + 1);
1041
+ }
1042
+ const topFolders = [...folderCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([f]) => f);
1043
+ communities.push({
1044
+ id: commId++,
1045
+ label,
1046
+ nodes,
1047
+ size: members.length,
1048
+ cohesion,
1049
+ topFolders
1050
+ });
1051
+ }
1052
+ communities.sort((a, b) => b.size - a.size);
1053
+ return {
1054
+ communities: communities.slice(0, maxCommunities),
1055
+ totalNodes: graph.nodes.length,
1056
+ totalEdges: graph.totalWeight,
1057
+ modularity: Math.round(modularity * 1e4) / 1e4
1058
+ };
1059
+ }
1060
+
1061
+ //#endregion
1062
+ export { zettelCommunities, zettelConverse, zettelExplore, zettelGodNotes, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
1063
+ //# sourceMappingURL=zettelkasten-DhBKZQHF.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zettelkasten-DhBKZQHF.mjs","names":["MAX_CHUNKS"],"sources":["../src/zettelkasten/explore.ts","../src/zettelkasten/surprise.ts","../src/zettelkasten/converse.ts","../src/zettelkasten/health.ts","../src/zettelkasten/suggest.ts","../src/zettelkasten/god-notes.ts","../src/zettelkasten/communities.ts"],"sourcesContent":["import type { StorageBackend } from \"../storage/interface.js\";\nimport { dirname } from \"node:path\";\n\nexport interface ExploreOptions {\n startNote: string;\n depth?: number;\n direction?: \"forward\" | \"backward\" | \"both\";\n mode?: \"sequential\" | \"associative\" | \"all\";\n}\n\nexport interface ExploreNode {\n path: string;\n title: string | null;\n depth: number;\n linkType: \"sequential\" | \"associative\";\n inbound: number;\n outbound: number;\n}\n\nexport interface ExploreResult {\n root: string;\n nodes: ExploreNode[];\n edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }>;\n branchingPoints: string[];\n maxDepthReached: boolean;\n}\n\nfunction classifyEdge(source: string, target: string): \"sequential\" | \"associative\" {\n return dirname(source) === dirname(target) ? \"sequential\" : \"associative\";\n}\n\nasync function resolveStart(backend: StorageBackend, startNote: string): Promise<string | null> {\n // Try direct lookup first\n const files = await backend.getVaultFilesByPaths([startNote]);\n if (files.length > 0) return files[0].vaultPath;\n\n // Try alias lookup\n const alias = await backend.getVaultAlias(startNote);\n if (!alias) return null;\n\n const canonical = await backend.getVaultFilesByPaths([alias.canonicalPath]);\n return canonical.length > 0 ? canonical[0].vaultPath : null;\n}\n\nasync function getForwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksFromSource(path);\n return links.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n}\n\nasync function getBackwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksToTarget(path);\n return links.map(l => l.sourcePath);\n}\n\nasync function getFileInfo(\n backend: StorageBackend,\n path: string,\n): Promise<{ title: string | null; inbound: number; outbound: number }> {\n const [files, health] = await Promise.all([\n backend.getVaultFilesByPaths([path]),\n backend.getVaultHealth(path),\n ]);\n\n return {\n title: files[0]?.title ?? null,\n inbound: health?.inboundCount ?? 0,\n outbound: health?.outboundCount ?? 0,\n };\n}\n\n/**\n * Traverse the Zettelkasten link graph using BFS, following chains of thought\n * from a starting note up to a configurable depth.\n */\nexport async function zettelExplore(backend: StorageBackend, opts: ExploreOptions): Promise<ExploreResult> {\n const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);\n const direction = opts.direction ?? \"both\";\n const mode = opts.mode ?? \"all\";\n\n const root = await resolveStart(backend, opts.startNote);\n if (!root) {\n return {\n root: opts.startNote,\n nodes: [],\n edges: [],\n branchingPoints: [],\n maxDepthReached: false,\n };\n }\n\n const visited = new Set<string>([root]);\n const nodes: ExploreNode[] = [];\n const edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }> = [];\n let maxDepthReached = false;\n\n const queue: Array<{ path: string; depth: number }> = [{ path: root, depth: 0 }];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n if (current.depth >= depth) {\n maxDepthReached = true;\n continue;\n }\n\n const neighbors: Array<{ neighbor: string; from: string; to: string }> = [];\n\n if (direction === \"forward\" || direction === \"both\") {\n for (const n of await getForwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: current.path, to: n });\n }\n }\n\n if (direction === \"backward\" || direction === \"both\") {\n for (const n of await getBackwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: n, to: current.path });\n }\n }\n\n for (const { neighbor, from, to } of neighbors) {\n const edgeType = classifyEdge(from, to);\n\n if (mode !== \"all\" && edgeType !== mode) {\n continue;\n }\n\n const alreadyHasEdge = edges.some((e) => e.from === from && e.to === to);\n if (!alreadyHasEdge) {\n edges.push({ from, to, type: edgeType });\n }\n\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n\n const info = await getFileInfo(backend, neighbor);\n nodes.push({\n path: neighbor,\n title: info.title,\n depth: current.depth + 1,\n linkType: edgeType,\n inbound: info.inbound,\n outbound: info.outbound,\n });\n\n queue.push({ path: neighbor, depth: current.depth + 1 });\n }\n }\n }\n\n const branchingPoints = nodes\n .filter((n) => n.outbound > 2)\n .map((n) => n.path);\n\n const rootInfo = await getFileInfo(backend, root);\n if (rootInfo.outbound > 2) {\n branchingPoints.unshift(root);\n }\n\n return { root, nodes, edges, branchingPoints, maxDepthReached };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport {\n deserializeEmbedding,\n generateEmbedding,\n cosineSimilarity,\n} from \"../memory/embeddings.js\";\n\nexport interface SurpriseOptions {\n referencePath: string;\n vaultProjectId: number;\n limit?: number;\n minSimilarity?: number;\n minGraphDistance?: number;\n}\n\nexport interface SurpriseResult {\n path: string;\n title: string | null;\n cosineSimilarity: number;\n graphDistance: number;\n surpriseScore: number;\n sharedSnippet: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst BFS_HOP_CAP = 20;\n\nasync function getFileEmbeddings(\n backend: StorageBackend,\n projectId: number,\n): Promise<Map<string, { embedding: Float32Array; text: string }>> {\n const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number; text: string }>();\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1, text: row.text });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const result = new Map<string, { embedding: Float32Array; text: string }>();\n for (const [path, { sum, count, text }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n result.set(path, { embedding: avg, text });\n }\n return result;\n}\n\nasync function getReferenceEmbedding(\n backend: StorageBackend,\n projectId: number,\n path: string,\n): Promise<{ embedding: Float32Array; found: boolean }> {\n const rows = await backend.getChunksForPath(projectId, path);\n\n if (rows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const embRows = rows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (embRows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const dim = deserializeEmbedding(embRows[0].embedding).length;\n const sum = new Float32Array(dim);\n for (const row of embRows) {\n const vec = deserializeEmbedding(row.embedding);\n for (let i = 0; i < dim; i++) {\n sum[i] += vec[i];\n }\n }\n const avg = new Float32Array(dim);\n for (let i = 0; i < dim; i++) {\n avg[i] = sum[i] / embRows.length;\n }\n return { embedding: avg, found: true };\n}\n\nasync function bfsGraphDistance(backend: StorageBackend, source: string, target: string): Promise<number> {\n if (source === target) return 0;\n\n const visited = new Set<string>([source]);\n const queue: Array<{ path: string; hops: number }> = [{ path: source, hops: 0 }];\n\n while (queue.length > 0) {\n const { path, hops } = queue.shift()!;\n if (hops >= BFS_HOP_CAP) continue;\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getLinksFromSource(path),\n backend.getLinksToTarget(path),\n ]);\n\n const neighbors: string[] = [\n ...forwardLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string),\n ...backwardLinks.map(l => l.sourcePath),\n ];\n\n for (const neighbor of neighbors) {\n if (neighbor === target) return hops + 1;\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n queue.push({ path: neighbor, hops: hops + 1 });\n }\n }\n }\n\n return Infinity;\n}\n\nfunction getBestChunkText(\n chunkRows: Array<{ text: string; embedding: Buffer | null }>,\n refEmbedding: Float32Array,\n): string {\n const rows = chunkRows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (rows.length === 0) return \"\";\n\n let bestText = rows[0].text;\n let bestSim = -Infinity;\n\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const sim = cosineSimilarity(refEmbedding, vec);\n if (sim > bestSim) {\n bestSim = sim;\n bestText = row.text;\n }\n }\n\n return bestText.trim().slice(0, 200);\n}\n\n/**\n * Find notes that are semantically similar to a reference note but graph-distant —\n * revealing surprising conceptual connections across unrelated areas of the Zettelkasten.\n */\nexport async function zettelSurprise(\n backend: StorageBackend,\n opts: SurpriseOptions,\n): Promise<SurpriseResult[]> {\n const limit = opts.limit ?? 10;\n const minSimilarity = opts.minSimilarity ?? 0.3;\n const minGraphDistance = opts.minGraphDistance ?? 3;\n\n let { embedding: refEmbedding, found } = await getReferenceEmbedding(\n backend,\n opts.vaultProjectId,\n opts.referencePath,\n );\n\n // Fall back to generating an embedding from the file title if no chunks exist\n if (!found) {\n const files = await backend.getVaultFilesByPaths([opts.referencePath]);\n const text = files[0]?.title ?? opts.referencePath;\n refEmbedding = await generateEmbedding(text, true);\n }\n\n const allFileEmbeddings = await getFileEmbeddings(backend, opts.vaultProjectId);\n\n // Remove the reference note itself from candidates\n allFileEmbeddings.delete(opts.referencePath);\n\n // First pass: filter by semantic similarity to avoid BFS on all nodes\n const semanticCandidates: Array<{ path: string; sim: number }> = [];\n for (const [path, { embedding }] of allFileEmbeddings) {\n const sim = cosineSimilarity(refEmbedding, embedding);\n if (sim >= minSimilarity) {\n semanticCandidates.push({ path, sim });\n }\n }\n\n // Compute graph distances for semantic candidates\n const results: SurpriseResult[] = [];\n\n for (const { path, sim } of semanticCandidates) {\n const graphDistance = await bfsGraphDistance(backend, opts.referencePath, path);\n\n const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;\n if (effectiveDistance < minGraphDistance) continue;\n\n const files = await backend.getVaultFilesByPaths([path]);\n const chunkRows = await backend.getChunksForPath(opts.vaultProjectId, path, 20);\n\n const surpriseScore = sim * Math.log2(effectiveDistance + 1);\n const sharedSnippet = getBestChunkText(chunkRows, refEmbedding);\n\n results.push({\n path,\n title: files[0]?.title ?? null,\n cosineSimilarity: sim,\n graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,\n surpriseScore,\n sharedSnippet,\n });\n }\n\n results.sort((a, b) => b.surpriseScore - a.surpriseScore);\n return results.slice(0, limit);\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport type { SearchResult } from \"../memory/search.js\";\nimport { generateEmbedding } from \"../memory/embeddings.js\";\n\nexport interface ConverseOptions {\n /** The user's question or topic to explore. */\n question: string;\n /** project_id for vault chunks in memory_chunks. */\n vaultProjectId: number;\n /** Graph expansion depth. Default 2. */\n depth?: number;\n /** Maximum number of relevant notes to return. Default 15. */\n limit?: number;\n}\n\nexport interface ConverseConnection {\n fromPath: string;\n toPath: string;\n /** Top-level folder of fromPath. */\n fromDomain: string;\n /** Top-level folder of toPath. */\n toDomain: string;\n /** Link count between these two notes (can be > 1). */\n strength: number;\n}\n\nexport interface ConverseResult {\n relevantNotes: Array<{\n path: string;\n title: string | null;\n snippet: string;\n score: number;\n domain: string;\n }>;\n /** Cross-domain connections found among the selected notes. */\n connections: ConverseConnection[];\n /** Unique domains involved across all selected notes. */\n domains: string[];\n /** AI-ready prompt combining notes + connections for insight generation. */\n synthesisPrompt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract the top-level folder from a vault path (first path segment). */\nfunction extractDomain(vaultPath: string): string {\n const slash = vaultPath.indexOf(\"/\");\n return slash === -1 ? vaultPath : vaultPath.slice(0, slash);\n}\n\n/**\n * Expand one level of graph neighbors for a set of paths.\n * Returns all outbound and inbound neighbor paths (excluding already-visited).\n */\nasync function expandNeighbors(backend: StorageBackend, paths: Set<string>): Promise<string[]> {\n if (paths.size === 0) return [];\n const pathList = Array.from(paths);\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getVaultLinksFromPaths(pathList),\n Promise.all(pathList.map(p => backend.getLinksToTarget(p))),\n ]);\n\n const neighbors: string[] = [];\n for (const link of forwardLinks) {\n if (link.targetPath) neighbors.push(link.targetPath);\n }\n for (const linkList of backwardLinks) {\n for (const link of linkList) {\n neighbors.push(link.sourcePath);\n }\n }\n return neighbors;\n}\n\n/**\n * Hybrid search combining keyword + semantic results using the StorageBackend.\n */\nasync function hybridSearch(\n backend: StorageBackend,\n query: string,\n queryEmbedding: Float32Array,\n opts: { projectIds?: number[]; maxResults?: number },\n): Promise<SearchResult[]> {\n const maxResults = opts.maxResults ?? 10;\n const kw = 0.5;\n const sw = 0.5;\n\n const [keywordResults, semanticResults] = await Promise.all([\n backend.searchKeyword(query, { ...opts, maxResults: 50 }),\n backend.searchSemantic(queryEmbedding, { ...opts, maxResults: 50 }),\n ]);\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(scores: number[]): number[] {\n const min = Math.min(...scores);\n const max = Math.max(...scores);\n const range = max - min;\n if (range === 0) return scores.map(() => 1.0);\n return scores.map(s => (s - min) / range);\n }\n\n const kwNorm = minMaxNormalize(keywordResults.map(r => r.score));\n const semNorm = minMaxNormalize(semanticResults.map(r => r.score));\n\n const combined = new Map<string, SearchResult & { combinedScore: number }>();\n\n for (let i = 0; i < keywordResults.length; i++) {\n const r = keywordResults[i];\n const k = keyFor(r);\n combined.set(k, { ...r, combinedScore: kw * kwNorm[i] });\n }\n\n for (let i = 0; i < semanticResults.length; i++) {\n const r = semanticResults[i];\n const k = keyFor(r);\n const existing = combined.get(k);\n if (existing) {\n existing.combinedScore += sw * semNorm[i];\n } else {\n combined.set(k, { ...r, combinedScore: sw * semNorm[i] });\n }\n }\n\n const sorted = Array.from(combined.values())\n .sort((a, b) => b.combinedScore - a.combinedScore)\n .slice(0, maxResults);\n\n return sorted.map(r => ({ ...r, score: r.combinedScore }));\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Let the vault \"talk back\" — find notes relevant to a question, expand\n * through the link graph, identify cross-domain connections, and return a\n * structured result including a synthesis prompt for an AI to generate insights.\n */\nexport async function zettelConverse(\n backend: StorageBackend,\n opts: ConverseOptions,\n): Promise<ConverseResult> {\n const depth = Math.max(opts.depth ?? 2, 0);\n const limit = Math.max(opts.limit ?? 15, 1);\n const candidateLimit = 20;\n\n // ------------------------------------------------------------------\n // 1. Hybrid search: find top candidates via BM25 + semantic similarity\n // ------------------------------------------------------------------\n const queryEmbedding = await generateEmbedding(opts.question, true);\n\n const searchResults = await hybridSearch(\n backend,\n opts.question,\n queryEmbedding,\n {\n projectIds: [opts.vaultProjectId],\n maxResults: candidateLimit,\n },\n );\n\n // Map of path -> best score + snippet from search results\n const searchHits = new Map<string, { score: number; snippet: string }>();\n for (const r of searchResults) {\n const existing = searchHits.get(r.path);\n if (!existing || r.score > existing.score) {\n searchHits.set(r.path, { score: r.score, snippet: r.snippet });\n }\n }\n\n // ------------------------------------------------------------------\n // 2. Graph expansion: BFS from each search result up to `depth` levels\n // ------------------------------------------------------------------\n const allPaths = new Set<string>(searchHits.keys());\n let frontier = new Set<string>(searchHits.keys());\n\n for (let d = 0; d < depth; d++) {\n const neighbors = await expandNeighbors(backend, frontier);\n const newFrontier = new Set<string>();\n for (const n of neighbors) {\n if (!allPaths.has(n)) {\n allPaths.add(n);\n newFrontier.add(n);\n }\n }\n if (newFrontier.size === 0) break;\n frontier = newFrontier;\n }\n\n // ------------------------------------------------------------------\n // 3. Deduplicate + trim to limit\n // ------------------------------------------------------------------\n const searchRanked = Array.from(searchHits.entries())\n .sort((a, b) => b[1].score - a[1].score)\n .map(([path, info]) => ({ path, ...info, isSearchResult: true }));\n\n const neighborPaths = Array.from(allPaths).filter((p) => !searchHits.has(p));\n\n // Fetch health data for neighbor ranking\n const neighborHealthRows = await Promise.all(\n neighborPaths.map(p => backend.getVaultHealth(p))\n );\n const neighborRanked = neighborPaths\n .map((path, idx) => ({\n path,\n score: 0,\n snippet: \"\",\n inbound: neighborHealthRows[idx]?.inboundCount ?? 0,\n isSearchResult: false,\n }))\n .sort((a, b) => b.inbound - a.inbound);\n\n const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);\n const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);\n\n const selectedSearchPaths = searchRanked.slice(0, limit);\n const selectedPaths = new Set<string>([\n ...selectedSearchPaths.map((r) => r.path),\n ...selectedNeighbors.map((r) => r.path),\n ]);\n\n // ------------------------------------------------------------------\n // 4. Build relevantNotes with titles + domains\n // ------------------------------------------------------------------\n\n // Fetch titles in bulk\n const allSelectedPaths = Array.from(selectedPaths);\n const fileRows = await backend.getVaultFilesByPaths(allSelectedPaths);\n const titleMap = new Map<string, string | null>(fileRows.map(f => [f.vaultPath, f.title]));\n\n const relevantNotes: ConverseResult[\"relevantNotes\"] = [];\n\n for (const r of selectedSearchPaths) {\n if (!selectedPaths.has(r.path)) continue;\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: r.score,\n domain: extractDomain(r.path),\n });\n }\n\n for (const r of selectedNeighbors) {\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: 0,\n domain: extractDomain(r.path),\n });\n }\n\n // ------------------------------------------------------------------\n // 5. Find connections between the selected notes\n // ------------------------------------------------------------------\n let connections: ConverseConnection[] = [];\n\n if (selectedPaths.size > 0) {\n const pathList = Array.from(selectedPaths);\n const pathSet = new Set(pathList);\n\n // Get all outbound links from the selected paths\n const linkRows = await backend.getVaultLinksFromPaths(pathList);\n\n // Count links between selected paths\n const edgeCounts = new Map<string, number>();\n for (const link of linkRows) {\n if (link.targetPath && pathSet.has(link.targetPath)) {\n const key = `${link.sourcePath}|||${link.targetPath}`;\n edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);\n }\n }\n\n for (const [key, cnt] of edgeCounts) {\n const [sourcePath, targetPath] = key.split(\"|||\");\n connections.push({\n fromPath: sourcePath,\n toPath: targetPath,\n fromDomain: extractDomain(sourcePath),\n toDomain: extractDomain(targetPath),\n strength: cnt,\n });\n }\n }\n\n // ------------------------------------------------------------------\n // 6. Domains + cross-domain filter\n // ------------------------------------------------------------------\n const domainSet = new Set<string>(relevantNotes.map((n) => n.domain));\n const domains = Array.from(domainSet).sort();\n\n const crossDomainConnections = connections.filter(\n (c) => c.fromDomain !== c.toDomain,\n );\n\n // ------------------------------------------------------------------\n // 7. Build synthesis prompt\n // ------------------------------------------------------------------\n const notesSummary = relevantNotes\n .map((n, i) => {\n const title = n.title ? `\"${n.title}\"` : \"(untitled)\";\n const domain = n.domain;\n const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : \" [context]\";\n const snippet = n.snippet.trim().slice(0, 300);\n return `${i + 1}. [${domain}] ${title}${scoreLabel}\\n Path: ${n.path}\\n \"${snippet}\"`;\n })\n .join(\"\\n\\n\");\n\n const connectionSummary =\n crossDomainConnections.length > 0\n ? crossDomainConnections\n .map(\n (c) =>\n `- \"${c.fromPath}\" (${c.fromDomain}) → \"${c.toPath}\" (${c.toDomain}) [strength: ${c.strength}]`,\n )\n .join(\"\\n\")\n : \"(no cross-domain connections found)\";\n\n const domainList = domains.join(\", \");\n\n const synthesisPrompt = `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:\n\nQUESTION: ${opts.question}\n\n---\n\nRELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):\n\n${notesSummary}\n\n---\n\nCROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):\n\n${connectionSummary}\n\n---\n\nSYNTHESIS TASK:\n\nBased on these notes and the connections between them, please:\n\n1. Identify the key insights that emerge in direct response to the question.\n2. Highlight any unexpected connections between notes from different domains (${domainList}).\n3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.\n4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.\n5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.\n\nThink like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`;\n\n return {\n relevantNotes,\n connections: crossDomainConnections,\n domains,\n synthesisPrompt,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface HealthOptions {\n scope?: \"full\" | \"recent\" | \"project\";\n projectPath?: string;\n recentDays?: number;\n include?: Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\">;\n}\n\nexport interface DeadLink {\n sourcePath: string;\n targetRaw: string;\n lineNumber: number;\n}\n\nexport interface HealthResult {\n totalFiles: number;\n totalLinks: number;\n deadLinks: DeadLink[];\n orphans: string[];\n disconnectedClusters: number;\n lowConnectivity: string[];\n healthScore: number;\n computedAt: number;\n /** Breakdown of links by confidence level (if confidence tagging is active). */\n linkConfidence?: {\n extracted: number;\n inferred: number;\n ambiguous: number;\n };\n}\n\nfunction countComponents(nodes: string[], edges: Array<{ source: string; target: string }>): number {\n if (nodes.length === 0) return 0;\n\n const parent = new Map<string, string>();\n const rank = new Map<string, number>();\n\n for (const n of nodes) {\n parent.set(n, n);\n rank.set(n, 0);\n }\n\n function find(x: string): string {\n let root = x;\n while (parent.get(root) !== root) {\n root = parent.get(root)!;\n }\n let current = x;\n while (current !== root) {\n const next = parent.get(current)!;\n parent.set(current, root);\n current = next;\n }\n return root;\n }\n\n function union(a: string, b: string): void {\n const ra = find(a);\n const rb = find(b);\n if (ra === rb) return;\n const rankA = rank.get(ra) ?? 0;\n const rankB = rank.get(rb) ?? 0;\n if (rankA < rankB) {\n parent.set(ra, rb);\n } else if (rankA > rankB) {\n parent.set(rb, ra);\n } else {\n parent.set(rb, ra);\n rank.set(ra, rankA + 1);\n }\n }\n\n for (const { source, target } of edges) {\n if (parent.has(source) && parent.has(target)) {\n union(source, target);\n }\n }\n\n const roots = new Set<string>();\n for (const n of nodes) {\n roots.add(find(n));\n }\n return roots.size;\n}\n\n/**\n * Audit the structural health of the Zettelkasten vault using graph metrics.\n */\nexport async function zettelHealth(backend: StorageBackend, opts?: HealthOptions): Promise<HealthResult> {\n const options = opts ?? {};\n const scope = options.scope ?? \"full\";\n const include = options.include ?? [\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"];\n\n const computedAt = Date.now();\n\n // --- totalFiles ---\n let totalFiles = 0;\n if (scope === \"full\") {\n totalFiles = await backend.countVaultFiles();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalFiles = await backend.countVaultFilesWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalFiles = await backend.countVaultFilesAfter(cutoff);\n }\n\n // --- totalLinks ---\n let totalLinks = 0;\n if (scope === \"full\") {\n // Count total links via link graph length\n const graph = await backend.getVaultLinkGraph();\n totalLinks = graph.length;\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalLinks = await backend.countVaultLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalLinks = await backend.countVaultLinksAfter(cutoff);\n }\n\n // --- deadLinks ---\n let deadLinks: DeadLink[] = [];\n if (include.includes(\"dead_links\")) {\n if (scope === \"full\") {\n deadLinks = await backend.getDeadLinksWithLineNumbers();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n deadLinks = await backend.getDeadLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n deadLinks = await backend.getDeadLinksAfter(cutoff);\n }\n }\n\n // --- orphans ---\n let orphans: string[] = [];\n if (include.includes(\"orphans\")) {\n if (scope === \"full\") {\n const orphanRows = await backend.getOrphans();\n orphans = orphanRows.map(r => r.vaultPath);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n orphans = await backend.getOrphansWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n orphans = await backend.getOrphansAfter(cutoff);\n }\n }\n\n // --- disconnectedClusters (union-find) ---\n let disconnectedClusters = 1;\n if (include.includes(\"disconnected\")) {\n let allNodes: string[];\n let allEdges: Array<{ source: string; target: string }>;\n\n if (scope === \"full\") {\n [allNodes, allEdges] = await Promise.all([\n backend.getAllVaultFilePaths(),\n backend.getVaultLinkEdges(),\n ]);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsWithPrefix(prefix),\n backend.getVaultLinkEdgesWithPrefix(prefix),\n ]);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsAfter(cutoff),\n backend.getVaultLinkEdgesAfter(cutoff),\n ]);\n }\n\n disconnectedClusters = countComponents(allNodes, allEdges);\n }\n\n // --- lowConnectivity ---\n let lowConnectivity: string[] = [];\n if (include.includes(\"low_connectivity\")) {\n if (scope === \"full\") {\n lowConnectivity = await backend.getLowConnectivity();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n lowConnectivity = await backend.getLowConnectivityWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n lowConnectivity = await backend.getLowConnectivityAfter(cutoff);\n }\n }\n\n // --- linkConfidence ---\n // Count links by confidence level using a sampling of all vault file paths.\n // We reuse the link graph data we already fetched for totalLinks when scope=full.\n let linkConfidence: HealthResult[\"linkConfidence\"] | undefined;\n if (scope === \"full\") {\n try {\n // Get all file paths to iterate their outbound links with confidence\n const samplePaths = await backend.getAllVaultFilePaths();\n const sampleSize = Math.min(samplePaths.length, 200); // limit for performance\n const sampled = samplePaths.slice(0, sampleSize);\n let extracted = 0;\n let inferred = 0;\n let ambiguous = 0;\n for (const path of sampled) {\n const links = await backend.getLinksFromSource(path);\n for (const link of links) {\n const c = link.confidence ?? \"EXTRACTED\";\n if (c === \"EXTRACTED\") extracted++;\n else if (c === \"INFERRED\") inferred++;\n else ambiguous++;\n }\n }\n // Extrapolate if we sampled\n const factor = samplePaths.length > 0 ? samplePaths.length / sampleSize : 1;\n linkConfidence = {\n extracted: Math.round(extracted * factor),\n inferred: Math.round(inferred * factor),\n ambiguous: Math.round(ambiguous * factor),\n };\n } catch {\n // Backend may not support confidence field yet — ignore\n }\n }\n\n // --- healthScore ---\n const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;\n const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;\n const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;\n const healthScore = Math.round(\n 100 * (1 - deadRatio) * (1 - orphanRatio * 0.5) * (1 - lowConnRatio * 0.3),\n );\n\n return {\n totalFiles,\n totalLinks,\n deadLinks,\n orphans,\n disconnectedClusters,\n lowConnectivity,\n healthScore,\n computedAt,\n linkConfidence,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../memory/embeddings.js\";\nimport { basename } from \"node:path\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\nexport interface SuggestOptions {\n notePath: string;\n vaultProjectId: number;\n limit?: number;\n excludeLinked?: boolean;\n}\n\nexport interface Suggestion {\n path: string;\n title: string | null;\n score: number;\n semanticScore: number;\n tagScore: number;\n neighborScore: number;\n reason: string;\n suggestedWikilink: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst SEMANTIC_WEIGHT = 0.5;\nconst TAG_WEIGHT = 0.2;\nconst NEIGHBOR_WEIGHT = 0.3;\n\n// STOP_WORDS imported from utils/stop-words.ts\n\nfunction extractTagsFromChunkTexts(texts: string[]): Set<string> {\n const tags = new Set<string>();\n for (const text of texts) {\n // Match YAML frontmatter tags block: \"tags:\\n - tag1\\n - tag2\"\n const match = text.match(/^tags:\\s*\\n((?:[ \\t]*-[ \\t]*.+\\n?)*)/m);\n if (!match) continue;\n const block = match[1];\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n const tagMatch = line.match(/^[ \\t]*-[ \\t]*(.+)/);\n if (tagMatch) {\n const tag = tagMatch[1].trim().toLowerCase();\n if (tag) tags.add(tag);\n }\n }\n }\n return tags;\n}\n\nfunction jaccardSimilarity(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const tag of a) {\n if (b.has(tag)) intersection++;\n }\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nfunction buildReason(\n semanticScore: number,\n tagScore: number,\n neighborScore: number,\n neighborCount: number,\n): string {\n const signals: Array<{ label: string; value: number }> = [\n { label: `Semantically similar (${semanticScore.toFixed(2)})`, value: semanticScore * SEMANTIC_WEIGHT },\n { label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`, value: tagScore * TAG_WEIGHT },\n { label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? \"s\" : \"\"}`, value: neighborScore * NEIGHBOR_WEIGHT },\n ];\n signals.sort((a, b) => b.value - a.value);\n return signals[0].label;\n}\n\nfunction suggestedWikilink(vaultPath: string): string {\n const base = basename(vaultPath);\n const name = base.endsWith(\".md\") ? base.slice(0, -3) : base;\n return `[[${name}]]`;\n}\n\n/**\n * Proactively find notes worth linking to a given note, combining semantic similarity,\n * shared tags, and graph-neighborhood signals into a ranked list of suggestions.\n */\nexport async function zettelSuggest(\n backend: StorageBackend,\n opts: SuggestOptions,\n): Promise<Suggestion[]> {\n const limit = opts.limit ?? 5;\n const excludeLinked = opts.excludeLinked ?? true;\n\n // Step 1: get current outbound links\n const outboundLinks = await backend.getLinksFromSource(opts.notePath);\n const linkedPaths = new Set(outboundLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string));\n\n // Step 2a: get all file-level embeddings for semantic scoring\n const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number }>();\n for (const row of chunkRows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1 });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const allEmbeddings = new Map<string, Float32Array>();\n for (const [path, { sum, count }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n allEmbeddings.set(path, avg);\n }\n allEmbeddings.delete(opts.notePath);\n\n // Step 2b: get source embedding\n const sourceEmbedding = allEmbeddings.get(opts.notePath) ?? null;\n\n // Step 2c: get source tags\n const sourceChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, opts.notePath, 5);\n const sourceTags = extractTagsFromChunkTexts(sourceChunkTexts.map(r => r.text));\n\n // Step 2d: compute graph neighborhood (friends-of-friends)\n const directLinks = await backend.getLinksFromSource(opts.notePath);\n const directTargets = directLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n\n const friendLinkCounts = new Map<string, number>();\n for (const target of directTargets) {\n const friendLinks = await backend.getLinksFromSource(target);\n for (const link of friendLinks) {\n if (link.targetPath && link.targetPath !== opts.notePath) {\n friendLinkCounts.set(link.targetPath, (friendLinkCounts.get(link.targetPath) ?? 0) + 1);\n }\n }\n }\n const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());\n\n // Get all vault files to enumerate candidates\n const allFiles = await backend.getAllVaultFiles();\n\n const suggestions: Suggestion[] = [];\n\n for (const fileRow of allFiles) {\n const vault_path = fileRow.vaultPath;\n const title = fileRow.title;\n\n if (vault_path === opts.notePath) continue;\n if (excludeLinked && linkedPaths.has(vault_path)) continue;\n\n // Semantic score\n let semanticScore = 0;\n if (sourceEmbedding) {\n const candidateEmbedding = allEmbeddings.get(vault_path);\n if (candidateEmbedding) {\n semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));\n }\n }\n\n // Tag score (only compute if candidate might have chunks)\n let tagScore = 0;\n if (allEmbeddings.has(vault_path)) {\n const candidateChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, vault_path, 5);\n const candidateTags = extractTagsFromChunkTexts(candidateChunkTexts.map(r => r.text));\n tagScore = jaccardSimilarity(sourceTags, candidateTags);\n }\n\n // Neighbor score\n const friendCount = friendLinkCounts.get(vault_path) ?? 0;\n const neighborScore = friendCount / maxFriendLinks;\n\n const score =\n SEMANTIC_WEIGHT * semanticScore +\n TAG_WEIGHT * tagScore +\n NEIGHBOR_WEIGHT * neighborScore;\n\n // Only include if there is at least some signal\n if (score <= 0) continue;\n\n const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);\n\n suggestions.push({\n path: vault_path,\n title,\n score,\n semanticScore,\n tagScore,\n neighborScore,\n reason,\n suggestedWikilink: suggestedWikilink(vault_path),\n });\n }\n\n suggestions.sort((a, b) => b.score - a.score);\n return suggestions.slice(0, limit);\n}\n","/**\n * God-note detection — find hub notes via degree centrality on the vault link graph.\n *\n * Hub notes (or \"god notes\") are notes with unusually high inbound link counts.\n * They represent core concepts that the rest of the vault frequently references.\n * Structural pages (index, MOC, master notes, tag pages) are filtered out.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface GodNoteOptions {\n /** Maximum number of hub notes to return. Default: 20. */\n limit?: number;\n /** Minimum inbound link count to qualify as a hub. Default: 3. */\n minInbound?: number;\n /** Include outbound counts in the result. Default: true. */\n includeOutbound?: boolean;\n}\n\nexport interface GodNote {\n path: string;\n title: string | null;\n inboundCount: number;\n outboundCount: number;\n /** Ratio of inbound to total degree — higher means more \"sink\"-like. */\n inboundRatio: number;\n}\n\nexport interface GodNoteResult {\n godNotes: GodNote[];\n totalVaultFiles: number;\n /** Median inbound count across all vault files (for context). */\n medianInbound: number;\n}\n\n/** Patterns that identify structural/meta pages rather than concept notes. */\nconst STRUCTURAL_PATTERNS = [\n /\\bindex\\b/i,\n /\\bMOC\\b/,\n /\\bmaster\\b/i,\n /\\btag\\s*page/i,\n /\\bhome\\b/i,\n /\\bdashboard\\b/i,\n /\\btemplate/i,\n /^_/,\n];\n\nfunction isStructuralNote(title: string | null, path: string): boolean {\n const text = title ?? path;\n return STRUCTURAL_PATTERNS.some((pattern) => pattern.test(text));\n}\n\n/**\n * Find hub/\"god\" notes in the vault — notes with the highest inbound link counts,\n * excluding structural pages.\n */\nexport async function zettelGodNotes(\n backend: StorageBackend,\n opts?: GodNoteOptions,\n): Promise<GodNoteResult> {\n const limit = opts?.limit ?? 20;\n const minInbound = opts?.minInbound ?? 3;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n\n // Count inbound and outbound per path\n const inboundCounts = new Map<string, number>();\n const outboundCounts = new Map<string, number>();\n\n for (const { source_path, target_path } of linkGraph) {\n inboundCounts.set(target_path, (inboundCounts.get(target_path) ?? 0) + 1);\n outboundCounts.set(source_path, (outboundCounts.get(source_path) ?? 0) + 1);\n }\n\n // Get all vault files for title lookup and total count\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n const totalVaultFiles = allFiles.length;\n\n // Compute median inbound for context\n const allInbounds = allFiles.map((f) => inboundCounts.get(f.vaultPath) ?? 0);\n allInbounds.sort((a, b) => a - b);\n const medianInbound =\n allInbounds.length > 0\n ? allInbounds[Math.floor(allInbounds.length / 2)]\n : 0;\n\n // Build candidate list: all paths with inbound >= minInbound, excluding structural\n const candidates: GodNote[] = [];\n\n for (const [path, inbound] of inboundCounts) {\n if (inbound < minInbound) continue;\n\n const title = titleMap.get(path) ?? null;\n if (isStructuralNote(title, path)) continue;\n\n const outbound = outboundCounts.get(path) ?? 0;\n const totalDegree = inbound + outbound;\n const inboundRatio = totalDegree > 0 ? inbound / totalDegree : 0;\n\n candidates.push({\n path,\n title,\n inboundCount: inbound,\n outboundCount: outbound,\n inboundRatio: Math.round(inboundRatio * 1000) / 1000,\n });\n }\n\n // Sort by inbound count descending\n candidates.sort((a, b) => b.inboundCount - a.inboundCount);\n\n return {\n godNotes: candidates.slice(0, limit),\n totalVaultFiles,\n medianInbound,\n };\n}\n","/**\n * Community detection for the Zettelkasten vault link graph.\n *\n * Implements the Louvain algorithm in pure TypeScript for discovering\n * densely connected clusters of notes. This reveals emergent knowledge\n * communities that may not be obvious from folder structure alone.\n *\n * The Louvain method optimizes modularity in two phases:\n * 1. Local: each node greedily moves to the community that maximizes modularity gain.\n * 2. Aggregation: communities are collapsed into super-nodes, forming a new graph.\n * Repeat until no further improvement.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface CommunityOptions {\n /** Minimum community size to include in results. Default: 3. */\n minSize?: number;\n /** Maximum number of communities to return. Default: 20. */\n maxCommunities?: number;\n /** Resolution parameter for Louvain (higher = more communities). Default: 1.0. */\n resolution?: number;\n}\n\nexport interface CommunityNode {\n path: string;\n title: string | null;\n /** Degree (inbound + outbound) within the community. */\n internalDegree: number;\n}\n\nexport interface Community {\n id: number;\n /** Label derived from most common words in note titles. */\n label: string;\n nodes: CommunityNode[];\n size: number;\n /** Cohesion: ratio of internal edges to total possible internal edges. */\n cohesion: number;\n /** Top folders represented in this community. */\n topFolders: string[];\n}\n\nexport interface CommunityResult {\n communities: Community[];\n totalNodes: number;\n totalEdges: number;\n /** Global modularity score of the partition. */\n modularity: number;\n}\n\n// ---------------------------------------------------------------------------\n// Louvain algorithm implementation\n// ---------------------------------------------------------------------------\n\ninterface Graph {\n /** All node IDs */\n nodes: string[];\n /** Adjacency list: node -> Map<neighbor, weight> */\n adj: Map<string, Map<string, number>>;\n /** Total edge weight (sum of all edge weights, counting undirected edges once) */\n totalWeight: number;\n /** Weighted degree per node */\n degree: Map<string, number>;\n}\n\nfunction buildUndirectedGraph(\n edges: Array<{ source_path: string; target_path: string }>\n): Graph {\n const adj = new Map<string, Map<string, number>>();\n const nodeSet = new Set<string>();\n\n function getOrCreate(node: string): Map<string, number> {\n let m = adj.get(node);\n if (!m) {\n m = new Map();\n adj.set(node, m);\n }\n nodeSet.add(node);\n return m;\n }\n\n let totalWeight = 0;\n\n for (const { source_path, target_path } of edges) {\n if (source_path === target_path) continue; // skip self-loops\n\n const aMap = getOrCreate(source_path);\n const bMap = getOrCreate(target_path);\n\n // Undirected: add weight in both directions\n aMap.set(target_path, (aMap.get(target_path) ?? 0) + 1);\n bMap.set(source_path, (bMap.get(source_path) ?? 0) + 1);\n totalWeight += 1; // each undirected edge counted once\n }\n\n // Compute weighted degree\n const degree = new Map<string, number>();\n for (const [node, neighbors] of adj) {\n let d = 0;\n for (const w of neighbors.values()) d += w;\n degree.set(node, d);\n }\n\n return {\n nodes: Array.from(nodeSet),\n adj,\n totalWeight,\n degree,\n };\n}\n\n/**\n * Run one pass of Phase 1: local node movement.\n * Returns true if any node changed community.\n */\nfunction louvainPhase1(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): boolean {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return false;\n\n // Community -> sum of degrees of nodes in community\n const communityDegreeSum = new Map<number, number>();\n // Community -> sum of internal edge weights\n const communityInternalWeight = new Map<number, number>();\n\n for (const node of graph.nodes) {\n const c = community.get(node)!;\n communityDegreeSum.set(c, (communityDegreeSum.get(c) ?? 0) + (graph.degree.get(node) ?? 0));\n }\n\n // Compute internal weights\n for (const [node, neighbors] of graph.adj) {\n const nc = community.get(node)!;\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === nc) {\n communityInternalWeight.set(nc, (communityInternalWeight.get(nc) ?? 0) + weight);\n }\n }\n }\n // Each internal edge is counted twice (both endpoints), so divide\n for (const [c, w] of communityInternalWeight) {\n communityInternalWeight.set(c, w / 2);\n }\n\n let improved = false;\n // Shuffle node order for better convergence\n const shuffled = [...graph.nodes];\n for (let i = shuffled.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];\n }\n\n for (const node of shuffled) {\n const currentComm = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n const neighbors = graph.adj.get(node) ?? new Map();\n\n // Compute weight to each neighboring community\n const weightToComm = new Map<number, number>();\n for (const [neighbor, weight] of neighbors) {\n const nc = community.get(neighbor)!;\n weightToComm.set(nc, (weightToComm.get(nc) ?? 0) + weight);\n }\n\n // Remove node from its current community\n communityDegreeSum.set(currentComm, (communityDegreeSum.get(currentComm) ?? 0) - ki);\n const weightToCurrent = weightToComm.get(currentComm) ?? 0;\n communityInternalWeight.set(\n currentComm,\n (communityInternalWeight.get(currentComm) ?? 0) - weightToCurrent,\n );\n\n // Find best community\n let bestComm = currentComm;\n let bestDelta = 0;\n\n for (const [candidateComm, weightToCandidate] of weightToComm) {\n const sigmaTot = communityDegreeSum.get(candidateComm) ?? 0;\n // Modularity gain of moving node to candidateComm\n const delta = weightToCandidate - resolution * (ki * sigmaTot) / m2;\n if (delta > bestDelta) {\n bestDelta = delta;\n bestComm = candidateComm;\n }\n }\n\n // Also consider staying (delta = 0, but need to re-add)\n // Move node to best community\n community.set(node, bestComm);\n communityDegreeSum.set(bestComm, (communityDegreeSum.get(bestComm) ?? 0) + ki);\n const weightToBest = weightToComm.get(bestComm) ?? 0;\n communityInternalWeight.set(\n bestComm,\n (communityInternalWeight.get(bestComm) ?? 0) + weightToBest,\n );\n\n if (bestComm !== currentComm) {\n improved = true;\n }\n }\n\n return improved;\n}\n\n/**\n * Compute modularity for a given partition.\n */\nfunction computeModularity(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): number {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return 0;\n\n let q = 0;\n for (const [node, neighbors] of graph.adj) {\n const ci = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === ci) {\n q += weight - resolution * (ki * (graph.degree.get(neighbor) ?? 0)) / m2;\n }\n }\n }\n return q / m2;\n}\n\n/**\n * Run Louvain community detection on the vault link graph.\n */\nfunction runLouvain(\n graph: Graph,\n resolution: number,\n): Map<string, number> {\n // Initialize: each node in its own community\n const community = new Map<string, number>();\n let nextComm = 0;\n for (const node of graph.nodes) {\n community.set(node, nextComm++);\n }\n\n // Phase 1: iteratively move nodes until no improvement\n const MAX_ITERATIONS = 20;\n for (let iter = 0; iter < MAX_ITERATIONS; iter++) {\n const improved = louvainPhase1(graph, community, resolution);\n if (!improved) break;\n }\n\n // Compact community IDs\n const commRemap = new Map<number, number>();\n let remapIdx = 0;\n for (const [, c] of community) {\n if (!commRemap.has(c)) {\n commRemap.set(c, remapIdx++);\n }\n }\n for (const [node, c] of community) {\n community.set(node, commRemap.get(c)!);\n }\n\n return community;\n}\n\n// ---------------------------------------------------------------------------\n// Label generation\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"the\", \"and\", \"for\", \"are\", \"but\", \"not\", \"you\", \"all\", \"can\", \"her\",\n \"was\", \"one\", \"our\", \"out\", \"has\", \"had\", \"how\", \"its\", \"may\", \"new\",\n \"now\", \"old\", \"see\", \"way\", \"who\", \"did\", \"get\", \"let\", \"say\", \"she\",\n \"too\", \"use\", \"from\", \"with\", \"this\", \"that\", \"will\", \"been\", \"have\",\n \"each\", \"make\", \"like\", \"long\", \"look\", \"many\", \"them\", \"then\", \"what\",\n \"when\", \"some\", \"time\", \"very\", \"your\", \"about\", \"could\", \"into\", \"just\",\n \"more\", \"note\", \"notes\", \"than\", \"over\",\n]);\n\nfunction generateCommunityLabel(titles: Array<string | null>): string {\n const wordCounts = new Map<string, number>();\n for (const title of titles) {\n if (!title) continue;\n const words = title\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted\n .slice(0, 3)\n .map(([w]) => w)\n .join(\" / \") || \"unnamed\";\n}\n\nfunction getTopFolder(path: string): string {\n const slash = path.indexOf(\"/\");\n return slash === -1 ? path : path.slice(0, slash);\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Detect communities in the vault link graph using the Louvain algorithm.\n * Returns clusters of densely connected notes with labels and cohesion scores.\n */\nexport async function zettelCommunities(\n backend: StorageBackend,\n opts?: CommunityOptions,\n): Promise<CommunityResult> {\n const minSize = opts?.minSize ?? 3;\n const maxCommunities = opts?.maxCommunities ?? 20;\n const resolution = opts?.resolution ?? 1.0;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n const graph = buildUndirectedGraph(linkGraph);\n\n if (graph.nodes.length === 0) {\n return {\n communities: [],\n totalNodes: 0,\n totalEdges: 0,\n modularity: 0,\n };\n }\n\n // Run Louvain\n const communityMap = runLouvain(graph, resolution);\n\n // Compute modularity\n const modularity = computeModularity(graph, communityMap, resolution);\n\n // Group nodes by community\n const groups = new Map<number, string[]>();\n for (const [node, comm] of communityMap) {\n const arr = groups.get(comm);\n if (arr) arr.push(node);\n else groups.set(comm, [node]);\n }\n\n // Get all vault files for title lookup\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n // Build community results\n const communities: Community[] = [];\n let commId = 0;\n\n for (const [, members] of groups) {\n if (members.length < minSize) continue;\n\n const memberSet = new Set(members);\n const titles = members.map((p) => titleMap.get(p) ?? null);\n const label = generateCommunityLabel(titles);\n\n // Compute internal degree per node\n const nodes: CommunityNode[] = members.map((path) => {\n const neighbors = graph.adj.get(path) ?? new Map();\n let internalDeg = 0;\n for (const [neighbor, weight] of neighbors) {\n if (memberSet.has(neighbor)) internalDeg += weight;\n }\n return {\n path,\n title: titleMap.get(path) ?? null,\n internalDegree: internalDeg,\n };\n });\n\n // Sort nodes by internal degree descending\n nodes.sort((a, b) => b.internalDegree - a.internalDegree);\n\n // Compute cohesion: ratio of actual internal edges to possible\n let internalEdges = 0;\n for (const node of nodes) {\n internalEdges += node.internalDegree;\n }\n internalEdges /= 2; // each edge counted twice\n const possibleEdges = (members.length * (members.length - 1)) / 2;\n const cohesion = possibleEdges > 0 ? Math.round((internalEdges / possibleEdges) * 1000) / 1000 : 0;\n\n // Top folders\n const folderCounts = new Map<string, number>();\n for (const path of members) {\n const folder = getTopFolder(path);\n folderCounts.set(folder, (folderCounts.get(folder) ?? 0) + 1);\n }\n const topFolders = [...folderCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([f]) => f);\n\n communities.push({\n id: commId++,\n label,\n nodes,\n size: members.length,\n cohesion,\n topFolders,\n });\n }\n\n // Sort by size descending\n communities.sort((a, b) => b.size - a.size);\n\n return {\n communities: communities.slice(0, maxCommunities),\n totalNodes: graph.nodes.length,\n totalEdges: graph.totalWeight,\n modularity: Math.round(modularity * 10000) / 10000,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",