@codragraph/cli 2.1.4 → 2.1.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 (109) hide show
  1. package/README.md +36 -7
  2. package/dist/cli/ai-context.js +297 -0
  3. package/dist/cli/analyze.d.ts +9 -4
  4. package/dist/cli/analyze.js +37 -13
  5. package/dist/cli/index.js +40 -14
  6. package/dist/cli/status.d.ts +1 -1
  7. package/dist/cli/status.js +8 -0
  8. package/dist/cli/tool.d.ts +10 -2
  9. package/dist/cli/tool.js +100 -39
  10. package/dist/config/ignore-service.js +1 -0
  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 +130 -20
  16. package/dist/core/ingestion/parsing-processor.js +7 -1
  17. package/dist/core/ingestion/pipeline-phases/parse-impl.js +7 -1
  18. package/dist/core/ingestion/pipeline-phases/structure.js +19 -3
  19. package/dist/core/ingestion/pipeline.d.ts +10 -0
  20. package/dist/core/ingestion/workers/parse-worker.js +1 -1
  21. package/dist/core/ingestion/workers/worker-pool.d.ts +14 -1
  22. package/dist/core/ingestion/workers/worker-pool.js +33 -17
  23. package/dist/core/run-analyze.d.ts +27 -2
  24. package/dist/core/run-analyze.js +626 -32
  25. package/dist/core/search/bm25-index.d.ts +16 -8
  26. package/dist/core/search/bm25-index.js +72 -110
  27. package/dist/mcp/local/local-backend.d.ts +2 -0
  28. package/dist/mcp/local/local-backend.js +241 -21
  29. package/dist/storage/repo-manager.d.ts +29 -0
  30. package/dist/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  31. package/dist/web/assets/agent-DcdaQnmu.js +1104 -0
  32. package/dist/web/assets/architectureDiagram-UL44E2DR-DFSpa3Hb.js +36 -0
  33. package/dist/web/assets/blockDiagram-7IZFK4PR-DlFaxH1b.js +132 -0
  34. package/dist/web/assets/{c4Diagram-DFAF54RM-C4Hl3J2U.js → c4Diagram-Y2BXMSZH-BjJ_Yrim.js} +1 -1
  35. package/dist/web/assets/{chunk-7RZVMHOQ-BitYcNVR.js → chunk-3SSMPTDK-KGZSzG3Y.js} +1 -1
  36. package/dist/web/assets/{chunk-TBF5ZNIQ-DL5stGM1.js → chunk-6764PJDD-p1sGJgVm.js} +1 -1
  37. package/dist/web/assets/{chunk-KSICW3F5-BYzvDLNI.js → chunk-AZZRMDJM-DIDkQA4V.js} +1 -1
  38. package/dist/web/assets/{chunk-AEOMTBSW-BgTIXPsY.js → chunk-JQRUD6KW-DAwg-yCU.js} +1 -1
  39. package/dist/web/assets/chunk-KRXBNO2N-ChVO_XdS.js +1 -0
  40. package/dist/web/assets/chunk-LCXTWHL2-DGYdb_Eh.js +231 -0
  41. package/dist/web/assets/{chunk-O5ABG6QK-dHwHzA6n.js → chunk-LII3EMHJ-Bzh9SNgD.js} +1 -1
  42. package/dist/web/assets/chunk-RG4AUYOV-Bcl7U_IV.js +206 -0
  43. package/dist/web/assets/{chunk-TU3PZOEN-RLyvLcv-.js → chunk-T5OCTHI4-CZYMg5sc.js} +1 -1
  44. package/dist/web/assets/chunk-W44A43WB-REOI67PN.js +13 -0
  45. package/dist/web/assets/{chunk-RWUO3TPN-BgRTY0_k.js → chunk-ZXARS5L4-BfFdV1tf.js} +1 -1
  46. package/dist/web/assets/classDiagram-KGZ6W3CR-B-qkKMYi.js +1 -0
  47. package/dist/web/assets/classDiagram-v2-72OJOZXJ-B-qkKMYi.js +1 -0
  48. package/dist/web/assets/{cose-bilkent-PNC4W37J-DVhePRYg.js → cose-bilkent-UX7MHV2Q-D6vANJGG.js} +1 -1
  49. package/dist/web/assets/dagre-ND4H6XIP-BiHe5Lal.js +4 -0
  50. package/dist/web/assets/diagram-3NCE3AQN-CEutBCOW.js +43 -0
  51. package/dist/web/assets/diagram-GF46GFSD-CZns6HPQ.js +24 -0
  52. package/dist/web/assets/diagram-HNR7UZ2L-Vz8fE5of.js +3 -0
  53. package/dist/web/assets/diagram-QXG6HAR7-D60HKZ_y.js +24 -0
  54. package/dist/web/assets/diagram-WEQXMOUZ-vGAf1p3E.js +10 -0
  55. package/dist/web/assets/{erDiagram-GCSMX5X6-C3dhDFA8.js → erDiagram-L5TCEMPS-DZaplJA6.js} +5 -5
  56. package/dist/web/assets/{flowDiagram-OTCZ4VVT-CWSFWmhr.js → flowDiagram-H6V6AXG4-BqUqeAsI.js} +9 -9
  57. package/dist/web/assets/ganttDiagram-JCBTUEKG-XEB6H-0G.js +292 -0
  58. package/dist/web/assets/gitGraphDiagram-S2ZK5IYY-7G50u1Cd.js +106 -0
  59. package/dist/web/assets/index-B5WxtMpv.js +1415 -0
  60. package/dist/web/assets/infoDiagram-3YFTVSEB-Cut_rzaf.js +2 -0
  61. package/dist/web/assets/{ishikawaDiagram-YMYX4NHK-DUoJvNP2.js → ishikawaDiagram-BNXS4ZKH-B4DGfGi3.js} +3 -3
  62. package/dist/web/assets/{journeyDiagram-SO5T7YLQ-RMFPNNqz.js → journeyDiagram-M6C3CM3L-BBFhsL3E.js} +1 -1
  63. package/dist/web/assets/{kanban-definition-LJHFXRCJ-BzpDs1K9.js → kanban-definition-75IXJCU3-DarGRyn3.js} +4 -4
  64. package/dist/web/assets/{katex-GD7MH7QM-DBQvrix-.js → katex-K3KEBU37-W5XTYMhr.js} +1 -1
  65. package/dist/web/assets/mindmap-definition-2TDM6QVE-BgeczIJM.js +96 -0
  66. package/dist/web/assets/pieDiagram-CU6KROY3-Kkoo-Noq.js +30 -0
  67. package/dist/web/assets/quadrantDiagram-VICAPDV7-CDQFeRWN.js +7 -0
  68. package/dist/web/assets/{requirementDiagram-M5DCFWZL-DLHOVTSv.js → requirementDiagram-JXO7QTGE-Cz9-XnkA.js} +2 -2
  69. package/dist/web/assets/sankeyDiagram-URQDO5SZ-CU26z0n7.js +40 -0
  70. package/dist/web/assets/sequenceDiagram-VS2MUI6T-OGK1FLOt.js +162 -0
  71. package/dist/web/assets/stateDiagram-7D4R322I-DJ9brq0U.js +1 -0
  72. package/dist/web/assets/stateDiagram-v2-36443NZ5-DhJ4Ky-7.js +1 -0
  73. package/dist/web/assets/{timeline-definition-5SPVSISX-TRSDRgPw.js → timeline-definition-O6YCAMPW-XZvnjqTT.js} +4 -4
  74. package/dist/web/assets/{vennDiagram-IE5QUKF5-DNy7HRBM.js → vennDiagram-MWXL3ELB-CJUssEjA.js} +6 -6
  75. package/dist/web/assets/wardley-L42UT6IY-5TKZOOLJ-DZr11zBG.js +173 -0
  76. package/dist/web/assets/wardleyDiagram-CUQ6CDDI-C276iqrN.js +78 -0
  77. package/dist/web/assets/{xychartDiagram-ZHJ5623Y-Dr9r7a35.js → xychartDiagram-N2JHSOCM-B9-uCZyP.js} +4 -4
  78. package/dist/web/index.html +1 -1
  79. package/hooks/claude/codragraph-hook.cjs +15 -122
  80. package/package.json +1 -1
  81. package/vendor/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
  82. package/dist/web/assets/agent-D5lb0zXz.js +0 -1089
  83. package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +0 -36
  84. package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +0 -132
  85. package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +0 -231
  86. package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +0 -1
  87. package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +0 -13
  88. package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +0 -1
  89. package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +0 -206
  90. package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +0 -1
  91. package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +0 -1
  92. package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +0 -4
  93. package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +0 -43
  94. package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +0 -24
  95. package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +0 -10
  96. package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +0 -24
  97. package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +0 -292
  98. package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +0 -106
  99. package/dist/web/assets/index-BgeqpYgd.js +0 -1415
  100. package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +0 -2
  101. package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +0 -96
  102. package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +0 -30
  103. package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +0 -7
  104. package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +0 -10
  105. package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +0 -157
  106. package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +0 -1
  107. package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +0 -1
  108. package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +0 -161
  109. package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +0 -20
