@codragraph/cli 2.0.0 → 2.1.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 +60 -22
- package/dist/_shared/cgdb/schema-constants.d.ts +16 -0
- package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -0
- package/dist/_shared/cgdb/schema-constants.js +70 -0
- package/dist/_shared/cgdb/schema-constants.js.map +1 -0
- package/dist/_shared/feature-clusters.d.ts +99 -0
- package/dist/_shared/feature-clusters.d.ts.map +1 -0
- package/dist/_shared/feature-clusters.js +2 -0
- package/dist/_shared/feature-clusters.js.map +1 -0
- package/dist/_shared/graph/types.d.ts +16 -2
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +3 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/pipeline.d.ts +1 -1
- package/dist/_shared/pipeline.d.ts.map +1 -1
- package/dist/cli/ai-context.js +4 -0
- package/dist/cli/analyze.js +30 -27
- package/dist/cli/graphstore.js +21 -21
- package/dist/cli/index-repo.js +3 -3
- package/dist/cli/index.js +37 -0
- package/dist/cli/setup.js +9 -5
- package/dist/cli/tool.d.ts +25 -0
- package/dist/cli/tool.js +74 -0
- package/dist/cli/wiki.js +3 -3
- package/dist/config/supported-languages.d.ts +3 -3
- package/dist/config/supported-languages.js +3 -3
- package/dist/core/augmentation/engine.js +7 -7
- package/dist/core/cgdb/cgdb-adapter.d.ts +176 -0
- package/dist/core/cgdb/cgdb-adapter.js +1336 -0
- package/dist/core/cgdb/content-read.d.ts +46 -0
- package/dist/core/cgdb/content-read.js +64 -0
- package/dist/core/cgdb/csv-generator.d.ts +29 -0
- package/dist/core/cgdb/csv-generator.js +523 -0
- package/dist/core/cgdb/pool-adapter.d.ts +93 -0
- package/dist/core/cgdb/pool-adapter.js +550 -0
- package/dist/core/cgdb/schema.d.ts +63 -0
- package/dist/core/cgdb/schema.js +557 -0
- package/dist/core/embeddings/embedder.js +4 -2
- package/dist/core/embeddings/embedding-pipeline.js +4 -4
- package/dist/core/graphstore/cgdb-row-source.d.ts +19 -0
- package/dist/core/graphstore/cgdb-row-source.js +141 -0
- package/dist/core/graphstore/index.d.ts +2 -2
- package/dist/core/graphstore/index.js +4 -4
- package/dist/core/group/bridge-db.d.ts +2 -2
- package/dist/core/group/bridge-db.js +18 -18
- package/dist/core/group/bridge-schema.d.ts +4 -4
- package/dist/core/group/bridge-schema.js +4 -4
- package/dist/core/group/cross-impact.js +3 -3
- package/dist/core/group/service.d.ts +16 -0
- package/dist/core/group/service.js +360 -0
- package/dist/core/group/sync.js +4 -4
- package/dist/core/ingestion/emit-references.d.ts +1 -1
- package/dist/core/ingestion/emit-references.js +1 -1
- package/dist/core/ingestion/feature-cluster-processor.d.ts +62 -0
- package/dist/core/ingestion/feature-cluster-processor.js +626 -0
- package/dist/core/ingestion/finalize-orchestrator.js +1 -1
- package/dist/core/ingestion/model/registration-table.js +1 -0
- package/dist/core/ingestion/model/resolve.d.ts +2 -2
- package/dist/core/ingestion/model/resolve.js +3 -3
- package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
- package/dist/core/ingestion/model/semantic-model.js +1 -1
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/model/symbol-table.js +1 -1
- package/dist/core/ingestion/pipeline-phases/feature-clusters.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/feature-clusters.js +88 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline.d.ts +4 -0
- package/dist/core/ingestion/pipeline.js +9 -5
- package/dist/core/run-analyze.d.ts +1 -0
- package/dist/core/run-analyze.js +36 -30
- package/dist/core/search/bm25-index.d.ts +3 -3
- package/dist/core/search/bm25-index.js +9 -9
- package/dist/core/search/hybrid-search.js +2 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +2 -2
- package/dist/core/wiki/graph-queries.js +5 -5
- package/dist/mcp/core/cgdb-adapter.d.ts +5 -0
- package/dist/mcp/core/cgdb-adapter.js +5 -0
- package/dist/mcp/core/embedder.js +6 -3
- package/dist/mcp/local/local-backend.d.ts +14 -2
- package/dist/mcp/local/local-backend.js +396 -18
- package/dist/mcp/resources.js +139 -0
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools.js +175 -3
- package/dist/server/analyze-worker.js +2 -2
- package/dist/server/api.js +147 -31
- package/dist/storage/repo-manager.d.ts +10 -5
- package/dist/storage/repo-manager.js +10 -6
- package/dist/types/pipeline.d.ts +2 -0
- package/hooks/claude/codragraph-hook.cjs +4 -4
- package/package.json +15 -6
- package/scripts/build.js +21 -21
- package/skills/codragraph-cli.md +17 -1
- package/skills/codragraph-guide.md +6 -2
- package/skills/codragraph-onboarding.md +2 -2
- package/vendor/tree-sitter-proto/bindings/node/index.js +3 -3
- package/vendor/tree-sitter-proto/src/node-types.json +1 -1
package/dist/server/api.js
CHANGED
|
@@ -13,11 +13,11 @@ import path from 'path';
|
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
import { loadMeta, listRegisteredRepos, getStoragePath } from '../storage/repo-manager.js';
|
|
16
|
-
import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery,
|
|
17
|
-
import { isWriteQuery } from '../core/
|
|
18
|
-
import { decodeContentField } from '../core/
|
|
16
|
+
import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery, closeCgdb, withCgdbDb, } from '../core/cgdb/cgdb-adapter.js';
|
|
17
|
+
import { isWriteQuery } from '../core/cgdb/pool-adapter.js';
|
|
18
|
+
import { decodeContentField } from '../core/cgdb/content-read.js';
|
|
19
19
|
import { NODE_TABLES } from '../_shared/index.js';
|
|
20
|
-
import {
|
|
20
|
+
import { searchFTSFromCgdb } from '../core/search/bm25-index.js';
|
|
21
21
|
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
22
22
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
23
23
|
// at server startup — crashes on unsupported Node ABI versions (#89)
|
|
@@ -202,6 +202,9 @@ const getNodeQuery = (table, includeContent) => {
|
|
|
202
202
|
if (table === 'Process') {
|
|
203
203
|
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
|
|
204
204
|
}
|
|
205
|
+
if (table === 'FeatureCluster') {
|
|
206
|
+
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.slug AS slug, n.featureKind AS featureKind, n.summary AS summary, n.description AS description, n.repo AS repo, n.service AS service, n.signals AS signals, n.memberCount AS memberCount, n.entryPointIds AS entryPointIds, n.routes AS routes, n.tools AS tools, n.testCoverageHints AS testCoverageHints, n.lastIndexedCommit AS lastIndexedCommit, n.confidence AS confidence, n.source AS source`;
|
|
207
|
+
}
|
|
205
208
|
if (table === 'Route') {
|
|
206
209
|
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware`;
|
|
207
210
|
}
|
|
@@ -233,6 +236,20 @@ const mapGraphNodeRow = (table, row, includeContent) => ({
|
|
|
233
236
|
communities: row.communities,
|
|
234
237
|
entryPointId: row.entryPointId,
|
|
235
238
|
terminalId: row.terminalId,
|
|
239
|
+
slug: row.slug,
|
|
240
|
+
featureKind: row.featureKind,
|
|
241
|
+
summary: row.summary,
|
|
242
|
+
repo: row.repo,
|
|
243
|
+
service: row.service,
|
|
244
|
+
signals: row.signals,
|
|
245
|
+
memberCount: row.memberCount,
|
|
246
|
+
entryPointIds: row.entryPointIds,
|
|
247
|
+
routes: row.routes,
|
|
248
|
+
tools: row.tools,
|
|
249
|
+
testCoverageHints: row.testCoverageHints,
|
|
250
|
+
lastIndexedCommit: row.lastIndexedCommit,
|
|
251
|
+
confidence: row.confidence,
|
|
252
|
+
source: row.source,
|
|
236
253
|
},
|
|
237
254
|
});
|
|
238
255
|
const mapGraphRelationshipRow = (row) => ({
|
|
@@ -388,7 +405,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
388
405
|
const cleanupMcp = mountMCPEndpoints(app, backend);
|
|
389
406
|
const jobManager = new JobManager();
|
|
390
407
|
// Shared repo lock — prevents concurrent analyze + embed on the same repo path,
|
|
391
|
-
// which would corrupt LadybugDB (analyze calls
|
|
408
|
+
// which would corrupt LadybugDB (analyze calls closeCgdb + initCgdb while embed has queries in flight).
|
|
392
409
|
const activeRepoPaths = new Set();
|
|
393
410
|
const acquireRepoLock = (repoPath) => {
|
|
394
411
|
if (activeRepoPaths.has(repoPath)) {
|
|
@@ -571,7 +588,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
571
588
|
try {
|
|
572
589
|
// Close any open LadybugDB handle before deleting files
|
|
573
590
|
try {
|
|
574
|
-
await
|
|
591
|
+
await closeCgdb();
|
|
575
592
|
}
|
|
576
593
|
catch { }
|
|
577
594
|
// 1. Delete the .codragraph index/storage directory
|
|
@@ -895,21 +912,21 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
895
912
|
res.status(404).json({ error: 'Repository not found' });
|
|
896
913
|
return;
|
|
897
914
|
}
|
|
898
|
-
const
|
|
915
|
+
const cgdbPath = path.join(entry.storagePath, 'cgdb');
|
|
899
916
|
const includeContent = req.query.includeContent === 'true';
|
|
900
917
|
const stream = req.query.stream === 'true';
|
|
901
|
-
// Guard: when a repo has no materialized
|
|
902
|
-
// seeded CAS-only repos), or the
|
|
918
|
+
// Guard: when a repo has no materialized cgdb schema (fixture-
|
|
919
|
+
// seeded CAS-only repos), or the cgdb WAL is corrupt/stale from
|
|
903
920
|
// a prior failed analyze, LadybugDB native will abort with
|
|
904
921
|
// UNREACHABLE_CODE or an ANY-vector exception. Detect both
|
|
905
922
|
// shapes — missing file (cheap fs.access) AND empty/4096-byte
|
|
906
923
|
// schema-only file (fs.stat) — and return an empty graph so
|
|
907
924
|
// the dashboard doesn't blow up. The Graph tab keeps working
|
|
908
|
-
// for repos that actually have a real
|
|
909
|
-
const
|
|
925
|
+
// for repos that actually have a real cgdb.
|
|
926
|
+
const isCgdbMaterialized = await (async () => {
|
|
910
927
|
try {
|
|
911
|
-
const stat = await fs.stat(
|
|
912
|
-
// Schema-only
|
|
928
|
+
const stat = await fs.stat(cgdbPath);
|
|
929
|
+
// Schema-only cgdb is exactly 4096 bytes (one page, no real
|
|
913
930
|
// data). Real graphs are larger.
|
|
914
931
|
return stat.isFile() && stat.size > 4096;
|
|
915
932
|
}
|
|
@@ -917,14 +934,14 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
917
934
|
return false;
|
|
918
935
|
}
|
|
919
936
|
})();
|
|
920
|
-
if (!
|
|
937
|
+
if (!isCgdbMaterialized) {
|
|
921
938
|
if (stream) {
|
|
922
939
|
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
|
|
923
940
|
res.flushHeaders();
|
|
924
941
|
res.write(JSON.stringify({
|
|
925
942
|
type: 'meta',
|
|
926
943
|
repoName: entry.name,
|
|
927
|
-
note: 'no
|
|
944
|
+
note: 'no cgdb file — graph not yet materialized',
|
|
928
945
|
nodeCount: 0,
|
|
929
946
|
relationshipCount: 0,
|
|
930
947
|
}) + '\n');
|
|
@@ -937,7 +954,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
937
954
|
nodes: [],
|
|
938
955
|
relationships: [],
|
|
939
956
|
stats: { nodes: 0, edges: 0 },
|
|
940
|
-
note: 'no
|
|
957
|
+
note: 'no cgdb file — graph not yet materialized',
|
|
941
958
|
});
|
|
942
959
|
return;
|
|
943
960
|
}
|
|
@@ -959,7 +976,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
959
976
|
res.once('finish', markFinished);
|
|
960
977
|
res.once('close', abortStreaming);
|
|
961
978
|
try {
|
|
962
|
-
await
|
|
979
|
+
await withCgdbDb(cgdbPath, async () => streamGraphNdjson(res, includeContent, abortController.signal));
|
|
963
980
|
if (!abortController.signal.aborted && !res.writableEnded) {
|
|
964
981
|
res.end();
|
|
965
982
|
}
|
|
@@ -971,7 +988,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
971
988
|
}
|
|
972
989
|
return;
|
|
973
990
|
}
|
|
974
|
-
const graph = await
|
|
991
|
+
const graph = await withCgdbDb(cgdbPath, async () => buildGraph(includeContent));
|
|
975
992
|
res.json(graph);
|
|
976
993
|
}
|
|
977
994
|
catch (err) {
|
|
@@ -1009,14 +1026,53 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1009
1026
|
res.status(404).json({ error: 'Repository not found' });
|
|
1010
1027
|
return;
|
|
1011
1028
|
}
|
|
1012
|
-
const
|
|
1013
|
-
const result = await
|
|
1029
|
+
const cgdbPath = path.join(entry.storagePath, 'cgdb');
|
|
1030
|
+
const result = await withCgdbDb(cgdbPath, () => executeQuery(cypher));
|
|
1014
1031
|
res.json({ result });
|
|
1015
1032
|
}
|
|
1016
1033
|
catch (err) {
|
|
1017
1034
|
res.status(500).json({ error: err.message || 'Query failed' });
|
|
1018
1035
|
}
|
|
1019
1036
|
});
|
|
1037
|
+
// Symbol context through the same LocalBackend path as MCP `context`
|
|
1038
|
+
app.post('/api/context', async (req, res) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const name = String(req.body?.name ?? '').trim();
|
|
1041
|
+
if (!name) {
|
|
1042
|
+
res.status(400).json({ error: 'Missing "name" in request body' });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const result = await backend.callTool('context', {
|
|
1046
|
+
...req.body,
|
|
1047
|
+
name,
|
|
1048
|
+
repo: requestedRepo(req),
|
|
1049
|
+
});
|
|
1050
|
+
res.json(result);
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
res.status(statusFromError(err)).json({ error: err.message || 'Context query failed' });
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
// Symbol impact through the same LocalBackend path as MCP `impact`
|
|
1057
|
+
app.post('/api/impact', async (req, res) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const target = String(req.body?.target ?? '').trim();
|
|
1060
|
+
if (!target) {
|
|
1061
|
+
res.status(400).json({ error: 'Missing "target" in request body' });
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const result = await backend.callTool('impact', {
|
|
1065
|
+
...req.body,
|
|
1066
|
+
target,
|
|
1067
|
+
direction: req.body?.direction ?? 'upstream',
|
|
1068
|
+
repo: requestedRepo(req),
|
|
1069
|
+
});
|
|
1070
|
+
res.json(result);
|
|
1071
|
+
}
|
|
1072
|
+
catch (err) {
|
|
1073
|
+
res.status(statusFromError(err)).json({ error: err.message || 'Impact query failed' });
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1020
1076
|
// Search (supports mode: 'hybrid' | 'semantic' | 'bm25', and optional enrichment)
|
|
1021
1077
|
app.post('/api/search', async (req, res) => {
|
|
1022
1078
|
try {
|
|
@@ -1030,14 +1086,14 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1030
1086
|
res.status(404).json({ error: 'Repository not found' });
|
|
1031
1087
|
return;
|
|
1032
1088
|
}
|
|
1033
|
-
const
|
|
1089
|
+
const cgdbPath = path.join(entry.storagePath, 'cgdb');
|
|
1034
1090
|
const parsedLimit = Number(req.body.limit ?? 10);
|
|
1035
1091
|
const limit = Number.isFinite(parsedLimit)
|
|
1036
1092
|
? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
|
|
1037
1093
|
: 10;
|
|
1038
1094
|
const mode = req.body.mode ?? 'hybrid';
|
|
1039
1095
|
const enrich = req.body.enrich !== false; // default true
|
|
1040
|
-
const results = await
|
|
1096
|
+
const results = await withCgdbDb(cgdbPath, async () => {
|
|
1041
1097
|
let searchResults;
|
|
1042
1098
|
if (mode === 'semantic') {
|
|
1043
1099
|
const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
|
|
@@ -1055,7 +1111,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1055
1111
|
}));
|
|
1056
1112
|
}
|
|
1057
1113
|
else if (mode === 'bm25') {
|
|
1058
|
-
searchResults = await
|
|
1114
|
+
searchResults = await searchFTSFromCgdb(query, limit);
|
|
1059
1115
|
searchResults = searchResults.map((r, i) => ({
|
|
1060
1116
|
...r,
|
|
1061
1117
|
rank: i + 1,
|
|
@@ -1070,7 +1126,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1070
1126
|
searchResults = await hybridSearch(query, limit, executeQuery, semSearch);
|
|
1071
1127
|
}
|
|
1072
1128
|
else {
|
|
1073
|
-
searchResults = await
|
|
1129
|
+
searchResults = await searchFTSFromCgdb(query, limit);
|
|
1074
1130
|
}
|
|
1075
1131
|
}
|
|
1076
1132
|
if (!enrich)
|
|
@@ -1230,8 +1286,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1230
1286
|
const results = [];
|
|
1231
1287
|
const repoRoot = path.resolve(entry.path);
|
|
1232
1288
|
// Get file paths from the graph (lightweight — no content loaded)
|
|
1233
|
-
const
|
|
1234
|
-
const fileRows = await
|
|
1289
|
+
const cgdbPath = path.join(entry.storagePath, 'cgdb');
|
|
1290
|
+
const fileRows = await withCgdbDb(cgdbPath, () => executeQuery(`MATCH (n:File) WHERE n.content IS NOT NULL RETURN n.filePath AS filePath`));
|
|
1235
1291
|
// Search files on disk one at a time (constant memory)
|
|
1236
1292
|
for (const row of fileRows) {
|
|
1237
1293
|
if (results.length >= limit)
|
|
@@ -1326,6 +1382,66 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1326
1382
|
.json({ error: err.message || 'Failed to query cluster detail' });
|
|
1327
1383
|
}
|
|
1328
1384
|
});
|
|
1385
|
+
// List all feature clusters
|
|
1386
|
+
app.get('/api/feature-clusters', async (req, res) => {
|
|
1387
|
+
try {
|
|
1388
|
+
const limit = req.query.limit ? Number.parseInt(String(req.query.limit), 10) : undefined;
|
|
1389
|
+
const query = String(req.query.query ?? '');
|
|
1390
|
+
const result = await backend.queryFeatureClusters(requestedRepo(req), limit, query);
|
|
1391
|
+
res.json(result);
|
|
1392
|
+
}
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
res
|
|
1395
|
+
.status(statusFromError(err))
|
|
1396
|
+
.json({ error: err.message || 'Failed to query feature clusters' });
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
// Feature cluster detail
|
|
1400
|
+
app.get('/api/feature-cluster', async (req, res) => {
|
|
1401
|
+
try {
|
|
1402
|
+
const name = String(req.query.name ?? '').trim();
|
|
1403
|
+
if (!name) {
|
|
1404
|
+
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const limit = req.query.limit ? Number.parseInt(String(req.query.limit), 10) : undefined;
|
|
1408
|
+
const result = await backend.queryFeatureContext(name, requestedRepo(req), limit);
|
|
1409
|
+
if (result?.error) {
|
|
1410
|
+
res.status(404).json({ error: result.error });
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
res.json(result);
|
|
1414
|
+
}
|
|
1415
|
+
catch (err) {
|
|
1416
|
+
res
|
|
1417
|
+
.status(statusFromError(err))
|
|
1418
|
+
.json({ error: err.message || 'Failed to query feature cluster detail' });
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
// Feature cluster impact/context pack
|
|
1422
|
+
app.get(['/api/feature-impact', '/api/cluster-impact'], async (req, res) => {
|
|
1423
|
+
try {
|
|
1424
|
+
const name = String(req.query.name ?? '').trim();
|
|
1425
|
+
if (!name) {
|
|
1426
|
+
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const limit = req.query.limit ? Number.parseInt(String(req.query.limit), 10) : undefined;
|
|
1430
|
+
const directionText = String(req.query.direction ?? 'upstream');
|
|
1431
|
+
const direction = directionText === 'downstream' || directionText === 'both' ? directionText : 'upstream';
|
|
1432
|
+
const result = await backend.queryFeatureImpact(name, requestedRepo(req), direction, limit);
|
|
1433
|
+
if (result?.error) {
|
|
1434
|
+
res.status(404).json({ error: result.error });
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
res.json(result);
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
res
|
|
1441
|
+
.status(statusFromError(err))
|
|
1442
|
+
.json({ error: err.message || 'Failed to query feature cluster impact' });
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1329
1445
|
// ── Analyze API ──────────────────────────────────────────────────────
|
|
1330
1446
|
// POST /api/analyze — start a new analysis job
|
|
1331
1447
|
app.post('/api/analyze', async (req, res) => {
|
|
@@ -1602,12 +1718,12 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1602
1718
|
// Run embedding pipeline asynchronously
|
|
1603
1719
|
(async () => {
|
|
1604
1720
|
try {
|
|
1605
|
-
const
|
|
1606
|
-
await
|
|
1721
|
+
const cgdbPath = path.join(entry.storagePath, 'cgdb');
|
|
1722
|
+
await withCgdbDb(cgdbPath, async () => {
|
|
1607
1723
|
const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js');
|
|
1608
1724
|
// Fetch existing content hashes for incremental embedding.
|
|
1609
|
-
// Delegated to
|
|
1610
|
-
const { fetchExistingEmbeddingHashes } = await import('../core/
|
|
1725
|
+
// Delegated to cgdb-adapter which owns the DB query logic and legacy-fallback handling.
|
|
1726
|
+
const { fetchExistingEmbeddingHashes } = await import('../core/cgdb/cgdb-adapter.js');
|
|
1611
1727
|
const existingEmbeddings = await fetchExistingEmbeddingHashes(executeQuery);
|
|
1612
1728
|
if (existingEmbeddings && existingEmbeddings.size > 0) {
|
|
1613
1729
|
console.log(`[embed] ${existingEmbeddings.size} nodes already embedded — incremental run with content-hash comparison`);
|
|
@@ -1718,7 +1834,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1718
1834
|
jobManager.dispose();
|
|
1719
1835
|
embedJobManager.dispose();
|
|
1720
1836
|
await cleanupMcp();
|
|
1721
|
-
await
|
|
1837
|
+
await closeCgdb();
|
|
1722
1838
|
await backend.disconnect();
|
|
1723
1839
|
process.exit(0);
|
|
1724
1840
|
};
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
export declare const canonicalizePath: (p: string) => string;
|
|
39
39
|
/**
|
|
40
|
-
* On-disk schema version for `.codragraph/
|
|
40
|
+
* On-disk schema version for `.codragraph/cgdb` and `.codragraph/meta.json`.
|
|
41
41
|
*
|
|
42
42
|
* 1 — pre-RFC-0001-Phase-2 layout. Node tables have `content STRING`
|
|
43
43
|
* but no `contentEncoding` column. Implicit/missing on existing
|
|
@@ -47,6 +47,10 @@ export declare const canonicalizePath: (p: string) => string;
|
|
|
47
47
|
* opt into compression via `--compress brotli|zstd` (compression
|
|
48
48
|
* is OFF by default, so existing readers keep working). Readers
|
|
49
49
|
* decode based on the per-row encoding tag.
|
|
50
|
+
* 3 - FeatureCluster layer: adds FeatureCluster nodes plus
|
|
51
|
+
* FEATURE_MEMBER_OF / FEATURE_DEPENDS_ON relation rows.
|
|
52
|
+
* 4 - FeatureCluster context-pack metadata: summary, repo/service,
|
|
53
|
+
* routes, tools, test coverage hints, and indexed commit fields.
|
|
50
54
|
*
|
|
51
55
|
* Bumping this is the migration trigger: `runFullAnalysis` forces a
|
|
52
56
|
* full re-analyze when an existing index has a missing or older
|
|
@@ -54,7 +58,7 @@ export declare const canonicalizePath: (p: string) => string;
|
|
|
54
58
|
* LadybugDB table via ALTER is not validated end-to-end yet — fresh
|
|
55
59
|
* `CREATE NODE TABLE` is the supported path.
|
|
56
60
|
*/
|
|
57
|
-
export declare const INDEX_SCHEMA_VERSION:
|
|
61
|
+
export declare const INDEX_SCHEMA_VERSION: 4;
|
|
58
62
|
export interface RepoMeta {
|
|
59
63
|
repoPath: string;
|
|
60
64
|
lastCommit: string;
|
|
@@ -103,6 +107,7 @@ export interface RepoMeta {
|
|
|
103
107
|
nodes?: number;
|
|
104
108
|
edges?: number;
|
|
105
109
|
communities?: number;
|
|
110
|
+
featureClusters?: number;
|
|
106
111
|
processes?: number;
|
|
107
112
|
embeddings?: number;
|
|
108
113
|
};
|
|
@@ -110,7 +115,7 @@ export interface RepoMeta {
|
|
|
110
115
|
export interface IndexedRepo {
|
|
111
116
|
repoPath: string;
|
|
112
117
|
storagePath: string;
|
|
113
|
-
|
|
118
|
+
cgdbPath: string;
|
|
114
119
|
metaPath: string;
|
|
115
120
|
meta: RepoMeta;
|
|
116
121
|
}
|
|
@@ -140,7 +145,7 @@ export declare const getStoragePath: (repoPath: string) => string;
|
|
|
140
145
|
*/
|
|
141
146
|
export declare const getStoragePaths: (repoPath: string) => {
|
|
142
147
|
storagePath: string;
|
|
143
|
-
|
|
148
|
+
cgdbPath: string;
|
|
144
149
|
metaPath: string;
|
|
145
150
|
};
|
|
146
151
|
/**
|
|
@@ -153,7 +158,7 @@ export declare const hasKuzuIndex: (storagePath: string) => Promise<boolean>;
|
|
|
153
158
|
*
|
|
154
159
|
* Returns:
|
|
155
160
|
* found — true if .codragraph/kuzu existed and was deleted
|
|
156
|
-
* needsReindex — true if kuzu existed but
|
|
161
|
+
* needsReindex — true if kuzu existed but cgdb does not (re-analyze required)
|
|
157
162
|
*
|
|
158
163
|
* Callers own the user-facing messaging; this function only deletes files.
|
|
159
164
|
*/
|
|
@@ -50,7 +50,7 @@ export const canonicalizePath = (p) => {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
|
-
* On-disk schema version for `.codragraph/
|
|
53
|
+
* On-disk schema version for `.codragraph/cgdb` and `.codragraph/meta.json`.
|
|
54
54
|
*
|
|
55
55
|
* 1 — pre-RFC-0001-Phase-2 layout. Node tables have `content STRING`
|
|
56
56
|
* but no `contentEncoding` column. Implicit/missing on existing
|
|
@@ -60,6 +60,10 @@ export const canonicalizePath = (p) => {
|
|
|
60
60
|
* opt into compression via `--compress brotli|zstd` (compression
|
|
61
61
|
* is OFF by default, so existing readers keep working). Readers
|
|
62
62
|
* decode based on the per-row encoding tag.
|
|
63
|
+
* 3 - FeatureCluster layer: adds FeatureCluster nodes plus
|
|
64
|
+
* FEATURE_MEMBER_OF / FEATURE_DEPENDS_ON relation rows.
|
|
65
|
+
* 4 - FeatureCluster context-pack metadata: summary, repo/service,
|
|
66
|
+
* routes, tools, test coverage hints, and indexed commit fields.
|
|
63
67
|
*
|
|
64
68
|
* Bumping this is the migration trigger: `runFullAnalysis` forces a
|
|
65
69
|
* full re-analyze when an existing index has a missing or older
|
|
@@ -67,7 +71,7 @@ export const canonicalizePath = (p) => {
|
|
|
67
71
|
* LadybugDB table via ALTER is not validated end-to-end yet — fresh
|
|
68
72
|
* `CREATE NODE TABLE` is the supported path.
|
|
69
73
|
*/
|
|
70
|
-
export const INDEX_SCHEMA_VERSION =
|
|
74
|
+
export const INDEX_SCHEMA_VERSION = 4;
|
|
71
75
|
const CODRAGRAPH_DIR = '.codragraph';
|
|
72
76
|
// ─── Local Storage Helpers ─────────────────────────────────────────────
|
|
73
77
|
/**
|
|
@@ -83,7 +87,7 @@ export const getStoragePaths = (repoPath) => {
|
|
|
83
87
|
const storagePath = getStoragePath(repoPath);
|
|
84
88
|
return {
|
|
85
89
|
storagePath,
|
|
86
|
-
|
|
90
|
+
cgdbPath: path.join(storagePath, 'cgdb'),
|
|
87
91
|
metaPath: path.join(storagePath, 'meta.json'),
|
|
88
92
|
};
|
|
89
93
|
};
|
|
@@ -105,16 +109,16 @@ export const hasKuzuIndex = async (storagePath) => {
|
|
|
105
109
|
*
|
|
106
110
|
* Returns:
|
|
107
111
|
* found — true if .codragraph/kuzu existed and was deleted
|
|
108
|
-
* needsReindex — true if kuzu existed but
|
|
112
|
+
* needsReindex — true if kuzu existed but cgdb does not (re-analyze required)
|
|
109
113
|
*
|
|
110
114
|
* Callers own the user-facing messaging; this function only deletes files.
|
|
111
115
|
*/
|
|
112
116
|
export const cleanupOldKuzuFiles = async (storagePath) => {
|
|
113
117
|
const oldPath = path.join(storagePath, 'kuzu');
|
|
114
|
-
const newPath = path.join(storagePath, '
|
|
118
|
+
const newPath = path.join(storagePath, 'cgdb');
|
|
115
119
|
try {
|
|
116
120
|
await fs.stat(oldPath);
|
|
117
|
-
// Old kuzu file/dir exists — determine if
|
|
121
|
+
// Old kuzu file/dir exists — determine if cgdb is already present
|
|
118
122
|
let needsReindex = false;
|
|
119
123
|
try {
|
|
120
124
|
await fs.stat(newPath);
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { KnowledgeGraph } from '../core/graph/types.js';
|
|
2
2
|
import { CommunityDetectionResult } from '../core/ingestion/community-processor.js';
|
|
3
|
+
import { FeatureClusterDetectionResult } from '../core/ingestion/feature-cluster-processor.js';
|
|
3
4
|
import { ProcessDetectionResult } from '../core/ingestion/process-processor.js';
|
|
4
5
|
export interface PipelineResult {
|
|
5
6
|
graph: KnowledgeGraph;
|
|
@@ -9,6 +10,7 @@ export interface PipelineResult {
|
|
|
9
10
|
totalFileCount: number;
|
|
10
11
|
communityResult?: CommunityDetectionResult;
|
|
11
12
|
processResult?: ProcessDetectionResult;
|
|
13
|
+
featureClusterResult?: FeatureClusterDetectionResult;
|
|
12
14
|
/**
|
|
13
15
|
* True if the parse phase spawned a worker pool for this run. False means
|
|
14
16
|
* the sequential fallback handled every chunk. Primarily a test affordance
|
|
@@ -236,8 +236,8 @@ function handlePostToolUse(input) {
|
|
|
236
236
|
|
|
237
237
|
const cwd = input.cwd || process.cwd();
|
|
238
238
|
if (!path.isAbsolute(cwd)) return;
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
239
|
+
const codragraphDir = findCodraGraphDir(cwd);
|
|
240
|
+
if (!codragraphDir) return;
|
|
241
241
|
|
|
242
242
|
// Compare HEAD against last indexed commit — skip if unchanged
|
|
243
243
|
let currentHead = '';
|
|
@@ -258,7 +258,7 @@ function handlePostToolUse(input) {
|
|
|
258
258
|
let lastCommit = '';
|
|
259
259
|
let hadEmbeddings = false;
|
|
260
260
|
try {
|
|
261
|
-
const meta = JSON.parse(fs.readFileSync(path.join(
|
|
261
|
+
const meta = JSON.parse(fs.readFileSync(path.join(codragraphDir, 'meta.json'), 'utf-8'));
|
|
262
262
|
lastCommit = meta.lastCommit || '';
|
|
263
263
|
hadEmbeddings = meta.stats && meta.stats.embeddings > 0;
|
|
264
264
|
} catch {
|
|
@@ -282,7 +282,7 @@ function handlePostToolUse(input) {
|
|
|
282
282
|
// reindex is in flight. The spawned analyze removes it on exit (success or
|
|
283
283
|
// failure) via CODRAGRAPH_REINDEX_LOCK_PATH; the 10-min mtime fallback
|
|
284
284
|
// catches the rare crash that bypasses analyze's exit handler.
|
|
285
|
-
const coalescePath = path.join(
|
|
285
|
+
const coalescePath = path.join(codragraphDir, '.reindex.coalesce');
|
|
286
286
|
const crashSafetyTtlMs = 10 * 60 * 1000;
|
|
287
287
|
let inFlight = false;
|
|
288
288
|
try {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codragraph/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": {
|
|
6
|
-
"name": "
|
|
7
|
-
"email": "
|
|
8
|
-
"url": "https://
|
|
6
|
+
"name": "Thinqmesh Technologies",
|
|
7
|
+
"email": "hello@thinqmesh.com",
|
|
8
|
+
"url": "https://thinqmesh.com"
|
|
9
9
|
},
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"keywords": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepack": "node scripts/build.js"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@codragraph/graphstore": "^1.0
|
|
59
|
+
"@codragraph/graphstore": "^2.1.0",
|
|
60
60
|
"@huggingface/transformers": "^4.1.0",
|
|
61
61
|
"@ladybugdb/core": "^0.16.0",
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"tree-sitter-swift": "^0.6.0"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
|
-
"@codragraph/shared": "file:../
|
|
102
|
+
"@codragraph/shared": "file:../shared",
|
|
103
103
|
"@types/cli-progress": "^3.11.6",
|
|
104
104
|
"@types/cors": "^2.8.17",
|
|
105
105
|
"@types/express": "^4.17.21",
|
|
@@ -123,5 +123,14 @@
|
|
|
123
123
|
"publishConfig": {
|
|
124
124
|
"access": "public",
|
|
125
125
|
"registry": "https://registry.npmjs.org/"
|
|
126
|
+
},
|
|
127
|
+
"homepage": "https://github.com/AnitChaudhry/CodraGraph#readme",
|
|
128
|
+
"repository": {
|
|
129
|
+
"type": "git",
|
|
130
|
+
"url": "git+https://github.com/AnitChaudhry/CodraGraph.git",
|
|
131
|
+
"directory": "packages/core"
|
|
132
|
+
},
|
|
133
|
+
"bugs": {
|
|
134
|
+
"url": "https://github.com/AnitChaudhry/CodraGraph/issues"
|
|
126
135
|
}
|
|
127
136
|
}
|
package/scripts/build.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Build script that compiles codragraph and inlines codragraph
|
|
3
|
+
* Build script that compiles @codragraph/cli and inlines @codragraph/shared
|
|
4
|
+
* into the published dist.
|
|
4
5
|
*
|
|
5
6
|
* Steps:
|
|
6
|
-
* 1. Build codragraph
|
|
7
|
-
* 2. Build codragraph
|
|
7
|
+
* 1. Build @codragraph/shared (tsc)
|
|
8
|
+
* 2. Build @codragraph/graphstore (tsc) — @codragraph/cli imports it; without
|
|
8
9
|
* a populated dist/ here, step 3 fails to resolve types.
|
|
9
|
-
* 3. Build codragraph (tsc)
|
|
10
|
-
* 4. Copy
|
|
10
|
+
* 3. Build @codragraph/cli (tsc)
|
|
11
|
+
* 4. Copy packages/shared/dist → dist/_shared
|
|
11
12
|
* 5. Rewrite bare '@codragraph/shared' specifiers → relative paths
|
|
12
13
|
*/
|
|
13
14
|
import { execSync } from 'node:child_process';
|
|
@@ -17,39 +18,38 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const ROOT = path.resolve(__dirname, '..');
|
|
20
|
-
// Workspace directory names
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const GRAPHSTORE_ROOT = path.resolve(ROOT, '..', 'codragraph-graphstore');
|
|
21
|
+
// Workspace directory names — packages live under `packages/<short>` on disk
|
|
22
|
+
// and publish as `@codragraph/<short>` on npm.
|
|
23
|
+
const SHARED_ROOT = path.resolve(ROOT, '..', 'shared');
|
|
24
|
+
const GRAPHSTORE_ROOT = path.resolve(ROOT, '..', 'graphstore');
|
|
25
25
|
const DIST = path.join(ROOT, 'dist');
|
|
26
26
|
const SHARED_DEST = path.join(DIST, '_shared');
|
|
27
27
|
|
|
28
|
-
// ── 1. Build codragraph
|
|
29
|
-
console.log('[build] compiling codragraph
|
|
28
|
+
// ── 1. Build @codragraph/shared ──────────────────────────────────────
|
|
29
|
+
console.log('[build] compiling @codragraph/shared…');
|
|
30
30
|
execSync('npx tsc', { cwd: SHARED_ROOT, stdio: 'inherit' });
|
|
31
31
|
|
|
32
|
-
// ── 2. Build codragraph
|
|
33
|
-
//
|
|
32
|
+
// ── 2. Build @codragraph/graphstore ──────────────────────────────────
|
|
33
|
+
// core depends on this for snapshot/branch/diff types. On a
|
|
34
34
|
// fresh checkout (CI, npm ci) the graphstore dist is empty until we
|
|
35
35
|
// build it here, so step 3 would otherwise fail to resolve
|
|
36
36
|
// `@codragraph/graphstore` imports. Skip gracefully if the workspace
|
|
37
37
|
// is not present (e.g. someone pinned an older monorepo layout).
|
|
38
38
|
if (fs.existsSync(GRAPHSTORE_ROOT)) {
|
|
39
|
-
console.log('[build] compiling codragraph
|
|
39
|
+
console.log('[build] compiling @codragraph/graphstore…');
|
|
40
40
|
execSync('npx tsc', { cwd: GRAPHSTORE_ROOT, stdio: 'inherit' });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// ── 3. Build codragraph
|
|
44
|
-
console.log('[build] compiling codragraph…');
|
|
43
|
+
// ── 3. Build @codragraph/cli ─────────────────────────────────────────
|
|
44
|
+
console.log('[build] compiling @codragraph/cli…');
|
|
45
45
|
execSync('npx tsc', { cwd: ROOT, stdio: 'inherit' });
|
|
46
46
|
|
|
47
|
-
// ──
|
|
47
|
+
// ── 4. Copy shared dist ────────────────────────────────────────────
|
|
48
48
|
console.log('[build] copying shared module into dist/_shared…');
|
|
49
49
|
fs.cpSync(path.join(SHARED_ROOT, 'dist'), SHARED_DEST, { recursive: true });
|
|
50
50
|
|
|
51
|
-
// ──
|
|
52
|
-
console.log('[build] rewriting codragraph
|
|
51
|
+
// ── 5. Rewrite imports ─────────────────────────────────────────────
|
|
52
|
+
console.log('[build] rewriting @codragraph/shared imports…');
|
|
53
53
|
let rewritten = 0;
|
|
54
54
|
|
|
55
55
|
function rewriteFile(filePath) {
|
|
@@ -83,7 +83,7 @@ function walk(dir, extensions, cb) {
|
|
|
83
83
|
|
|
84
84
|
walk(DIST, ['.js', '.d.ts'], rewriteFile);
|
|
85
85
|
|
|
86
|
-
// ──
|
|
86
|
+
// ── 6. Make CLI entry executable ────────────────────────────────────
|
|
87
87
|
const cliEntry = path.join(DIST, 'cli', 'index.js');
|
|
88
88
|
if (fs.existsSync(cliEntry)) fs.chmodSync(cliEntry, 0o755);
|
|
89
89
|
|