@codragraph/cli 2.1.5 → 2.2.0-rc.6
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 +18 -13
- package/dist/cli/analyze.d.ts +9 -4
- package/dist/cli/analyze.js +37 -13
- package/dist/cli/graphpack.d.ts +48 -0
- package/dist/cli/graphpack.js +217 -0
- package/dist/cli/index.js +81 -3
- package/dist/cli/status.d.ts +1 -1
- package/dist/cli/status.js +8 -0
- package/dist/cli/tool.d.ts +11 -2
- package/dist/cli/tool.js +138 -8
- package/dist/core/adaptive-profile.d.ts +52 -0
- package/dist/core/adaptive-profile.js +180 -0
- package/dist/core/cgdb/cgdb-adapter.d.ts +34 -5
- package/dist/core/cgdb/cgdb-adapter.js +418 -5
- package/dist/core/cgdb/pool-adapter.js +1 -1
- package/dist/core/graphpack/index.d.ts +14 -0
- package/dist/core/graphpack/index.js +474 -0
- package/dist/core/graphpack/types.d.ts +129 -0
- package/dist/core/graphpack/types.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +3 -1
- package/dist/core/ingestion/pipeline-phases/structure.js +19 -3
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/run-analyze.d.ts +27 -2
- package/dist/core/run-analyze.js +598 -27
- package/dist/core/search/bm25-index.d.ts +19 -0
- package/dist/core/search/bm25-index.js +68 -29
- package/dist/core/semantic/relationships.d.ts +36 -0
- package/dist/core/semantic/relationships.js +261 -0
- package/dist/mcp/local/local-backend.js +48 -3
- package/dist/mcp/resources.js +125 -0
- package/dist/mcp/tools.js +105 -0
- package/dist/server/api.js +112 -0
- package/dist/storage/repo-manager.d.ts +29 -0
- package/dist/web/assets/agent-CQNZQ-hg.js +1139 -0
- package/dist/web/assets/architectureDiagram-UL44E2DR-B5_goS_i.js +36 -0
- package/dist/web/assets/blockDiagram-7IZFK4PR-D7ZAlDyv.js +132 -0
- package/dist/web/assets/{c4Diagram-DFAF54RM-C4Hl3J2U.js → c4Diagram-Y2BXMSZH-Djcgm_54.js} +1 -1
- package/dist/web/assets/{chunk-7RZVMHOQ-BitYcNVR.js → chunk-3SSMPTDK-Cv2Zy2FO.js} +1 -1
- package/dist/web/assets/{chunk-TBF5ZNIQ-DL5stGM1.js → chunk-6764PJDD-Cppb-jH-.js} +1 -1
- package/dist/web/assets/{chunk-KSICW3F5-BYzvDLNI.js → chunk-AZZRMDJM-BHlLC7p3.js} +1 -1
- package/dist/web/assets/{chunk-AEOMTBSW-BgTIXPsY.js → chunk-JQRUD6KW-3F8Zg-1N.js} +1 -1
- package/dist/web/assets/chunk-KRXBNO2N-C0mbN9a7.js +1 -0
- package/dist/web/assets/chunk-LCXTWHL2-BoiuJpIF.js +231 -0
- package/dist/web/assets/{chunk-O5ABG6QK-dHwHzA6n.js → chunk-LII3EMHJ-Dqq0Qguw.js} +1 -1
- package/dist/web/assets/chunk-RG4AUYOV-Bl5F_gDs.js +206 -0
- package/dist/web/assets/{chunk-TU3PZOEN-RLyvLcv-.js → chunk-T5OCTHI4-B2tIcggA.js} +1 -1
- package/dist/web/assets/chunk-W44A43WB-BHe37iN7.js +13 -0
- package/dist/web/assets/{chunk-RWUO3TPN-BgRTY0_k.js → chunk-ZXARS5L4-wcrIaQvY.js} +1 -1
- package/dist/web/assets/classDiagram-KGZ6W3CR-IbI6v_24.js +1 -0
- package/dist/web/assets/classDiagram-v2-72OJOZXJ-IbI6v_24.js +1 -0
- package/dist/web/assets/{cose-bilkent-PNC4W37J-DVhePRYg.js → cose-bilkent-UX7MHV2Q-BWr7v0Wr.js} +1 -1
- package/dist/web/assets/dagre-ND4H6XIP-De5LIh1B.js +4 -0
- package/dist/web/assets/diagram-3NCE3AQN-Dd22FSHy.js +43 -0
- package/dist/web/assets/diagram-GF46GFSD-Cev3THY8.js +24 -0
- package/dist/web/assets/diagram-HNR7UZ2L-D8Z8RQGs.js +3 -0
- package/dist/web/assets/diagram-QXG6HAR7-B8VOJOiE.js +24 -0
- package/dist/web/assets/diagram-WEQXMOUZ-va1bLoMD.js +10 -0
- package/dist/web/assets/{erDiagram-GCSMX5X6-C3dhDFA8.js → erDiagram-L5TCEMPS-B3_9uAoP.js} +5 -5
- package/dist/web/assets/{flowDiagram-OTCZ4VVT-CWSFWmhr.js → flowDiagram-H6V6AXG4-98m6maI1.js} +9 -9
- package/dist/web/assets/ganttDiagram-JCBTUEKG-vE2nzETb.js +292 -0
- package/dist/web/assets/gitGraphDiagram-S2ZK5IYY-DKc8uUg_.js +106 -0
- package/dist/web/assets/index-BAhe1HSk.css +1 -0
- package/dist/web/assets/index-VTKdaklA.js +1415 -0
- package/dist/web/assets/infoDiagram-3YFTVSEB-DYP-Srzx.js +2 -0
- package/dist/web/assets/{ishikawaDiagram-YMYX4NHK-DUoJvNP2.js → ishikawaDiagram-BNXS4ZKH-QZnkpmmb.js} +3 -3
- package/dist/web/assets/{journeyDiagram-SO5T7YLQ-RMFPNNqz.js → journeyDiagram-M6C3CM3L-B5ojIuqu.js} +1 -1
- package/dist/web/assets/{kanban-definition-LJHFXRCJ-BzpDs1K9.js → kanban-definition-75IXJCU3-BJA8liRR.js} +4 -4
- package/dist/web/assets/{katex-GD7MH7QM-DBQvrix-.js → katex-K3KEBU37-DUqZiCRL.js} +1 -1
- package/dist/web/assets/mindmap-definition-2TDM6QVE-BQj5yylD.js +96 -0
- package/dist/web/assets/pieDiagram-CU6KROY3-4eSrPiQz.js +30 -0
- package/dist/web/assets/quadrantDiagram-VICAPDV7-PzxN8j55.js +7 -0
- package/dist/web/assets/{requirementDiagram-M5DCFWZL-DLHOVTSv.js → requirementDiagram-JXO7QTGE-CtplTc5y.js} +2 -2
- package/dist/web/assets/sankeyDiagram-URQDO5SZ-CoSgvkxv.js +40 -0
- package/dist/web/assets/sequenceDiagram-VS2MUI6T-D7ygyXvJ.js +162 -0
- package/dist/web/assets/stateDiagram-7D4R322I-v01gvwji.js +1 -0
- package/dist/web/assets/stateDiagram-v2-36443NZ5-DFD2b8_x.js +1 -0
- package/dist/web/assets/{timeline-definition-5SPVSISX-TRSDRgPw.js → timeline-definition-O6YCAMPW-CTI3M65J.js} +4 -4
- package/dist/web/assets/{vennDiagram-IE5QUKF5-DNy7HRBM.js → vennDiagram-MWXL3ELB-RnB0XMP7.js} +6 -6
- package/dist/web/assets/wardley-L42UT6IY-5TKZOOLJ-C-ZcgEBb.js +173 -0
- package/dist/web/assets/wardleyDiagram-CUQ6CDDI-EwRi4kwo.js +78 -0
- package/dist/web/assets/{xychartDiagram-ZHJ5623Y-Dr9r7a35.js → xychartDiagram-N2JHSOCM-DA38II6y.js} +4 -4
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/vendor/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/dist/web/assets/agent-D5lb0zXz.js +0 -1089
- package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +0 -36
- package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +0 -132
- package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +0 -231
- package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +0 -1
- package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +0 -13
- package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +0 -1
- package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +0 -206
- package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +0 -1
- package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +0 -1
- package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +0 -4
- package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +0 -43
- package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +0 -24
- package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +0 -10
- package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +0 -24
- package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +0 -292
- package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +0 -106
- package/dist/web/assets/index-BgeqpYgd.js +0 -1415
- package/dist/web/assets/index-CT0GtFLZ.css +0 -1
- package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +0 -2
- package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +0 -96
- package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +0 -30
- package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +0 -7
- package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +0 -10
- package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +0 -157
- package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +0 -1
- package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +0 -1
- package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +0 -161
- package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +0 -20
|
@@ -15,7 +15,26 @@ export interface BM25SearchResult {
|
|
|
15
15
|
score: number;
|
|
16
16
|
rank: number;
|
|
17
17
|
nodeIds?: string[];
|
|
18
|
+
/** False when results came from the bounded fallback scan instead of FTS. */
|
|
19
|
+
ftsUsed?: boolean;
|
|
18
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* FTS table set served by `searchFTSFromCgdb`. Centralised so that both
|
|
23
|
+
* the CLI/pipeline path and the MCP pool path stay in lockstep.
|
|
24
|
+
*
|
|
25
|
+
* The properties list is computed at FTS-create time via `ftsPropertiesFor`
|
|
26
|
+
* — for repos that were analysed with `--compress brotli|zstd`, the
|
|
27
|
+
* `content` column holds base64-of-encoded-bytes and would tokenise to
|
|
28
|
+
* useless tokens. Those repos get name-only FTS so search at least
|
|
29
|
+
* matches function/class names instead of returning random hits on
|
|
30
|
+
* base64 alphabet. Plain (compress='none' / unset) repos get the full
|
|
31
|
+
* `name + content` index for body-text matches. RFC 0001 Phase 2.5.
|
|
32
|
+
*/
|
|
33
|
+
export declare const FTS_TABLES: ReadonlyArray<{
|
|
34
|
+
table: string;
|
|
35
|
+
indexName: string;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const ftsPropertiesFor: (compress: "none" | "brotli" | "zstd" | undefined) => readonly string[];
|
|
19
38
|
/**
|
|
20
39
|
* Search using LadybugDB's built-in FTS (always fresh, reads from disk)
|
|
21
40
|
*
|
|
@@ -23,22 +23,21 @@ import { queryFTS, ensureFTSIndex, executeQuery as executeCoreQuery, } from '../
|
|
|
23
23
|
* base64 alphabet. Plain (compress='none' / unset) repos get the full
|
|
24
24
|
* `name + content` index for body-text matches. RFC 0001 Phase 2.5.
|
|
25
25
|
*/
|
|
26
|
-
const FTS_TABLES = [
|
|
26
|
+
export const FTS_TABLES = [
|
|
27
27
|
{ table: 'File', indexName: 'file_fts' },
|
|
28
28
|
{ table: 'Function', indexName: 'function_fts' },
|
|
29
29
|
{ table: 'Class', indexName: 'class_fts' },
|
|
30
30
|
{ table: 'Method', indexName: 'method_fts' },
|
|
31
31
|
{ table: 'Interface', indexName: 'interface_fts' },
|
|
32
32
|
];
|
|
33
|
-
const ftsPropertiesFor = (compress) => !compress || compress === 'none' ? ['name', 'content'] : ['name'];
|
|
33
|
+
export const ftsPropertiesFor = (compress) => (!compress || compress === 'none' ? ['name', 'content'] : ['name']);
|
|
34
34
|
/**
|
|
35
|
-
* Look up
|
|
36
|
-
* (registry-derived); the CLI path passes nothing and we walk up from
|
|
37
|
-
* cwd.
|
|
38
|
-
*
|
|
39
|
-
* quality, never wrong results.
|
|
35
|
+
* Look up query-related index metadata for a repo. The MCP path passes `repoId`
|
|
36
|
+
* (registry-derived); the CLI/core path passes nothing and we walk up from
|
|
37
|
+
* cwd. Read-only callers use `ftsReady` to avoid expensive probes for
|
|
38
|
+
* FTS indexes that older analyses never wrote.
|
|
40
39
|
*/
|
|
41
|
-
async function
|
|
40
|
+
async function getSearchMeta(repoId) {
|
|
42
41
|
try {
|
|
43
42
|
const repoMod = await import('../../storage/repo-manager.js');
|
|
44
43
|
if (repoId) {
|
|
@@ -50,16 +49,22 @@ async function getCompressMode(repoId) {
|
|
|
50
49
|
const base = entry.name.toLowerCase();
|
|
51
50
|
if (base === repoId || repoId.startsWith(`${base}-`)) {
|
|
52
51
|
const meta = await repoMod.loadMeta(entry.storagePath);
|
|
53
|
-
return
|
|
52
|
+
return {
|
|
53
|
+
compress: meta?.compress ?? 'none',
|
|
54
|
+
ftsReady: meta?.searchIndexes?.fts === true,
|
|
55
|
+
};
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
|
-
return 'none';
|
|
58
|
+
return { compress: 'none', ftsReady: false };
|
|
57
59
|
}
|
|
58
60
|
const repo = await repoMod.findRepo(process.cwd());
|
|
59
|
-
return
|
|
61
|
+
return {
|
|
62
|
+
compress: repo?.meta?.compress ?? 'none',
|
|
63
|
+
ftsReady: repo?.meta?.searchIndexes?.fts,
|
|
64
|
+
};
|
|
60
65
|
}
|
|
61
66
|
catch {
|
|
62
|
-
return 'none';
|
|
67
|
+
return { compress: 'none', ftsReady: repoId ? false : undefined };
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
const FALLBACK_SCAN_LIMIT = 50_000;
|
|
@@ -105,6 +110,22 @@ function searchTerms(query) {
|
|
|
105
110
|
?.filter((term) => term.length > 1 && !BOOLEAN_QUERY_TOKENS.has(term));
|
|
106
111
|
return [...new Set(terms ?? [])];
|
|
107
112
|
}
|
|
113
|
+
function searchTermVariants(query) {
|
|
114
|
+
const rawTerms = query.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
115
|
+
const variants = new Set();
|
|
116
|
+
for (const raw of rawTerms) {
|
|
117
|
+
const lower = raw.toLowerCase();
|
|
118
|
+
if (raw.length <= 1 || BOOLEAN_QUERY_TOKENS.has(lower))
|
|
119
|
+
continue;
|
|
120
|
+
variants.add(raw);
|
|
121
|
+
variants.add(lower);
|
|
122
|
+
variants.add(raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase());
|
|
123
|
+
}
|
|
124
|
+
return [...variants];
|
|
125
|
+
}
|
|
126
|
+
function cypherString(value) {
|
|
127
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
128
|
+
}
|
|
108
129
|
function scoreFallbackNode(node, query, properties) {
|
|
109
130
|
const terms = searchTerms(query);
|
|
110
131
|
if (terms.length === 0)
|
|
@@ -131,10 +152,15 @@ function scoreFallbackNode(node, query, properties) {
|
|
|
131
152
|
}
|
|
132
153
|
async function queryFallbackViaExecutor(executor, tableName, properties, query, limit) {
|
|
133
154
|
try {
|
|
155
|
+
const variants = searchTermVariants(query);
|
|
156
|
+
const conditions = variants.flatMap((term) => properties.map((property) => `node.${property} CONTAINS ${cypherString(term)}`));
|
|
157
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' OR ')}` : '';
|
|
158
|
+
const candidateLimit = Math.min(FALLBACK_SCAN_LIMIT, Math.max(limit * 25, 200));
|
|
134
159
|
const rows = await executor(`
|
|
135
160
|
MATCH (node:${tableName})
|
|
161
|
+
${whereClause}
|
|
136
162
|
RETURN node
|
|
137
|
-
LIMIT ${
|
|
163
|
+
LIMIT ${candidateLimit}
|
|
138
164
|
`);
|
|
139
165
|
return rows
|
|
140
166
|
.map((row) => {
|
|
@@ -158,10 +184,7 @@ async function fallbackSearchAllTables(executor, query, limit,
|
|
|
158
184
|
// pre-Phase-2 behaviour (`['name', 'content']`) for callers that don't
|
|
159
185
|
// pass a value.
|
|
160
186
|
properties = ['name', 'content']) {
|
|
161
|
-
const results =
|
|
162
|
-
for (const { table } of FTS_TABLES) {
|
|
163
|
-
results.push(await queryFallbackViaExecutor(executor, table, properties, query, limit));
|
|
164
|
-
}
|
|
187
|
+
const results = await Promise.all(FTS_TABLES.map(({ table }) => queryFallbackViaExecutor(executor, table, properties, query, limit)));
|
|
165
188
|
return results;
|
|
166
189
|
}
|
|
167
190
|
/**
|
|
@@ -179,6 +202,7 @@ export const searchFTSFromCgdb = async (query, limit = 20, repoId) => {
|
|
|
179
202
|
if (!query.trim() || limit <= 0)
|
|
180
203
|
return [];
|
|
181
204
|
let fileResults, functionResults, classResults, methodResults, interfaceResults;
|
|
205
|
+
let usedFallback = false;
|
|
182
206
|
if (repoId) {
|
|
183
207
|
// Use MCP connection pool via dynamic import
|
|
184
208
|
// IMPORTANT: FTS queries run sequentially to avoid connection contention.
|
|
@@ -188,34 +212,46 @@ export const searchFTSFromCgdb = async (query, limit = 20, repoId) => {
|
|
|
188
212
|
const executor = (cypher) => executeQuery(repoId, cypher);
|
|
189
213
|
// The MCP/LocalBackend pool is opened read-only so it can safely coexist
|
|
190
214
|
// with `codragraph analyze`. Do not issue CREATE_FTS_INDEX here: when
|
|
191
|
-
// persisted FTS indexes are missing,
|
|
192
|
-
//
|
|
215
|
+
// persisted FTS indexes are missing, meta.searchIndexes lets us skip
|
|
216
|
+
// QUERY_FTS_INDEX and go straight to the bounded fallback below.
|
|
193
217
|
// RFC 0001 Phase 2.5: drop `content` from fallback scoring for repos
|
|
194
218
|
// analysed with --compress brotli|zstd — the column holds encoded bytes.
|
|
195
|
-
const
|
|
196
|
-
const properties = ftsPropertiesFor(compress);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
const meta = await getSearchMeta(repoId);
|
|
220
|
+
const properties = ftsPropertiesFor(meta.compress);
|
|
221
|
+
if (meta.ftsReady) {
|
|
222
|
+
fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
|
|
223
|
+
functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
|
|
224
|
+
classResults = await queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit);
|
|
225
|
+
methodResults = await queryFTSViaExecutor(executor, 'Method', 'method_fts', query, limit);
|
|
226
|
+
interfaceResults = await queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
usedFallback = true;
|
|
230
|
+
fileResults = [];
|
|
231
|
+
functionResults = [];
|
|
232
|
+
classResults = [];
|
|
233
|
+
methodResults = [];
|
|
234
|
+
interfaceResults = [];
|
|
235
|
+
}
|
|
202
236
|
if (fileResults.length +
|
|
203
237
|
functionResults.length +
|
|
204
238
|
classResults.length +
|
|
205
239
|
methodResults.length +
|
|
206
240
|
interfaceResults.length ===
|
|
207
241
|
0) {
|
|
242
|
+
usedFallback = true;
|
|
208
243
|
[fileResults, functionResults, classResults, methodResults, interfaceResults] =
|
|
209
244
|
await fallbackSearchAllTables(executor, query, limit, properties);
|
|
210
245
|
}
|
|
211
246
|
}
|
|
212
247
|
else {
|
|
213
248
|
// Use core cgdb adapter (CLI / pipeline context) — also sequential for safety.
|
|
214
|
-
//
|
|
249
|
+
// Defensive fallback for older indexes or direct core callers: create FTS
|
|
250
|
+
// indexes on first query when this process owns a writable connection.
|
|
215
251
|
// RFC 0001 Phase 2.5 — same `compress`-aware property selection as the MCP
|
|
216
252
|
// path; the CLI walks up from cwd to find the repo's meta.json.
|
|
217
|
-
const
|
|
218
|
-
const properties = ftsPropertiesFor(compress);
|
|
253
|
+
const meta = await getSearchMeta();
|
|
254
|
+
const properties = ftsPropertiesFor(meta.compress);
|
|
219
255
|
for (const { table, indexName } of FTS_TABLES) {
|
|
220
256
|
await ensureFTSIndex(table, indexName, [...properties]).catch(() => { });
|
|
221
257
|
}
|
|
@@ -230,6 +266,7 @@ export const searchFTSFromCgdb = async (query, limit = 20, repoId) => {
|
|
|
230
266
|
methodResults.length +
|
|
231
267
|
interfaceResults.length ===
|
|
232
268
|
0) {
|
|
269
|
+
usedFallback = true;
|
|
233
270
|
[fileResults, functionResults, classResults, methodResults, interfaceResults] =
|
|
234
271
|
await fallbackSearchAllTables(executeCoreQuery, query, limit, properties);
|
|
235
272
|
}
|
|
@@ -258,6 +295,7 @@ export const searchFTSFromCgdb = async (query, limit = 20, repoId) => {
|
|
|
258
295
|
filePath,
|
|
259
296
|
score: top3.reduce((acc, e) => acc + e.score, 0),
|
|
260
297
|
nodeIds: top3.map((e) => e.nodeId).filter((id) => id),
|
|
298
|
+
ftsUsed: !usedFallback,
|
|
261
299
|
});
|
|
262
300
|
}
|
|
263
301
|
// Sort by score descending and add rank
|
|
@@ -269,5 +307,6 @@ export const searchFTSFromCgdb = async (query, limit = 20, repoId) => {
|
|
|
269
307
|
score: r.score,
|
|
270
308
|
rank: index + 1,
|
|
271
309
|
nodeIds: r.nodeIds,
|
|
310
|
+
ftsUsed: r.ftsUsed,
|
|
272
311
|
}));
|
|
273
312
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const SEMANTIC_RELATIONSHIP_VERSION: "semantic-extractor-v1";
|
|
2
|
+
export type SemanticRelationshipFamily = 'COMPOSES' | 'ADAPTS' | 'DELEGATES_TO' | 'WRAPS' | 'CONFIGURES' | 'FACTORY_CREATES' | 'ORCHESTRATES' | 'PROXIES_TO' | 'MAPS_TO';
|
|
3
|
+
export type SemanticRelationshipProvenance = 'extracted' | 'inferred' | 'LLM_INFERRED' | 'human-confirmed';
|
|
4
|
+
export interface SemanticRelationshipEvidence {
|
|
5
|
+
readonly filePath?: string;
|
|
6
|
+
readonly startLine?: number;
|
|
7
|
+
readonly endLine?: number;
|
|
8
|
+
readonly rawEdgeType: string;
|
|
9
|
+
readonly rawEdgeConfidence?: number;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SemanticRelationship {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly family: SemanticRelationshipFamily;
|
|
15
|
+
readonly sourceId: string;
|
|
16
|
+
readonly sourceName?: string;
|
|
17
|
+
readonly targetId: string;
|
|
18
|
+
readonly targetName?: string;
|
|
19
|
+
readonly confidence: number;
|
|
20
|
+
readonly provenance: SemanticRelationshipProvenance;
|
|
21
|
+
readonly extractorVersion: typeof SEMANTIC_RELATIONSHIP_VERSION;
|
|
22
|
+
readonly evidence: SemanticRelationshipEvidence;
|
|
23
|
+
}
|
|
24
|
+
export interface SemanticRelationshipReport {
|
|
25
|
+
readonly snapshotId?: string;
|
|
26
|
+
readonly extractorVersion: typeof SEMANTIC_RELATIONSHIP_VERSION;
|
|
27
|
+
readonly llmEnabled: boolean;
|
|
28
|
+
readonly relationships: readonly SemanticRelationship[];
|
|
29
|
+
readonly summary: Record<SemanticRelationshipFamily, number>;
|
|
30
|
+
}
|
|
31
|
+
export declare const analyzeSemanticRelationships: (opts: {
|
|
32
|
+
readonly storagePath: string;
|
|
33
|
+
readonly limit?: number;
|
|
34
|
+
readonly llm?: boolean;
|
|
35
|
+
readonly write?: boolean;
|
|
36
|
+
}) => Promise<SemanticRelationshipReport>;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { FsCAS, getJson, parseObjectId, readCommit, resolveHeadCommit, } from '@codragraph/graphstore';
|
|
5
|
+
import { GRAPHSTORE_SUBDIR } from '../graphstore/index.js';
|
|
6
|
+
export const SEMANTIC_RELATIONSHIP_VERSION = 'semantic-extractor-v1';
|
|
7
|
+
export const analyzeSemanticRelationships = async (opts) => {
|
|
8
|
+
const graphstoreRoot = path.join(opts.storagePath, GRAPHSTORE_SUBDIR);
|
|
9
|
+
const cas = new FsCAS({ root: graphstoreRoot });
|
|
10
|
+
const head = await resolveHeadCommit({ root: graphstoreRoot });
|
|
11
|
+
if (head === null) {
|
|
12
|
+
throw new Error('semantic analyze requires a graphstore HEAD. Run `codragraph analyze` first.');
|
|
13
|
+
}
|
|
14
|
+
const commit = await readCommit(cas, head);
|
|
15
|
+
const snapshot = await getJson(cas, commit.snapshot);
|
|
16
|
+
const manifest = await getJson(cas, parseObjectId(snapshot.manifestId));
|
|
17
|
+
const nodes = await loadNodes(cas, manifest);
|
|
18
|
+
const relationships = await inferRelationships({
|
|
19
|
+
cas,
|
|
20
|
+
manifest,
|
|
21
|
+
nodes,
|
|
22
|
+
limit: opts.limit ?? 1000,
|
|
23
|
+
});
|
|
24
|
+
const report = {
|
|
25
|
+
snapshotId: commit.snapshot,
|
|
26
|
+
extractorVersion: SEMANTIC_RELATIONSHIP_VERSION,
|
|
27
|
+
llmEnabled: opts.llm === true,
|
|
28
|
+
relationships,
|
|
29
|
+
summary: summarize(relationships),
|
|
30
|
+
};
|
|
31
|
+
if (opts.write) {
|
|
32
|
+
await fs.writeFile(path.join(opts.storagePath, 'semantic-relationships.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
return report;
|
|
35
|
+
};
|
|
36
|
+
const loadNodes = async (cas, manifest) => {
|
|
37
|
+
const nodes = new Map();
|
|
38
|
+
for (const [table, tableManifest] of Object.entries(manifest.nodeTables)) {
|
|
39
|
+
for (const [id, objectId] of Object.entries(tableManifest.rows)) {
|
|
40
|
+
const row = await getJson(cas, parseObjectId(objectId));
|
|
41
|
+
nodes.set(id, {
|
|
42
|
+
id,
|
|
43
|
+
table,
|
|
44
|
+
name: stringProp(row, 'name'),
|
|
45
|
+
filePath: stringProp(row, 'filePath'),
|
|
46
|
+
startLine: numberProp(row, 'startLine'),
|
|
47
|
+
endLine: numberProp(row, 'endLine'),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return nodes;
|
|
52
|
+
};
|
|
53
|
+
const inferRelationships = async (opts) => {
|
|
54
|
+
const highSignal = [];
|
|
55
|
+
const fallback = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const edgeEntries = Object.entries(opts.manifest.edges.rows);
|
|
58
|
+
for (const [edgeKey, objectId] of edgeEntries) {
|
|
59
|
+
let edge;
|
|
60
|
+
const parsed = parseEdgeKey(edgeKey);
|
|
61
|
+
if (!parsed)
|
|
62
|
+
edge = await getJson(opts.cas, parseObjectId(objectId));
|
|
63
|
+
const from = parsed?.from ?? stringProp(edge, 'from');
|
|
64
|
+
const to = parsed?.to ?? stringProp(edge, 'to');
|
|
65
|
+
const rawType = parsed?.rawType ?? stringProp(edge, 'type');
|
|
66
|
+
if (!from || !to || !rawType)
|
|
67
|
+
continue;
|
|
68
|
+
if (!SEMANTIC_RAW_EDGE_TYPES.has(rawType))
|
|
69
|
+
continue;
|
|
70
|
+
const source = opts.nodes.get(from);
|
|
71
|
+
const target = opts.nodes.get(to);
|
|
72
|
+
const classified = classifySemanticEdge({ rawType, source, target });
|
|
73
|
+
if (!classified)
|
|
74
|
+
continue;
|
|
75
|
+
const key = `${from}|${classified.family}|${to}`;
|
|
76
|
+
if (seen.has(key))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
const relationship = {
|
|
80
|
+
id: `semantic:${hashKey(key).slice(0, 32)}`,
|
|
81
|
+
family: classified.family,
|
|
82
|
+
sourceId: from,
|
|
83
|
+
...(source?.name ? { sourceName: source.name } : {}),
|
|
84
|
+
targetId: to,
|
|
85
|
+
...(target?.name ? { targetName: target.name } : {}),
|
|
86
|
+
confidence: classified.confidence,
|
|
87
|
+
provenance: rawType === classified.family ? 'extracted' : 'inferred',
|
|
88
|
+
extractorVersion: SEMANTIC_RELATIONSHIP_VERSION,
|
|
89
|
+
evidence: {
|
|
90
|
+
filePath: source?.filePath ?? target?.filePath,
|
|
91
|
+
startLine: source?.startLine ?? target?.startLine,
|
|
92
|
+
endLine: source?.endLine ?? target?.endLine,
|
|
93
|
+
rawEdgeType: rawType,
|
|
94
|
+
rawEdgeConfidence: edge ? numberProp(edge, 'confidence') : undefined,
|
|
95
|
+
reason: classified.reason,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const bucket = classified.family === 'COMPOSES' ? fallback : highSignal;
|
|
99
|
+
bucket.push(relationship);
|
|
100
|
+
if (highSignal.length >= opts.limit)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
...highSignal.sort(compareSemanticRelationships),
|
|
105
|
+
...fallback.sort(compareSemanticRelationships),
|
|
106
|
+
].slice(0, opts.limit);
|
|
107
|
+
};
|
|
108
|
+
const SEMANTIC_RAW_EDGE_TYPES = new Set([
|
|
109
|
+
'CALLS',
|
|
110
|
+
'IMPORTS',
|
|
111
|
+
'EXTENDS',
|
|
112
|
+
'IMPLEMENTS',
|
|
113
|
+
'HAS_METHOD',
|
|
114
|
+
'HAS_PROPERTY',
|
|
115
|
+
'ACCESSES',
|
|
116
|
+
'METHOD_OVERRIDES',
|
|
117
|
+
'METHOD_IMPLEMENTS',
|
|
118
|
+
'WRAPS',
|
|
119
|
+
'FETCHES',
|
|
120
|
+
'HANDLES_ROUTE',
|
|
121
|
+
'HANDLES_TOOL',
|
|
122
|
+
'QUERIES',
|
|
123
|
+
]);
|
|
124
|
+
const parseEdgeKey = (edgeKey) => {
|
|
125
|
+
const parts = edgeKey.split('|');
|
|
126
|
+
if (parts.length < 3)
|
|
127
|
+
return null;
|
|
128
|
+
const [from, rawType, ...toParts] = parts;
|
|
129
|
+
if (!from || !rawType || toParts.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
return { from, rawType, to: toParts.join('|') };
|
|
132
|
+
};
|
|
133
|
+
const classifySemanticEdge = (input) => {
|
|
134
|
+
const sourceName = input.source?.name ?? input.source?.id ?? '';
|
|
135
|
+
const targetName = input.target?.name ?? input.target?.id ?? '';
|
|
136
|
+
const source = sourceName.toLowerCase();
|
|
137
|
+
const target = targetName.toLowerCase();
|
|
138
|
+
const rawType = input.rawType;
|
|
139
|
+
if (rawType === 'WRAPS' || hasAny(source, ['wrapper', 'middleware', 'decorator'])) {
|
|
140
|
+
return {
|
|
141
|
+
family: 'WRAPS',
|
|
142
|
+
confidence: rawType === 'WRAPS' ? 0.95 : 0.78,
|
|
143
|
+
reason: 'wrapper/decorator naming or raw WRAPS edge',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (rawType === 'IMPLEMENTS' && hasAny(source, ['adapter', 'adaptor'])) {
|
|
147
|
+
return { family: 'ADAPTS', confidence: 0.86, reason: 'adapter implementation edge' };
|
|
148
|
+
}
|
|
149
|
+
if (hasAny(source, ['adapter', 'adaptor']) && ['CALLS', 'IMPORTS'].includes(rawType)) {
|
|
150
|
+
return { family: 'ADAPTS', confidence: 0.72, reason: 'adapter symbol delegates to target' };
|
|
151
|
+
}
|
|
152
|
+
if (hasAny(source, ['proxy', 'client']) && ['CALLS', 'FETCHES', 'IMPORTS'].includes(rawType)) {
|
|
153
|
+
return {
|
|
154
|
+
family: 'PROXIES_TO',
|
|
155
|
+
confidence: 0.74,
|
|
156
|
+
reason: 'proxy/client symbol forwards to target',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (hasAny(source, ['factory', 'builder']) || /^(create|make|build)[A-Z_]/.test(sourceName)) {
|
|
160
|
+
if (['CALLS', 'HAS_METHOD', 'IMPORTS'].includes(rawType)) {
|
|
161
|
+
return {
|
|
162
|
+
family: 'FACTORY_CREATES',
|
|
163
|
+
confidence: 0.75,
|
|
164
|
+
reason: 'factory/create/build symbol reaches constructed target',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (hasAny(source, ['config', 'configure', 'setup', 'options']) &&
|
|
169
|
+
['CALLS', 'ACCESSES', 'IMPORTS'].includes(rawType)) {
|
|
170
|
+
return {
|
|
171
|
+
family: 'CONFIGURES',
|
|
172
|
+
confidence: 0.7,
|
|
173
|
+
reason: 'configuration symbol controls target behavior',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (hasAny(source, ['orchestrator', 'coordinator', 'workflow', 'pipeline', 'runner']) &&
|
|
177
|
+
rawType === 'CALLS') {
|
|
178
|
+
return {
|
|
179
|
+
family: 'ORCHESTRATES',
|
|
180
|
+
confidence: 0.76,
|
|
181
|
+
reason: 'orchestrator/coordinator call edge',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (hasAny(source, ['mapper', 'map', 'dto', 'transformer', 'serializer']) ||
|
|
185
|
+
hasAny(target, ['dto', 'schema', 'model'])) {
|
|
186
|
+
if (['CALLS', 'IMPORTS', 'ACCESSES'].includes(rawType)) {
|
|
187
|
+
return { family: 'MAPS_TO', confidence: 0.7, reason: 'mapper/DTO transformation signal' };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (rawType === 'HAS_PROPERTY' || rawType === 'ACCESSES') {
|
|
191
|
+
return {
|
|
192
|
+
family: 'COMPOSES',
|
|
193
|
+
confidence: 0.68,
|
|
194
|
+
reason: 'field/property ownership or access edge',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (rawType === 'CALLS' && /^(handle|run|execute|process|dispatch)/i.test(sourceName)) {
|
|
198
|
+
return {
|
|
199
|
+
family: 'DELEGATES_TO',
|
|
200
|
+
confidence: 0.66,
|
|
201
|
+
reason: 'handler/executor delegates to callee',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
const summarize = (relationships) => {
|
|
207
|
+
const summary = {
|
|
208
|
+
COMPOSES: 0,
|
|
209
|
+
ADAPTS: 0,
|
|
210
|
+
DELEGATES_TO: 0,
|
|
211
|
+
WRAPS: 0,
|
|
212
|
+
CONFIGURES: 0,
|
|
213
|
+
FACTORY_CREATES: 0,
|
|
214
|
+
ORCHESTRATES: 0,
|
|
215
|
+
PROXIES_TO: 0,
|
|
216
|
+
MAPS_TO: 0,
|
|
217
|
+
};
|
|
218
|
+
for (const rel of relationships)
|
|
219
|
+
summary[rel.family]++;
|
|
220
|
+
return summary;
|
|
221
|
+
};
|
|
222
|
+
const compareSemanticRelationships = (a, b) => {
|
|
223
|
+
const byFamily = familyPriority(a.family) - familyPriority(b.family);
|
|
224
|
+
if (byFamily !== 0)
|
|
225
|
+
return byFamily;
|
|
226
|
+
return b.confidence - a.confidence;
|
|
227
|
+
};
|
|
228
|
+
const familyPriority = (family) => {
|
|
229
|
+
switch (family) {
|
|
230
|
+
case 'ADAPTS':
|
|
231
|
+
return 0;
|
|
232
|
+
case 'WRAPS':
|
|
233
|
+
return 1;
|
|
234
|
+
case 'PROXIES_TO':
|
|
235
|
+
return 2;
|
|
236
|
+
case 'FACTORY_CREATES':
|
|
237
|
+
return 3;
|
|
238
|
+
case 'ORCHESTRATES':
|
|
239
|
+
return 4;
|
|
240
|
+
case 'CONFIGURES':
|
|
241
|
+
return 5;
|
|
242
|
+
case 'MAPS_TO':
|
|
243
|
+
return 6;
|
|
244
|
+
case 'DELEGATES_TO':
|
|
245
|
+
return 7;
|
|
246
|
+
case 'COMPOSES':
|
|
247
|
+
return 8;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const hasAny = (value, needles) => needles.some((needle) => value.includes(needle));
|
|
251
|
+
const stringProp = (row, key) => {
|
|
252
|
+
const value = row[key];
|
|
253
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
254
|
+
};
|
|
255
|
+
const numberProp = (row, key) => {
|
|
256
|
+
const value = row[key];
|
|
257
|
+
return typeof value === 'number' ? value : undefined;
|
|
258
|
+
};
|
|
259
|
+
const hashKey = (key) => {
|
|
260
|
+
return crypto.createHash('sha256').update(key).digest('hex');
|
|
261
|
+
};
|
|
@@ -768,6 +768,48 @@ export class LocalBackend {
|
|
|
768
768
|
params?.id ??
|
|
769
769
|
''), params?.repo, params?.direction ??
|
|
770
770
|
'upstream', clampNumber(params?.limit, 1, 500, 100));
|
|
771
|
+
case 'graphpack_status': {
|
|
772
|
+
const { getGraphpackStatus } = await import('../../core/graphpack/index.js');
|
|
773
|
+
return getGraphpackStatus({
|
|
774
|
+
repoPath: repo.repoPath,
|
|
775
|
+
storagePath: repo.storagePath,
|
|
776
|
+
strict: params?.strict === true,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
case 'graphpack_publish': {
|
|
780
|
+
const { publishGraphpack } = await import('../../core/graphpack/index.js');
|
|
781
|
+
const args = params ?? {};
|
|
782
|
+
return publishGraphpack({
|
|
783
|
+
repoPath: repo.repoPath,
|
|
784
|
+
storagePath: repo.storagePath,
|
|
785
|
+
repoName: typeof args['repo'] === 'string' ? args['repo'] : repo.name,
|
|
786
|
+
analyzerVersion: 'mcp',
|
|
787
|
+
target: args['target'] === 'pr' ? 'pr' : 'main',
|
|
788
|
+
artifactDir: typeof args['artifact_dir'] === 'string' ? args['artifact_dir'] : undefined,
|
|
789
|
+
artifactUrl: typeof args['artifact_url'] === 'string' ? args['artifact_url'] : undefined,
|
|
790
|
+
baseSnapshotId: typeof args['base_snapshot_id'] === 'string' ? args['base_snapshot_id'] : undefined,
|
|
791
|
+
headSnapshotId: typeof args['head_snapshot_id'] === 'string' ? args['head_snapshot_id'] : undefined,
|
|
792
|
+
pullRequest: typeof args['pull_request'] === 'string' ? args['pull_request'] : undefined,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
case 'graphpack_pull': {
|
|
796
|
+
const { pullGraphpack } = await import('../../core/graphpack/index.js');
|
|
797
|
+
const args = params ?? {};
|
|
798
|
+
return pullGraphpack({
|
|
799
|
+
repoPath: repo.repoPath,
|
|
800
|
+
storagePath: repo.storagePath,
|
|
801
|
+
artifactDir: typeof args['artifact_dir'] === 'string' ? args['artifact_dir'] : undefined,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
case 'semantic_relationships': {
|
|
805
|
+
const { analyzeSemanticRelationships } = await import('../../core/semantic/relationships.js');
|
|
806
|
+
return analyzeSemanticRelationships({
|
|
807
|
+
storagePath: repo.storagePath,
|
|
808
|
+
llm: params?.llm === true,
|
|
809
|
+
limit: clampNumber(params?.limit, 1, 10000, 1000),
|
|
810
|
+
write: false,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
771
813
|
case 'harness_swarm_run': {
|
|
772
814
|
// Same lazy-import dance as harness_run (see comments below) — keeps
|
|
773
815
|
// codragraph-harness optional and avoids a circular build-time dep.
|
|
@@ -2107,10 +2149,12 @@ export class LocalBackend {
|
|
|
2107
2149
|
queryParams[`hunkEnd${i}`] = hunk.endLine;
|
|
2108
2150
|
});
|
|
2109
2151
|
const symbolQuery = `
|
|
2110
|
-
MATCH (n)
|
|
2152
|
+
MATCH (f:File)-[r:CodeRelation]->(n)
|
|
2153
|
+
WHERE r.type IN ['DEFINES', 'CONTAINS']
|
|
2154
|
+
AND f.filePath ENDS WITH $filePath
|
|
2111
2155
|
AND n.startLine IS NOT NULL AND n.endLine IS NOT NULL
|
|
2112
2156
|
AND (${overlapConditions})
|
|
2113
|
-
RETURN n.id AS id, n.name AS name,
|
|
2157
|
+
RETURN n.id AS id, n.name AS name, '' AS type,
|
|
2114
2158
|
n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
2115
2159
|
`;
|
|
2116
2160
|
try {
|
|
@@ -2119,7 +2163,8 @@ export class LocalBackend {
|
|
|
2119
2163
|
changedSymbols.push({
|
|
2120
2164
|
id: sym.id || sym[0],
|
|
2121
2165
|
name: sym.name || sym[1],
|
|
2122
|
-
type: sym.type || sym[
|
|
2166
|
+
type: safeNodeLabelForCypher(sym.type, sym.id || sym[0]) ??
|
|
2167
|
+
firstNonEmptyString(sym.type, sym.__cgLabel, sym[2]),
|
|
2123
2168
|
filePath: sym.filePath || sym[3],
|
|
2124
2169
|
change_type: 'touched',
|
|
2125
2170
|
});
|