monomind 1.10.18 → 1.10.19

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.
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ua-import.mjs — Option A
4
+ *
5
+ * Imports an Understand-Anything graph.json into a monograph SQLite database.
6
+ *
7
+ * Usage:
8
+ * node scripts/ua-import.mjs <graph.json> [<monograph.db>]
9
+ *
10
+ * If <monograph.db> is omitted it defaults to .monomind/monograph.db in the
11
+ * current working directory. The script merges (upserts) UA data so it can be
12
+ * re-run after incremental UA analyses without duplicating rows.
13
+ *
14
+ * Mapping:
15
+ * UA GraphNode → monograph nodes (summary + tags stored in properties)
16
+ * UA GraphEdge → monograph edges (type → relation, weight → weight)
17
+ * UA Layer → monograph communities (id → community_id on nodes)
18
+ */
19
+
20
+ import { readFileSync, existsSync } from 'fs';
21
+ import { resolve, join, dirname } from 'path';
22
+ import { createRequire } from 'module';
23
+ import { fileURLToPath } from 'url';
24
+
25
+ const __dir = dirname(fileURLToPath(import.meta.url));
26
+ const CWD = process.cwd();
27
+
28
+ // ── Resolve better-sqlite3 via pnpm virtual store ───────────────────────────
29
+ function requireBetterSqlite() {
30
+ const require = createRequire(import.meta.url);
31
+ const candidates = [
32
+ join(CWD, 'node_modules/.pnpm/node_modules/@monoes/monograph'),
33
+ join(CWD, 'packages/node_modules/.pnpm/node_modules/@monoes/monograph'),
34
+ join(CWD, 'node_modules/@monoes/monograph'),
35
+ ];
36
+ for (const c of candidates) {
37
+ try { if (existsSync(c)) return require(c); } catch {}
38
+ }
39
+ try { return require('@monoes/monograph'); } catch {}
40
+ throw new Error('Cannot find @monoes/monograph — run pnpm install from the monomind root');
41
+ }
42
+
43
+ // ── CLI args ────────────────────────────────────────────────────────────────
44
+ const [,, graphJsonPath, dbPathArg] = process.argv;
45
+ if (!graphJsonPath) {
46
+ console.error('Usage: node scripts/ua-import.mjs <graph.json> [<monograph.db>]');
47
+ process.exit(1);
48
+ }
49
+ const graphPath = resolve(graphJsonPath);
50
+ const dbPath = resolve(dbPathArg || join(CWD, '.monomind', 'monograph.db'));
51
+
52
+ if (!existsSync(graphPath)) { console.error('graph.json not found:', graphPath); process.exit(1); }
53
+ if (!existsSync(dbPath)) { console.error('monograph.db not found:', dbPath, '— build the graph first'); process.exit(1); }
54
+
55
+ // ── Load graph.json ─────────────────────────────────────────────────────────
56
+ console.log('Reading', graphPath);
57
+ const graph = JSON.parse(readFileSync(graphPath, 'utf-8'));
58
+ const { nodes: uaNodes = [], edges: uaEdges = [], layers = [] } = graph;
59
+ console.log(`UA graph: ${uaNodes.length} nodes, ${uaEdges.length} edges, ${layers.length} layers`);
60
+
61
+ // ── Open monograph DB ───────────────────────────────────────────────────────
62
+ const mg = requireBetterSqlite();
63
+ const db = mg.openDb(dbPath);
64
+
65
+ // ── Ensure communities table exists ─────────────────────────────────────────
66
+ db.prepare(`CREATE TABLE IF NOT EXISTS communities (
67
+ id INTEGER PRIMARY KEY,
68
+ label TEXT,
69
+ size INTEGER NOT NULL DEFAULT 0,
70
+ cohesion_score REAL NOT NULL DEFAULT 0.0
71
+ )`).run();
72
+
73
+ try { db.prepare(`ALTER TABLE nodes ADD COLUMN properties TEXT`).run(); } catch { /* column already exists */ }
74
+
75
+ // ── Build layer → community_id map ──────────────────────────────────────────
76
+ // Layers from UA become communities in monograph.
77
+ // Layer IDs look like "layer:api" — we assign sequential integers.
78
+ const layerIdToInt = new Map();
79
+ let communityIdx = 1000; // start high to avoid colliding with existing graph-algo communities
80
+
81
+ const upsertCommunity = db.prepare(
82
+ `INSERT INTO communities (id, label, size, cohesion_score)
83
+ VALUES (?, ?, ?, 0.8)
84
+ ON CONFLICT(id) DO UPDATE SET label=excluded.label, size=excluded.size`
85
+ );
86
+
87
+ for (const layer of layers) {
88
+ layerIdToInt.set(layer.id, communityIdx);
89
+ upsertCommunity.run(communityIdx, layer.name, layer.nodeIds?.length ?? 0);
90
+ communityIdx++;
91
+ }
92
+ console.log(`Mapped ${layerIdToInt.size} UA layers → monograph communities`);
93
+
94
+ // ── Map UA NodeType → monograph NodeLabel ────────────────────────────────────
95
+ const TYPE_TO_LABEL = {
96
+ file: 'File',
97
+ function: 'Function',
98
+ class: 'Class',
99
+ module: 'Module',
100
+ concept: 'Concept',
101
+ config: 'File', // closest monograph label
102
+ document: 'File',
103
+ service: 'Module',
104
+ table: 'Concept', // DB table — store kind in properties
105
+ endpoint: 'Route',
106
+ pipeline: 'Process',
107
+ schema: 'Concept',
108
+ resource: 'Concept',
109
+ domain: 'Concept',
110
+ flow: 'Process',
111
+ step: 'Section',
112
+ article: 'Section',
113
+ entity: 'Concept',
114
+ topic: 'Concept',
115
+ claim: 'Concept',
116
+ source: 'File',
117
+ };
118
+
119
+ // ── Map UA EdgeType → monograph EdgeRelation ─────────────────────────────────
120
+ const EDGE_TO_RELATION = {
121
+ imports: 'IMPORTS',
122
+ exports: 'DEFINES',
123
+ contains: 'CONTAINS',
124
+ inherits: 'EXTENDS',
125
+ implements: 'IMPLEMENTS',
126
+ calls: 'CALLS',
127
+ subscribes: 'REFERENCES',
128
+ publishes: 'REFERENCES',
129
+ middleware: 'WRAPS',
130
+ reads_from: 'FETCHES',
131
+ writes_to: 'ACCESSES',
132
+ transforms: 'USES',
133
+ validates: 'USES',
134
+ depends_on: 'IMPORTS',
135
+ tested_by: 'REFERENCES',
136
+ configures: 'REFERENCES',
137
+ related: 'RELATED_TO',
138
+ similar_to: 'RELATED_TO',
139
+ deploys: 'REFERENCES',
140
+ serves: 'REFERENCES',
141
+ provisions: 'REFERENCES',
142
+ triggers: 'REFERENCES',
143
+ migrates: 'REFERENCES',
144
+ documents: 'DESCRIBES',
145
+ routes: 'HANDLES_ROUTE',
146
+ defines_schema: 'DEFINES',
147
+ contains_flow: 'CONTAINS',
148
+ flow_step: 'STEP_IN_PROCESS',
149
+ cross_domain: 'RELATED_TO',
150
+ cites: 'REFERENCES',
151
+ contradicts: 'CONTRASTS_WITH',
152
+ builds_on: 'PART_OF',
153
+ exemplifies: 'DESCRIBES',
154
+ categorized_under: 'PART_OF',
155
+ authored_by: 'REFERENCES',
156
+ };
157
+
158
+ // ── Build nodeId lookup: UA node ids may differ from monograph ids ────────────
159
+ // UA uses "file:src/foo.ts", "function:src/foo.ts:bar" etc.
160
+ // We'll upsert UA nodes as new rows, using the UA id directly (prefixed with "ua:").
161
+ // For nodes that already exist in monograph (matched by file_path + name), we
162
+ // update their community_id and properties with UA data rather than duplicating.
163
+
164
+ const findByFilePath = db.prepare(
165
+ `SELECT id FROM nodes WHERE file_path = ? AND name = ? LIMIT 1`
166
+ );
167
+ const findByName = db.prepare(
168
+ `SELECT id FROM nodes WHERE name = ? AND label = ? LIMIT 1`
169
+ );
170
+ const updateNodeEnrichment = db.prepare(
171
+ `UPDATE nodes SET community_id = ?, properties = ? WHERE id = ?`
172
+ );
173
+ const upsertNode = db.prepare(
174
+ `INSERT INTO nodes (id, label, name, norm_label, file_path, start_line, end_line, community_id, is_exported, language, properties)
175
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
176
+ ON CONFLICT(id) DO UPDATE SET
177
+ community_id = excluded.community_id,
178
+ properties = excluded.properties`
179
+ );
180
+
181
+ // Build a reverse map: UA node id → monograph node id (for edge mapping)
182
+ const uaToMgId = new Map();
183
+
184
+ // Build layer node membership map first
185
+ const nodeLayerMap = new Map(); // UA node id → layer id
186
+ for (const layer of layers) {
187
+ for (const nid of (layer.nodeIds || [])) {
188
+ nodeLayerMap.set(nid, layer.id);
189
+ }
190
+ }
191
+
192
+ let enriched = 0, inserted = 0;
193
+ const insertMany = db.transaction((nodes) => {
194
+ for (const uaNode of nodes) {
195
+ const label = TYPE_TO_LABEL[uaNode.type] || 'Concept';
196
+ const layerId = nodeLayerMap.get(uaNode.id);
197
+ const communityId = layerId ? layerIdToInt.get(layerId) ?? null : null;
198
+ const properties = JSON.stringify({
199
+ ua_type: uaNode.type,
200
+ summary: uaNode.summary || '',
201
+ tags: uaNode.tags || [],
202
+ complexity: uaNode.complexity || '',
203
+ languageNotes: uaNode.languageNotes || '',
204
+ domainMeta: uaNode.domainMeta || null,
205
+ knowledgeMeta: uaNode.knowledgeMeta || null,
206
+ });
207
+
208
+ // Try to match existing monograph node
209
+ let existingId = null;
210
+ if (uaNode.filePath) {
211
+ const row = findByFilePath.get(uaNode.filePath, uaNode.name);
212
+ if (row) existingId = row.id;
213
+ }
214
+ if (!existingId) {
215
+ const row = findByName.get(uaNode.name, label);
216
+ if (row) existingId = row.id;
217
+ }
218
+
219
+ if (existingId) {
220
+ updateNodeEnrichment.run(communityId, properties, existingId);
221
+ uaToMgId.set(uaNode.id, existingId);
222
+ enriched++;
223
+ } else {
224
+ // Insert as new node with "ua:" prefix to avoid id collision
225
+ const newId = 'ua:' + uaNode.id;
226
+ const normLabel = (uaNode.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_');
227
+ const lang = uaNode.filePath
228
+ ? uaNode.filePath.split('.').pop() || null
229
+ : null;
230
+ upsertNode.run(
231
+ newId, label, uaNode.name || uaNode.id, normLabel,
232
+ uaNode.filePath || null,
233
+ uaNode.lineRange?.[0] ?? null,
234
+ uaNode.lineRange?.[1] ?? null,
235
+ communityId, lang, properties
236
+ );
237
+ uaToMgId.set(uaNode.id, newId);
238
+ inserted++;
239
+ }
240
+ }
241
+ });
242
+ insertMany(uaNodes);
243
+ console.log(`Nodes: ${enriched} enriched existing, ${inserted} new inserted`);
244
+
245
+ // ── Upsert edges ─────────────────────────────────────────────────────────────
246
+ const upsertEdge = db.prepare(
247
+ `INSERT INTO edges (id, source_id, target_id, relation, confidence, confidence_score, weight)
248
+ VALUES (?, ?, ?, ?, 'INFERRED', 0.5, ?)
249
+ ON CONFLICT(id) DO UPDATE SET
250
+ relation = excluded.relation,
251
+ confidence_score = excluded.confidence_score,
252
+ weight = excluded.weight`
253
+ );
254
+
255
+ let edgesInserted = 0, edgesSkipped = 0;
256
+ const insertEdges = db.transaction((edges) => {
257
+ for (const e of edges) {
258
+ const srcId = uaToMgId.get(e.source);
259
+ const tgtId = uaToMgId.get(e.target);
260
+ if (!srcId || !tgtId) { edgesSkipped++; continue; }
261
+ const relation = EDGE_TO_RELATION[e.type] || 'RELATED_TO';
262
+ const edgeId = 'ua:' + e.source + ':' + e.type + ':' + e.target;
263
+ upsertEdge.run(edgeId, srcId, tgtId, relation, e.weight ?? 0.5);
264
+ edgesInserted++;
265
+ }
266
+ });
267
+ insertEdges(uaEdges);
268
+ console.log(`Edges: ${edgesInserted} upserted, ${edgesSkipped} skipped (unresolved node refs)`);
269
+
270
+ // ── Rebuild FTS index ─────────────────────────────────────────────────────────
271
+ try {
272
+ db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run();
273
+ console.log('FTS index rebuilt');
274
+ } catch { /* may not exist — safe to ignore */ }
275
+
276
+ // ── Update index_meta ─────────────────────────────────────────────────────────
277
+ db.prepare(
278
+ `INSERT INTO index_meta (key, value) VALUES ('ua_import_at', ?)
279
+ ON CONFLICT(key) DO UPDATE SET value=excluded.value`
280
+ ).run(new Date().toISOString());
281
+
282
+ mg.closeDb(db);
283
+
284
+ console.log('\n✓ Import complete');
285
+ console.log(` DB: ${dbPath}`);
286
+ console.log(` Communities from UA layers: ${layerIdToInt.size}`);
287
+ console.log(` Nodes enriched: ${enriched}, inserted: ${inserted}`);
288
+ console.log(` Edges: ${edgesInserted} added`);