@@ -79,6 +79,105 @@ export const VALID_NODE_LABELS = new Set([
79
79
  'Route',
80
80
  'Tool',
81
81
  ]);
82
+ const SIMPLE_LABELLESS_MATCH_RE = /^MATCH\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/i;
83
+ const CYPHER_LIMIT_RE = /\bLIMIT\s+(\d+)\s*;?\s*$/i;
84
+ const CYPHER_RELATION_RE = /--|-\[|\]-|->|<-/;
85
+ const NATIVE_UNSAFE_NODE_LABELS = new Set(['Union']);
86
+ function quoteKnownNodeLabels(query) {
87
+ return query;
88
+ }
89
+ function getNativeUnsafeNodeLabel(query) {
90
+ const labelRe = /\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*`?([A-Za-z_][A-Za-z0-9_]*)`?(?=[\s){])/g;
91
+ for (const match of query.matchAll(labelRe)) {
92
+ const label = match[1];
93
+ if (NATIVE_UNSAFE_NODE_LABELS.has(label))
94
+ return label;
95
+ }
96
+ return null;
97
+ }
98
+ function safeNodeLabelForCypher(typeOrLabel, nodeId) {
99
+ const explicit = typeof typeOrLabel === 'string' ? typeOrLabel.trim() : '';
100
+ if (explicit && VALID_NODE_LABELS.has(explicit) && !NATIVE_UNSAFE_NODE_LABELS.has(explicit)) {
101
+ return explicit;
102
+ }
103
+ const id = typeof nodeId === 'string' ? nodeId : '';
104
+ const fromId = id.includes(':') ? id.slice(0, id.indexOf(':')) : '';
105
+ if (fromId && VALID_NODE_LABELS.has(fromId) && !NATIVE_UNSAFE_NODE_LABELS.has(fromId)) {
106
+ return fromId;
107
+ }
108
+ return null;
109
+ }
110
+ function firstNonEmptyString(...values) {
111
+ for (const value of values) {
112
+ if (typeof value === 'string' && value.trim().length > 0)
113
+ return value.trim();
114
+ }
115
+ return '';
116
+ }
117
+ function parseSimpleNodeScanQuery(query) {
118
+ const match = /^MATCH\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)(?:\s*:\s*`?([A-Za-z_][A-Za-z0-9_]*)`?)?\s*\)\s*RETURN\s+(.+?)\s+LIMIT\s+(\d+)\s*;?\s*$/i.exec(query.trim());
119
+ if (!match)
120
+ return null;
121
+ const limit = Number.parseInt(match[4], 10);
122
+ if (!Number.isFinite(limit) || limit <= 0)
123
+ return null;
124
+ return {
125
+ alias: match[1],
126
+ label: match[2],
127
+ returnClause: match[3].trim(),
128
+ limit: Math.min(Math.trunc(limit), 1000),
129
+ };
130
+ }
131
+ function projectSimpleNodeRow(node, label, scan) {
132
+ const row = {};
133
+ const parts = scan.returnClause.split(/\s*,\s*/).filter(Boolean);
134
+ for (const part of parts) {
135
+ const asMatch = /^(.+?)\s+AS\s+([A-Za-z_][A-Za-z0-9_]*)$/i.exec(part.trim());
136
+ const expr = (asMatch?.[1] ?? part).trim();
137
+ const outKey = asMatch?.[2] ?? expr;
138
+ if (expr === scan.alias) {
139
+ row[outKey] = { ...node, _label: label };
140
+ continue;
141
+ }
142
+ const labelsMatch = new RegExp(`^labels\\(\\s*${scan.alias}\\s*\\)\\[0\\]$`, 'i').exec(expr);
143
+ if (labelsMatch) {
144
+ row[outKey] = label;
145
+ continue;
146
+ }
147
+ const propMatch = new RegExp(`^${scan.alias}\\.([A-Za-z_][A-Za-z0-9_]*)$`).exec(expr);
148
+ if (propMatch) {
149
+ row[outKey] = node[propMatch[1]];
150
+ continue;
151
+ }
152
+ return null;
153
+ }
154
+ return row;
155
+ }
156
+ function getSimpleLabellessNodeAlias(query) {
157
+ const trimmed = query.trim();
158
+ const match = SIMPLE_LABELLESS_MATCH_RE.exec(trimmed);
159
+ if (!match)
160
+ return null;
161
+ if (CYPHER_RELATION_RE.test(trimmed))
162
+ return null;
163
+ if (/\bMATCH\b/i.test(trimmed.slice(match[0].length)))
164
+ return null;
165
+ return match[1];
166
+ }
167
+ function getCypherLimit(query) {
168
+ const match = CYPHER_LIMIT_RE.exec(query);
169
+ if (!match)
170
+ return null;
171
+ const limit = Number.parseInt(match[1], 10);
172
+ return Number.isFinite(limit) && limit > 0 ? limit : null;
173
+ }
174
+ function withCypherLimit(query, limit) {
175
+ const safeLimit = Math.max(1, Math.trunc(limit));
176
+ if (CYPHER_LIMIT_RE.test(query)) {
177
+ return query.replace(CYPHER_LIMIT_RE, `LIMIT ${safeLimit}`);
178
+ }
179
+ return `${query.replace(/;\s*$/, '')} LIMIT ${safeLimit}`;
180
+ }
82
181
  /** Valid relation types for impact analysis filtering */
