@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,61 @@
1
+ import type { PipelinePhase, PipelineContext } from './types.js';
2
+ import { MonographError } from '../types.js';
3
+
4
+ export class PipelineRunner {
5
+ private readonly order: string[];
6
+
7
+ constructor(private readonly phases: PipelinePhase<unknown>[]) {
8
+ this.order = topoSort(phases);
9
+ }
10
+
11
+ async run(ctx: PipelineContext): Promise<Map<string, unknown>> {
12
+ const outputs = new Map<string, unknown>();
13
+ const phaseMap = new Map(this.phases.map(p => [p.name, p]));
14
+
15
+ for (const name of this.order) {
16
+ const phase = phaseMap.get(name)!;
17
+ ctx.onProgress?.({ phase: name });
18
+ const output = await phase.execute(ctx, outputs);
19
+ outputs.set(name, output);
20
+ }
21
+
22
+ return outputs;
23
+ }
24
+ }
25
+
26
+ function topoSort(phases: PipelinePhase<unknown>[]): string[] {
27
+ const names = new Set(phases.map(p => p.name));
28
+ const inDegree = new Map<string, number>();
29
+ const adjList = new Map<string, string[]>();
30
+
31
+ for (const p of phases) {
32
+ inDegree.set(p.name, p.deps.length);
33
+ for (const dep of p.deps) {
34
+ if (!names.has(dep)) {
35
+ throw new MonographError(`Phase '${p.name}' depends on unknown phase '${dep}'`);
36
+ }
37
+ const adj = adjList.get(dep) ?? [];
38
+ adj.push(p.name);
39
+ adjList.set(dep, adj);
40
+ }
41
+ }
42
+
43
+ const queue = phases.filter(p => (inDegree.get(p.name) ?? 0) === 0).map(p => p.name);
44
+ const result: string[] = [];
45
+
46
+ while (queue.length) {
47
+ const name = queue.shift()!;
48
+ result.push(name);
49
+ for (const next of (adjList.get(name) ?? [])) {
50
+ const deg = (inDegree.get(next) ?? 0) - 1;
51
+ inDegree.set(next, deg);
52
+ if (deg === 0) queue.push(next);
53
+ }
54
+ }
55
+
56
+ if (result.length !== phases.length) {
57
+ throw new MonographError('Cycle detected in pipeline phase graph');
58
+ }
59
+
60
+ return result;
61
+ }
@@ -0,0 +1,33 @@
1
+ import type { MonographDb } from '../storage/db.js';
2
+ import type { PipelineProgress } from '../types.js';
3
+ import type Graph from 'graphology';
4
+
5
+ export interface PipelineContext {
6
+ repoPath: string;
7
+ db: MonographDb;
8
+ graph: Graph;
9
+ onProgress: (p: PipelineProgress) => void;
10
+ options: PipelineOptions;
11
+ }
12
+
13
+ export interface PipelineOptions {
14
+ codeOnly: boolean;
15
+ maxFileSizeBytes: number;
16
+ workerPoolThreshold: number;
17
+ workerChunkBudgetBytes: number;
18
+ ignore: string[];
19
+ }
20
+
21
+ export const DEFAULT_OPTIONS: PipelineOptions = {
22
+ codeOnly: false,
23
+ maxFileSizeBytes: 524288,
24
+ workerPoolThreshold: 15,
25
+ workerChunkBudgetBytes: 20971520,
26
+ ignore: [],
27
+ };
28
+
29
+ export interface PipelinePhase<TOutput> {
30
+ name: string;
31
+ deps: string[];
32
+ execute(ctx: PipelineContext, depOutputs: Map<string, unknown>): Promise<TOutput>;
33
+ }
@@ -0,0 +1,54 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync, existsSync, renameSync, unlinkSync } from 'fs';
3
+ import { dirname } from 'path';
4
+ import {
5
+ CREATE_NODES, CREATE_EDGES, CREATE_COMMUNITIES,
6
+ CREATE_INDEX_META, CREATE_NODES_FTS, CREATE_INDEXES, FTS_SYNC_TRIGGERS,
7
+ } from './schema.js';
8
+ import { MonographError } from '../types.js';
9
+
10
+ export type MonographDb = Database.Database;
11
+
12
+ export function openDb(dbPath: string): MonographDb {
13
+ try {
14
+ mkdirSync(dirname(dbPath), { recursive: true });
15
+ const db = new Database(dbPath);
16
+ db.pragma('journal_mode = WAL');
17
+ db.pragma('foreign_keys = ON');
18
+ applyMigrations(db);
19
+ return db;
20
+ } catch (err) {
21
+ throw new MonographError(`Failed to open database at ${dbPath}`, err);
22
+ }
23
+ }
24
+
25
+ export function closeDb(db: MonographDb): void {
26
+ db.close();
27
+ }
28
+
29
+ function applyMigrations(db: MonographDb): void {
30
+ db.exec(CREATE_NODES);
31
+ db.exec(CREATE_EDGES);
32
+ db.exec(CREATE_COMMUNITIES);
33
+ db.exec(CREATE_INDEX_META);
34
+ db.exec(CREATE_NODES_FTS);
35
+ for (const idx of CREATE_INDEXES) db.exec(idx);
36
+ db.exec(FTS_SYNC_TRIGGERS);
37
+ }
38
+
39
+ /** Write to a .tmp file then rename for atomic replacement. */
40
+ export function atomicRebuild(dbPath: string, buildFn: (db: MonographDb) => void): void {
41
+ const tmpPath = dbPath + '.tmp';
42
+ const db = openDb(tmpPath);
43
+ try {
44
+ buildFn(db);
45
+ db.close();
46
+ renameSync(tmpPath, dbPath);
47
+ } catch (err) {
48
+ db.close();
49
+ if (existsSync(tmpPath)) {
50
+ try { unlinkSync(tmpPath); } catch { /* ignore */ }
51
+ }
52
+ throw err;
53
+ }
54
+ }
@@ -0,0 +1,61 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { MonographEdge } from '../types.js';
3
+
4
+ export function insertEdge(db: Database.Database, edge: MonographEdge): void {
5
+ db.prepare(`
6
+ INSERT OR REPLACE INTO edges (id, source_id, target_id, relation, confidence, confidence_score)
7
+ VALUES (?, ?, ?, ?, ?, ?)
8
+ `).run(
9
+ edge.id,
10
+ edge.sourceId,
11
+ edge.targetId,
12
+ edge.relation,
13
+ edge.confidence,
14
+ edge.confidenceScore,
15
+ );
16
+ }
17
+
18
+ export function insertEdges(db: Database.Database, edges: MonographEdge[]): void {
19
+ const insertMany = db.transaction((rows: MonographEdge[]) => {
20
+ for (const e of rows) {
21
+ insertEdge(db, e);
22
+ }
23
+ });
24
+ insertMany(edges);
25
+ }
26
+
27
+ export function getEdgesForSource(db: Database.Database, sourceId: string): MonographEdge[] {
28
+ const rows = db
29
+ .prepare('SELECT * FROM edges WHERE source_id = ?')
30
+ .all(sourceId) as Record<string, unknown>[];
31
+ return rows.map(rowToEdge);
32
+ }
33
+
34
+ export function getEdgesForTarget(db: Database.Database, targetId: string): MonographEdge[] {
35
+ const rows = db
36
+ .prepare('SELECT * FROM edges WHERE target_id = ?')
37
+ .all(targetId) as Record<string, unknown>[];
38
+ return rows.map(rowToEdge);
39
+ }
40
+
41
+ export function deleteEdgesForFile(db: Database.Database, filePath: string): void {
42
+ db.prepare(`
43
+ DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file_path = ?)
44
+ `).run(filePath);
45
+ }
46
+
47
+ export function countEdges(db: Database.Database): number {
48
+ const row = db.prepare('SELECT COUNT(*) as n FROM edges').get() as { n: number };
49
+ return row.n;
50
+ }
51
+
52
+ function rowToEdge(row: Record<string, unknown>): MonographEdge {
53
+ return {
54
+ id: row.id as string,
55
+ sourceId: row.source_id as string,
56
+ targetId: row.target_id as string,
57
+ relation: row.relation as MonographEdge['relation'],
58
+ confidence: row.confidence as MonographEdge['confidence'],
59
+ confidenceScore: row.confidence_score as number,
60
+ };
61
+ }
@@ -0,0 +1,52 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export interface FtsResult {
4
+ id: string;
5
+ name: string;
6
+ normLabel: string;
7
+ filePath: string | null;
8
+ label: string;
9
+ rank: number;
10
+ }
11
+
12
+ export function ftsSearch(
13
+ db: Database.Database,
14
+ query: string,
15
+ limit: number,
16
+ label?: string,
17
+ ): FtsResult[] {
18
+ // Clean and sanitize the query for FTS5
19
+ const safeQuery = query.replace(/['"*]/g, ' ').trim();
20
+ if (!safeQuery) return [];
21
+
22
+ // Add wildcard for prefix matching
23
+ const ftsPrefixQuery = safeQuery.split(/\s+/).map((term) => term + '*').join(' ');
24
+
25
+ let sql = `
26
+ SELECT n.id, n.name, n.norm_label, n.file_path, n.label,
27
+ nodes_fts.rank
28
+ FROM nodes_fts
29
+ JOIN nodes n ON n.rowid = nodes_fts.rowid
30
+ WHERE nodes_fts MATCH ?
31
+ `;
32
+ const params: unknown[] = [ftsPrefixQuery];
33
+ if (label) {
34
+ sql += ' AND n.label = ?';
35
+ params.push(label);
36
+ }
37
+ sql += ' ORDER BY nodes_fts.rank LIMIT ?';
38
+ params.push(limit);
39
+
40
+ const rows = db.prepare(sql).all(...(params as [string, ...unknown[]])) as Record<
41
+ string,
42
+ unknown
43
+ >[];
44
+ return rows.map((r) => ({
45
+ id: r.id as string,
46
+ name: r.name as string,
47
+ normLabel: r.norm_label as string,
48
+ filePath: (r.file_path as string | null) ?? null,
49
+ label: r.label as string,
50
+ rank: r.rank as number,
51
+ }));
52
+ }
@@ -0,0 +1,72 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { MonographNode } from '../types.js';
3
+ import { toNormLabel } from '../types.js';
4
+
5
+ export function insertNode(db: Database.Database, node: MonographNode): void {
6
+ db.prepare(`
7
+ INSERT OR REPLACE INTO nodes
8
+ (id, label, name, norm_label, file_path, start_line, end_line,
9
+ community_id, is_exported, language, properties)
10
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11
+ `).run(
12
+ node.id,
13
+ node.label,
14
+ node.name,
15
+ node.normLabel ?? toNormLabel(node.name),
16
+ node.filePath ?? null,
17
+ node.startLine ?? null,
18
+ node.endLine ?? null,
19
+ node.communityId ?? null,
20
+ node.isExported ? 1 : 0,
21
+ node.language ?? null,
22
+ node.properties ? JSON.stringify(node.properties) : null,
23
+ );
24
+ }
25
+
26
+ export function insertNodes(db: Database.Database, nodes: MonographNode[]): void {
27
+ const insertMany = db.transaction((rows: MonographNode[]) => {
28
+ for (const n of rows) {
29
+ insertNode(db, n);
30
+ }
31
+ });
32
+ insertMany(nodes);
33
+ }
34
+
35
+ export function getNode(db: Database.Database, id: string): MonographNode | undefined {
36
+ const row = db
37
+ .prepare('SELECT * FROM nodes WHERE id = ?')
38
+ .get(id) as Record<string, unknown> | undefined;
39
+ return row ? rowToNode(row) : undefined;
40
+ }
41
+
42
+ export function getNodesForFile(db: Database.Database, filePath: string): MonographNode[] {
43
+ const rows = db
44
+ .prepare('SELECT * FROM nodes WHERE file_path = ?')
45
+ .all(filePath) as Record<string, unknown>[];
46
+ return rows.map(rowToNode);
47
+ }
48
+
49
+ export function deleteNodesForFile(db: Database.Database, filePath: string): void {
50
+ db.prepare('DELETE FROM nodes WHERE file_path = ?').run(filePath);
51
+ }
52
+
53
+ export function countNodes(db: Database.Database): number {
54
+ const row = db.prepare('SELECT COUNT(*) as n FROM nodes').get() as { n: number };
55
+ return row.n;
56
+ }
57
+
58
+ function rowToNode(row: Record<string, unknown>): MonographNode {
59
+ return {
60
+ id: row.id as string,
61
+ label: row.label as MonographNode['label'],
62
+ name: row.name as string,
63
+ normLabel: row.norm_label as string,
64
+ filePath: row.file_path as string | undefined,
65
+ startLine: row.start_line as number | undefined,
66
+ endLine: row.end_line as number | undefined,
67
+ communityId: row.community_id as number | undefined,
68
+ isExported: (row.is_exported as number) === 1,
69
+ language: row.language as string | undefined,
70
+ properties: row.properties ? JSON.parse(row.properties as string) : undefined,
71
+ };
72
+ }
@@ -0,0 +1,77 @@
1
+ export const CREATE_NODES = `
2
+ CREATE TABLE IF NOT EXISTS nodes (
3
+ id TEXT PRIMARY KEY,
4
+ label TEXT NOT NULL,
5
+ name TEXT NOT NULL,
6
+ norm_label TEXT NOT NULL DEFAULT '',
7
+ file_path TEXT,
8
+ start_line INTEGER,
9
+ end_line INTEGER,
10
+ community_id INTEGER,
11
+ is_exported INTEGER NOT NULL DEFAULT 0,
12
+ language TEXT,
13
+ properties TEXT
14
+ )`;
15
+
16
+ export const CREATE_EDGES = `
17
+ CREATE TABLE IF NOT EXISTS edges (
18
+ id TEXT PRIMARY KEY,
19
+ source_id TEXT NOT NULL,
20
+ target_id TEXT NOT NULL,
21
+ relation TEXT NOT NULL,
22
+ confidence TEXT NOT NULL DEFAULT 'EXTRACTED',
23
+ confidence_score REAL NOT NULL DEFAULT 1.0,
24
+ FOREIGN KEY (source_id) REFERENCES nodes(id),
25
+ FOREIGN KEY (target_id) REFERENCES nodes(id)
26
+ )`;
27
+
28
+ export const CREATE_COMMUNITIES = `
29
+ CREATE TABLE IF NOT EXISTS communities (
30
+ id INTEGER PRIMARY KEY,
31
+ label TEXT,
32
+ size INTEGER NOT NULL DEFAULT 0,
33
+ cohesion_score REAL NOT NULL DEFAULT 0.0
34
+ )`;
35
+
36
+ export const CREATE_INDEX_META = `
37
+ CREATE TABLE IF NOT EXISTS index_meta (
38
+ key TEXT PRIMARY KEY,
39
+ value TEXT NOT NULL
40
+ )`;
41
+
42
+ export const CREATE_NODES_FTS = `
43
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
44
+ id UNINDEXED,
45
+ name,
46
+ norm_label,
47
+ file_path,
48
+ label UNINDEXED,
49
+ content='nodes',
50
+ content_rowid='rowid'
51
+ )`;
52
+
53
+ export const CREATE_INDEXES = [
54
+ `CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_path)`,
55
+ `CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label)`,
56
+ `CREATE INDEX IF NOT EXISTS idx_nodes_norm ON nodes(norm_label)`,
57
+ `CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id)`,
58
+ `CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id)`,
59
+ `CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)`,
60
+ `CREATE INDEX IF NOT EXISTS idx_edges_confidence ON edges(confidence)`,
61
+ ];
62
+
63
+ export const FTS_SYNC_TRIGGERS = `
64
+ CREATE TRIGGER IF NOT EXISTS nodes_fts_insert AFTER INSERT ON nodes BEGIN
65
+ INSERT INTO nodes_fts(rowid, id, name, norm_label, file_path, label)
66
+ VALUES (new.rowid, new.id, new.name, new.norm_label, new.file_path, new.label);
67
+ END;
68
+ CREATE TRIGGER IF NOT EXISTS nodes_fts_delete AFTER DELETE ON nodes BEGIN
69
+ INSERT INTO nodes_fts(nodes_fts, rowid, id, name, norm_label, file_path, label)
70
+ VALUES ('delete', old.rowid, old.id, old.name, old.norm_label, old.file_path, old.label);
71
+ END;
72
+ CREATE TRIGGER IF NOT EXISTS nodes_fts_update AFTER UPDATE ON nodes BEGIN
73
+ INSERT INTO nodes_fts(nodes_fts, rowid, id, name, norm_label, file_path, label)
74
+ VALUES ('delete', old.rowid, old.id, old.name, old.norm_label, old.file_path, old.label);
75
+ INSERT INTO nodes_fts(rowid, id, name, norm_label, file_path, label)
76
+ VALUES (new.rowid, new.id, new.name, new.norm_label, new.file_path, new.label);
77
+ END`;
package/src/types.ts ADDED
@@ -0,0 +1,131 @@
1
+ // ── Node labels ───────────────────────────────────────────────────────────────
2
+
3
+ export type NodeLabel =
4
+ | 'File' | 'Folder' | 'Function' | 'Class' | 'Method' | 'Interface'
5
+ | 'Variable' | 'Struct' | 'Enum' | 'Macro' | 'Typedef' | 'Union'
6
+ | 'Namespace' | 'Trait' | 'Impl' | 'TypeAlias' | 'Const' | 'Static'
7
+ | 'Property' | 'Record' | 'Delegate' | 'Annotation' | 'Constructor'
8
+ | 'Template' | 'Module' | 'Process' | 'Route' | 'Community' | 'Concept';
9
+
10
+ export const SYMBOL_NODE_LABELS = new Set<NodeLabel>([
11
+ 'Function', 'Class', 'Method', 'Interface', 'Variable', 'Struct', 'Enum',
12
+ 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl', 'TypeAlias',
13
+ 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation',
14
+ 'Constructor', 'Template', 'Module',
15
+ ]);
16
+
17
+ // ── Edge relations ────────────────────────────────────────────────────────────
18
+
19
+ export type EdgeRelation =
20
+ | 'CONTAINS' | 'DEFINES' | 'CALLS' | 'IMPORTS' | 'EXTENDS' | 'IMPLEMENTS'
21
+ | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'METHOD_OVERRIDES'
22
+ | 'METHOD_IMPLEMENTS' | 'MEMBER_OF' | 'STEP_IN_PROCESS' | 'HANDLES_ROUTE'
23
+ | 'FETCHES' | 'HANDLES_TOOL' | 'ENTRY_POINT_OF' | 'WRAPS' | 'QUERIES';
24
+
25
+ // ── Confidence ────────────────────────────────────────────────────────────────
26
+
27
+ export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
28
+
29
+ export const CONFIDENCE_SCORE: Record<EdgeConfidence, number> = {
30
+ EXTRACTED: 1.0,
31
+ INFERRED: 0.5,
32
+ AMBIGUOUS: 0.2,
33
+ };
34
+
35
+ // ── Nodes ─────────────────────────────────────────────────────────────────────
36
+
37
+ export interface MonographNode {
38
+ id: string;
39
+ label: NodeLabel;
40
+ name: string;
41
+ normLabel: string;
42
+ filePath?: string;
43
+ startLine?: number;
44
+ endLine?: number;
45
+ communityId?: number;
46
+ isExported: boolean;
47
+ language?: string;
48
+ properties?: Record<string, unknown>;
49
+ }
50
+
51
+ // ── Edges ─────────────────────────────────────────────────────────────────────
52
+
53
+ export interface MonographEdge {
54
+ id: string;
55
+ sourceId: string;
56
+ targetId: string;
57
+ relation: EdgeRelation;
58
+ confidence: EdgeConfidence;
59
+ confidenceScore: number;
60
+ }
61
+
62
+ // ── Communities ───────────────────────────────────────────────────────────────
63
+
64
+ export interface MonographCommunity {
65
+ id: number;
66
+ label?: string;
67
+ size: number;
68
+ cohesionScore: number;
69
+ }
70
+
71
+ // ── God nodes ─────────────────────────────────────────────────────────────────
72
+
73
+ export interface GodNode extends MonographNode {
74
+ degree: number;
75
+ inDegree: number;
76
+ outDegree: number;
77
+ }
78
+
79
+ // ── Surprising connections ────────────────────────────────────────────────────
80
+
81
+ export interface SurprisingConnection {
82
+ edge: MonographEdge;
83
+ score: number;
84
+ reasons: string[];
85
+ }
86
+
87
+ // ── Suggested questions ───────────────────────────────────────────────────────
88
+
89
+ export type SuggestedQuestion =
90
+ | { type: 'ambiguous_edge'; edge: MonographEdge; reason: string }
91
+ | { type: 'bridge_node'; node: MonographNode; commA: number; commB: number }
92
+ | { type: 'verify_inferred'; edge: MonographEdge; inferredFrom: string }
93
+ | { type: 'isolated_nodes'; nodes: MonographNode[]; reason: string }
94
+ | { type: 'low_cohesion'; community: MonographCommunity };
95
+
96
+ // ── ID generation ─────────────────────────────────────────────────────────────
97
+
98
+ export function makeId(...parts: string[]): string {
99
+ return parts
100
+ .join('_')
101
+ .replace(/[^a-z0-9_]/gi, '_')
102
+ .replace(/^_+|_+$/g, '')
103
+ .toLowerCase();
104
+ }
105
+
106
+ // ── Norm label ────────────────────────────────────────────────────────────────
107
+
108
+ export function toNormLabel(name: string): string {
109
+ return name
110
+ .normalize('NFD')
111
+ .replace(/[̀-ͯ]/g, '')
112
+ .toLowerCase();
113
+ }
114
+
115
+ // ── Pipeline progress ─────────────────────────────────────────────────────────
116
+
117
+ export interface PipelineProgress {
118
+ phase: string;
119
+ filesProcessed?: number;
120
+ totalFiles?: number;
121
+ message?: string;
122
+ }
123
+
124
+ // ── Errors ────────────────────────────────────────────────────────────────────
125
+
126
+ export class MonographError extends Error {
127
+ constructor(message: string, public readonly cause?: unknown) {
128
+ super(message);
129
+ this.name = 'MonographError';
130
+ }
131
+ }
@@ -0,0 +1,66 @@
1
+ import chokidar from 'chokidar';
2
+ import { EventEmitter } from 'events';
3
+ import { isSupportedExtension } from '../parsers/loader.js';
4
+ import { extname } from 'path';
5
+ import { platform } from 'os';
6
+
7
+ export interface WatcherOptions {
8
+ debounceMs?: number; // default 3000ms
9
+ }
10
+
11
+ export class MonographWatcher extends EventEmitter {
12
+ private watcher: chokidar.FSWatcher | null = null;
13
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null;
14
+ private pendingChanges = new Set<string>();
15
+ private readonly debounceMs: number;
16
+
17
+ constructor(private readonly repoPath: string, opts: WatcherOptions = {}) {
18
+ super();
19
+ this.debounceMs = opts.debounceMs ?? 3000;
20
+ }
21
+
22
+ async start(): Promise<void> {
23
+ const usePolling = platform() === 'darwin';
24
+
25
+ this.watcher = chokidar.watch(this.repoPath, {
26
+ ignored: [
27
+ /(^|[/\\])\../, // dotfiles
28
+ /node_modules/,
29
+ /\.monomind/,
30
+ /dist\//,
31
+ /build\//,
32
+ ],
33
+ persistent: true,
34
+ ignoreInitial: true,
35
+ usePolling,
36
+ interval: usePolling ? 1000 : undefined,
37
+ });
38
+
39
+ this.watcher.on('change', (path: string) => this.handleChange(path));
40
+ this.watcher.on('add', (path: string) => this.handleChange(path));
41
+ this.watcher.on('unlink', (path: string) => this.handleChange(path));
42
+ this.watcher.on('error', (err: unknown) => this.emit('monograph:error', err));
43
+
44
+ await new Promise<void>(resolve => this.watcher!.once('ready', resolve));
45
+ }
46
+
47
+ async stop(): Promise<void> {
48
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
49
+ if (this.watcher) await this.watcher.close();
50
+ this.watcher = null;
51
+ }
52
+
53
+ private handleChange(filePath: string): void {
54
+ const ext = extname(filePath);
55
+ if (!isSupportedExtension(ext)) return;
56
+
57
+ this.pendingChanges.add(filePath);
58
+
59
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
60
+ this.debounceTimer = setTimeout(() => {
61
+ const changed = [...this.pendingChanges];
62
+ this.pendingChanges.clear();
63
+ this.emit('monograph:updated', changed);
64
+ }, this.debounceMs);
65
+ }
66
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": ".",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": ["node_modules", "dist", "__tests__"]
10
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['__tests__/**/*.test.ts', 'src/__tests__/**/*.test.ts'],
7
+ globals: true,
8
+ coverage: {
9
+ enabled: false,
10
+ },
11
+ },
12
+ });