@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,25 @@
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 rustConfig: LanguageConfig = {
7
+ name: 'rust',
8
+ extensions: ['.rs'],
9
+ treeSitterModule: 'tree-sitter-rust',
10
+ getLanguage: () => require('tree-sitter-rust'),
11
+ classNodeTypes: new Set([]),
12
+ structNodeTypes: new Set(['struct_item']),
13
+ enumNodeTypes: new Set(['enum_item']),
14
+ functionNodeTypes: new Set(['function_item']),
15
+ methodNodeTypes: new Set(['function_item']),
16
+ constructorNodeTypes: new Set([]),
17
+ interfaceNodeTypes: new Set(['trait_item']),
18
+ importNodeTypes: new Set(['use_declaration']),
19
+ callNodeTypes: new Set(['call_expression', 'method_call_expression']),
20
+ decoratorNodeTypes: new Set(['attribute_item']),
21
+ nameField: 'name',
22
+ importExtractor: (_source, node) => {
23
+ return node.text.replace(/^use\s+/, '').replace(/;$/, '').trim();
24
+ },
25
+ };
@@ -0,0 +1,41 @@
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 typescriptConfig: LanguageConfig = {
7
+ name: 'typescript',
8
+ extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
9
+ treeSitterModule: 'tree-sitter-typescript',
10
+ getLanguage: () => {
11
+ const ts = require('tree-sitter-typescript');
12
+ return ts.typescript;
13
+ },
14
+ classNodeTypes: new Set(['class_declaration', 'class']),
15
+ structNodeTypes: new Set([]),
16
+ enumNodeTypes: new Set(['enum_declaration']),
17
+ functionNodeTypes: new Set([
18
+ 'function_declaration', 'function', 'arrow_function',
19
+ 'generator_function_declaration', 'generator_function',
20
+ ]),
21
+ methodNodeTypes: new Set(['method_definition', 'method_signature']),
22
+ constructorNodeTypes: new Set(['constructor']),
23
+ interfaceNodeTypes: new Set(['interface_declaration', 'type_alias_declaration']),
24
+ importNodeTypes: new Set(['import_statement', 'import_declaration']),
25
+ callNodeTypes: new Set(['call_expression', 'new_expression']),
26
+ decoratorNodeTypes: new Set(['decorator']),
27
+ nameField: 'name',
28
+ importExtractor: (_source, node) => {
29
+ for (let i = 0; i < node.childCount; i++) {
30
+ const child = node.child(i)!;
31
+ if (child.type === 'string') {
32
+ return child.text.replace(/['"]/g, '');
33
+ }
34
+ }
35
+ return null;
36
+ },
37
+ exportDetector: (node, _source) => {
38
+ const parent = node.parent;
39
+ return parent?.type === 'export_statement' || parent?.type === 'export_default_declaration';
40
+ },
41
+ };
@@ -0,0 +1,46 @@
1
+ import { resolve, join } from 'path';
2
+ import Graph from 'graphology';
3
+ import { openDb, closeDb } from '../storage/db.js';
4
+ import { PipelineRunner } from './runner.js';
5
+ import { scanPhase } from './phases/scan.js';
6
+ import { structurePhase } from './phases/structure.js';
7
+ import { parsePhase } from './phases/parse.js';
8
+ import { crossFilePhase } from './phases/cross-file.js';
9
+ import { mroPhase } from './phases/mro.js';
10
+ import { communitiesPhase } from './phases/communities.js';
11
+ import { godNodesPhase } from './phases/god-nodes.js';
12
+ import { surprisesPhase } from './phases/surprises.js';
13
+ import { suggestPhase } from './phases/suggest.js';
14
+ import type { PipelineOptions, PipelineContext } from './types.js';
15
+ import { DEFAULT_OPTIONS } from './types.js';
16
+ import type { PipelineProgress } from '../types.js';
17
+
18
+ export interface BuildOptions extends Partial<PipelineOptions> {
19
+ onProgress?: (p: PipelineProgress) => void;
20
+ force?: boolean;
21
+ }
22
+
23
+ export async function buildAsync(repoPath: string, options: BuildOptions = {}): Promise<void> {
24
+ const dbPath = resolve(join(repoPath, '.monomind', 'monograph.db'));
25
+ const fullOptions: PipelineOptions = { ...DEFAULT_OPTIONS, ...options };
26
+ const db = openDb(dbPath);
27
+
28
+ try {
29
+ const graph = new Graph({ multi: true, type: 'directed' });
30
+ const ctx: PipelineContext = {
31
+ repoPath: resolve(repoPath),
32
+ db, graph,
33
+ onProgress: options.onProgress ?? (() => {}),
34
+ options: fullOptions,
35
+ };
36
+
37
+ const runner = new PipelineRunner([
38
+ scanPhase, structurePhase, parsePhase, crossFilePhase,
39
+ mroPhase, communitiesPhase, godNodesPhase, surprisesPhase, suggestPhase,
40
+ ]);
41
+
42
+ await runner.run(ctx);
43
+ } finally {
44
+ closeDb(db);
45
+ }
46
+ }
@@ -0,0 +1,45 @@
1
+ import louvain from 'graphology-communities-louvain';
2
+ import type { PipelinePhase, PipelineContext } from '../types.js';
3
+ import type { MonographEdge } from '../../types.js';
4
+ import { loadGraphFromEdges } from '../../graph/loader.js';
5
+ import type { CrossFileOutput } from './cross-file.js';
6
+ import type { ParseOutput } from './parse.js';
7
+
8
+ export interface CommunitiesOutput {
9
+ memberships: Map<string, number>;
10
+ communityLabels: Map<number, string>;
11
+ }
12
+
13
+ export const communitiesPhase: PipelinePhase<CommunitiesOutput> = {
14
+ name: 'communities',
15
+ deps: ['cross-file', 'mro'],
16
+ async execute(_ctx, deps) {
17
+ const { resolvedEdges } = deps.get('cross-file') as CrossFileOutput;
18
+ const { allEdges } = deps.get('parse') as ParseOutput;
19
+ const allUsedEdges: MonographEdge[] = [...allEdges, ...resolvedEdges];
20
+
21
+ const graph = loadGraphFromEdges(allUsedEdges);
22
+ let communities: Record<string, number> = {};
23
+ try {
24
+ communities = louvain(graph);
25
+ } catch {
26
+ // Empty or disconnected graph
27
+ }
28
+
29
+ const memberships = new Map<string, number>(Object.entries(communities).map(([k, v]) => [k, v]));
30
+ const communityLabels = new Map<number, string>();
31
+
32
+ const communityDegrees = new Map<number, Map<string, number>>();
33
+ for (const [nodeId, commId] of memberships) {
34
+ if (!communityDegrees.has(commId)) communityDegrees.set(commId, new Map());
35
+ const deg = graph.degree(nodeId) ?? 0;
36
+ communityDegrees.get(commId)!.set(nodeId, deg);
37
+ }
38
+ for (const [commId, nodeDegs] of communityDegrees) {
39
+ const topNode = [...nodeDegs.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? '';
40
+ communityLabels.set(commId, `community-${commId}(${topNode.slice(0, 20)})`);
41
+ }
42
+
43
+ return { memberships, communityLabels };
44
+ },
45
+ };
@@ -0,0 +1,48 @@
1
+ import type { PipelinePhase, PipelineContext } from '../types.js';
2
+ import type { MonographEdge } from '../../types.js';
3
+ import { makeId, CONFIDENCE_SCORE } from '../../types.js';
4
+ import { insertEdges } from '../../storage/edge-store.js';
5
+ import type { ParseOutput } from './parse.js';
6
+
7
+ export interface CrossFileOutput {
8
+ resolvedEdges: MonographEdge[];
9
+ }
10
+
11
+ export const crossFilePhase: PipelinePhase<CrossFileOutput> = {
12
+ name: 'cross-file',
13
+ deps: ['parse'],
14
+ async execute(_ctx, deps) {
15
+ const { allEdges, symbolNodes } = deps.get('parse') as ParseOutput;
16
+
17
+ const nameIndex = new Map<string, string>();
18
+ for (const node of symbolNodes) {
19
+ nameIndex.set(node.name, node.id);
20
+ if (node.normLabel) nameIndex.set(node.normLabel, node.id);
21
+ }
22
+
23
+ const resolvedEdges: MonographEdge[] = [];
24
+
25
+ for (const edge of allEdges) {
26
+ if (edge.relation !== 'IMPORTS') continue;
27
+
28
+ const targetName = edge.targetId.replace(/^import_/, '').split('/').pop() ?? '';
29
+ const resolvedId = nameIndex.get(targetName) ?? nameIndex.get(targetName.toLowerCase());
30
+
31
+ if (resolvedId && resolvedId !== edge.targetId) {
32
+ resolvedEdges.push({
33
+ ...edge,
34
+ id: makeId(edge.sourceId, resolvedId, 'resolved'),
35
+ targetId: resolvedId,
36
+ confidence: 'INFERRED',
37
+ confidenceScore: CONFIDENCE_SCORE.INFERRED,
38
+ });
39
+ }
40
+ }
41
+
42
+ if (_ctx.db && resolvedEdges.length > 0) {
43
+ insertEdges(_ctx.db, resolvedEdges);
44
+ }
45
+
46
+ return { resolvedEdges };
47
+ },
48
+ };
@@ -0,0 +1,40 @@
1
+ import type { PipelinePhase, PipelineContext } from '../types.js';
2
+ import type { GodNode, MonographEdge } from '../../types.js';
3
+ import type { ParseOutput } from './parse.js';
4
+ import type { CrossFileOutput } from './cross-file.js';
5
+
6
+ const EXCLUDED_LABELS = new Set(['File', 'Folder', 'Community', 'Concept']);
7
+
8
+ export interface GodNodesOutput {
9
+ godNodes: GodNode[];
10
+ }
11
+
12
+ export const godNodesPhase: PipelinePhase<GodNodesOutput> = {
13
+ name: 'god-nodes',
14
+ deps: ['cross-file', 'parse'],
15
+ async execute(ctx, deps) {
16
+ const { resolvedEdges } = deps.get('cross-file') as CrossFileOutput;
17
+ const { allEdges, symbolNodes } = deps.get('parse') as ParseOutput;
18
+ const allEdgesCombined: MonographEdge[] = [...allEdges, ...resolvedEdges];
19
+
20
+ const inDeg = new Map<string, number>();
21
+ const outDeg = new Map<string, number>();
22
+ for (const e of allEdgesCombined) {
23
+ outDeg.set(e.sourceId, (outDeg.get(e.sourceId) ?? 0) + 1);
24
+ inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
25
+ }
26
+
27
+ const godNodes: GodNode[] = symbolNodes
28
+ .filter(n => !EXCLUDED_LABELS.has(n.label) && (inDeg.get(n.id) ?? 0) + (outDeg.get(n.id) ?? 0) > 0)
29
+ .map(n => ({
30
+ ...n,
31
+ inDegree: inDeg.get(n.id) ?? 0,
32
+ outDegree: outDeg.get(n.id) ?? 0,
33
+ degree: (inDeg.get(n.id) ?? 0) + (outDeg.get(n.id) ?? 0),
34
+ }))
35
+ .sort((a, b) => b.degree - a.degree)
36
+ .slice(0, 20);
37
+
38
+ return { godNodes };
39
+ },
40
+ };
@@ -0,0 +1,52 @@
1
+ import type { PipelinePhase, PipelineContext } from '../types.js';
2
+ import type { MonographEdge } from '../../types.js';
3
+ import { makeId, CONFIDENCE_SCORE } from '../../types.js';
4
+ import type { ParseOutput } from './parse.js';
5
+
6
+ export interface MroOutput {
7
+ mroEdges: MonographEdge[];
8
+ }
9
+
10
+ export const mroPhase: PipelinePhase<MroOutput> = {
11
+ name: 'mro',
12
+ deps: ['cross-file'],
13
+ async execute(_ctx, deps) {
14
+ const { allEdges, symbolNodes } = deps.get('parse') as ParseOutput;
15
+
16
+ const classMethodsIndex = new Map<string, string[]>();
17
+ for (const edge of allEdges) {
18
+ if (edge.relation === 'HAS_METHOD') {
19
+ const methods = classMethodsIndex.get(edge.sourceId) ?? [];
20
+ methods.push(edge.targetId);
21
+ classMethodsIndex.set(edge.sourceId, methods);
22
+ }
23
+ }
24
+
25
+ const nameIndex = new Map(symbolNodes.map(n => [n.id, n.name]));
26
+ const mroEdges: MonographEdge[] = [];
27
+
28
+ for (const edge of allEdges) {
29
+ if (edge.relation !== 'EXTENDS' && edge.relation !== 'IMPLEMENTS') continue;
30
+
31
+ const parentMethods = classMethodsIndex.get(edge.targetId) ?? [];
32
+ const childMethods = classMethodsIndex.get(edge.sourceId) ?? [];
33
+ const parentMethodNames = new Map(parentMethods.map(id => [nameIndex.get(id) ?? id, id]));
34
+
35
+ for (const childMethodId of childMethods) {
36
+ const childName = nameIndex.get(childMethodId);
37
+ if (!childName) continue;
38
+ const parentMethodId = parentMethodNames.get(childName);
39
+ if (!parentMethodId) continue;
40
+
41
+ const relation = edge.relation === 'IMPLEMENTS' ? 'METHOD_IMPLEMENTS' : 'METHOD_OVERRIDES';
42
+ mroEdges.push({
43
+ id: makeId(childMethodId, parentMethodId, relation.toLowerCase()),
44
+ sourceId: childMethodId, targetId: parentMethodId,
45
+ relation, confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
46
+ });
47
+ }
48
+ }
49
+
50
+ return { mroEdges };
51
+ },
52
+ };
@@ -0,0 +1,62 @@
1
+ import { readFileSync, statSync } from 'fs';
2
+ import { extname } from 'path';
3
+ import type { PipelinePhase, PipelineContext } from '../types.js';
4
+ import type { MonographNode, MonographEdge } from '../../types.js';
5
+ import { parseFile } from '../../parsers/loader.js';
6
+ import { insertNodes } from '../../storage/node-store.js';
7
+ import { insertEdges } from '../../storage/edge-store.js';
8
+ import type { StructureOutput } from './structure.js';
9
+
10
+ export interface ParseOutput {
11
+ symbolNodes: MonographNode[];
12
+ allEdges: MonographEdge[];
13
+ parseErrors: string[];
14
+ }
15
+
16
+ export const parsePhase: PipelinePhase<ParseOutput> = {
17
+ name: 'parse',
18
+ deps: ['structure'],
19
+ async execute(ctx, deps) {
20
+ const { fileNodes } = deps.get('structure') as StructureOutput;
21
+ const symbolNodes: MonographNode[] = [];
22
+ const allEdges: MonographEdge[] = [];
23
+ const parseErrors: string[] = [];
24
+ let processed = 0;
25
+
26
+ for (const fileNode of fileNodes) {
27
+ const absPath = fileNode.filePath ? `${ctx.repoPath}/${fileNode.filePath}` : '';
28
+ const ext = extname(absPath).toLowerCase();
29
+ let source: string;
30
+ try {
31
+ const stat = statSync(absPath);
32
+ if (stat.size > ctx.options.maxFileSizeBytes) {
33
+ parseErrors.push(`${fileNode.filePath}: skipped (too large)`);
34
+ continue;
35
+ }
36
+ source = readFileSync(absPath, 'utf-8');
37
+ } catch { continue; }
38
+
39
+ const result = await parseFile(absPath, source, fileNode.filePath ?? '');
40
+ symbolNodes.push(...result.nodes);
41
+ allEdges.push(...result.edges);
42
+ parseErrors.push(...result.parseErrors);
43
+ processed++;
44
+
45
+ if (processed % 50 === 0) {
46
+ ctx.onProgress?.({ phase: 'parse', filesProcessed: processed, totalFiles: fileNodes.length });
47
+ }
48
+ }
49
+
50
+ if (ctx.db) {
51
+ insertNodes(ctx.db, symbolNodes);
52
+ // Only insert edges whose target already exists in the DB (intra-file edges).
53
+ // Cross-file import edges with unresolved targets are handled by crossFilePhase
54
+ // after it resolves them to real node IDs.
55
+ const knownIds = new Set(symbolNodes.map(n => n.id));
56
+ const resolvableEdges = allEdges.filter(e => knownIds.has(e.targetId));
57
+ insertEdges(ctx.db, resolvableEdges);
58
+ }
59
+
60
+ return { symbolNodes, allEdges, parseErrors };
61
+ },
62
+ };
@@ -0,0 +1,58 @@
1
+ import { readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import type { PipelinePhase, PipelineContext } from '../types.js';
4
+ import { isSupportedExtension } from '../../parsers/loader.js';
5
+
6
+ const DEFAULT_IGNORE = new Set([
7
+ 'node_modules', '.git', 'dist', 'build', '__pycache__',
8
+ '.cache', 'coverage', '.monomind', 'vendor', 'target',
9
+ ]);
10
+
11
+ const BINARY_EXTENSIONS = new Set([
12
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2',
13
+ '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.gz', '.tar', '.pdf',
14
+ '.exe', '.dll', '.so', '.dylib', '.class', '.jar',
15
+ ]);
16
+
17
+ const GENERATED_PATTERNS = [/\.min\.(js|css)$/, /\.pb\.go$/, /_generated\.ts$/];
18
+
19
+ export interface ScanOutput {
20
+ filePaths: string[];
21
+ totalBytes: number;
22
+ }
23
+
24
+ export const scanPhase: PipelinePhase<ScanOutput> = {
25
+ name: 'scan',
26
+ deps: [],
27
+ async execute(ctx) {
28
+ const filePaths: string[] = [];
29
+ let totalBytes = 0;
30
+ const ignoreDirs = new Set([...DEFAULT_IGNORE, ...ctx.options.ignore]);
31
+
32
+ function walk(dir: string) {
33
+ let entries: string[];
34
+ try { entries = readdirSync(dir); } catch { return; }
35
+
36
+ for (const entry of entries) {
37
+ if (ignoreDirs.has(entry)) continue;
38
+ const fullPath = join(dir, entry);
39
+ let stat: ReturnType<typeof statSync>;
40
+ try { stat = statSync(fullPath); } catch { continue; }
41
+
42
+ if (stat.isDirectory()) { walk(fullPath); continue; }
43
+
44
+ const ext = extname(entry).toLowerCase();
45
+ if (BINARY_EXTENSIONS.has(ext)) continue;
46
+ if (GENERATED_PATTERNS.some(r => r.test(entry))) continue;
47
+ if (ctx.options.codeOnly && !isSupportedExtension(ext)) continue;
48
+
49
+ filePaths.push(fullPath);
50
+ totalBytes += stat.size;
51
+ }
52
+ }
53
+
54
+ walk(ctx.repoPath);
55
+ ctx.onProgress?.({ phase: 'scan', totalFiles: filePaths.length });
56
+ return { filePaths, totalBytes };
57
+ },
58
+ };
@@ -0,0 +1,68 @@
1
+ import { relative, dirname, basename } from 'path';
2
+ import type { PipelinePhase, PipelineContext } from '../types.js';
3
+ import type { MonographNode, MonographEdge } from '../../types.js';
4
+ import { makeId, toNormLabel, CONFIDENCE_SCORE } from '../../types.js';
5
+ import type { ScanOutput } from './scan.js';
6
+
7
+ export interface StructureOutput {
8
+ fileNodes: MonographNode[];
9
+ folderNodes: MonographNode[];
10
+ containsEdges: MonographEdge[];
11
+ }
12
+
13
+ export const structurePhase: PipelinePhase<StructureOutput> = {
14
+ name: 'structure',
15
+ deps: ['scan'],
16
+ async execute(ctx, deps) {
17
+ const { filePaths } = deps.get('scan') as ScanOutput;
18
+ const fileNodes: MonographNode[] = [];
19
+ const folderNodes: MonographNode[] = [];
20
+ const containsEdges: MonographEdge[] = [];
21
+ const seenFolders = new Set<string>();
22
+
23
+ for (const absPath of filePaths) {
24
+ const rel = relative(ctx.repoPath, absPath);
25
+ const fileId = makeId(rel.replace(/\//g, '_'), 'file');
26
+ fileNodes.push({
27
+ id: fileId, label: 'File',
28
+ name: basename(rel), normLabel: toNormLabel(basename(rel)),
29
+ filePath: rel, isExported: false,
30
+ });
31
+
32
+ const parts = dirname(rel).split('/');
33
+ let current = '';
34
+ let parentFolderId: string | null = null;
35
+ for (const part of parts) {
36
+ if (part === '.') continue;
37
+ current = current ? `${current}/${part}` : part;
38
+ const folderId = makeId(current.replace(/\//g, '_'), 'folder');
39
+ if (!seenFolders.has(folderId)) {
40
+ seenFolders.add(folderId);
41
+ folderNodes.push({
42
+ id: folderId, label: 'Folder',
43
+ name: part, normLabel: toNormLabel(part),
44
+ filePath: current, isExported: false,
45
+ });
46
+ if (parentFolderId) {
47
+ containsEdges.push({
48
+ id: makeId(parentFolderId, folderId, 'contains'),
49
+ sourceId: parentFolderId, targetId: folderId,
50
+ relation: 'CONTAINS', confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
51
+ });
52
+ }
53
+ }
54
+ parentFolderId = folderId;
55
+ }
56
+
57
+ if (parentFolderId) {
58
+ containsEdges.push({
59
+ id: makeId(parentFolderId, fileId, 'contains'),
60
+ sourceId: parentFolderId, targetId: fileId,
61
+ relation: 'CONTAINS', confidence: 'EXTRACTED', confidenceScore: CONFIDENCE_SCORE.EXTRACTED,
62
+ });
63
+ }
64
+ }
65
+
66
+ return { fileNodes, folderNodes, containsEdges };
67
+ },
68
+ };
@@ -0,0 +1,71 @@
1
+ import type { PipelinePhase, PipelineContext } from '../types.js';
2
+ import type { SuggestedQuestion } from '../../types.js';
3
+ import type { ParseOutput } from './parse.js';
4
+ import type { CommunitiesOutput } from './communities.js';
5
+
6
+ export interface SuggestOutput {
7
+ questions: SuggestedQuestion[];
8
+ }
9
+
10
+ export const suggestPhase: PipelinePhase<SuggestOutput> = {
11
+ name: 'suggest',
12
+ deps: ['parse', 'cross-file', 'mro', 'communities', 'god-nodes', 'surprises'],
13
+ async execute(_ctx, deps) {
14
+ const { allEdges, symbolNodes } = deps.get('parse') as ParseOutput;
15
+ const { memberships } = deps.get('communities') as CommunitiesOutput;
16
+ const questions: SuggestedQuestion[] = [];
17
+
18
+ // Signal 1: ambiguous edges
19
+ for (const edge of allEdges) {
20
+ if (edge.confidence === 'AMBIGUOUS') {
21
+ questions.push({ type: 'ambiguous_edge', edge, reason: 'Dynamic dispatch or unresolved target' });
22
+ }
23
+ }
24
+
25
+ // Signal 2: bridge nodes
26
+ const nodeCommSet = new Map<string, Set<number>>();
27
+ for (const edge of allEdges) {
28
+ const srcComm = memberships.get(edge.sourceId);
29
+ const tgtComm = memberships.get(edge.targetId);
30
+ if (srcComm !== undefined && tgtComm !== undefined && srcComm !== tgtComm) {
31
+ for (const [nid, comm] of [[edge.sourceId, srcComm], [edge.targetId, tgtComm]] as const) {
32
+ const s = nodeCommSet.get(nid) ?? new Set();
33
+ s.add(comm);
34
+ nodeCommSet.set(nid, s);
35
+ }
36
+ }
37
+ }
38
+ for (const [nodeId, comms] of nodeCommSet) {
39
+ if (comms.size >= 2) {
40
+ const node = symbolNodes.find(n => n.id === nodeId);
41
+ if (!node) continue;
42
+ const [commA, commB] = [...comms];
43
+ questions.push({ type: 'bridge_node', node, commA, commB });
44
+ }
45
+ }
46
+
47
+ // Signal 3: verify_inferred
48
+ for (const edge of allEdges) {
49
+ if (edge.confidence === 'INFERRED') {
50
+ questions.push({ type: 'verify_inferred', edge, inferredFrom: 'type inference / alias resolution' });
51
+ }
52
+ }
53
+
54
+ // Signal 4: isolated nodes
55
+ const connectedIds = new Set<string>();
56
+ for (const e of allEdges) { connectedIds.add(e.sourceId); connectedIds.add(e.targetId); }
57
+ const isolated = symbolNodes.filter(n =>
58
+ !connectedIds.has(n.id) && n.label !== 'File' && n.label !== 'Folder'
59
+ );
60
+ if (isolated.length > 0) {
61
+ questions.push({ type: 'isolated_nodes', nodes: isolated.slice(0, 10), reason: 'No edges found' });
62
+ }
63
+
64
+ const PRIORITY: Record<string, number> = {
65
+ bridge_node: 5, ambiguous_edge: 4, verify_inferred: 3, low_cohesion: 2, isolated_nodes: 1
66
+ };
67
+ questions.sort((a, b) => (PRIORITY[b.type] ?? 0) - (PRIORITY[a.type] ?? 0));
68
+
69
+ return { questions };
70
+ },
71
+ };
@@ -0,0 +1,70 @@
1
+ import type { PipelinePhase, PipelineContext } from '../types.js';
2
+ import type { SurprisingConnection } from '../../types.js';
3
+ import type { ParseOutput } from './parse.js';
4
+ import type { CrossFileOutput } from './cross-file.js';
5
+ import type { CommunitiesOutput } from './communities.js';
6
+
7
+ const WEIGHTS = {
8
+ confidence: 0.30,
9
+ crossType: 0.20,
10
+ crossRepo: 0.15,
11
+ crossCommunity: 0.25,
12
+ peripheral: 0.10,
13
+ };
14
+
15
+ export interface SurprisesOutput {
16
+ surprises: SurprisingConnection[];
17
+ }
18
+
19
+ export const surprisesPhase: PipelinePhase<SurprisesOutput> = {
20
+ name: 'surprises',
21
+ deps: ['cross-file', 'communities', 'parse'],
22
+ async execute(_ctx, deps) {
23
+ const { allEdges, symbolNodes } = deps.get('parse') as ParseOutput;
24
+ const { resolvedEdges } = deps.get('cross-file') as CrossFileOutput;
25
+ const { memberships } = deps.get('communities') as CommunitiesOutput;
26
+
27
+ const edges = [...allEdges, ...resolvedEdges];
28
+ const labelIndex = new Map(symbolNodes.map(n => [n.id, n.label]));
29
+ const inDeg = new Map<string, number>();
30
+ for (const e of edges) inDeg.set(e.targetId, (inDeg.get(e.targetId) ?? 0) + 1);
31
+ const maxDeg = Math.max(...inDeg.values(), 1);
32
+ const peripheralThreshold = maxDeg * 0.1;
33
+
34
+ const surprises: SurprisingConnection[] = edges
35
+ .filter(e => e.confidence !== 'EXTRACTED')
36
+ .map(e => {
37
+ const reasons: string[] = [];
38
+ let score = 0;
39
+
40
+ const conf = 1 - e.confidenceScore;
41
+ score += WEIGHTS.confidence * conf;
42
+ if (conf > 0) reasons.push(`${e.confidence} confidence`);
43
+
44
+ const srcLabel = labelIndex.get(e.sourceId);
45
+ const tgtLabel = labelIndex.get(e.targetId);
46
+ if (srcLabel && tgtLabel && srcLabel !== tgtLabel) {
47
+ score += WEIGHTS.crossType;
48
+ reasons.push(`cross-type (${srcLabel}→${tgtLabel})`);
49
+ }
50
+
51
+ const srcComm = memberships.get(e.sourceId);
52
+ const tgtComm = memberships.get(e.targetId);
53
+ if (srcComm !== undefined && tgtComm !== undefined && srcComm !== tgtComm) {
54
+ score += WEIGHTS.crossCommunity;
55
+ reasons.push('cross-community');
56
+ }
57
+
58
+ if ((inDeg.get(e.targetId) ?? 0) < peripheralThreshold) {
59
+ score += WEIGHTS.peripheral;
60
+ reasons.push('peripheral target');
61
+ }
62
+
63
+ return { edge: e, score, reasons };
64
+ })
65
+ .sort((a, b) => b.score - a.score)
66
+ .slice(0, 50);
67
+
68
+ return { surprises };
69
+ },
70
+ };