83
182
  export const VALID_RELATION_TYPES = new Set([
84
183
  'CALLS',
@@ -1173,25 +1272,119 @@ export class LocalBackend {
1173
1272
  const repo = await this.resolveRepo(repoName);
1174
1273
  return this.cypher(repo, { query });
1175
1274
  }
1176
- async cypher(repo, params) {
1177
- await this.ensureInitialized(repo.id);
1178
- if (!isCgdbReady(repo.id)) {
1179
- return { error: 'LadybugDB not ready. Index may be corrupted.' };
1275
+ async trySimpleGraphstoreNodeScan(repo, query) {
1276
+ const scan = parseSimpleNodeScanQuery(query);
1277
+ if (!scan)
1278
+ return null;
1279
+ if (scan.label && NATIVE_UNSAFE_NODE_LABELS.has(scan.label))
1280
+ return [];
1281
+ const objectsRoot = path.join(repo.storagePath, 'graphstore', 'objects');
1282
+ try {
1283
+ const stat = await fs.stat(objectsRoot);
1284
+ if (!stat.isDirectory())
1285
+ return null;
1180
1286
  }
1287
+ catch {
1288
+ return null;
1289
+ }
1290
+ const rows = [];
1291
+ const visitDir = async (dir) => {
1292
+ if (rows.length >= scan.limit)
1293
+ return;
1294
+ let entries;
1295
+ try {
1296
+ entries = await fs.readdir(dir, { withFileTypes: true });
1297
+ }
1298
+ catch {
1299
+ return;
1300
+ }
1301
+ for (const entry of entries) {
1302
+ if (rows.length >= scan.limit)
1303
+ return;
1304
+ const entryPath = path.join(dir, entry.name);
1305
+ if (entry.isDirectory()) {
1306
+ await visitDir(entryPath);
1307
+ continue;
1308
+ }
1309
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
1310
+ continue;
1311
+ try {
1312
+ const raw = await fs.readFile(entryPath, 'utf-8');
1313
+ const obj = JSON.parse(raw);
1314
+ const id = typeof obj.id === 'string' ? obj.id : '';
1315
+ if (!id || typeof obj.from === 'string' || typeof obj.to === 'string')
1316
+ continue;
1317
+ const label = id.includes(':') ? id.slice(0, id.indexOf(':')) : '';
1318
+ if (!label || (scan.label && label !== scan.label))
1319
+ continue;
1320
+ if (NATIVE_UNSAFE_NODE_LABELS.has(label))
1321
+ continue;
1322
+ const row = projectSimpleNodeRow(obj, label, scan);
1323
+ if (row)
1324
+ rows.push(row);
1325
+ }
1326
+ catch {
1327
+ // Ignore malformed graphstore objects; the native path remains available.
1328
+ }
1329
+ }
1330
+ };
1331
+ await visitDir(objectsRoot);
1332
+ return rows;
1333
+ }
1334
+ async cypher(repo, params) {
1181
1335
  // Block write operations (defense-in-depth — DB is already read-only)
1182
1336
  if (isWriteQuery(params.query)) {
1183
1337
  return {
1184
1338
  error: 'Write operations (CREATE, DELETE, SET, MERGE, REMOVE, DROP, ALTER, COPY, DETACH) are not allowed. The knowledge graph is read-only.',
1185
1339
  };
1186
1340
  }
1341
+ const query = quoteKnownNodeLabels(params.query);
1342
+ const unsafeLabel = getNativeUnsafeNodeLabel(query);
1343
+ if (unsafeLabel) {
1344
+ return [];
1345
+ }
1346
+ const graphstoreRows = await this.trySimpleGraphstoreNodeScan(repo, query);
1347
+ if (graphstoreRows) {
1348
+ return graphstoreRows;
1349
+ }
1350
+ await this.ensureInitialized(repo.id);
1351
+ if (!isCgdbReady(repo.id)) {
1352
+ return { error: 'LadybugDB not ready. Index may be corrupted.' };
1353
+ }
1354
+ const labellessAlias = getSimpleLabellessNodeAlias(query);
1187
1355
  try {
1188
- const result = await executeQuery(repo.id, params.query);
1356
+ const result = labellessAlias
1357
+ ? await this.executeLabellessNodeScan(repo.id, query, labellessAlias)
1358
+ : await executeQuery(repo.id, query);
1189
1359
  return result;
1190
1360
  }
1191
1361
  catch (err) {
1192
1362
  return { error: err.message || 'Query failed' };
1193
1363
  }
1194
1364
  }
1365
+ async executeLabellessNodeScan(repoId, query, alias) {
1366
+ const limit = getCypherLimit(query) ?? 100;
1367
+ const rows = [];
1368
+ let lastError = null;
1369
+ for (const label of VALID_NODE_LABELS) {
1370
+ if (NATIVE_UNSAFE_NODE_LABELS.has(label))
1371
+ continue;
1372
+ if (rows.length >= limit)
1373
+ break;
1374
+ const labelQuery = withCypherLimit(query.replace(SIMPLE_LABELLESS_MATCH_RE, `MATCH (${alias}:\`${label}\`)`), limit - rows.length);
1375
+ try {
1376
+ const labelRows = await executeQuery(repoId, labelQuery);
1377
+ rows.push(...labelRows);
1378
+ }
1379
+ catch (err) {
1380
+ lastError = err instanceof Error ? err : new Error(String(err));
1381
+ }
1382
+ }
1383
+ if (rows.length === 0 && lastError) {
1384
+ throw lastError;
1385
+ }
1386
+ return rows.slice(0, limit);
1387
+ }
1195
1388
  /**
1196
1389
  * Format raw Cypher result rows as a markdown table for LLM readability.
1197
1390
  * Falls back to raw result if rows aren't tabular objects.
@@ -1436,7 +1629,7 @@ export class LocalBackend {
1436
1629
  const symbol = {
1437
1630
  id: (r.id ?? r[0]),
1438
1631
  name: (r.name ?? r[1]),
1439
- type: (r.type ?? r[2] ?? ''),
1632
+ type: firstNonEmptyString(r.type, r.__cgLabel, r[2]),
1440
1633
  filePath: (r.filePath ?? r[3]),
1441
1634
  startLine: (r.startLine ?? r[4]),
1442
1635
  endLine: (r.endLine ?? r[5]),
@@ -1476,7 +1669,7 @@ export class LocalBackend {
1476
1669
  const normalized = rows.map((r) => ({
1477
1670
  id: (r.id ?? r[0]),
1478
1671
  name: (r.name ?? r[1]),
1479
- type: (r.type ?? r[2] ?? ''),
1672
+ type: firstNonEmptyString(r.type, r.__cgLabel, r[2]),
1480
1673
  filePath: (r.filePath ?? r[3]),
1481
1674
  startLine: (r.startLine ?? r[4]),
1482
1675
  endLine: (r.endLine ?? r[5]),
@@ -1914,10 +2107,12 @@ export class LocalBackend {
1914
2107
  queryParams[`hunkEnd${i}`] = hunk.endLine;
1915
2108
  });
1916
2109
  const symbolQuery = `
1917
- MATCH (n) WHERE n.filePath ENDS WITH $filePath
2110
+ MATCH (f:File)-[r:CodeRelation]->(n)
2111
+ WHERE r.type IN ['DEFINES', 'CONTAINS']
2112
+ AND f.filePath ENDS WITH $filePath
1918
2113
  AND n.startLine IS NOT NULL AND n.endLine IS NOT NULL
1919
2114
  AND (${overlapConditions})
1920
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type,
2115
+ RETURN n.id AS id, n.name AS name, '' AS type,
1921
2116
  n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
1922
2117
  `;
1923
2118
  try {
@@ -1926,7 +2121,8 @@ export class LocalBackend {
1926
2121
  changedSymbols.push({
1927
2122
  id: sym.id || sym[0],
1928
2123
  name: sym.name || sym[1],
1929
- type: sym.type || sym[2],
2124
+ type: safeNodeLabelForCypher(sym.type, sym.id || sym[0]) ??
2125
+ firstNonEmptyString(sym.type, sym.__cgLabel, sym[2]),
1930
2126
  filePath: sym.filePath || sym[3],
1931
2127
  change_type: 'touched',
1932
2128
  });
@@ -2266,6 +2462,10 @@ export class LocalBackend {
2266
2462
  const impacted = [];
2267
2463
  const visited = new Set([symId]);
2268
2464
  let frontier = [symId];
2465
+ const frontierTypes = new Map();
2466
+ const symLabel = safeNodeLabelForCypher(symType, symId);
2467
+ if (symLabel)
2468
+ frontierTypes.set(symId, symLabel);
2269
2469
  let traversalComplete = true;
2270
2470
  // Fix #480: For Java (and other JVM) Class/Interface nodes, CALLS edges
2271
2471
  // point to Constructor nodes and IMPORTS edges point to File nodes — not
@@ -2279,16 +2479,16 @@ export class LocalBackend {
2279
2479
  // Run both seed queries in parallel — they are independent.
2280
2480
  const [ctorRows, fileRows] = await Promise.all([
2281
2481
  executeParameterized(repo.id, `
2282
- MATCH (n)-[hm:CodeRelation]->(c:Constructor)
2482
+ MATCH (n:${symLabel ?? symType})-[hm:CodeRelation]->(c:Constructor)
2283
2483
  WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
2284
- RETURN c.id AS id, c.name AS name, labels(c)[0] AS type, c.filePath AS filePath
2484
+ RETURN c.id AS id, c.name AS name, 'Constructor' AS type, c.filePath AS filePath
2285
2485
  `, { symId }),
2286
2486
  // Restrict to DEFINES edges only — other File->Class edge types (if
2287
2487
  // any) should not be treated as the owning file relationship.
2288
2488
  executeParameterized(repo.id, `
2289
- MATCH (f:File)-[rel:CodeRelation]->(n)
2489
+ MATCH (f:File)-[rel:CodeRelation]->(n:${symLabel ?? symType})
2290
2490
  WHERE n.id = $symId AND rel.type = 'DEFINES'
2291
- RETURN f.id AS id, f.name AS name, labels(f)[0] AS type, f.filePath AS filePath
2491
+ RETURN f.id AS id, f.name AS name, 'File' AS type, f.filePath AS filePath
2292
2492
  `, { symId }),
2293
2493
  ]);
2294
2494
  for (const r of ctorRows) {
@@ -2296,6 +2496,9 @@ export class LocalBackend {
2296
2496
  if (rid && !visited.has(rid)) {
2297
2497
  visited.add(rid);
2298
2498
  frontier.push(rid);
2499
+ const label = safeNodeLabelForCypher(r.type ?? r[2], rid);
2500
+ if (label)
2501
+ frontierTypes.set(rid, label);
2299
2502
  }
2300
2503
  }
2301
2504
  for (const r of fileRows) {
@@ -2303,6 +2506,9 @@ export class LocalBackend {
2303
2506
  if (rid && !visited.has(rid)) {
2304
2507
  visited.add(rid);
2305
2508
  frontier.push(rid);
2509
+ const label = safeNodeLabelForCypher(r.type ?? r[2], rid);
2510
+ if (label)
2511
+ frontierTypes.set(rid, label);
2306
2512
  }
2307
2513
  }
2308
2514
  }
@@ -2312,13 +2518,23 @@ export class LocalBackend {
2312
2518
  }
2313
2519
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
2314
2520
  const nextFrontier = [];
2315
- // Batch frontier nodes into a single Cypher query per depth level
2316
- const idList = frontier.map((id) => `'${id.replace(/'/g, "''")}'`).join(', ');
2317
- const query = direction === 'upstream'
2318
- ? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
2319
- : `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
2320
2521
  try {
2321
- const related = await executeQuery(repo.id, query);
2522
+ const frontierByLabel = new Map();
2523
+ for (const id of frontier) {
2524
+ const label = frontierTypes.get(id) ?? safeNodeLabelForCypher(undefined, id) ?? '';
2525
+ const ids = frontierByLabel.get(label) ?? [];
2526
+ ids.push(id);
2527
+ frontierByLabel.set(label, ids);
2528
+ }
2529
+ const related = [];
2530
+ for (const [label, ids] of frontierByLabel) {
2531
+ const idList = ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(', ');
2532
+ const targetPattern = label ? `(n:${label})` : '(n)';
2533
+ const query = direction === 'upstream'
2534
+ ? `MATCH (caller)-[r:CodeRelation]->${targetPattern} WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
2535
+ : `MATCH ${targetPattern}-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
2536
+ related.push(...(await executeQuery(repo.id, query)));
2537
+ }
2322
2538
  for (const rel of related) {
2323
2539
  const relId = rel.id || rel[1];
2324
2540
  const filePath = rel.filePath || rel[4] || '';
@@ -2327,6 +2543,10 @@ export class LocalBackend {
2327
2543
  if (!visited.has(relId)) {
2328
2544
  visited.add(relId);
2329
2545
  nextFrontier.push(relId);
2546
+ const relTypeLabel = firstNonEmptyString(rel.type, rel.__cgLabel, rel[3]);
2547
+ const relLabel = safeNodeLabelForCypher(relTypeLabel, relId);
2548
+ if (relLabel)
2549
+ frontierTypes.set(relId, relLabel);
2330
2550
  const storedConfidence = rel.confidence ?? rel[6];
2331
2551
  const relationType = rel.relType || rel[5];
2332
2552
  // Prefer the stored confidence from the graph (set at analysis time);
@@ -2338,7 +2558,7 @@ export class LocalBackend {
2338
2558
  depth,
2339
2559
  id: relId,
2340
2560
  name: rel.name || rel[2],
2341
- type: rel.type || rel[3],
2561
+ type: relTypeLabel,
2342
2562
  filePath,
2343
2563
  relationType,
2344
2564
  confidence: effectiveConfidence,
@@ -83,6 +83,35 @@ export interface RepoMeta {
83
83
  * — they decode at the read boundary.
84
84
  */
85
85
  compress?: 'none' | 'brotli' | 'zstd';
86
+ /**
87
+ * Query-time index capabilities written by analyze after optional search
88
+ * indexes are successfully created. Read-only MCP/CLI query paths use this
89
+ * to avoid expensive probes for indexes that older analyses never wrote.
90
+ */
91
+ searchIndexes?: {
92
+ fts?: boolean;
93
+ };
94
+ /**
95
+ * Runtime policy chosen by `analyze --profile auto|lean|balanced|power`.
96
+ * This is diagnostic metadata only; readers must not require it because
97
+ * older indexes do not have a policy record.
98
+ */
99
+ adaptiveProfile?: {
100
+ requested?: 'auto' | 'lean' | 'balanced' | 'power';
101
+ resolved?: 'lean' | 'balanced' | 'power';
102
+ platform?: NodeJS.Platform;
103
+ arch?: NodeJS.Architecture;
104
+ cpuCount?: number;
105
+ totalMemoryBytes?: number;
106
+ heapLimitBytes?: number;
107
+ compression?: 'none' | 'brotli' | 'zstd';
108
+ embeddingMode?: 'auto' | 'off' | 'on';
109
+ embeddingNodeLimit?: number;
110
+ embeddingDecision?: 'enabled' | 'skipped';
111
+ embeddingReason?: string;
112
+ workerPoolSize?: number;
113
+ workerSubBatchSize?: number;
114
+ };
86
115
  /**
87
116
  * Canonical `origin` remote URL captured at index time. Used to
88
117
  * fingerprint the same logical repo across multiple on-disk clones
@@ -0,0 +1 @@
1
+ const e={};export{e as default};