@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.
Files changed (113) hide show
  1. package/README.md +18 -13
  2. package/dist/cli/analyze.d.ts +9 -4
  3. package/dist/cli/analyze.js +37 -13
  4. package/dist/cli/graphpack.d.ts +48 -0
  5. package/dist/cli/graphpack.js +217 -0
  6. package/dist/cli/index.js +81 -3
  7. package/dist/cli/status.d.ts +1 -1
  8. package/dist/cli/status.js +8 -0
  9. package/dist/cli/tool.d.ts +11 -2
  10. package/dist/cli/tool.js +138 -8
  11. package/dist/core/adaptive-profile.d.ts +52 -0
  12. package/dist/core/adaptive-profile.js +180 -0
  13. package/dist/core/cgdb/cgdb-adapter.d.ts +34 -5
  14. package/dist/core/cgdb/cgdb-adapter.js +418 -5
  15. package/dist/core/cgdb/pool-adapter.js +1 -1
  16. package/dist/core/graphpack/index.d.ts +14 -0
  17. package/dist/core/graphpack/index.js +474 -0
  18. package/dist/core/graphpack/types.d.ts +129 -0
  19. package/dist/core/graphpack/types.js +4 -0
  20. package/dist/core/ingestion/pipeline-phases/parse-impl.js +3 -1
  21. package/dist/core/ingestion/pipeline-phases/structure.js +19 -3
  22. package/dist/core/ingestion/pipeline.d.ts +10 -0
  23. package/dist/core/run-analyze.d.ts +27 -2
  24. package/dist/core/run-analyze.js +598 -27
  25. package/dist/core/search/bm25-index.d.ts +19 -0
  26. package/dist/core/search/bm25-index.js +68 -29
  27. package/dist/core/semantic/relationships.d.ts +36 -0
  28. package/dist/core/semantic/relationships.js +261 -0
  29. package/dist/mcp/local/local-backend.js +48 -3
  30. package/dist/mcp/resources.js +125 -0
  31. package/dist/mcp/tools.js +105 -0
  32. package/dist/server/api.js +112 -0
  33. package/dist/storage/repo-manager.d.ts +29 -0
  34. package/dist/web/assets/agent-CQNZQ-hg.js +1139 -0
  35. package/dist/web/assets/architectureDiagram-UL44E2DR-B5_goS_i.js +36 -0
  36. package/dist/web/assets/blockDiagram-7IZFK4PR-D7ZAlDyv.js +132 -0
  37. package/dist/web/assets/{c4Diagram-DFAF54RM-C4Hl3J2U.js → c4Diagram-Y2BXMSZH-Djcgm_54.js} +1 -1
  38. package/dist/web/assets/{chunk-7RZVMHOQ-BitYcNVR.js → chunk-3SSMPTDK-Cv2Zy2FO.js} +1 -1
  39. package/dist/web/assets/{chunk-TBF5ZNIQ-DL5stGM1.js → chunk-6764PJDD-Cppb-jH-.js} +1 -1
  40. package/dist/web/assets/{chunk-KSICW3F5-BYzvDLNI.js → chunk-AZZRMDJM-BHlLC7p3.js} +1 -1
  41. package/dist/web/assets/{chunk-AEOMTBSW-BgTIXPsY.js → chunk-JQRUD6KW-3F8Zg-1N.js} +1 -1
  42. package/dist/web/assets/chunk-KRXBNO2N-C0mbN9a7.js +1 -0
  43. package/dist/web/assets/chunk-LCXTWHL2-BoiuJpIF.js +231 -0
  44. package/dist/web/assets/{chunk-O5ABG6QK-dHwHzA6n.js → chunk-LII3EMHJ-Dqq0Qguw.js} +1 -1
  45. package/dist/web/assets/chunk-RG4AUYOV-Bl5F_gDs.js +206 -0
  46. package/dist/web/assets/{chunk-TU3PZOEN-RLyvLcv-.js → chunk-T5OCTHI4-B2tIcggA.js} +1 -1
  47. package/dist/web/assets/chunk-W44A43WB-BHe37iN7.js +13 -0
  48. package/dist/web/assets/{chunk-RWUO3TPN-BgRTY0_k.js → chunk-ZXARS5L4-wcrIaQvY.js} +1 -1
  49. package/dist/web/assets/classDiagram-KGZ6W3CR-IbI6v_24.js +1 -0
  50. package/dist/web/assets/classDiagram-v2-72OJOZXJ-IbI6v_24.js +1 -0
  51. package/dist/web/assets/{cose-bilkent-PNC4W37J-DVhePRYg.js → cose-bilkent-UX7MHV2Q-BWr7v0Wr.js} +1 -1
  52. package/dist/web/assets/dagre-ND4H6XIP-De5LIh1B.js +4 -0
  53. package/dist/web/assets/diagram-3NCE3AQN-Dd22FSHy.js +43 -0
  54. package/dist/web/assets/diagram-GF46GFSD-Cev3THY8.js +24 -0
  55. package/dist/web/assets/diagram-HNR7UZ2L-D8Z8RQGs.js +3 -0
  56. package/dist/web/assets/diagram-QXG6HAR7-B8VOJOiE.js +24 -0
  57. package/dist/web/assets/diagram-WEQXMOUZ-va1bLoMD.js +10 -0
  58. package/dist/web/assets/{erDiagram-GCSMX5X6-C3dhDFA8.js → erDiagram-L5TCEMPS-B3_9uAoP.js} +5 -5
  59. package/dist/web/assets/{flowDiagram-OTCZ4VVT-CWSFWmhr.js → flowDiagram-H6V6AXG4-98m6maI1.js} +9 -9
  60. package/dist/web/assets/ganttDiagram-JCBTUEKG-vE2nzETb.js +292 -0
  61. package/dist/web/assets/gitGraphDiagram-S2ZK5IYY-DKc8uUg_.js +106 -0
  62. package/dist/web/assets/index-BAhe1HSk.css +1 -0
  63. package/dist/web/assets/index-VTKdaklA.js +1415 -0
  64. package/dist/web/assets/infoDiagram-3YFTVSEB-DYP-Srzx.js +2 -0
  65. package/dist/web/assets/{ishikawaDiagram-YMYX4NHK-DUoJvNP2.js → ishikawaDiagram-BNXS4ZKH-QZnkpmmb.js} +3 -3
  66. package/dist/web/assets/{journeyDiagram-SO5T7YLQ-RMFPNNqz.js → journeyDiagram-M6C3CM3L-B5ojIuqu.js} +1 -1
  67. package/dist/web/assets/{kanban-definition-LJHFXRCJ-BzpDs1K9.js → kanban-definition-75IXJCU3-BJA8liRR.js} +4 -4
  68. package/dist/web/assets/{katex-GD7MH7QM-DBQvrix-.js → katex-K3KEBU37-DUqZiCRL.js} +1 -1
  69. package/dist/web/assets/mindmap-definition-2TDM6QVE-BQj5yylD.js +96 -0
  70. package/dist/web/assets/pieDiagram-CU6KROY3-4eSrPiQz.js +30 -0
  71. package/dist/web/assets/quadrantDiagram-VICAPDV7-PzxN8j55.js +7 -0
  72. package/dist/web/assets/{requirementDiagram-M5DCFWZL-DLHOVTSv.js → requirementDiagram-JXO7QTGE-CtplTc5y.js} +2 -2
  73. package/dist/web/assets/sankeyDiagram-URQDO5SZ-CoSgvkxv.js +40 -0
  74. package/dist/web/assets/sequenceDiagram-VS2MUI6T-D7ygyXvJ.js +162 -0
  75. package/dist/web/assets/stateDiagram-7D4R322I-v01gvwji.js +1 -0
  76. package/dist/web/assets/stateDiagram-v2-36443NZ5-DFD2b8_x.js +1 -0
  77. package/dist/web/assets/{timeline-definition-5SPVSISX-TRSDRgPw.js → timeline-definition-O6YCAMPW-CTI3M65J.js} +4 -4
  78. package/dist/web/assets/{vennDiagram-IE5QUKF5-DNy7HRBM.js → vennDiagram-MWXL3ELB-RnB0XMP7.js} +6 -6
  79. package/dist/web/assets/wardley-L42UT6IY-5TKZOOLJ-C-ZcgEBb.js +173 -0
  80. package/dist/web/assets/wardleyDiagram-CUQ6CDDI-EwRi4kwo.js +78 -0
  81. package/dist/web/assets/{xychartDiagram-ZHJ5623Y-Dr9r7a35.js → xychartDiagram-N2JHSOCM-DA38II6y.js} +4 -4
  82. package/dist/web/index.html +2 -2
  83. package/package.json +2 -2
  84. package/vendor/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
  85. package/dist/web/assets/agent-D5lb0zXz.js +0 -1089
  86. package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +0 -36
  87. package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +0 -132
  88. package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +0 -231
  89. package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +0 -1
  90. package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +0 -13
  91. package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +0 -1
  92. package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +0 -206
  93. package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +0 -1
  94. package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +0 -1
  95. package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +0 -4
  96. package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +0 -43
  97. package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +0 -24
  98. package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +0 -10
  99. package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +0 -24
  100. package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +0 -292
  101. package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +0 -106
  102. package/dist/web/assets/index-BgeqpYgd.js +0 -1415
  103. package/dist/web/assets/index-CT0GtFLZ.css +0 -1
  104. package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +0 -2
  105. package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +0 -96
  106. package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +0 -30
  107. package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +0 -7
  108. package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +0 -10
  109. package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +0 -157
  110. package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +0 -1
  111. package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +0 -1
  112. package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +0 -161
  113. 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 `meta.compress` for a repo. The MCP path passes `repoId`
36
- * (registry-derived); the CLI path passes nothing and we walk up from
37
- * cwd. Returns `'none'` whenever the lookup fails so the safe default
38
- * (full FTS index) is used the failure mode is reduced search
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 getCompressMode(repoId) {
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 meta?.compress ?? 'none';
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 repo?.meta?.compress ?? 'none';
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 ${FALLBACK_SCAN_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, QUERY_FTS_INDEX falls through to the
192
- // bounded BM25 fallback below without noisy write-failure retries.
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 compress = await getCompressMode(repoId);
196
- const properties = ftsPropertiesFor(compress);
197
- fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
198
- functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
199
- classResults = await queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit);
200
- methodResults = await queryFTSViaExecutor(executor, 'Method', 'method_fts', query, limit);
201
- interfaceResults = await queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit);
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
- // Lazy-create FTS indexes on first query (analyze no longer does it).
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 compress = await getCompressMode();
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) WHERE n.filePath ENDS WITH $filePath
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, labels(n)[0] AS type,
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[2],
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
  });