@monoes/monograph 1.0.0

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.
Files changed (221) hide show
  1. package/.monomind/data/ranked-context.json +5 -0
  2. package/.monomind/sessions/current.json +13 -0
  3. package/__tests__/export/formats.test.ts +82 -0
  4. package/__tests__/fixtures/sample.py +11 -0
  5. package/__tests__/fixtures/sample.ts +17 -0
  6. package/__tests__/integration/full-pipeline.test.ts +64 -0
  7. package/__tests__/parsers/loader.test.ts +14 -0
  8. package/__tests__/parsers/python.test.ts +32 -0
  9. package/__tests__/parsers/typescript.test.ts +45 -0
  10. package/__tests__/pipeline/analysis.test.ts +58 -0
  11. package/__tests__/pipeline/phases.test.ts +41 -0
  12. package/__tests__/pipeline/runner.test.ts +28 -0
  13. package/__tests__/setup.ts +1 -0
  14. package/__tests__/storage/db.test.ts +37 -0
  15. package/__tests__/storage/stores.test.ts +100 -0
  16. package/__tests__/types.test.ts +13 -0
  17. package/__tests__/watch/watcher.test.ts +31 -0
  18. package/dist/src/export/canvas.d.ts +3 -0
  19. package/dist/src/export/canvas.d.ts.map +1 -0
  20. package/dist/src/export/canvas.js +39 -0
  21. package/dist/src/export/canvas.js.map +1 -0
  22. package/dist/src/export/cypher.d.ts +3 -0
  23. package/dist/src/export/cypher.d.ts.map +1 -0
  24. package/dist/src/export/cypher.js +24 -0
  25. package/dist/src/export/cypher.js.map +1 -0
  26. package/dist/src/export/graphml.d.ts +3 -0
  27. package/dist/src/export/graphml.d.ts.map +1 -0
  28. package/dist/src/export/graphml.js +38 -0
  29. package/dist/src/export/graphml.js.map +1 -0
  30. package/dist/src/export/html.d.ts +3 -0
  31. package/dist/src/export/html.d.ts.map +1 -0
  32. package/dist/src/export/html.js +54 -0
  33. package/dist/src/export/html.js.map +1 -0
  34. package/dist/src/export/index.d.ts +8 -0
  35. package/dist/src/export/index.d.ts.map +1 -0
  36. package/dist/src/export/index.js +8 -0
  37. package/dist/src/export/index.js.map +1 -0
  38. package/dist/src/export/json.d.ts +3 -0
  39. package/dist/src/export/json.d.ts.map +1 -0
  40. package/dist/src/export/json.js +31 -0
  41. package/dist/src/export/json.js.map +1 -0
  42. package/dist/src/export/obsidian.d.ts +3 -0
  43. package/dist/src/export/obsidian.d.ts.map +1 -0
  44. package/dist/src/export/obsidian.js +38 -0
  45. package/dist/src/export/obsidian.js.map +1 -0
  46. package/dist/src/export/svg.d.ts +3 -0
  47. package/dist/src/export/svg.d.ts.map +1 -0
  48. package/dist/src/export/svg.js +46 -0
  49. package/dist/src/export/svg.js.map +1 -0
  50. package/dist/src/graph/analyzer.d.ts +8 -0
  51. package/dist/src/graph/analyzer.d.ts.map +1 -0
  52. package/dist/src/graph/analyzer.js +25 -0
  53. package/dist/src/graph/analyzer.js.map +1 -0
  54. package/dist/src/graph/diff.d.ts +19 -0
  55. package/dist/src/graph/diff.d.ts.map +1 -0
  56. package/dist/src/graph/diff.js +44 -0
  57. package/dist/src/graph/diff.js.map +1 -0
  58. package/dist/src/graph/loader.d.ts +5 -0
  59. package/dist/src/graph/loader.d.ts.map +1 -0
  60. package/dist/src/graph/loader.js +42 -0
  61. package/dist/src/graph/loader.js.map +1 -0
  62. package/dist/src/index.d.ts +18 -0
  63. package/dist/src/index.d.ts.map +1 -0
  64. package/dist/src/index.js +18 -0
  65. package/dist/src/index.js.map +1 -0
  66. package/dist/src/mcp/resources.d.ts +11 -0
  67. package/dist/src/mcp/resources.d.ts.map +1 -0
  68. package/dist/src/mcp/resources.js +69 -0
  69. package/dist/src/mcp/resources.js.map +1 -0
  70. package/dist/src/parsers/extractor.d.ts +5 -0
  71. package/dist/src/parsers/extractor.d.ts.map +1 -0
  72. package/dist/src/parsers/extractor.js +142 -0
  73. package/dist/src/parsers/extractor.js.map +1 -0
  74. package/dist/src/parsers/go.d.ts +3 -0
  75. package/dist/src/parsers/go.d.ts.map +1 -0
  76. package/dist/src/parsers/go.js +25 -0
  77. package/dist/src/parsers/go.js.map +1 -0
  78. package/dist/src/parsers/java.d.ts +3 -0
  79. package/dist/src/parsers/java.d.ts.map +1 -0
  80. package/dist/src/parsers/java.js +25 -0
  81. package/dist/src/parsers/java.js.map +1 -0
  82. package/dist/src/parsers/language-config.d.ts +27 -0
  83. package/dist/src/parsers/language-config.d.ts.map +1 -0
  84. package/dist/src/parsers/language-config.js +2 -0
  85. package/dist/src/parsers/language-config.js.map +1 -0
  86. package/dist/src/parsers/loader.d.ts +15 -0
  87. package/dist/src/parsers/loader.d.ts.map +1 -0
  88. package/dist/src/parsers/loader.js +73 -0
  89. package/dist/src/parsers/loader.js.map +1 -0
  90. package/dist/src/parsers/python.d.ts +3 -0
  91. package/dist/src/parsers/python.d.ts.map +1 -0
  92. package/dist/src/parsers/python.js +24 -0
  93. package/dist/src/parsers/python.js.map +1 -0
  94. package/dist/src/parsers/rust.d.ts +3 -0
  95. package/dist/src/parsers/rust.d.ts.map +1 -0
  96. package/dist/src/parsers/rust.js +23 -0
  97. package/dist/src/parsers/rust.js.map +1 -0
  98. package/dist/src/parsers/typescript.d.ts +3 -0
  99. package/dist/src/parsers/typescript.d.ts.map +1 -0
  100. package/dist/src/parsers/typescript.js +39 -0
  101. package/dist/src/parsers/typescript.js.map +1 -0
  102. package/dist/src/pipeline/orchestrator.d.ts +8 -0
  103. package/dist/src/pipeline/orchestrator.d.ts.map +1 -0
  104. package/dist/src/pipeline/orchestrator.js +37 -0
  105. package/dist/src/pipeline/orchestrator.js.map +1 -0
  106. package/dist/src/pipeline/phases/communities.d.ts +7 -0
  107. package/dist/src/pipeline/phases/communities.d.ts.map +1 -0
  108. package/dist/src/pipeline/phases/communities.js +34 -0
  109. package/dist/src/pipeline/phases/communities.js.map +1 -0
  110. package/dist/src/pipeline/phases/cross-file.d.ts +7 -0
  111. package/dist/src/pipeline/phases/cross-file.d.ts.map +1 -0
  112. package/dist/src/pipeline/phases/cross-file.js +36 -0
  113. package/dist/src/pipeline/phases/cross-file.js.map +1 -0
  114. package/dist/src/pipeline/phases/god-nodes.d.ts +7 -0
  115. package/dist/src/pipeline/phases/god-nodes.d.ts.map +1 -0
  116. package/dist/src/pipeline/phases/god-nodes.js +28 -0
  117. package/dist/src/pipeline/phases/god-nodes.js.map +1 -0
  118. package/dist/src/pipeline/phases/mro.d.ts +7 -0
  119. package/dist/src/pipeline/phases/mro.d.ts.map +1 -0
  120. package/dist/src/pipeline/phases/mro.js +41 -0
  121. package/dist/src/pipeline/phases/mro.js.map +1 -0
  122. package/dist/src/pipeline/phases/parse.d.ts +9 -0
  123. package/dist/src/pipeline/phases/parse.d.ts.map +1 -0
  124. package/dist/src/pipeline/phases/parse.js +51 -0
  125. package/dist/src/pipeline/phases/parse.js.map +1 -0
  126. package/dist/src/pipeline/phases/scan.d.ts +7 -0
  127. package/dist/src/pipeline/phases/scan.d.ts.map +1 -0
  128. package/dist/src/pipeline/phases/scan.js +60 -0
  129. package/dist/src/pipeline/phases/scan.js.map +1 -0
  130. package/dist/src/pipeline/phases/structure.d.ts +9 -0
  131. package/dist/src/pipeline/phases/structure.d.ts.map +1 -0
  132. package/dist/src/pipeline/phases/structure.js +56 -0
  133. package/dist/src/pipeline/phases/structure.js.map +1 -0
  134. package/dist/src/pipeline/phases/suggest.d.ts +7 -0
  135. package/dist/src/pipeline/phases/suggest.d.ts.map +1 -0
  136. package/dist/src/pipeline/phases/suggest.js +59 -0
  137. package/dist/src/pipeline/phases/suggest.js.map +1 -0
  138. package/dist/src/pipeline/phases/surprises.d.ts +7 -0
  139. package/dist/src/pipeline/phases/surprises.d.ts.map +1 -0
  140. package/dist/src/pipeline/phases/surprises.js +54 -0
  141. package/dist/src/pipeline/phases/surprises.js.map +1 -0
  142. package/dist/src/pipeline/runner.d.ts +8 -0
  143. package/dist/src/pipeline/runner.d.ts.map +1 -0
  144. package/dist/src/pipeline/runner.js +53 -0
  145. package/dist/src/pipeline/runner.js.map +1 -0
  146. package/dist/src/pipeline/types.d.ts +24 -0
  147. package/dist/src/pipeline/types.d.ts.map +1 -0
  148. package/dist/src/pipeline/types.js +8 -0
  149. package/dist/src/pipeline/types.js.map +1 -0
  150. package/dist/src/storage/db.d.ts +7 -0
  151. package/dist/src/storage/db.d.ts.map +1 -0
  152. package/dist/src/storage/db.js +52 -0
  153. package/dist/src/storage/db.js.map +1 -0
  154. package/dist/src/storage/edge-store.d.ts +9 -0
  155. package/dist/src/storage/edge-store.d.ts.map +1 -0
  156. package/dist/src/storage/edge-store.js +46 -0
  157. package/dist/src/storage/edge-store.js.map +1 -0
  158. package/dist/src/storage/fts-store.d.ts +11 -0
  159. package/dist/src/storage/fts-store.d.ts.map +1 -0
  160. package/dist/src/storage/fts-store.js +32 -0
  161. package/dist/src/storage/fts-store.js.map +1 -0
  162. package/dist/src/storage/node-store.d.ts +9 -0
  163. package/dist/src/storage/node-store.d.ts.map +1 -0
  164. package/dist/src/storage/node-store.js +52 -0
  165. package/dist/src/storage/node-store.js.map +1 -0
  166. package/dist/src/storage/schema.d.ts +8 -0
  167. package/dist/src/storage/schema.d.ts.map +1 -0
  168. package/dist/src/storage/schema.js +72 -0
  169. package/dist/src/storage/schema.js.map +1 -0
  170. package/dist/src/types.d.ts +76 -0
  171. package/dist/src/types.d.ts.map +1 -0
  172. package/dist/src/types.js +37 -0
  173. package/dist/src/types.js.map +1 -0
  174. package/dist/src/watch/watcher.d.ts +16 -0
  175. package/dist/src/watch/watcher.d.ts.map +1 -0
  176. package/dist/src/watch/watcher.js +59 -0
  177. package/dist/src/watch/watcher.js.map +1 -0
  178. package/dist/tsconfig.tsbuildinfo +1 -0
  179. package/package.json +44 -0
  180. package/src/export/canvas.ts +42 -0
  181. package/src/export/cypher.ts +33 -0
  182. package/src/export/graphml.ts +48 -0
  183. package/src/export/html.ts +61 -0
  184. package/src/export/index.ts +7 -0
  185. package/src/export/json.ts +36 -0
  186. package/src/export/obsidian.ts +44 -0
  187. package/src/export/svg.ts +55 -0
  188. package/src/graph/analyzer.ts +29 -0
  189. package/src/graph/diff.ts +65 -0
  190. package/src/graph/loader.ts +41 -0
  191. package/src/index.ts +17 -0
  192. package/src/mcp/resources.ts +69 -0
  193. package/src/parsers/extractor.ts +180 -0
  194. package/src/parsers/go.ts +27 -0
  195. package/src/parsers/java.ts +27 -0
  196. package/src/parsers/language-config.ts +28 -0
  197. package/src/parsers/loader.ts +85 -0
  198. package/src/parsers/python.ts +26 -0
  199. package/src/parsers/rust.ts +25 -0
  200. package/src/parsers/typescript.ts +41 -0
  201. package/src/pipeline/orchestrator.ts +46 -0
  202. package/src/pipeline/phases/communities.ts +45 -0
  203. package/src/pipeline/phases/cross-file.ts +48 -0
  204. package/src/pipeline/phases/god-nodes.ts +40 -0
  205. package/src/pipeline/phases/mro.ts +52 -0
  206. package/src/pipeline/phases/parse.ts +62 -0
  207. package/src/pipeline/phases/scan.ts +58 -0
  208. package/src/pipeline/phases/structure.ts +68 -0
  209. package/src/pipeline/phases/suggest.ts +71 -0
  210. package/src/pipeline/phases/surprises.ts +70 -0
  211. package/src/pipeline/runner.ts +61 -0
  212. package/src/pipeline/types.ts +33 -0
  213. package/src/storage/db.ts +54 -0
  214. package/src/storage/edge-store.ts +61 -0
  215. package/src/storage/fts-store.ts +52 -0
  216. package/src/storage/node-store.ts +72 -0
  217. package/src/storage/schema.ts +77 -0
  218. package/src/types.ts +131 -0
  219. package/src/watch/watcher.ts +66 -0
  220. package/tsconfig.json +10 -0
  221. package/vitest.config.ts +12 -0
