@monoes/monomindcli 1.10.0 → 1.10.2
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/.claude/commands/monomind/understand.md +111 -70
- package/package.json +2 -1
- package/scripts/deploy-ipfs-node.sh +153 -0
- package/scripts/publish-registry.ts +345 -0
- package/scripts/publish.sh +55 -0
- package/scripts/setup-ipfs-registry.md +366 -0
- package/scripts/sync-claude-assets.sh +34 -0
- package/scripts/understand-analyze.mjs +855 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* understand-analyze.mjs — Built-in semantic enrichment engine for monomind:understand
|
|
4
|
+
*
|
|
5
|
+
* Ported from Understand-Anything (understand-anything-plugin/packages/core).
|
|
6
|
+
* Ships inside @monomind/cli — no external plugin needed.
|
|
7
|
+
*
|
|
8
|
+
* Reads file nodes from monograph.db, calls the Anthropic API to generate
|
|
9
|
+
* summaries, tags, complexity, and architectural layers, then writes results
|
|
10
|
+
* back into the DB (and optionally emits a graph.json).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node understand-analyze.mjs [options]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --dir <path> Project directory (default: cwd)
|
|
17
|
+
* --db <path> monograph.db path (default: <dir>/.monomind/monograph.db)
|
|
18
|
+
* --output <path> Write a graph.json here (default: <dir>/.understand/knowledge-graph.json)
|
|
19
|
+
* --batch-size <N> Files per LLM batch (default: 5)
|
|
20
|
+
* --max-files <N> Stop after N files (0 = all, default: 0)
|
|
21
|
+
* --dry-run Print what would happen without writing to DB
|
|
22
|
+
* --no-llm Heuristic-only mode (layers + tags from paths, no API calls)
|
|
23
|
+
* --layers-only Skip per-file analysis, only (re-)detect layers
|
|
24
|
+
*
|
|
25
|
+
* Env:
|
|
26
|
+
* ANTHROPIC_API_KEY Required unless --no-llm is set
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
30
|
+
import { resolve, join, dirname, basename, relative } from 'node:path';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { execFileSync } from 'node:child_process';
|
|
34
|
+
|
|
35
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const CWD = process.cwd();
|
|
37
|
+
|
|
38
|
+
// ── CLI argument helpers ─────────────────────────────────────────────────────
|
|
39
|
+
function argVal(name) {
|
|
40
|
+
const i = process.argv.indexOf('--' + name);
|
|
41
|
+
return i !== -1 ? process.argv[i + 1] : null;
|
|
42
|
+
}
|
|
43
|
+
const hasFlag = (f) => process.argv.includes('--' + f);
|
|
44
|
+
|
|
45
|
+
const projectDir = resolve(argVal('dir') || CWD);
|
|
46
|
+
const dbPathArg = argVal('db') ? resolve(argVal('db')) : join(projectDir, '.monomind', 'monograph.db');
|
|
47
|
+
const outputArg = argVal('output')? resolve(argVal('output')) : join(projectDir, '.understand', 'knowledge-graph.json');
|
|
48
|
+
const batchSize = parseInt(argVal('batch-size') || '5', 10);
|
|
49
|
+
const maxFiles = parseInt(argVal('max-files') || '0', 10);
|
|
50
|
+
const dryRun = hasFlag('dry-run');
|
|
51
|
+
const noLlm = hasFlag('no-llm');
|
|
52
|
+
const layersOnly = hasFlag('layers-only');
|
|
53
|
+
const incremental = hasFlag('incremental');
|
|
54
|
+
const onboard = hasFlag('onboard');
|
|
55
|
+
const onboardOut = argVal('onboard-out') ? resolve(argVal('onboard-out')) : join(projectDir, 'ONBOARDING.md');
|
|
56
|
+
|
|
57
|
+
// ── Resolve @monoes/monograph for DB access ──────────────────────────────────
|
|
58
|
+
function resolveMonograph() {
|
|
59
|
+
const req = createRequire(import.meta.url);
|
|
60
|
+
const candidates = [
|
|
61
|
+
// CLI package's own node_modules (npx / global npm install)
|
|
62
|
+
join(__dir, '..', 'node_modules', '@monoes', 'monograph'),
|
|
63
|
+
// Global npm / homebrew
|
|
64
|
+
(() => { try { return join(req.resolve('npm/bin/npm-cli.js'), '..', '..', '..', '@monoes', 'monograph'); } catch { return null; } })(),
|
|
65
|
+
// Monorepo root
|
|
66
|
+
join(__dir, '..', '..', '..', '..', 'node_modules', '@monoes', 'monograph'),
|
|
67
|
+
// User project
|
|
68
|
+
join(projectDir, 'node_modules', '@monoes', 'monograph'),
|
|
69
|
+
].filter(Boolean);
|
|
70
|
+
|
|
71
|
+
for (const c of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
if (existsSync(c)) return req(c);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
// Last resort: require by name (works when installed globally)
|
|
77
|
+
try { return req('@monoes/monograph'); } catch {}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Heuristic layer detection (ported from understand-anything layer-detector) ─
|
|
82
|
+
const LAYER_PATTERNS = [
|
|
83
|
+
{ patterns: ['routes', 'controller', 'handler', 'endpoint', 'api'], name: 'API Layer', description: 'HTTP endpoints, route handlers, and API controllers' },
|
|
84
|
+
{ patterns: ['service', 'usecase', 'use-case', 'business'], name: 'Service Layer', description: 'Business logic and application services' },
|
|
85
|
+
{ patterns: ['model', 'entity', 'schema', 'database', 'db', 'migration','repository', 'repo'],
|
|
86
|
+
name: 'Data Layer', description: 'Data models, database access, and persistence' },
|
|
87
|
+
{ patterns: ['component', 'view', 'page', 'screen', 'layout', 'widget', 'ui'],
|
|
88
|
+
name: 'UI Layer', description: 'User interface components and views' },
|
|
89
|
+
{ patterns: ['middleware', 'interceptor', 'guard', 'filter', 'pipe'], name: 'Middleware Layer', description: 'Request/response middleware and interceptors' },
|
|
90
|
+
{ patterns: ['client', 'integration', 'external', 'sdk', 'vendor', 'adapter'],
|
|
91
|
+
name: 'External Services', description: 'External service integrations, SDKs, and third-party adapters' },
|
|
92
|
+
{ patterns: ['worker', 'job', 'queue', 'cron', 'consumer', 'processor', 'scheduler', 'background'],
|
|
93
|
+
name: 'Background Tasks', description: 'Background workers, job processors, and scheduled tasks' },
|
|
94
|
+
{ patterns: ['util', 'helper', 'lib', 'common', 'shared'], name: 'Utility Layer', description: 'Shared utilities, helpers, and common libraries' },
|
|
95
|
+
{ patterns: ['test', 'spec', '__test__', '__spec__', '__tests__'], name: 'Test Layer', description: 'Test files and test utilities' },
|
|
96
|
+
{ patterns: ['config', 'setting', 'env'], name: 'Configuration Layer', description: 'Application configuration and environment settings' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
function matchFileToLayer(filePath) {
|
|
100
|
+
const norm = filePath.replace(/\\/g, '/').toLowerCase();
|
|
101
|
+
const segments = norm.split('/');
|
|
102
|
+
for (const { patterns, name } of LAYER_PATTERNS) {
|
|
103
|
+
for (const seg of segments) {
|
|
104
|
+
for (const p of patterns) {
|
|
105
|
+
if (seg === p || seg === p + 's') return name;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toLayerId(name) {
|
|
113
|
+
return 'layer:' + name.toLowerCase().replace(/\s+/g, '-');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function detectLayersHeuristic(fileNodes) {
|
|
117
|
+
const map = new Map(); // layerName → nodeIds[]
|
|
118
|
+
for (const node of fileNodes) {
|
|
119
|
+
const layerName = (node.file_path && matchFileToLayer(node.file_path)) || 'Core';
|
|
120
|
+
if (!map.has(layerName)) map.set(layerName, []);
|
|
121
|
+
map.get(layerName).push(node.id);
|
|
122
|
+
}
|
|
123
|
+
const layers = [];
|
|
124
|
+
for (const [name, nodeIds] of map) {
|
|
125
|
+
const pattern = LAYER_PATTERNS.find(p => p.name === name);
|
|
126
|
+
layers.push({ id: toLayerId(name), name, description: pattern?.description ?? 'Core application files', nodeIds });
|
|
127
|
+
}
|
|
128
|
+
return layers;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Anthropic API helpers (raw fetch — no SDK needed) ────────────────────────
|
|
132
|
+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
133
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
134
|
+
const MODEL = 'claude-haiku-4-5-20251001'; // cheapest for bulk analysis
|
|
135
|
+
|
|
136
|
+
async function callClaude(systemPrompt, userPrompt, maxTokens = 1024) {
|
|
137
|
+
const resp = await fetch(ANTHROPIC_URL, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
'x-api-key': ANTHROPIC_API_KEY,
|
|
142
|
+
'anthropic-version': '2023-06-01',
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
model: MODEL,
|
|
146
|
+
max_tokens: maxTokens,
|
|
147
|
+
system: systemPrompt,
|
|
148
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
if (!resp.ok) {
|
|
152
|
+
const text = await resp.text();
|
|
153
|
+
throw new Error(`Anthropic API ${resp.status}: ${text.slice(0, 200)}`);
|
|
154
|
+
}
|
|
155
|
+
const data = await resp.json();
|
|
156
|
+
return data.content?.[0]?.text ?? '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseJson(text) {
|
|
160
|
+
try {
|
|
161
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
162
|
+
const src = fenceMatch ? fenceMatch[1] : text;
|
|
163
|
+
const objMatch = src.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
|
|
164
|
+
if (objMatch) return JSON.parse(objMatch[0]);
|
|
165
|
+
} catch {}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Per-file analysis prompt (ported from llm-analyzer.ts)
|
|
170
|
+
function buildFilePrompt(filePath, content, projectContext) {
|
|
171
|
+
const truncated = content.length > 6000 ? content.slice(0, 6000) + '\n... (truncated)' : content;
|
|
172
|
+
return `You are a code analysis assistant. Analyze the following source file and return a JSON object.
|
|
173
|
+
|
|
174
|
+
Project context: ${projectContext}
|
|
175
|
+
|
|
176
|
+
File: ${filePath}
|
|
177
|
+
|
|
178
|
+
\`\`\`
|
|
179
|
+
${truncated}
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
Return a JSON object with exactly these fields:
|
|
183
|
+
- "fileSummary": A concise summary of what this file does (1-2 sentences).
|
|
184
|
+
- "tags": An array of 2-5 relevant tags (e.g., ["utility", "async", "api"]).
|
|
185
|
+
- "complexity": One of "simple", "moderate", or "complex".
|
|
186
|
+
- "functionSummaries": An object mapping each function/method name to a 1-sentence summary (top 5 only).
|
|
187
|
+
- "classSummaries": An object mapping each class name to a 1-sentence summary.
|
|
188
|
+
|
|
189
|
+
Respond ONLY with the JSON object, no additional text.`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Batch file analysis prompt (multiple files at once for efficiency)
|
|
193
|
+
function buildBatchPrompt(files, projectContext) {
|
|
194
|
+
const fileBlocks = files.map(({ path, content }) => {
|
|
195
|
+
const truncated = content.length > 2000 ? content.slice(0, 2000) + '\n...' : content;
|
|
196
|
+
return `### ${path}\n\`\`\`\n${truncated}\n\`\`\``;
|
|
197
|
+
}).join('\n\n');
|
|
198
|
+
|
|
199
|
+
return `You are a code analysis assistant. Analyze the following source files and return a JSON object.
|
|
200
|
+
|
|
201
|
+
Project context: ${projectContext}
|
|
202
|
+
|
|
203
|
+
${fileBlocks}
|
|
204
|
+
|
|
205
|
+
Return a JSON object where each key is the exact file path and the value is:
|
|
206
|
+
- "fileSummary": 1-2 sentence summary of what the file does.
|
|
207
|
+
- "tags": 2-5 relevant tags.
|
|
208
|
+
- "complexity": "simple", "moderate", or "complex".
|
|
209
|
+
- "functionSummaries": object of function name → 1-sentence summary (top 5 per file).
|
|
210
|
+
- "classSummaries": object of class name → 1-sentence summary.
|
|
211
|
+
|
|
212
|
+
Respond ONLY with the JSON object mapping file paths to their analysis.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Layer detection prompt (ported from layer-detector.ts)
|
|
216
|
+
function buildLayerPrompt(filePaths) {
|
|
217
|
+
const list = filePaths.slice(0, 200).map(f => ` - ${f}`).join('\n');
|
|
218
|
+
return `You are a software architecture analyst. Given these file paths, identify 3-8 logical architectural layers.
|
|
219
|
+
|
|
220
|
+
${list}
|
|
221
|
+
|
|
222
|
+
Return a JSON array where each element has:
|
|
223
|
+
- "name": Short layer name (e.g., "API", "Data", "UI")
|
|
224
|
+
- "description": What this layer does (1 sentence)
|
|
225
|
+
- "filePatterns": Path prefixes that belong to this layer (e.g., ["src/routes/", "src/controllers/"])
|
|
226
|
+
|
|
227
|
+
Every file should belong to exactly one layer. Respond ONLY with the JSON array.`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── .understandignore support (ported from ignore-filter.ts) ────────────────
|
|
231
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
232
|
+
'node_modules/', '.git/', 'vendor/', 'venv/', '.venv/', '__pycache__/',
|
|
233
|
+
'dist/', 'build/', 'out/', 'coverage/', '.next/', '.cache/', '.turbo/', 'target/', 'obj/',
|
|
234
|
+
'*.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
235
|
+
'*.png', '*.jpg', '*.jpeg', '*.gif', '*.svg', '*.ico', '*.woff', '*.woff2',
|
|
236
|
+
'*.ttf', '*.eot', '*.mp3', '*.mp4', '*.pdf', '*.zip', '*.tar', '*.gz',
|
|
237
|
+
'*.min.js', '*.min.css', '*.map', '*.generated.*',
|
|
238
|
+
'.idea/', '.vscode/', '*.log',
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
function loadIgnorePatterns(dir) {
|
|
242
|
+
const patterns = [...DEFAULT_IGNORE_PATTERNS];
|
|
243
|
+
const locations = [
|
|
244
|
+
join(dir, '.understand-anything', '.understandignore'),
|
|
245
|
+
join(dir, '.understandignore'),
|
|
246
|
+
];
|
|
247
|
+
for (const p of locations) {
|
|
248
|
+
if (existsSync(p)) {
|
|
249
|
+
try {
|
|
250
|
+
const lines = readFileSync(p, 'utf-8').split('\n')
|
|
251
|
+
.map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
252
|
+
patterns.push(...lines);
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return patterns;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function makeIgnoreMatcher(patterns) {
|
|
260
|
+
return function isIgnored(filePath) {
|
|
261
|
+
const norm = filePath.replace(/\\/g, '/');
|
|
262
|
+
for (const pat of patterns) {
|
|
263
|
+
if (pat.startsWith('!')) continue; // negation — skip for simplicity
|
|
264
|
+
if (pat.endsWith('/')) {
|
|
265
|
+
// directory pattern
|
|
266
|
+
if (norm.includes('/' + pat.slice(0, -1) + '/') || norm.startsWith(pat)) return true;
|
|
267
|
+
} else if (pat.startsWith('*.')) {
|
|
268
|
+
// extension glob
|
|
269
|
+
if (norm.endsWith(pat.slice(1))) return true;
|
|
270
|
+
} else if (pat.includes('*')) {
|
|
271
|
+
// simple wildcard — match anywhere in path
|
|
272
|
+
const re = new RegExp('^' + pat.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
273
|
+
if (re.test(norm) || re.test(norm.split('/').pop() || '')) return true;
|
|
274
|
+
} else {
|
|
275
|
+
// exact segment or prefix
|
|
276
|
+
if (norm === pat || norm.includes('/' + pat) || norm.startsWith(pat)) return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const _ignorePatterns = loadIgnorePatterns(projectDir);
|
|
284
|
+
const isIgnoredByUser = makeIgnoreMatcher(_ignorePatterns);
|
|
285
|
+
|
|
286
|
+
// ── Incremental mode helpers (ported from staleness.ts) ──────────────────────
|
|
287
|
+
function getChangedFiles(dir, lastHash) {
|
|
288
|
+
try {
|
|
289
|
+
const out = execFileSync('git', ['diff', `${lastHash}..HEAD`, '--name-only'], {
|
|
290
|
+
cwd: dir, encoding: 'utf-8',
|
|
291
|
+
});
|
|
292
|
+
return out.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
293
|
+
} catch {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function getCurrentCommitHash(dir) {
|
|
299
|
+
try {
|
|
300
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf-8' }).trim();
|
|
301
|
+
} catch {
|
|
302
|
+
return '';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Language detection (slim port of language-registry.ts) ──────────────────
|
|
307
|
+
const LANGUAGE_BY_EXT = {
|
|
308
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript',
|
|
309
|
+
'.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
|
|
310
|
+
'.py': 'Python', '.pyi': 'Python',
|
|
311
|
+
'.rs': 'Rust', '.go': 'Go',
|
|
312
|
+
'.java': 'Java', '.kt': 'Kotlin', '.scala': 'Scala',
|
|
313
|
+
'.rb': 'Ruby', '.php': 'PHP', '.swift': 'Swift',
|
|
314
|
+
'.c': 'C', '.h': 'C', '.cpp': 'C++', '.cc': 'C++', '.hpp': 'C++', '.hxx': 'C++',
|
|
315
|
+
'.cs': 'C#', '.fs': 'F#',
|
|
316
|
+
'.sh': 'Shell', '.bash': 'Shell', '.zsh': 'Shell',
|
|
317
|
+
'.lua': 'Lua', '.r': 'R', '.dart': 'Dart', '.ex': 'Elixir', '.exs': 'Elixir',
|
|
318
|
+
'.sol': 'Solidity', '.zig': 'Zig', '.nim': 'Nim',
|
|
319
|
+
'.html': 'HTML', '.css': 'CSS', '.scss': 'SCSS', '.vue': 'Vue', '.svelte': 'Svelte',
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
function detectLanguages(fileNodes) {
|
|
323
|
+
const counts = new Map();
|
|
324
|
+
for (const n of fileNodes) {
|
|
325
|
+
if (!n.file_path) continue;
|
|
326
|
+
const lastDot = n.file_path.lastIndexOf('.');
|
|
327
|
+
if (lastDot === -1) continue;
|
|
328
|
+
const ext = n.file_path.slice(lastDot).toLowerCase();
|
|
329
|
+
const lang = LANGUAGE_BY_EXT[ext];
|
|
330
|
+
if (lang) counts.set(lang, (counts.get(lang) || 0) + 1);
|
|
331
|
+
}
|
|
332
|
+
// Return languages with >=3 files, sorted by count desc
|
|
333
|
+
return [...counts.entries()]
|
|
334
|
+
.filter(([, c]) => c >= 3)
|
|
335
|
+
.sort((a, b) => b[1] - a[1])
|
|
336
|
+
.map(([lang]) => lang);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Framework detection (slim port of framework-registry.ts) ────────────────
|
|
340
|
+
const FRAMEWORK_SIGNATURES = [
|
|
341
|
+
// [name, manifestFile, keywords[]]
|
|
342
|
+
['React', 'package.json', ['"react"']],
|
|
343
|
+
['Next.js', 'package.json', ['"next"']],
|
|
344
|
+
['Vue', 'package.json', ['"vue"']],
|
|
345
|
+
['Svelte', 'package.json', ['"svelte"']],
|
|
346
|
+
['Angular', 'package.json', ['"@angular/core"']],
|
|
347
|
+
['Express', 'package.json', ['"express"']],
|
|
348
|
+
['Fastify', 'package.json', ['"fastify"']],
|
|
349
|
+
['NestJS', 'package.json', ['"@nestjs/core"']],
|
|
350
|
+
['Vite', 'package.json', ['"vite"']],
|
|
351
|
+
['Webpack', 'package.json', ['"webpack"']],
|
|
352
|
+
['TypeScript', 'package.json', ['"typescript"']],
|
|
353
|
+
['Anthropic SDK','package.json', ['"@anthropic-ai/sdk"']],
|
|
354
|
+
['Django', 'requirements.txt', ['django']],
|
|
355
|
+
['Flask', 'requirements.txt', ['flask']],
|
|
356
|
+
['FastAPI', 'requirements.txt', ['fastapi']],
|
|
357
|
+
['Rails', 'Gemfile', ['rails']],
|
|
358
|
+
['Spring Boot', 'pom.xml', ['spring-boot']],
|
|
359
|
+
['Axum', 'Cargo.toml', ['axum']],
|
|
360
|
+
['Actix', 'Cargo.toml', ['actix-web']],
|
|
361
|
+
['Tokio', 'Cargo.toml', ['tokio']],
|
|
362
|
+
['Gin', 'go.mod', ['gin-gonic/gin']],
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
function detectFrameworks(dir) {
|
|
366
|
+
const detected = [];
|
|
367
|
+
const seen = new Set();
|
|
368
|
+
for (const [name, manifest, keywords] of FRAMEWORK_SIGNATURES) {
|
|
369
|
+
if (seen.has(name)) continue;
|
|
370
|
+
const p = join(dir, manifest);
|
|
371
|
+
if (!existsSync(p)) continue;
|
|
372
|
+
try {
|
|
373
|
+
const content = readFileSync(p, 'utf-8').toLowerCase();
|
|
374
|
+
if (keywords.some(k => content.includes(k.toLowerCase()))) {
|
|
375
|
+
detected.push(name);
|
|
376
|
+
seen.add(name);
|
|
377
|
+
}
|
|
378
|
+
} catch {}
|
|
379
|
+
}
|
|
380
|
+
return detected;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Onboarding guide builder (ported from onboard-builder.ts) ───────────────
|
|
384
|
+
function buildOnboardingGuide(graphJson) {
|
|
385
|
+
const { project, nodes, layers, tour = [] } = graphJson;
|
|
386
|
+
const lines = [];
|
|
387
|
+
|
|
388
|
+
lines.push(`# ${project.name}`);
|
|
389
|
+
lines.push('');
|
|
390
|
+
if (project.description) {
|
|
391
|
+
lines.push(`> ${project.description}`);
|
|
392
|
+
lines.push('');
|
|
393
|
+
}
|
|
394
|
+
lines.push('| | |');
|
|
395
|
+
lines.push('|---|---|');
|
|
396
|
+
if (project.languages?.length) lines.push(`| **Languages** | ${project.languages.join(', ')} |`);
|
|
397
|
+
if (project.frameworks?.length) lines.push(`| **Frameworks** | ${project.frameworks.join(', ')} |`);
|
|
398
|
+
lines.push(`| **Components** | ${nodes.length} nodes |`);
|
|
399
|
+
lines.push(`| **Last Analyzed** | ${project.analyzedAt} |`);
|
|
400
|
+
lines.push('');
|
|
401
|
+
|
|
402
|
+
if (layers.length > 0) {
|
|
403
|
+
lines.push('## Architecture');
|
|
404
|
+
lines.push('');
|
|
405
|
+
lines.push('The project is organized into the following layers:');
|
|
406
|
+
lines.push('');
|
|
407
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
408
|
+
for (const layer of layers) {
|
|
409
|
+
lines.push(`### ${layer.name}`);
|
|
410
|
+
lines.push('');
|
|
411
|
+
if (layer.description) { lines.push(layer.description); lines.push(''); }
|
|
412
|
+
const members = (layer.nodeIds || []).map(id => nodeMap.get(id)?.name).filter(Boolean);
|
|
413
|
+
if (members.length > 0) { lines.push(`Key components: ${members.slice(0, 8).join(', ')}`); lines.push(''); }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (tour.length > 0) {
|
|
418
|
+
lines.push('## Getting Started');
|
|
419
|
+
lines.push('');
|
|
420
|
+
lines.push('Follow this guided tour to understand the codebase:');
|
|
421
|
+
lines.push('');
|
|
422
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
423
|
+
for (const step of tour) {
|
|
424
|
+
lines.push(`### ${step.order}. ${step.title}`);
|
|
425
|
+
lines.push('');
|
|
426
|
+
lines.push(step.description);
|
|
427
|
+
lines.push('');
|
|
428
|
+
const stepNodes = (step.nodeIds || []).map(id => nodeMap.get(id)).filter(Boolean);
|
|
429
|
+
if (stepNodes.length > 0) {
|
|
430
|
+
lines.push('**Files to look at:**');
|
|
431
|
+
for (const node of stepNodes) {
|
|
432
|
+
if (node.filePath) lines.push(`- \`${node.filePath}\` — ${node.summary}`);
|
|
433
|
+
}
|
|
434
|
+
lines.push('');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const fileNodes = nodes.filter(n => n.type === 'file' && n.filePath && n.summary);
|
|
440
|
+
if (fileNodes.length > 0) {
|
|
441
|
+
lines.push('## File Map');
|
|
442
|
+
lines.push('');
|
|
443
|
+
lines.push('| File | Purpose | Complexity |');
|
|
444
|
+
lines.push('|------|---------|------------|');
|
|
445
|
+
for (const node of fileNodes) {
|
|
446
|
+
const summary = (node.summary || '').replace(/\|/g, '\\|');
|
|
447
|
+
lines.push(`| \`${node.filePath}\` | ${summary} | ${node.complexity || 'moderate'} |`);
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const complexNodes = nodes.filter(n => n.complexity === 'complex');
|
|
453
|
+
if (complexNodes.length > 0) {
|
|
454
|
+
lines.push('## Complexity Hotspots');
|
|
455
|
+
lines.push('');
|
|
456
|
+
lines.push('These components are the most complex and deserve extra attention:');
|
|
457
|
+
lines.push('');
|
|
458
|
+
for (const node of complexNodes) {
|
|
459
|
+
lines.push(`- **${node.name}** (${node.type}): ${node.summary || ''}`);
|
|
460
|
+
}
|
|
461
|
+
lines.push('');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
lines.push('---');
|
|
465
|
+
lines.push('');
|
|
466
|
+
lines.push(`*Generated by [monomind](https://github.com/nokhodian/monomind) from knowledge graph v${graphJson.version}*`);
|
|
467
|
+
lines.push('');
|
|
468
|
+
|
|
469
|
+
return lines.join('\n');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── File reading with graceful skip ─────────────────────────────────────────
|
|
473
|
+
function readFileSafe(absPath) {
|
|
474
|
+
try {
|
|
475
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
476
|
+
return content;
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const SKIP_EXTENSIONS = new Set([
|
|
483
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp',
|
|
484
|
+
'.pdf', '.zip', '.tar', '.gz', '.wasm', '.map',
|
|
485
|
+
'.lock', '.lockb', '.db', '.sqlite', '.bin', '.exe',
|
|
486
|
+
]);
|
|
487
|
+
function shouldAnalyze(filePath) {
|
|
488
|
+
if (!filePath) return false;
|
|
489
|
+
const ext = '.' + filePath.split('.').pop()?.toLowerCase();
|
|
490
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
491
|
+
if (isIgnoredByUser(filePath)) return false;
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
496
|
+
async function main() {
|
|
497
|
+
console.log('[understand] Starting semantic enrichment for', projectDir);
|
|
498
|
+
|
|
499
|
+
if (!existsSync(dbPathArg)) {
|
|
500
|
+
console.error('[understand] monograph.db not found at', dbPathArg);
|
|
501
|
+
console.error('[understand] Build the graph first: npx monomind monograph build');
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const mg = resolveMonograph();
|
|
506
|
+
if (!mg) {
|
|
507
|
+
console.error('[understand] Cannot find @monoes/monograph. Run: npm install @monoes/monograph');
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const db = mg.openDb(dbPathArg);
|
|
512
|
+
|
|
513
|
+
// Ensure properties column exists
|
|
514
|
+
try { db.prepare(`ALTER TABLE nodes ADD COLUMN properties TEXT`).run(); } catch {}
|
|
515
|
+
try { db.prepare(`CREATE TABLE IF NOT EXISTS communities (id INTEGER PRIMARY KEY, label TEXT, size INTEGER NOT NULL DEFAULT 0, cohesion_score REAL NOT NULL DEFAULT 0.0)`).run(); } catch {}
|
|
516
|
+
|
|
517
|
+
// ── Load all file nodes ──────────────────────────────────────────────────
|
|
518
|
+
let fileNodes = db.prepare(`SELECT id, name, file_path, properties FROM nodes WHERE label = 'File' AND file_path IS NOT NULL`).all();
|
|
519
|
+
console.log(`[understand] Found ${fileNodes.length} file nodes in DB`);
|
|
520
|
+
|
|
521
|
+
// ── Layers-only mode ─────────────────────────────────────────────────────
|
|
522
|
+
if (layersOnly) {
|
|
523
|
+
console.log('[understand] Layers-only mode — skipping per-file analysis');
|
|
524
|
+
await detectAndWriteLayers(db, fileNodes, noLlm, dryRun);
|
|
525
|
+
mg.closeDb(db);
|
|
526
|
+
console.log('[understand] Done (layers-only)');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ── Incremental mode: only re-analyze changed files ─────────────────────
|
|
531
|
+
let changedFileSet = null; // null means "analyze all"
|
|
532
|
+
if (incremental) {
|
|
533
|
+
let lastHash = '';
|
|
534
|
+
try {
|
|
535
|
+
const row = db.prepare(`SELECT value FROM index_meta WHERE key = 'ua_last_commit'`).get();
|
|
536
|
+
lastHash = row?.value || '';
|
|
537
|
+
} catch {}
|
|
538
|
+
if (lastHash) {
|
|
539
|
+
const changed = getChangedFiles(projectDir, lastHash);
|
|
540
|
+
if (changed.length > 0) {
|
|
541
|
+
changedFileSet = new Set(changed);
|
|
542
|
+
console.log(`[understand] Incremental mode: ${changed.length} files changed since ${lastHash.slice(0, 8)}`);
|
|
543
|
+
} else {
|
|
544
|
+
console.log('[understand] Incremental mode: no changes detected since last run — skipping analysis');
|
|
545
|
+
mg.closeDb(db);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
console.log('[understand] Incremental mode: no previous commit hash found — running full analysis');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ── Filter files that need analysis ─────────────────────────────────────
|
|
554
|
+
const toAnalyze = fileNodes.filter(n => {
|
|
555
|
+
if (!shouldAnalyze(n.file_path)) return false;
|
|
556
|
+
if (changedFileSet) {
|
|
557
|
+
// Match by relative or absolute path
|
|
558
|
+
const rel = n.file_path.startsWith('/') ? relative(projectDir, n.file_path) : n.file_path;
|
|
559
|
+
return changedFileSet.has(rel) || changedFileSet.has(n.file_path);
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
});
|
|
563
|
+
const limit = maxFiles > 0 ? Math.min(maxFiles, toAnalyze.length) : toAnalyze.length;
|
|
564
|
+
const batch = toAnalyze.slice(0, limit);
|
|
565
|
+
console.log(`[understand] Analyzing ${batch.length} files (${toAnalyze.length - batch.length} skipped/already enriched)`);
|
|
566
|
+
|
|
567
|
+
if (!ANTHROPIC_API_KEY && !noLlm) {
|
|
568
|
+
console.warn('[understand] ANTHROPIC_API_KEY not set — falling back to --no-llm heuristic mode');
|
|
569
|
+
// Fall through to heuristic only
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const useLlm = !noLlm && !!ANTHROPIC_API_KEY;
|
|
573
|
+
|
|
574
|
+
// ── Get project context for better prompts ────────────────────────────────
|
|
575
|
+
let projectContext = `Project directory: ${basename(projectDir)}`;
|
|
576
|
+
try {
|
|
577
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
578
|
+
if (existsSync(pkgPath)) {
|
|
579
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
580
|
+
projectContext = `${pkg.name || basename(projectDir)}: ${pkg.description || ''}`.trim();
|
|
581
|
+
}
|
|
582
|
+
} catch {}
|
|
583
|
+
|
|
584
|
+
// ── Per-file LLM analysis ────────────────────────────────────────────────
|
|
585
|
+
const analysisMap = {}; // file_path → { fileSummary, tags, complexity, functionSummaries, classSummaries }
|
|
586
|
+
|
|
587
|
+
if (useLlm && batch.length > 0) {
|
|
588
|
+
const system = 'You are an expert code analysis assistant. Always respond with valid JSON only.';
|
|
589
|
+
const chunks = [];
|
|
590
|
+
for (let i = 0; i < batch.length; i += batchSize) chunks.push(batch.slice(i, i + batchSize));
|
|
591
|
+
|
|
592
|
+
let done = 0;
|
|
593
|
+
for (const chunk of chunks) {
|
|
594
|
+
// Read file contents
|
|
595
|
+
const files = chunk
|
|
596
|
+
.map(n => {
|
|
597
|
+
const absPath = n.file_path.startsWith('/') ? n.file_path : join(projectDir, n.file_path);
|
|
598
|
+
const content = readFileSafe(absPath);
|
|
599
|
+
return content ? { path: n.file_path, content, nodeId: n.id } : null;
|
|
600
|
+
})
|
|
601
|
+
.filter(Boolean);
|
|
602
|
+
|
|
603
|
+
if (files.length === 0) { done += chunk.length; continue; }
|
|
604
|
+
|
|
605
|
+
process.stdout.write(`\r[understand] Analyzing files ${done + 1}–${Math.min(done + files.length, batch.length)} / ${batch.length}...`);
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
let text;
|
|
609
|
+
if (files.length === 1) {
|
|
610
|
+
text = await callClaude(system, buildFilePrompt(files[0].path, files[0].content, projectContext));
|
|
611
|
+
const parsed = parseJson(text);
|
|
612
|
+
if (parsed) analysisMap[files[0].path] = parsed;
|
|
613
|
+
} else {
|
|
614
|
+
text = await callClaude(system, buildBatchPrompt(files, projectContext), 2048);
|
|
615
|
+
const parsed = parseJson(text);
|
|
616
|
+
if (parsed && typeof parsed === 'object') {
|
|
617
|
+
for (const [fp, analysis] of Object.entries(parsed)) {
|
|
618
|
+
if (analysis && typeof analysis === 'object') analysisMap[fp] = analysis;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch (e) {
|
|
623
|
+
console.warn(`\n[understand] API error for batch at ${done}: ${e.message}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
done += files.length;
|
|
627
|
+
// Polite rate limiting
|
|
628
|
+
if (chunks.indexOf(chunk) < chunks.length - 1) await new Promise(r => setTimeout(r, 300));
|
|
629
|
+
}
|
|
630
|
+
console.log(`\n[understand] LLM analysis complete: ${Object.keys(analysisMap).length} files analyzed`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── Write analysis back to DB ─────────────────────────────────────────────
|
|
634
|
+
const updateNode = db.prepare(`UPDATE nodes SET properties = ? WHERE id = ?`);
|
|
635
|
+
let written = 0;
|
|
636
|
+
|
|
637
|
+
if (!dryRun) {
|
|
638
|
+
const tx = db.transaction(() => {
|
|
639
|
+
for (const node of batch) {
|
|
640
|
+
const existing = node.properties ? (() => { try { return JSON.parse(node.properties); } catch { return {}; } })() : {};
|
|
641
|
+
const llmData = analysisMap[node.file_path] || {};
|
|
642
|
+
const merged = {
|
|
643
|
+
...existing,
|
|
644
|
+
...(llmData.fileSummary ? { summary: llmData.fileSummary } : {}),
|
|
645
|
+
...(llmData.tags ? { tags: llmData.tags } : {}),
|
|
646
|
+
...(llmData.complexity ? { complexity: llmData.complexity } : {}),
|
|
647
|
+
...(llmData.languageNotes ? { languageNotes: llmData.languageNotes } : {}),
|
|
648
|
+
...(llmData.functionSummaries ? { functionSummaries: llmData.functionSummaries } : {}),
|
|
649
|
+
...(llmData.classSummaries ? { classSummaries: llmData.classSummaries } : {}),
|
|
650
|
+
ua_analyzed_at: new Date().toISOString(),
|
|
651
|
+
};
|
|
652
|
+
updateNode.run(JSON.stringify(merged), node.id);
|
|
653
|
+
written++;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
tx();
|
|
657
|
+
console.log(`[understand] Wrote enrichment data to ${written} nodes`);
|
|
658
|
+
} else {
|
|
659
|
+
console.log(`[understand] DRY RUN — would update ${batch.length} nodes`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── Layer detection ──────────────────────────────────────────────────────
|
|
663
|
+
const layers = await detectAndWriteLayers(db, fileNodes, noLlm || !useLlm, dryRun, projectDir);
|
|
664
|
+
|
|
665
|
+
// ── Emit graph.json (for compatibility with ua-import.mjs) ──────────────
|
|
666
|
+
const graphJson = buildGraphJson(projectDir, fileNodes, analysisMap, layers);
|
|
667
|
+
const outputDir = dirname(outputArg);
|
|
668
|
+
if (!dryRun) {
|
|
669
|
+
mkdirSync(outputDir, { recursive: true });
|
|
670
|
+
writeFileSync(outputArg, JSON.stringify(graphJson, null, 2), 'utf-8');
|
|
671
|
+
console.log(`[understand] graph.json written to ${outputArg}`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ── Rebuild FTS ──────────────────────────────────────────────────────────
|
|
675
|
+
if (!dryRun) {
|
|
676
|
+
try {
|
|
677
|
+
db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run();
|
|
678
|
+
console.log('[understand] FTS index rebuilt');
|
|
679
|
+
} catch {}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ── Update index_meta ────────────────────────────────────────────────────
|
|
683
|
+
if (!dryRun) {
|
|
684
|
+
const upsertMeta = db.prepare(
|
|
685
|
+
`INSERT INTO index_meta (key, value) VALUES (?, ?)
|
|
686
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value`
|
|
687
|
+
);
|
|
688
|
+
upsertMeta.run('ua_analyzed_at', new Date().toISOString());
|
|
689
|
+
const currentHash = getCurrentCommitHash(projectDir);
|
|
690
|
+
if (currentHash) upsertMeta.run('ua_last_commit', currentHash);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
mg.closeDb(db);
|
|
694
|
+
|
|
695
|
+
// ── Onboarding guide ─────────────────────────────────────────────────────
|
|
696
|
+
let onboardWritten = false;
|
|
697
|
+
if (onboard && !dryRun) {
|
|
698
|
+
const guide = buildOnboardingGuide(graphJson);
|
|
699
|
+
writeFileSync(onboardOut, guide, 'utf-8');
|
|
700
|
+
onboardWritten = true;
|
|
701
|
+
console.log(`[understand] Onboarding guide written to ${relative(CWD, onboardOut)}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ── Final report ─────────────────────────────────────────────────────────
|
|
705
|
+
console.log('\n╔══════════════════════════════════════════════════╗');
|
|
706
|
+
console.log('║ /monomind:understand — Enrichment Complete ║');
|
|
707
|
+
console.log('╠══════════════════════════════════════════════════╣');
|
|
708
|
+
console.log(`║ DB: ${relative(CWD, dbPathArg).padEnd(31)}║`);
|
|
709
|
+
console.log(`║ Nodes enriched: ${String(written).padEnd(31)}║`);
|
|
710
|
+
console.log(`║ Communities: ${String(layers.length).padEnd(31)}║`);
|
|
711
|
+
console.log(`║ graph.json: ${relative(CWD, outputArg).padEnd(31)}║`);
|
|
712
|
+
if (onboardWritten) {
|
|
713
|
+
console.log(`║ ONBOARDING.md: ${relative(CWD, onboardOut).padEnd(31)}║`);
|
|
714
|
+
}
|
|
715
|
+
console.log('╚══════════════════════════════════════════════════╝');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── Detect layers and write communities to DB ────────────────────────────────
|
|
719
|
+
async function detectAndWriteLayers(db, fileNodes, forceHeuristic, dryRun, dir) {
|
|
720
|
+
let layers;
|
|
721
|
+
|
|
722
|
+
if (!forceHeuristic && ANTHROPIC_API_KEY) {
|
|
723
|
+
console.log('[understand] Detecting architectural layers via LLM...');
|
|
724
|
+
const filePaths = fileNodes.map(n => n.file_path).filter(Boolean);
|
|
725
|
+
try {
|
|
726
|
+
const system = 'You are a software architecture expert. Respond with valid JSON only.';
|
|
727
|
+
const text = await callClaude(system, buildLayerPrompt(filePaths), 1024);
|
|
728
|
+
const parsed = parseJson(text);
|
|
729
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
730
|
+
layers = applyLlmLayers(fileNodes, parsed);
|
|
731
|
+
console.log(`[understand] LLM detected ${layers.length} layers`);
|
|
732
|
+
}
|
|
733
|
+
} catch (e) {
|
|
734
|
+
console.warn('[understand] LLM layer detection failed, falling back to heuristic:', e.message);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!layers) {
|
|
739
|
+
layers = detectLayersHeuristic(fileNodes);
|
|
740
|
+
console.log(`[understand] Heuristic layer detection: ${layers.length} layers`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!dryRun) {
|
|
744
|
+
let communityIdx = 1000;
|
|
745
|
+
const upsertCommunity = db.prepare(
|
|
746
|
+
`INSERT INTO communities (id, label, size, cohesion_score)
|
|
747
|
+
VALUES (?, ?, ?, 0.8)
|
|
748
|
+
ON CONFLICT(id) DO UPDATE SET label=excluded.label, size=excluded.size`
|
|
749
|
+
);
|
|
750
|
+
const updateNodeCommunity = db.prepare(`UPDATE nodes SET community_id = ? WHERE id = ?`);
|
|
751
|
+
|
|
752
|
+
const tx = db.transaction(() => {
|
|
753
|
+
for (const layer of layers) {
|
|
754
|
+
upsertCommunity.run(communityIdx, layer.name, layer.nodeIds.length);
|
|
755
|
+
for (const nodeId of layer.nodeIds) {
|
|
756
|
+
updateNodeCommunity.run(communityIdx, nodeId);
|
|
757
|
+
}
|
|
758
|
+
communityIdx++;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
tx();
|
|
762
|
+
console.log(`[understand] Wrote ${layers.length} communities to DB`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return layers;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Apply LLM-suggested layer patterns to file nodes (ported from applyLLMLayers)
|
|
769
|
+
function applyLlmLayers(fileNodes, llmLayers) {
|
|
770
|
+
const map = new Map();
|
|
771
|
+
for (const l of llmLayers) map.set(l.name, []);
|
|
772
|
+
|
|
773
|
+
for (const node of fileNodes) {
|
|
774
|
+
if (!node.file_path) {
|
|
775
|
+
const other = map.get('Other') ?? [];
|
|
776
|
+
other.push(node.id);
|
|
777
|
+
map.set('Other', other);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const norm = node.file_path.replace(/\\/g, '/');
|
|
781
|
+
let assigned = false;
|
|
782
|
+
for (const l of llmLayers) {
|
|
783
|
+
for (const pattern of (l.filePatterns || [])) {
|
|
784
|
+
if (norm.startsWith(pattern) || norm.includes('/' + pattern)) {
|
|
785
|
+
map.get(l.name).push(node.id);
|
|
786
|
+
assigned = true;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (assigned) break;
|
|
791
|
+
}
|
|
792
|
+
if (!assigned) {
|
|
793
|
+
const other = map.get('Other') ?? [];
|
|
794
|
+
other.push(node.id);
|
|
795
|
+
map.set('Other', other);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const layers = [];
|
|
800
|
+
for (const [name, nodeIds] of map) {
|
|
801
|
+
if (nodeIds.length === 0) continue;
|
|
802
|
+
const l = llmLayers.find(x => x.name === name);
|
|
803
|
+
layers.push({ id: toLayerId(name), name, description: l?.description ?? 'Uncategorized', nodeIds });
|
|
804
|
+
}
|
|
805
|
+
return layers;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Build a KnowledgeGraph-compatible graph.json for ua-import compatibility
|
|
809
|
+
function buildGraphJson(dir, fileNodes, analysisMap, layers) {
|
|
810
|
+
const projectName = basename(dir);
|
|
811
|
+
const nodes = fileNodes.map(n => {
|
|
812
|
+
const a = analysisMap[n.file_path] || {};
|
|
813
|
+
return {
|
|
814
|
+
id: 'file:' + (n.file_path || n.name),
|
|
815
|
+
type: 'file',
|
|
816
|
+
name: n.name,
|
|
817
|
+
filePath: n.file_path,
|
|
818
|
+
summary: a.fileSummary || '',
|
|
819
|
+
tags: a.tags || [],
|
|
820
|
+
complexity: a.complexity || 'moderate',
|
|
821
|
+
};
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Project metadata
|
|
825
|
+
let description = '';
|
|
826
|
+
try {
|
|
827
|
+
const pkgPath = join(dir, 'package.json');
|
|
828
|
+
if (existsSync(pkgPath)) {
|
|
829
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
830
|
+
description = pkg.description || '';
|
|
831
|
+
}
|
|
832
|
+
} catch {}
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
version: '2.7.0',
|
|
836
|
+
kind: 'codebase',
|
|
837
|
+
project: {
|
|
838
|
+
name: projectName,
|
|
839
|
+
languages: detectLanguages(fileNodes),
|
|
840
|
+
frameworks: detectFrameworks(dir),
|
|
841
|
+
description,
|
|
842
|
+
analyzedAt: new Date().toISOString(),
|
|
843
|
+
gitCommitHash: getCurrentCommitHash(dir),
|
|
844
|
+
},
|
|
845
|
+
nodes,
|
|
846
|
+
edges: [],
|
|
847
|
+
layers: layers.map(l => ({ id: l.id, name: l.name, description: l.description, nodeIds: l.nodeIds })),
|
|
848
|
+
tour: [],
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
main().catch(e => {
|
|
853
|
+
console.error('[understand] Fatal error:', e.message);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
});
|