@optave/codegraph 2.4.0 → 2.5.1
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/README.md +66 -10
- package/package.json +15 -5
- package/src/branch-compare.js +568 -0
- package/src/builder.js +183 -22
- package/src/cli.js +253 -8
- package/src/cochange.js +8 -8
- package/src/communities.js +303 -0
- package/src/complexity.js +2056 -0
- package/src/config.js +20 -1
- package/src/db.js +111 -1
- package/src/embedder.js +49 -12
- package/src/export.js +25 -1
- package/src/flow.js +361 -0
- package/src/index.js +32 -2
- package/src/manifesto.js +442 -0
- package/src/mcp.js +244 -5
- package/src/paginate.js +70 -0
- package/src/parser.js +21 -5
- package/src/queries.js +396 -7
- package/src/registry.js +6 -3
- package/src/structure.js +88 -24
- package/src/update-check.js +1 -0
- package/src/watcher.js +2 -2
package/src/builder.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
|
|
6
|
-
import { initSchema, openDb } from './db.js';
|
|
7
|
+
import { closeDb, getBuildMeta, initSchema, openDb, setBuildMeta } from './db.js';
|
|
7
8
|
import { readJournal, writeJournalHeader } from './journal.js';
|
|
8
9
|
import { debug, info, warn } from './logger.js';
|
|
9
10
|
import { getActiveEngine, parseFilesAuto } from './parser.js';
|
|
@@ -11,6 +12,11 @@ import { computeConfidence, resolveImportPath, resolveImportsBatch } from './res
|
|
|
11
12
|
|
|
12
13
|
export { resolveImportPath } from './resolve.js';
|
|
13
14
|
|
|
15
|
+
const __builderDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
|
|
16
|
+
const CODEGRAPH_VERSION = JSON.parse(
|
|
17
|
+
fs.readFileSync(path.join(__builderDir, '..', 'package.json'), 'utf-8'),
|
|
18
|
+
).version;
|
|
19
|
+
|
|
14
20
|
const BUILTIN_RECEIVERS = new Set([
|
|
15
21
|
'console',
|
|
16
22
|
'Math',
|
|
@@ -346,6 +352,22 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
346
352
|
const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
|
|
347
353
|
info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
|
|
348
354
|
|
|
355
|
+
// Check for engine/version mismatch on incremental builds
|
|
356
|
+
if (incremental) {
|
|
357
|
+
const prevEngine = getBuildMeta(db, 'engine');
|
|
358
|
+
const prevVersion = getBuildMeta(db, 'codegraph_version');
|
|
359
|
+
if (prevEngine && prevEngine !== engineName) {
|
|
360
|
+
warn(
|
|
361
|
+
`Engine changed (${prevEngine} → ${engineName}). Consider rebuilding with --no-incremental for consistency.`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (prevVersion && prevVersion !== CODEGRAPH_VERSION) {
|
|
365
|
+
warn(
|
|
366
|
+
`Codegraph version changed (${prevVersion} → ${CODEGRAPH_VERSION}). Consider rebuilding with --no-incremental for consistency.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
349
371
|
const aliases = loadPathAliases(rootDir);
|
|
350
372
|
// Merge config aliases
|
|
351
373
|
if (config.aliases) {
|
|
@@ -397,7 +419,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
397
419
|
}
|
|
398
420
|
}
|
|
399
421
|
info('No changes detected. Graph is up to date.');
|
|
400
|
-
db
|
|
422
|
+
closeDb(db);
|
|
401
423
|
writeJournalHeader(rootDir, Date.now());
|
|
402
424
|
return;
|
|
403
425
|
}
|
|
@@ -420,7 +442,45 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
420
442
|
: deletions,
|
|
421
443
|
);
|
|
422
444
|
} else {
|
|
423
|
-
|
|
445
|
+
// ── Reverse-dependency cascade (issue #116) ─────────────────────
|
|
446
|
+
// Find files with edges pointing TO changed/removed files.
|
|
447
|
+
// Their nodes stay intact (preserving IDs), but outgoing edges are
|
|
448
|
+
// deleted so they can be rebuilt during the edge-building pass.
|
|
449
|
+
const changedRelPaths = new Set();
|
|
450
|
+
for (const item of parseChanges) {
|
|
451
|
+
changedRelPaths.add(item.relPath || normalizePath(path.relative(rootDir, item.file)));
|
|
452
|
+
}
|
|
453
|
+
for (const relPath of removed) {
|
|
454
|
+
changedRelPaths.add(relPath);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const reverseDeps = new Set();
|
|
458
|
+
if (changedRelPaths.size > 0) {
|
|
459
|
+
const findReverseDeps = db.prepare(`
|
|
460
|
+
SELECT DISTINCT n_src.file FROM edges e
|
|
461
|
+
JOIN nodes n_src ON e.source_id = n_src.id
|
|
462
|
+
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
463
|
+
WHERE n_tgt.file = ? AND n_src.file != n_tgt.file
|
|
464
|
+
`);
|
|
465
|
+
for (const relPath of changedRelPaths) {
|
|
466
|
+
for (const row of findReverseDeps.all(relPath)) {
|
|
467
|
+
if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
|
|
468
|
+
// Verify the file still exists on disk
|
|
469
|
+
const absPath = path.join(rootDir, row.file);
|
|
470
|
+
if (fs.existsSync(absPath)) {
|
|
471
|
+
reverseDeps.add(row.file);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
info(
|
|
479
|
+
`Incremental: ${parseChanges.length} changed, ${removed.length} removed${reverseDeps.size > 0 ? `, ${reverseDeps.size} reverse-deps` : ''}`,
|
|
480
|
+
);
|
|
481
|
+
if (parseChanges.length > 0)
|
|
482
|
+
debug(`Changed files: ${parseChanges.map((c) => c.relPath).join(', ')}`);
|
|
483
|
+
if (removed.length > 0) debug(`Removed files: ${removed.join(', ')}`);
|
|
424
484
|
// Remove embeddings/metrics/edges/nodes for changed and removed files
|
|
425
485
|
// Embeddings must be deleted BEFORE nodes (we need node IDs to find them)
|
|
426
486
|
const deleteEmbeddingsForFile = hasEmbeddings
|
|
@@ -431,13 +491,25 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
431
491
|
DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f)
|
|
432
492
|
OR target_id IN (SELECT id FROM nodes WHERE file = @f)
|
|
433
493
|
`);
|
|
494
|
+
const deleteOutgoingEdgesForFile = db.prepare(
|
|
495
|
+
'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
496
|
+
);
|
|
434
497
|
const deleteMetricsForFile = db.prepare(
|
|
435
498
|
'DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
436
499
|
);
|
|
500
|
+
let deleteComplexityForFile;
|
|
501
|
+
try {
|
|
502
|
+
deleteComplexityForFile = db.prepare(
|
|
503
|
+
'DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
504
|
+
);
|
|
505
|
+
} catch {
|
|
506
|
+
deleteComplexityForFile = null;
|
|
507
|
+
}
|
|
437
508
|
for (const relPath of removed) {
|
|
438
509
|
deleteEmbeddingsForFile?.run(relPath);
|
|
439
510
|
deleteEdgesForFile.run({ f: relPath });
|
|
440
511
|
deleteMetricsForFile.run(relPath);
|
|
512
|
+
deleteComplexityForFile?.run(relPath);
|
|
441
513
|
deleteNodesForFile.run(relPath);
|
|
442
514
|
}
|
|
443
515
|
for (const item of parseChanges) {
|
|
@@ -445,8 +517,19 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
445
517
|
deleteEmbeddingsForFile?.run(relPath);
|
|
446
518
|
deleteEdgesForFile.run({ f: relPath });
|
|
447
519
|
deleteMetricsForFile.run(relPath);
|
|
520
|
+
deleteComplexityForFile?.run(relPath);
|
|
448
521
|
deleteNodesForFile.run(relPath);
|
|
449
522
|
}
|
|
523
|
+
|
|
524
|
+
// Process reverse deps: delete only outgoing edges (nodes/IDs preserved)
|
|
525
|
+
// then add them to the parse list so they participate in edge building
|
|
526
|
+
for (const relPath of reverseDeps) {
|
|
527
|
+
deleteOutgoingEdgesForFile.run(relPath);
|
|
528
|
+
}
|
|
529
|
+
for (const relPath of reverseDeps) {
|
|
530
|
+
const absPath = path.join(rootDir, relPath);
|
|
531
|
+
parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
532
|
+
}
|
|
450
533
|
}
|
|
451
534
|
|
|
452
535
|
const insertNode = db.prepare(
|
|
@@ -483,9 +566,14 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
483
566
|
|
|
484
567
|
const filesToParse = isFullBuild ? files.map((f) => ({ file: f })) : parseChanges;
|
|
485
568
|
|
|
569
|
+
// ── Phase timing ────────────────────────────────────────────────────
|
|
570
|
+
const _t = {};
|
|
571
|
+
|
|
486
572
|
// ── Unified parse via parseFilesAuto ───────────────────────────────
|
|
487
573
|
const filePaths = filesToParse.map((item) => item.file);
|
|
574
|
+
_t.parse0 = performance.now();
|
|
488
575
|
const allSymbols = await parseFilesAuto(filePaths, rootDir, engineOpts);
|
|
576
|
+
_t.parseMs = performance.now() - _t.parse0;
|
|
489
577
|
|
|
490
578
|
// Build a lookup from incremental data (changed items may carry pre-computed hashes + stats)
|
|
491
579
|
const precomputedData = new Map();
|
|
@@ -508,9 +596,12 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
508
596
|
}
|
|
509
597
|
|
|
510
598
|
// Update file hash with real mtime+size for incremental builds
|
|
599
|
+
// Skip for reverse-dep files — they didn't actually change
|
|
511
600
|
if (upsertHash) {
|
|
512
601
|
const precomputed = precomputedData.get(relPath);
|
|
513
|
-
if (precomputed?.
|
|
602
|
+
if (precomputed?._reverseDepOnly) {
|
|
603
|
+
// no-op: file unchanged, hash already correct
|
|
604
|
+
} else if (precomputed?.hash) {
|
|
514
605
|
const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
|
|
515
606
|
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
516
607
|
const size = stat ? stat.size : 0;
|
|
@@ -542,7 +633,9 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
542
633
|
}
|
|
543
634
|
}
|
|
544
635
|
});
|
|
636
|
+
_t.insert0 = performance.now();
|
|
545
637
|
insertAll();
|
|
638
|
+
_t.insertMs = performance.now() - _t.insert0;
|
|
546
639
|
|
|
547
640
|
const parsed = allSymbols.size;
|
|
548
641
|
const skipped = filesToParse.length - parsed;
|
|
@@ -558,6 +651,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
558
651
|
|
|
559
652
|
// ── Batch import resolution ────────────────────────────────────────
|
|
560
653
|
// Collect all (fromFile, importSource) pairs and resolve in one native call
|
|
654
|
+
_t.resolve0 = performance.now();
|
|
561
655
|
const batchInputs = [];
|
|
562
656
|
for (const [relPath, symbols] of fileSymbols) {
|
|
563
657
|
const absFile = path.join(rootDir, relPath);
|
|
@@ -566,6 +660,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
566
660
|
}
|
|
567
661
|
}
|
|
568
662
|
const batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases);
|
|
663
|
+
_t.resolveMs = performance.now() - _t.resolve0;
|
|
569
664
|
|
|
570
665
|
function getResolved(absFile, importSource) {
|
|
571
666
|
if (batchResolved) {
|
|
@@ -653,6 +748,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
653
748
|
}
|
|
654
749
|
|
|
655
750
|
// Second pass: build edges
|
|
751
|
+
_t.edges0 = performance.now();
|
|
656
752
|
let edgeCount = 0;
|
|
657
753
|
const buildEdges = db.transaction(() => {
|
|
658
754
|
for (const [relPath, symbols] of fileSymbols) {
|
|
@@ -712,10 +808,26 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
712
808
|
for (const call of symbols.calls) {
|
|
713
809
|
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
|
|
714
810
|
let caller = null;
|
|
811
|
+
let callerSpan = Infinity;
|
|
715
812
|
for (const def of symbols.definitions) {
|
|
716
813
|
if (def.line <= call.line) {
|
|
717
|
-
const
|
|
718
|
-
if (
|
|
814
|
+
const end = def.endLine || Infinity;
|
|
815
|
+
if (call.line <= end) {
|
|
816
|
+
// Call is inside this definition's range — pick narrowest
|
|
817
|
+
const span = end - def.line;
|
|
818
|
+
if (span < callerSpan) {
|
|
819
|
+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
820
|
+
if (row) {
|
|
821
|
+
caller = row;
|
|
822
|
+
callerSpan = span;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} else if (!caller) {
|
|
826
|
+
// Fallback: def starts before call but call is past end
|
|
827
|
+
// Only use if we haven't found an enclosing scope yet
|
|
828
|
+
const row = getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
829
|
+
if (row) caller = row;
|
|
830
|
+
}
|
|
719
831
|
}
|
|
720
832
|
}
|
|
721
833
|
if (!caller) caller = fileNodeRow;
|
|
@@ -779,12 +891,12 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
779
891
|
}
|
|
780
892
|
}
|
|
781
893
|
|
|
782
|
-
// Class extends edges
|
|
894
|
+
// Class extends edges (use pre-loaded maps instead of inline DB queries)
|
|
783
895
|
for (const cls of symbols.classes) {
|
|
784
896
|
if (cls.extends) {
|
|
785
|
-
const sourceRow =
|
|
786
|
-
|
|
787
|
-
|
|
897
|
+
const sourceRow = (nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
898
|
+
(n) => n.kind === 'class',
|
|
899
|
+
);
|
|
788
900
|
const targetCandidates = nodesByName.get(cls.extends) || [];
|
|
789
901
|
const targetRows = targetCandidates.filter((n) => n.kind === 'class');
|
|
790
902
|
if (sourceRow) {
|
|
@@ -796,9 +908,9 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
796
908
|
}
|
|
797
909
|
|
|
798
910
|
if (cls.implements) {
|
|
799
|
-
const sourceRow =
|
|
800
|
-
|
|
801
|
-
|
|
911
|
+
const sourceRow = (nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
|
|
912
|
+
(n) => n.kind === 'class',
|
|
913
|
+
);
|
|
802
914
|
const targetCandidates = nodesByName.get(cls.implements) || [];
|
|
803
915
|
const targetRows = targetCandidates.filter(
|
|
804
916
|
(n) => n.kind === 'interface' || n.kind === 'class',
|
|
@@ -814,16 +926,21 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
814
926
|
}
|
|
815
927
|
});
|
|
816
928
|
buildEdges();
|
|
929
|
+
_t.edgesMs = performance.now() - _t.edges0;
|
|
817
930
|
|
|
818
|
-
// Build line count map for structure metrics
|
|
931
|
+
// Build line count map for structure metrics (prefer cached _lineCount from parser)
|
|
819
932
|
const lineCountMap = new Map();
|
|
820
|
-
for (const [relPath] of fileSymbols) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
933
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
934
|
+
if (symbols._lineCount) {
|
|
935
|
+
lineCountMap.set(relPath, symbols._lineCount);
|
|
936
|
+
} else {
|
|
937
|
+
const absPath = path.join(rootDir, relPath);
|
|
938
|
+
try {
|
|
939
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
940
|
+
lineCountMap.set(relPath, content.split('\n').length);
|
|
941
|
+
} catch {
|
|
942
|
+
lineCountMap.set(relPath, 0);
|
|
943
|
+
}
|
|
827
944
|
}
|
|
828
945
|
}
|
|
829
946
|
|
|
@@ -881,6 +998,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
881
998
|
}
|
|
882
999
|
|
|
883
1000
|
// Build directory structure, containment edges, and metrics
|
|
1001
|
+
_t.structure0 = performance.now();
|
|
884
1002
|
const relDirs = new Set();
|
|
885
1003
|
for (const absDir of discoveredDirs) {
|
|
886
1004
|
relDirs.add(normalizePath(path.relative(rootDir, absDir)));
|
|
@@ -891,8 +1009,10 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
891
1009
|
} catch (err) {
|
|
892
1010
|
debug(`Structure analysis failed: ${err.message}`);
|
|
893
1011
|
}
|
|
1012
|
+
_t.structureMs = performance.now() - _t.structure0;
|
|
894
1013
|
|
|
895
1014
|
// Classify node roles (entry, core, utility, adapter, dead, leaf)
|
|
1015
|
+
_t.roles0 = performance.now();
|
|
896
1016
|
try {
|
|
897
1017
|
const { classifyNodeRoles } = await import('./structure.js');
|
|
898
1018
|
const roleSummary = classifyNodeRoles(db);
|
|
@@ -904,6 +1024,23 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
904
1024
|
} catch (err) {
|
|
905
1025
|
debug(`Role classification failed: ${err.message}`);
|
|
906
1026
|
}
|
|
1027
|
+
_t.rolesMs = performance.now() - _t.roles0;
|
|
1028
|
+
|
|
1029
|
+
// Compute per-function complexity metrics (cognitive, cyclomatic, nesting)
|
|
1030
|
+
_t.complexity0 = performance.now();
|
|
1031
|
+
try {
|
|
1032
|
+
const { buildComplexityMetrics } = await import('./complexity.js');
|
|
1033
|
+
await buildComplexityMetrics(db, allSymbols, rootDir, engineOpts);
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
debug(`Complexity analysis failed: ${err.message}`);
|
|
1036
|
+
}
|
|
1037
|
+
_t.complexityMs = performance.now() - _t.complexity0;
|
|
1038
|
+
|
|
1039
|
+
// Release any remaining cached WASM trees for GC
|
|
1040
|
+
for (const [, symbols] of allSymbols) {
|
|
1041
|
+
symbols._tree = null;
|
|
1042
|
+
symbols._langId = null;
|
|
1043
|
+
}
|
|
907
1044
|
|
|
908
1045
|
const nodeCount = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
909
1046
|
info(`Graph built: ${nodeCount} nodes, ${edgeCount} edges`);
|
|
@@ -925,7 +1062,19 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
925
1062
|
}
|
|
926
1063
|
}
|
|
927
1064
|
|
|
928
|
-
|
|
1065
|
+
// Persist build metadata for mismatch detection
|
|
1066
|
+
try {
|
|
1067
|
+
setBuildMeta(db, {
|
|
1068
|
+
engine: engineName,
|
|
1069
|
+
engine_version: engineVersion || '',
|
|
1070
|
+
codegraph_version: CODEGRAPH_VERSION,
|
|
1071
|
+
built_at: new Date().toISOString(),
|
|
1072
|
+
});
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
warn(`Failed to write build metadata: ${err.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
closeDb(db);
|
|
929
1078
|
|
|
930
1079
|
// Write journal header after successful build
|
|
931
1080
|
writeJournalHeader(rootDir, Date.now());
|
|
@@ -945,4 +1094,16 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
945
1094
|
}
|
|
946
1095
|
}
|
|
947
1096
|
}
|
|
1097
|
+
|
|
1098
|
+
return {
|
|
1099
|
+
phases: {
|
|
1100
|
+
parseMs: +_t.parseMs.toFixed(1),
|
|
1101
|
+
insertMs: +_t.insertMs.toFixed(1),
|
|
1102
|
+
resolveMs: +_t.resolveMs.toFixed(1),
|
|
1103
|
+
edgesMs: +_t.edgesMs.toFixed(1),
|
|
1104
|
+
structureMs: +_t.structureMs.toFixed(1),
|
|
1105
|
+
rolesMs: +_t.rolesMs.toFixed(1),
|
|
1106
|
+
complexityMs: +_t.complexityMs.toFixed(1),
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
948
1109
|
}
|