@@ -0,0 +1,55 @@
1
+ import type { MonographNode, MonographEdge } from '../types.js';
2
+
3
+ const W = 1200;
4
+ const H = 800;
5
+
6
+ export function toSvg(nodes: MonographNode[], edges: MonographEdge[]): string {
7
+ const positions = forceLayout(nodes.slice(0, 200));
8
+
9
+ const edgeSvg = edges
10
+ .filter(e => positions.has(e.sourceId) && positions.has(e.targetId))
11
+ .map(e => {
12
+ const s = positions.get(e.sourceId)!;
13
+ const t = positions.get(e.targetId)!;
14
+ return `<line x1="${s.x}" y1="${s.y}" x2="${t.x}" y2="${t.y}" stroke="#94a3b8" stroke-width="1" opacity="0.5"/>`;
15
+ })
16
+ .join('\n');
17
+
18
+ const nodeSvg = [...positions.entries()]
19
+ .map(([id, pos]) => {
20
+ const node = nodes.find(n => n.id === id);
21
+ const label = node?.name ?? id;
22
+ return (
23
+ `<circle cx="${pos.x}" cy="${pos.y}" r="6" fill="#3b82f6" stroke="#1e293b" stroke-width="0.5"/>` +
24
+ `<text x="${pos.x + 8}" y="${pos.y + 4}" font-size="10" fill="#1e293b">${sanitizeLabel(label)}</text>`
25
+ );
26
+ })
27
+ .join('\n');
28
+
29
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
30
+ <defs>
31
+ <style>
32
+ text { font-family: system-ui, -apple-system, sans-serif; user-select: none; }
33
+ </style>
34
+ </defs>
35
+ <rect width="${W}" height="${H}" fill="#f8fafc"/>
36
+ ${edgeSvg}
37
+ ${nodeSvg}
38
+ </svg>`;
39
+ }
40
+
41
+ function forceLayout(nodes: MonographNode[]): Map<string, { x: number; y: number }> {
42
+ const positions = new Map<string, { x: number; y: number }>();
43
+ nodes.forEach((n, i) => {
44
+ const angle = (i / Math.max(nodes.length, 1)) * 2 * Math.PI;
45
+ positions.set(n.id, {
46
+ x: W / 2 + Math.cos(angle) * (W * 0.35),
47
+ y: H / 2 + Math.sin(angle) * (H * 0.35),
48
+ });
49
+ });
50
+ return positions;
51
+ }
52
+
53
+ function sanitizeLabel(s: string): string {
54
+ return s.substring(0, 20).replace(/[&<>"']/g, '_');
55
+ }
@@ -0,0 +1,29 @@
1
+ import { bidirectional } from 'graphology-shortest-path';
2
+ import type Graph from 'graphology';
3
+ import type { MonographDb } from '../storage/db.js';
4
+ import { loadGraphFromDb } from './loader.js';
5
+
6
+ export function getShortestPath(
7
+ db: MonographDb,
8
+ sourceId: string,
9
+ targetId: string,
10
+ maxDepth = 6,
11
+ ): string[] | null {
12
+ const graph = loadGraphFromDb(db);
13
+ if (!graph.hasNode(sourceId) || !graph.hasNode(targetId)) return null;
14
+ try {
15
+ const path = bidirectional(graph, sourceId, targetId);
16
+ if (path && path.length <= maxDepth + 1) return path;
17
+ return null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export function getNodeDegrees(graph: Graph, nodeId: string): { in: number; out: number } {
24
+ if (!graph.hasNode(nodeId)) return { in: 0, out: 0 };
25
+ return {
26
+ in: graph.inDegree ? graph.inDegree(nodeId) : 0,
27
+ out: graph.outDegree ? graph.outDegree(nodeId) : 0,
28
+ };
29
+ }
@@ -0,0 +1,65 @@
1
+ import type { MonographNode, MonographEdge } from '../types.js';
2
+
3
+ export interface GraphSnapshot {
4
+ nodes: MonographNode[];
5
+ edges: MonographEdge[];
6
+ capturedAt: string;
7
+ }
8
+
9
+ export interface GraphDiff {
10
+ newNodes: MonographNode[];
11
+ removedNodes: MonographNode[];
12
+ newEdges: MonographEdge[];
13
+ removedEdges: MonographEdge[];
14
+ modifiedNodes: Array<{ before: MonographNode; after: MonographNode }>;
15
+ }
16
+
17
+ export function diffSnapshots(before: GraphSnapshot, after: GraphSnapshot): GraphDiff {
18
+ const beforeNodeIds = new Map(before.nodes.map(n => [n.id, n]));
19
+ const afterNodeIds = new Map(after.nodes.map(n => [n.id, n]));
20
+ const beforeEdgeIds = new Set(before.edges.map(e => e.id));
21
+ const afterEdgeIds = new Set(after.edges.map(e => e.id));
22
+
23
+ return {
24
+ newNodes: after.nodes.filter(n => !beforeNodeIds.has(n.id)),
25
+ removedNodes: before.nodes.filter(n => !afterNodeIds.has(n.id)),
26
+ newEdges: after.edges.filter(e => !beforeEdgeIds.has(e.id)),
27
+ removedEdges: before.edges.filter(e => !afterEdgeIds.has(e.id)),
28
+ modifiedNodes: after.nodes
29
+ .filter(n => beforeNodeIds.has(n.id))
30
+ .filter(n => JSON.stringify(n) !== JSON.stringify(beforeNodeIds.get(n.id)))
31
+ .map(n => ({ before: beforeNodeIds.get(n.id)!, after: n })),
32
+ };
33
+ }
34
+
35
+ export function snapshotFromDb(db: import('../storage/db.js').MonographDb): GraphSnapshot {
36
+ const rawNodes = db.prepare(`
37
+ SELECT id, label, name,
38
+ norm_label AS normLabel,
39
+ file_path AS filePath,
40
+ start_line AS startLine,
41
+ end_line AS endLine,
42
+ community_id AS communityId,
43
+ is_exported AS isExported,
44
+ language, properties
45
+ FROM nodes
46
+ `).all() as Array<Record<string, unknown>>;
47
+
48
+ const rawEdges = db.prepare(`
49
+ SELECT id,
50
+ source_id AS sourceId,
51
+ target_id AS targetId,
52
+ relation, confidence,
53
+ confidence_score AS confidenceScore
54
+ FROM edges
55
+ `).all() as Array<Record<string, unknown>>;
56
+
57
+ const nodes: MonographNode[] = rawNodes.map(r => ({
58
+ ...r,
59
+ isExported: Boolean(r['isExported']),
60
+ } as MonographNode));
61
+
62
+ const edges = rawEdges as unknown as MonographEdge[];
63
+
64
+ return { nodes, edges, capturedAt: new Date().toISOString() };
65
+ }
@@ -0,0 +1,41 @@
1
+ import Graph from 'graphology';
2
+ import type { MonographEdge } from '../types.js';
3
+
4
+ export function loadGraphFromEdges(edges: MonographEdge[]): Graph {
5
+ const graph = new Graph({ multi: true, type: 'directed' });
6
+ for (const edge of edges) {
7
+ if (!graph.hasNode(edge.sourceId)) graph.addNode(edge.sourceId);
8
+ if (!graph.hasNode(edge.targetId)) graph.addNode(edge.targetId);
9
+ try {
10
+ graph.addEdge(edge.sourceId, edge.targetId, {
11
+ id: edge.id, relation: edge.relation,
12
+ confidence: edge.confidence, confidenceScore: edge.confidenceScore,
13
+ });
14
+ } catch { /* duplicate edge */ }
15
+ }
16
+ return graph;
17
+ }
18
+
19
+ export function loadGraphFromDb(db: import('../storage/db.js').MonographDb): Graph {
20
+ const nodes = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
21
+ const edges = db.prepare('SELECT * FROM edges').all() as {
22
+ id: string; source_id: string; target_id: string; relation: string;
23
+ confidence: string; confidence_score: number;
24
+ }[];
25
+
26
+ const graph = new Graph({ multi: true, type: 'directed' });
27
+ for (const n of nodes) {
28
+ if (!graph.hasNode(n.id)) graph.addNode(n.id);
29
+ }
30
+ for (const e of edges) {
31
+ if (!graph.hasNode(e.source_id)) graph.addNode(e.source_id);
32
+ if (!graph.hasNode(e.target_id)) graph.addNode(e.target_id);
33
+ try {
34
+ graph.addEdge(e.source_id, e.target_id, {
35
+ id: e.id, relation: e.relation,
36
+ confidence: e.confidence, confidenceScore: e.confidence_score,
37
+ });
38
+ } catch { /* skip */ }
39
+ }
40
+ return graph;
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export * from './types.js';
2
+ export * from './storage/db.js';
3
+ export * from './storage/node-store.js';
4
+ export * from './storage/edge-store.js';
5
+ export * from './storage/fts-store.js';
6
+ export * from './pipeline/runner.js';
7
+ export * from './pipeline/types.js';
8
+ export * from './pipeline/orchestrator.js';
9
+ export * from './graph/loader.js';
10
+ export * from './graph/analyzer.js';
11
+ export * from './graph/diff.js';
12
+ export * from './watch/watcher.js';
13
+ export * from './export/json.js';
14
+ export * from './export/html.js';
15
+ export * from './export/cypher.js';
16
+ export * from './export/graphml.js';
17
+ export * from './export/svg.js';
@@ -0,0 +1,69 @@
1
+ export interface MCPResource {
2
+ uri: string;
3
+ name: string;
4
+ description: string;
5
+ mimeType: string;
6
+ handler: (uri: string) => Promise<{ content: string }>;
7
+ }
8
+
9
+ import { openDb, closeDb } from '../storage/db.js';
10
+ import { countNodes } from '../storage/node-store.js';
11
+ import { countEdges } from '../storage/edge-store.js';
12
+ import { join } from 'path';
13
+
14
+ function getDbPath(): string {
15
+ const base = process.env.MONOMIND_CWD ?? process.cwd();
16
+ return join(base, '.monomind', 'monograph.db');
17
+ }
18
+
19
+ export const monographResources: MCPResource[] = [
20
+ {
21
+ uri: 'monograph://repo/{name}/context',
22
+ name: 'Monograph Repo Context',
23
+ description: 'Full graph context summary for the indexed repository',
24
+ mimeType: 'application/json',
25
+ handler: async (_uri) => {
26
+ const db = openDb(getDbPath());
27
+ try {
28
+ const nodes = countNodes(db);
29
+ const edges = countEdges(db);
30
+ const meta = db.prepare('SELECT key, value FROM index_meta').all() as { key: string; value: string }[];
31
+ return { content: JSON.stringify({ nodes, edges, meta }, null, 2) };
32
+ } finally { closeDb(db); }
33
+ },
34
+ },
35
+ {
36
+ uri: 'monograph://repo/{name}/clusters',
37
+ name: 'Monograph Community Clusters',
38
+ description: 'Community cluster summary with sizes and labels',
39
+ mimeType: 'application/json',
40
+ handler: async (_uri) => {
41
+ const db = openDb(getDbPath());
42
+ try {
43
+ const communities = db.prepare('SELECT * FROM communities ORDER BY size DESC').all();
44
+ return { content: JSON.stringify(communities, null, 2) };
45
+ } finally { closeDb(db); }
46
+ },
47
+ },
48
+ {
49
+ uri: 'monograph://repo/{name}/processes',
50
+ name: 'Monograph Process Map',
51
+ description: 'Process detection results (populated in Sub-project 3)',
52
+ mimeType: 'application/json',
53
+ handler: async (_uri) => ({ content: JSON.stringify({ note: 'Process detection available in Sub-project 3' }) }),
54
+ },
55
+ {
56
+ uri: 'monograph://repo/{name}/schema',
57
+ name: 'Monograph Schema',
58
+ description: 'Node label and edge relation types present in this repo',
59
+ mimeType: 'application/json',
60
+ handler: async (_uri) => {
61
+ const db = openDb(getDbPath());
62
+ try {
63
+ const labels = db.prepare('SELECT DISTINCT label, COUNT(*) as count FROM nodes GROUP BY label ORDER BY count DESC').all();
64
+ const relations = db.prepare('SELECT DISTINCT relation, COUNT(*) as count FROM edges GROUP BY relation ORDER BY count DESC').all();
65
+ return { content: JSON.stringify({ nodeLabels: labels, edgeRelations: relations }, null, 2) };
66
+ } finally { closeDb(db); }
67
+ },
68
+ },
69
+ ];
@@ -0,0 +1,180 @@
1
+ import type Parser from 'tree-sitter';
2
+ import type { LanguageConfig } from './language-config.js';
3
+ import type { MonographNode, MonographEdge } from '../types.js';
4
+ import { makeId, toNormLabel, CONFIDENCE_SCORE } from '../types.js';
5
+ import type { ParseResult } from './loader.js';
6
+
7
+ export function extractSymbols(
8
+ tree: Parser.Tree,
9
+ source: string,
10
+ repoPath: string,
11
+ config: LanguageConfig,
12
+ ext: string,
13
+ ): ParseResult {
14
+ const nodes: MonographNode[] = [];
15
+ const edges: MonographEdge[] = [];
16
+ const parseErrors: string[] = [];
17
+ const language = config.name;
18
+
19
+ function nodeId(name: string, filePath: string, extra?: string): string {
20
+ return makeId(filePath.replace(/\//g, '_'), name, extra ?? '');
21
+ }
22
+
23
+ const fileNodeId = makeId(repoPath.replace(/\//g, '_'), 'file');
24
+ nodes.push({
25
+ id: fileNodeId,
26
+ label: 'File',
27
+ name: repoPath.split('/').pop() ?? repoPath,
28
+ normLabel: toNormLabel(repoPath.split('/').pop() ?? repoPath),
29
+ filePath: repoPath,
30
+ isExported: false,
31
+ language,
32
+ });
33
+
34
+ function walk(node: Parser.SyntaxNode, parentId?: string): void {
35
+ const { type } = node;
36
+
37
+ if (config.importNodeTypes.has(type)) {
38
+ handleImport(node, fileNodeId, source, config, edges, repoPath);
39
+ return;
40
+ }
41
+
42
+ const isClass = config.classNodeTypes.has(type);
43
+ const isStruct = config.structNodeTypes.has(type);
44
+ const isEnum = config.enumNodeTypes.has(type);
45
+ const isFunction = config.functionNodeTypes.has(type);
46
+ const isMethod = config.methodNodeTypes.has(type);
47
+ const isInterface = config.interfaceNodeTypes.has(type);
48
+ const isConstructor = config.constructorNodeTypes.has(type);
49
+
50
+ if (isClass || isStruct || isEnum || isFunction || isMethod || isInterface || isConstructor) {
51
+ const nameNode = node.childForFieldName(config.nameField);
52
+ const name = nameNode?.text ?? node.text.split('\n')[0].slice(0, 40);
53
+ // When a node type is in BOTH functionNodeTypes and methodNodeTypes (e.g. Python
54
+ // `function_definition`), check parent chain to disambiguate: a function_definition
55
+ // whose grandparent is a class_definition is a method, otherwise it's a function.
56
+ const isBothFunctionAndMethod = isFunction && isMethod;
57
+ const isActuallyMethod = isBothFunctionAndMethod
58
+ ? config.classNodeTypes.has(node.parent?.parent?.type ?? '')
59
+ : isMethod;
60
+ const label = isClass ? 'Class' : isStruct ? 'Struct' : isEnum ? 'Enum' :
61
+ isInterface ? 'Interface' : isActuallyMethod ? 'Method' :
62
+ isConstructor ? 'Constructor' : 'Function';
63
+ const id = nodeId(name, repoPath, label.toLowerCase());
64
+ const isExported = config.exportDetector
65
+ ? config.exportDetector(node, source)
66
+ : isNodeExported(node, source);
67
+
68
+ nodes.push({
69
+ id, label, name,
70
+ normLabel: toNormLabel(name),
71
+ filePath: repoPath,
72
+ startLine: node.startPosition.row + 1,
73
+ endLine: node.endPosition.row + 1,
74
+ isExported, language,
75
+ });
76
+
77
+ const containerId = parentId ?? fileNodeId;
78
+ edges.push({
79
+ id: makeId(containerId, id, 'contains'),
80
+ sourceId: containerId, targetId: id,
81
+ relation: 'CONTAINS',
82
+ confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
83
+ });
84
+
85
+ handleInheritance(node, id, edges, repoPath, config, source);
86
+
87
+ for (let i = 0; i < node.childCount; i++) {
88
+ walk(node.child(i)!, id);
89
+ }
90
+ return;
91
+ }
92
+
93
+ for (let i = 0; i < node.childCount; i++) {
94
+ walk(node.child(i)!, parentId);
95
+ }
96
+ }
97
+
98
+ walk(tree.rootNode);
99
+ return { nodes, edges, parseErrors };
100
+ }
101
+
102
+ function isNodeExported(node: Parser.SyntaxNode, _source: string): boolean {
103
+ const parent = node.parent;
104
+ if (!parent) return false;
105
+ return parent.type === 'export_statement' ||
106
+ parent.type === 'export_default_declaration';
107
+ }
108
+
109
+ function handleImport(
110
+ node: Parser.SyntaxNode,
111
+ fileNodeId: string,
112
+ source: string,
113
+ config: LanguageConfig,
114
+ edges: MonographEdge[],
115
+ repoPath: string,
116
+ ): void {
117
+ const targetPath = config.importExtractor ? config.importExtractor(source, node) : null;
118
+ const targetId = makeId('import', targetPath ?? node.text.slice(0, 60));
119
+ edges.push({
120
+ id: makeId(fileNodeId, targetId, 'imports'),
121
+ sourceId: fileNodeId, targetId,
122
+ relation: 'IMPORTS',
123
+ confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
124
+ });
125
+ }
126
+
127
+ function handleInheritance(
128
+ node: Parser.SyntaxNode,
129
+ nodeId: string,
130
+ edges: MonographEdge[],
131
+ _repoPath: string,
132
+ _config: LanguageConfig,
133
+ _source: string,
134
+ ): void {
135
+ for (let i = 0; i < node.childCount; i++) {
136
+ const child = node.child(i)!;
137
+ if (child.type === 'extends_clause') {
138
+ const targetId = makeId('import', child.text.replace(/extends?\s+/, '').trim());
139
+ edges.push({
140
+ id: makeId(nodeId, targetId, 'extends'),
141
+ sourceId: nodeId, targetId,
142
+ relation: 'EXTENDS',
143
+ confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
144
+ });
145
+ }
146
+ if (child.type === 'implements_clause') {
147
+ const targetId = makeId('import', child.text.replace(/implements\s+/, '').trim());
148
+ edges.push({
149
+ id: makeId(nodeId, targetId, 'implements'),
150
+ sourceId: nodeId, targetId,
151
+ relation: 'IMPLEMENTS',
152
+ confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
153
+ });
154
+ }
155
+ // class_heritage is the direct child; implements_clause is nested inside it
156
+ if (child.type === 'class_heritage') {
157
+ for (let j = 0; j < child.childCount; j++) {
158
+ const grandChild = child.child(j)!;
159
+ if (grandChild.type === 'extends_clause') {
160
+ const targetId = makeId('import', grandChild.text.replace(/extends?\s+/, '').trim());
161
+ edges.push({
162
+ id: makeId(nodeId, targetId, 'extends'),
163
+ sourceId: nodeId, targetId,
164
+ relation: 'EXTENDS',
165
+ confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
166
+ });
167
+ }
168
+ if (grandChild.type === 'implements_clause') {
169
+ const targetId = makeId('import', grandChild.text.replace(/implements\s+/, '').trim());
170
+ edges.push({
171
+ id: makeId(nodeId, targetId, 'implements'),
172
+ sourceId: nodeId, targetId,
173
+ relation: 'IMPLEMENTS',
174
+ confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
@@ -0,0 +1,27 @@
1
+ import { createRequire } from 'module';
2
+ import type { LanguageConfig } from './language-config.js';
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ export const goConfig: LanguageConfig = {
7
+ name: 'go',
8
+ extensions: ['.go'],
9
+ treeSitterModule: 'tree-sitter-go',
10
+ getLanguage: () => require('tree-sitter-go'),
11
+ classNodeTypes: new Set([]),
12
+ structNodeTypes: new Set(['type_spec']),
13
+ enumNodeTypes: new Set([]),
14
+ functionNodeTypes: new Set(['function_declaration']),
15
+ methodNodeTypes: new Set(['method_declaration']),
16
+ constructorNodeTypes: new Set([]),
17
+ interfaceNodeTypes: new Set(['interface_type']),
18
+ importNodeTypes: new Set(['import_declaration', 'import_spec']),
19
+ callNodeTypes: new Set(['call_expression']),
20
+ decoratorNodeTypes: new Set([]),
21
+ nameField: 'name',
22
+ packageScopeType: 'package_clause',
23
+ importExtractor: (_source, node) => {
24
+ const pathNode = node.childForFieldName('path') ?? node.child(1);
25
+ return pathNode?.text.replace(/['"]/g, '') ?? null;
26
+ },
27
+ };
@@ -0,0 +1,27 @@
1
+ import { createRequire } from 'module';
2
+ import type { LanguageConfig } from './language-config.js';
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ export const javaConfig: LanguageConfig = {
7
+ name: 'java',
8
+ extensions: ['.java'],
9
+ treeSitterModule: 'tree-sitter-java',
10
+ getLanguage: () => require('tree-sitter-java'),
11
+ classNodeTypes: new Set(['class_declaration']),
12
+ structNodeTypes: new Set(['record_declaration']),
13
+ enumNodeTypes: new Set(['enum_declaration']),
14
+ functionNodeTypes: new Set([]),
15
+ methodNodeTypes: new Set(['method_declaration']),
16
+ constructorNodeTypes: new Set(['constructor_declaration']),
17
+ interfaceNodeTypes: new Set(['interface_declaration', 'annotation_type_declaration']),
18
+ importNodeTypes: new Set(['import_declaration']),
19
+ callNodeTypes: new Set(['method_invocation', 'object_creation_expression']),
20
+ decoratorNodeTypes: new Set(['annotation']),
21
+ nameField: 'name',
22
+ importExtractor: (_source, node) => {
23
+ const path = node.text.replace(/^import\s+(static\s+)?/, '').replace(/;$/, '').trim();
24
+ return path || null;
25
+ },
26
+ exportDetector: (_node, _source) => true,
27
+ };
@@ -0,0 +1,28 @@
1
+ export interface ImportExtractor {
2
+ (sourceText: string, importNode: import('tree-sitter').SyntaxNode): string | null;
3
+ }
4
+
5
+ export interface ExportDetector {
6
+ (node: import('tree-sitter').SyntaxNode, sourceText: string): boolean;
7
+ }
8
+
9
+ export interface LanguageConfig {
10
+ name: string;
11
+ extensions: string[];
12
+ treeSitterModule: string;
13
+ getLanguage: () => import('tree-sitter').Language;
14
+ classNodeTypes: Set<string>;
15
+ structNodeTypes: Set<string>;
16
+ enumNodeTypes: Set<string>;
17
+ functionNodeTypes: Set<string>;
18
+ methodNodeTypes: Set<string>;
19
+ constructorNodeTypes: Set<string>;
20
+ interfaceNodeTypes: Set<string>;
21
+ importNodeTypes: Set<string>;
22
+ callNodeTypes: Set<string>;
23
+ decoratorNodeTypes: Set<string>;
24
+ nameField: string;
25
+ packageScopeType?: string;
26
+ importExtractor?: ImportExtractor;
27
+ exportDetector?: ExportDetector;
28
+ }
@@ -0,0 +1,85 @@
1
+ import Parser from 'tree-sitter';
2
+ import type { LanguageConfig } from './language-config.js';
3
+
4
+ const parserCache = new Map<string, Parser>();
5
+ const configCache = new Map<string, LanguageConfig>();
6
+
7
+ async function loadConfig(ext: string): Promise<LanguageConfig | null> {
8
+ if (configCache.has(ext)) return configCache.get(ext)!;
9
+
10
+ let config: LanguageConfig | null = null;
11
+ if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') {
12
+ const { typescriptConfig } = await import('./typescript.js');
13
+ config = typescriptConfig;
14
+ } else if (ext === '.py') {
15
+ const { pythonConfig } = await import('./python.js');
16
+ config = pythonConfig;
17
+ } else if (ext === '.go') {
18
+ const { goConfig } = await import('./go.js');
19
+ config = goConfig;
20
+ } else if (ext === '.rs') {
21
+ const { rustConfig } = await import('./rust.js');
22
+ config = rustConfig;
23
+ } else if (ext === '.java') {
24
+ const { javaConfig } = await import('./java.js');
25
+ config = javaConfig;
26
+ }
27
+
28
+ if (config) {
29
+ for (const e of config.extensions) configCache.set(e, config);
30
+ }
31
+ return config;
32
+ }
33
+
34
+ export async function getParser(ext: string): Promise<{ parser: Parser; config: LanguageConfig } | null> {
35
+ const config = await loadConfig(ext);
36
+ if (!config) return null;
37
+
38
+ if (parserCache.has(ext)) {
39
+ return { parser: parserCache.get(ext)!, config };
40
+ }
41
+
42
+ const parser = new Parser();
43
+ parser.setLanguage(config.getLanguage());
44
+ parserCache.set(ext, parser);
45
+ return { parser, config };
46
+ }
47
+
48
+ export function isSupportedExtension(ext: string): boolean {
49
+ const supported = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java'];
50
+ return supported.includes(ext);
51
+ }
52
+
53
+ export function getLanguageForExt(ext: string): string {
54
+ const map: Record<string, string> = {
55
+ '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript',
56
+ '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
57
+ '.py': 'python', '.go': 'go', '.rs': 'rust', '.java': 'java',
58
+ };
59
+ return map[ext] ?? 'unknown';
60
+ }
61
+
62
+ export interface ParseResult {
63
+ nodes: import('../types.js').MonographNode[];
64
+ edges: import('../types.js').MonographEdge[];
65
+ parseErrors: string[];
66
+ }
67
+
68
+ export async function parseFile(
69
+ absolutePath: string,
70
+ sourceText: string,
71
+ repoRelativePath: string,
72
+ ): Promise<ParseResult> {
73
+ const ext = absolutePath.slice(absolutePath.lastIndexOf('.'));
74
+ const entry = await getParser(ext);
75
+ if (!entry) return { nodes: [], edges: [], parseErrors: [] };
76
+
77
+ const { parser, config } = entry;
78
+ try {
79
+ const tree = parser.parse(sourceText);
80
+ const { extractSymbols } = await import('./extractor.js');
81
+ return extractSymbols(tree, sourceText, repoRelativePath, config, ext);
82
+ } catch (err) {
83
+ return { nodes: [], edges: [], parseErrors: [`${repoRelativePath}: ${err}`] };
84
+ }
85
+ }
@@ -0,0 +1,26 @@
1
+ import { createRequire } from 'module';
2
+ import type { LanguageConfig } from './language-config.js';
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ export const pythonConfig: LanguageConfig = {
7
+ name: 'python',
8
+ extensions: ['.py'],
9
+ treeSitterModule: 'tree-sitter-python',
10
+ getLanguage: () => require('tree-sitter-python'),
11
+ classNodeTypes: new Set(['class_definition']),
12
+ structNodeTypes: new Set([]),
13
+ enumNodeTypes: new Set([]),
14
+ functionNodeTypes: new Set(['function_definition']),
15
+ methodNodeTypes: new Set(['function_definition']),
16
+ constructorNodeTypes: new Set([]),
17
+ interfaceNodeTypes: new Set([]),
18
+ importNodeTypes: new Set(['import_statement', 'import_from_statement']),
19
+ callNodeTypes: new Set(['call']),
20
+ decoratorNodeTypes: new Set(['decorator']),
21
+ nameField: 'name',
22
+ importExtractor: (_source, node) => {
23
+ const moduleNode = node.childForFieldName('module_name') ?? node.child(1);
24
+ return moduleNode?.text ?? null;
25
+ },
26
+ };