monomind 1.10.18 → 1.10.20
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.
- package/package.json +3 -1
- package/packages/@monomind/cli/package.json +1 -1
- package/packages/@monomind/cli/scripts/understand-analyze.mjs +68 -10
- package/scripts/install.sh +392 -0
- package/scripts/ua-enrich.mjs +228 -0
- package/scripts/ua-import.mjs +288 -0
- package/scripts/verify-appliance.sh +592 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ua-enrich.mjs — Option C
|
|
4
|
+
*
|
|
5
|
+
* Background enrichment runner that:
|
|
6
|
+
* 1. Runs the Understand-Anything extract-structure.mjs deterministic extraction
|
|
7
|
+
* on changed (or all) files in a project directory.
|
|
8
|
+
* 2. Merges the resulting structural + metadata into an existing monograph DB
|
|
9
|
+
* WITHOUT requiring the full LLM agent pipeline.
|
|
10
|
+
*
|
|
11
|
+
* For full LLM semantic enrichment (summaries, layers), run the /understand
|
|
12
|
+
* skill separately and then call ua-import.mjs on the resulting graph.json.
|
|
13
|
+
*
|
|
14
|
+
* This script is designed to be called from the post-edit or post-build hook
|
|
15
|
+
* as a lightweight, non-LLM enrichment step that runs in <2s.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node scripts/ua-enrich.mjs [--dir <projectDir>] [--file <changedFile>] [--db <monograph.db>]
|
|
19
|
+
*
|
|
20
|
+
* Options:
|
|
21
|
+
* --dir Project root to scan (default: cwd)
|
|
22
|
+
* --file Single file to re-enrich (incremental mode)
|
|
23
|
+
* --db Path to monograph.db (default: <dir>/.monomind/monograph.db)
|
|
24
|
+
* --full Force full scan even if graph.json exists
|
|
25
|
+
*
|
|
26
|
+
* Env:
|
|
27
|
+
* UA_GRAPH_JSON Override path to UA graph.json
|
|
28
|
+
* UA_PLUGIN_DIR Override path to understand-anything-plugin directory
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync, existsSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
32
|
+
import { resolve, join, dirname, basename } from 'path';
|
|
33
|
+
import { createRequire } from 'module';
|
|
34
|
+
import { fileURLToPath } from 'url';
|
|
35
|
+
import { execSync, spawnSync } from 'child_process';
|
|
36
|
+
|
|
37
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const CWD = process.cwd();
|
|
39
|
+
|
|
40
|
+
// ── Parse CLI args ───────────────────────────────────────────────────────────
|
|
41
|
+
function arg(name) {
|
|
42
|
+
const i = process.argv.indexOf('--' + name);
|
|
43
|
+
return i !== -1 ? process.argv[i + 1] : null;
|
|
44
|
+
}
|
|
45
|
+
const hasFlag = (f) => process.argv.includes('--' + f);
|
|
46
|
+
|
|
47
|
+
const projectDir = resolve(arg('dir') || CWD);
|
|
48
|
+
const changedFile = arg('file') ? resolve(arg('file')) : null;
|
|
49
|
+
const dbPath = resolve(arg('db') || join(projectDir, '.monomind', 'monograph.db'));
|
|
50
|
+
const fullScan = hasFlag('full');
|
|
51
|
+
|
|
52
|
+
// ── Locate Understand-Anything plugin ───────────────────────────────────────
|
|
53
|
+
function findUAPlugin() {
|
|
54
|
+
const envPath = process.env.UA_PLUGIN_DIR;
|
|
55
|
+
if (envPath && existsSync(envPath)) return resolve(envPath);
|
|
56
|
+
|
|
57
|
+
// Common sibling locations relative to the monomind root
|
|
58
|
+
const candidates = [
|
|
59
|
+
join(__dir, '..', '..', 'knowledgegraph', 'Understand-Anything', 'understand-anything-plugin'),
|
|
60
|
+
join(dirname(__dir), '..', 'knowledgegraph', 'Understand-Anything', 'understand-anything-plugin'),
|
|
61
|
+
];
|
|
62
|
+
for (const c of candidates) {
|
|
63
|
+
if (existsSync(c)) return c;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Locate extract-structure.mjs ─────────────────────────────────────────────
|
|
69
|
+
function findExtractScript(pluginDir) {
|
|
70
|
+
const candidates = [
|
|
71
|
+
join(pluginDir, 'skills', 'understand', 'extract-structure.mjs'),
|
|
72
|
+
join(pluginDir, 'extract-structure.mjs'),
|
|
73
|
+
];
|
|
74
|
+
for (const c of candidates) {
|
|
75
|
+
if (existsSync(c)) return c;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Resolve monograph package ────────────────────────────────────────────────
|
|
81
|
+
function requireMonograph() {
|
|
82
|
+
const require = createRequire(import.meta.url);
|
|
83
|
+
const candidates = [
|
|
84
|
+
join(CWD, 'node_modules/.pnpm/node_modules/@monoes/monograph'),
|
|
85
|
+
join(CWD, 'packages/node_modules/.pnpm/node_modules/@monoes/monograph'),
|
|
86
|
+
join(CWD, 'node_modules/@monoes/monograph'),
|
|
87
|
+
];
|
|
88
|
+
for (const c of candidates) {
|
|
89
|
+
try { if (existsSync(c)) return require(c); } catch {}
|
|
90
|
+
}
|
|
91
|
+
try { return require('@monoes/monograph'); } catch {}
|
|
92
|
+
throw new Error('Cannot find @monoes/monograph');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Check for existing UA graph.json ────────────────────────────────────────
|
|
96
|
+
function findUAGraph(dir) {
|
|
97
|
+
if (process.env.UA_GRAPH_JSON) return process.env.UA_GRAPH_JSON;
|
|
98
|
+
const candidates = [
|
|
99
|
+
join(dir, '.understand', 'knowledge-graph.json'),
|
|
100
|
+
join(dir, '.understand', 'graph.json'),
|
|
101
|
+
join(dir, '.ua', 'knowledge-graph.json'),
|
|
102
|
+
join(dir, '.ua', 'graph.json'),
|
|
103
|
+
join(dir, 'graph.json'),
|
|
104
|
+
];
|
|
105
|
+
for (const c of candidates) {
|
|
106
|
+
if (existsSync(c)) return c;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
112
|
+
async function main() {
|
|
113
|
+
console.log('[UA-ENRICH] Starting enrichment for', projectDir);
|
|
114
|
+
|
|
115
|
+
if (!existsSync(dbPath)) {
|
|
116
|
+
console.log('[UA-ENRICH] monograph.db not found — skipping (build monograph first)');
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const mg = requireMonograph();
|
|
121
|
+
const db = mg.openDb(dbPath);
|
|
122
|
+
|
|
123
|
+
// ── Phase 1: Try importing existing UA graph.json ──────────────────────────
|
|
124
|
+
const existingGraph = findUAGraph(projectDir);
|
|
125
|
+
if (existingGraph && !fullScan) {
|
|
126
|
+
const stat = statSync(existingGraph);
|
|
127
|
+
const ageHours = (Date.now() - stat.mtimeMs) / 3_600_000;
|
|
128
|
+
if (ageHours < 24) {
|
|
129
|
+
console.log(`[UA-ENRICH] Found recent graph.json (${ageHours.toFixed(1)}h old) — importing`);
|
|
130
|
+
mg.closeDb(db);
|
|
131
|
+
// Delegate to ua-import.mjs
|
|
132
|
+
const importScript = join(__dir, 'ua-import.mjs');
|
|
133
|
+
if (existsSync(importScript)) {
|
|
134
|
+
const result = spawnSync(process.execPath, [importScript, existingGraph, dbPath], {
|
|
135
|
+
stdio: 'inherit', cwd: CWD,
|
|
136
|
+
});
|
|
137
|
+
process.exit(result.status ?? 0);
|
|
138
|
+
}
|
|
139
|
+
console.log('[UA-ENRICH] ua-import.mjs not found — continuing with direct enrichment');
|
|
140
|
+
} else {
|
|
141
|
+
console.log(`[UA-ENRICH] graph.json is ${ageHours.toFixed(0)}h old — will re-enrich from DB only`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Phase 2: Deterministic structural extraction (no LLM) ─────────────────
|
|
146
|
+
// Run UA's extract-structure.mjs on the target file/directory and capture output.
|
|
147
|
+
const pluginDir = findUAPlugin();
|
|
148
|
+
const extractScript = pluginDir ? findExtractScript(pluginDir) : null;
|
|
149
|
+
|
|
150
|
+
if (extractScript && changedFile) {
|
|
151
|
+
console.log('[UA-ENRICH] Running deterministic extraction on', basename(changedFile));
|
|
152
|
+
try {
|
|
153
|
+
const result = spawnSync(process.execPath, [extractScript, changedFile], {
|
|
154
|
+
cwd: projectDir,
|
|
155
|
+
timeout: 10_000,
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
});
|
|
158
|
+
if (result.stdout) {
|
|
159
|
+
let extracted;
|
|
160
|
+
try { extracted = JSON.parse(result.stdout); } catch { extracted = null; }
|
|
161
|
+
if (extracted && extracted.functions) {
|
|
162
|
+
// Write extracted structural data back into the node's properties
|
|
163
|
+
const normPath = changedFile.startsWith(projectDir)
|
|
164
|
+
? changedFile.slice(projectDir.length + 1)
|
|
165
|
+
: changedFile;
|
|
166
|
+
const row = db.prepare("SELECT id, properties FROM nodes WHERE file_path LIKE ? LIMIT 1")
|
|
167
|
+
.get('%' + basename(changedFile));
|
|
168
|
+
if (row) {
|
|
169
|
+
const existing = row.properties ? JSON.parse(row.properties) : {};
|
|
170
|
+
const merged = {
|
|
171
|
+
...existing,
|
|
172
|
+
ua_extracted: {
|
|
173
|
+
functions: extracted.functions?.length ?? 0,
|
|
174
|
+
classes: extracted.classes?.length ?? 0,
|
|
175
|
+
imports: extracted.imports?.length ?? 0,
|
|
176
|
+
exports: extracted.exports?.length ?? 0,
|
|
177
|
+
updatedAt: new Date().toISOString(),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
db.prepare("UPDATE nodes SET properties = ? WHERE id = ?")
|
|
181
|
+
.run(JSON.stringify(merged), row.id);
|
|
182
|
+
console.log(`[UA-ENRICH] Updated structural data for ${row.id}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.log('[UA-ENRICH] Extraction warning:', e.message);
|
|
188
|
+
}
|
|
189
|
+
} else if (!extractScript) {
|
|
190
|
+
console.log('[UA-ENRICH] UA extract script not found — skipping deterministic extraction');
|
|
191
|
+
console.log('[UA-ENRICH] Set UA_PLUGIN_DIR env var or place Understand-Anything beside monomind');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Phase 3: Propagate existing UA summaries to FTS ───────────────────────
|
|
195
|
+
// If nodes have ua_type/summary in properties but FTS is stale, rebuild it.
|
|
196
|
+
try {
|
|
197
|
+
const enrichedCount = db.prepare(
|
|
198
|
+
`SELECT COUNT(*) AS c FROM nodes WHERE properties LIKE '%ua_type%'`
|
|
199
|
+
).get().c;
|
|
200
|
+
if (enrichedCount > 0) {
|
|
201
|
+
console.log(`[UA-ENRICH] ${enrichedCount} UA-enriched nodes in DB`);
|
|
202
|
+
db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run();
|
|
203
|
+
console.log('[UA-ENRICH] FTS rebuilt');
|
|
204
|
+
} else {
|
|
205
|
+
console.log('[UA-ENRICH] No UA enrichment data yet — run ua-import.mjs after /understand');
|
|
206
|
+
}
|
|
207
|
+
} catch { /* FTS may not exist */ }
|
|
208
|
+
|
|
209
|
+
// ── Phase 4: Write enrichment status to .monomind/ ────────────────────────
|
|
210
|
+
try {
|
|
211
|
+
const statusPath = join(projectDir, '.monomind', 'ua-enrich-status.json');
|
|
212
|
+
writeFileSync(statusPath, JSON.stringify({
|
|
213
|
+
lastRun: new Date().toISOString(),
|
|
214
|
+
mode: changedFile ? 'incremental' : 'full',
|
|
215
|
+
file: changedFile || null,
|
|
216
|
+
pluginFound: !!pluginDir,
|
|
217
|
+
graphFound: !!existingGraph,
|
|
218
|
+
}, null, 2));
|
|
219
|
+
} catch {}
|
|
220
|
+
|
|
221
|
+
mg.closeDb(db);
|
|
222
|
+
console.log('[UA-ENRICH] Done');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
main().catch((e) => {
|
|
226
|
+
console.error('[UA-ENRICH] Error:', e.message);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
|
@@ -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`);
|