@codragraph/cli 2.1.1 → 2.1.5

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 (112) hide show
  1. package/README.md +36 -9
  2. package/dist/cli/ai-context.js +298 -1
  3. package/dist/cli/analyze.js +19 -2
  4. package/dist/cli/index.js +33 -12
  5. package/dist/cli/serve.d.ts +1 -0
  6. package/dist/cli/serve.js +3 -1
  7. package/dist/cli/setup.js +36 -19
  8. package/dist/cli/status.d.ts +13 -0
  9. package/dist/cli/status.js +99 -0
  10. package/dist/cli/tool.js +73 -33
  11. package/dist/config/ignore-service.js +3 -0
  12. package/dist/core/cgdb/pool-adapter.js +130 -20
  13. package/dist/core/graphstore/cgdb-row-source.js +3 -2
  14. package/dist/core/group/bridge-db.js +42 -10
  15. package/dist/core/ingestion/parsing-processor.js +7 -1
  16. package/dist/core/ingestion/pipeline-phases/parse-impl.js +4 -0
  17. package/dist/core/ingestion/workers/parse-worker.js +1 -1
  18. package/dist/core/ingestion/workers/worker-pool.d.ts +14 -1
  19. package/dist/core/ingestion/workers/worker-pool.js +33 -17
  20. package/dist/core/run-analyze.d.ts +20 -0
  21. package/dist/core/run-analyze.js +225 -1
  22. package/dist/core/search/bm25-index.d.ts +0 -11
  23. package/dist/core/search/bm25-index.js +7 -84
  24. package/dist/core/search/hybrid-search.js +11 -3
  25. package/dist/mcp/local/local-backend.d.ts +2 -0
  26. package/dist/mcp/local/local-backend.js +235 -18
  27. package/dist/mcp/resources.js +2 -2
  28. package/dist/server/api.d.ts +14 -2
  29. package/dist/server/api.js +90 -7
  30. package/dist/server/mcp-http.d.ts +22 -0
  31. package/dist/server/mcp-http.js +21 -2
  32. package/dist/server/web-dashboard.d.ts +28 -0
  33. package/dist/server/web-dashboard.js +61 -0
  34. package/dist/web/assets/agent-D5lb0zXz.js +1089 -0
  35. package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +36 -0
  36. package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +132 -0
  37. package/dist/web/assets/c4Diagram-DFAF54RM-C4Hl3J2U.js +10 -0
  38. package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +231 -0
  39. package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +1 -0
  40. package/dist/web/assets/chunk-7RZVMHOQ-BitYcNVR.js +338 -0
  41. package/dist/web/assets/chunk-AEOMTBSW-BgTIXPsY.js +1 -0
  42. package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +13 -0
  43. package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +1 -0
  44. package/dist/web/assets/chunk-KSICW3F5-BYzvDLNI.js +15 -0
  45. package/dist/web/assets/chunk-O5ABG6QK-dHwHzA6n.js +1 -0
  46. package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +206 -0
  47. package/dist/web/assets/chunk-RWUO3TPN-BgRTY0_k.js +1 -0
  48. package/dist/web/assets/chunk-TBF5ZNIQ-DL5stGM1.js +1 -0
  49. package/dist/web/assets/chunk-TU3PZOEN-RLyvLcv-.js +1 -0
  50. package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +1 -0
  51. package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +1 -0
  52. package/dist/web/assets/context-builder-22jU3V56.js +16 -0
  53. package/dist/web/assets/cose-bilkent-PNC4W37J-DVhePRYg.js +1 -0
  54. package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +4 -0
  55. package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +43 -0
  56. package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +24 -0
  57. package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +10 -0
  58. package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +24 -0
  59. package/dist/web/assets/erDiagram-GCSMX5X6-C3dhDFA8.js +85 -0
  60. package/dist/web/assets/flowDiagram-OTCZ4VVT-CWSFWmhr.js +162 -0
  61. package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +292 -0
  62. package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +106 -0
  63. package/dist/web/assets/index-BgeqpYgd.js +1415 -0
  64. package/dist/web/assets/index-CT0GtFLZ.css +1 -0
  65. package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +2 -0
  66. package/dist/web/assets/ishikawaDiagram-YMYX4NHK-DUoJvNP2.js +70 -0
  67. package/dist/web/assets/journeyDiagram-SO5T7YLQ-RMFPNNqz.js +139 -0
  68. package/dist/web/assets/kanban-definition-LJHFXRCJ-BzpDs1K9.js +89 -0
  69. package/dist/web/assets/katex-GD7MH7QM-DBQvrix-.js +261 -0
  70. package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +96 -0
  71. package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +30 -0
  72. package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +7 -0
  73. package/dist/web/assets/requirementDiagram-M5DCFWZL-DLHOVTSv.js +84 -0
  74. package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +10 -0
  75. package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +157 -0
  76. package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +1 -0
  77. package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +1 -0
  78. package/dist/web/assets/timeline-definition-5SPVSISX-TRSDRgPw.js +120 -0
  79. package/dist/web/assets/vennDiagram-IE5QUKF5-DNy7HRBM.js +34 -0
  80. package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +161 -0
  81. package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +20 -0
  82. package/dist/web/assets/xychartDiagram-ZHJ5623Y-Dr9r7a35.js +7 -0
  83. package/dist/web/codragraph-logo-512.png +0 -0
  84. package/dist/web/codragraph-logo.png +0 -0
  85. package/dist/web/favicon.png +0 -0
  86. package/dist/web/index.html +36 -0
  87. package/hooks/claude/codragraph-hook.cjs +18 -110
  88. package/hooks/claude/pre-tool-use.sh +6 -1
  89. package/package.json +3 -1
  90. package/scripts/build.js +62 -4
  91. package/scripts/patch-tree-sitter-swift.cjs +0 -1
  92. package/skills/codragraph-cli.md +1 -1
  93. package/vendor/leiden/index.cjs +272 -285
  94. package/vendor/leiden/utils.cjs +264 -274
  95. package/dist/_shared/lbug/schema-constants.d.ts +0 -16
  96. package/dist/_shared/lbug/schema-constants.d.ts.map +0 -1
  97. package/dist/_shared/lbug/schema-constants.js +0 -67
  98. package/dist/_shared/lbug/schema-constants.js.map +0 -1
  99. package/dist/core/graphstore/lbug-row-source.d.ts +0 -19
  100. package/dist/core/graphstore/lbug-row-source.js +0 -141
  101. package/dist/core/lbug/content-read.d.ts +0 -46
  102. package/dist/core/lbug/content-read.js +0 -64
  103. package/dist/core/lbug/csv-generator.d.ts +0 -29
  104. package/dist/core/lbug/csv-generator.js +0 -492
  105. package/dist/core/lbug/lbug-adapter.d.ts +0 -176
  106. package/dist/core/lbug/lbug-adapter.js +0 -1320
  107. package/dist/core/lbug/pool-adapter.d.ts +0 -93
  108. package/dist/core/lbug/pool-adapter.js +0 -550
  109. package/dist/core/lbug/schema.d.ts +0 -62
  110. package/dist/core/lbug/schema.js +0 -502
  111. package/dist/mcp/core/lbug-adapter.d.ts +0 -5
  112. package/dist/mcp/core/lbug-adapter.js +0 -5
@@ -69,76 +69,6 @@ const FALLBACK_FIELD_WEIGHTS = {
69
69
  content: 2,
70
70
  description: 1,
71
71
  };
72
- /**
73
- * Per-process cache for the MCP pool path: tracks which `(repoId, table)`
74
- * pairs have been ensured. The CLI/pipeline path gets its own cache inside
75
- * `cgdb-adapter.ts` keyed by table/index, scoped to the singleton connection.
76
- *
77
- * IMPORTANT: an entry is added ONLY when the index was confirmed to exist
78
- * (CREATE_FTS_INDEX succeeded, or failed with `'already exists'`). Other
79
- * failures (transient lock errors, missing extension, etc.) leave the key
80
- * unset so the next query retries instead of silently caching the failure.
81
- *
82
- * Entries for a given repoId are invalidated when its pool is closed —
83
- * see the `addPoolCloseListener` registration in `searchFTSFromCgdb`.
84
- */
85
- const ensuredPoolFTS = new Set();
86
- /**
87
- * Drop all ensured-FTS cache entries for a given repoId.
88
- *
89
- * Called from the pool-close listener so that a pool teardown / recreation
90
- * forces the next `searchFTSFromCgdb` call to re-issue `CREATE_FTS_INDEX`
91
- * against the fresh connection rather than trust stale ensure-state from a
92
- * previous pool lifetime.
93
- *
94
- * Exported for tests; the listener wiring is internal.
95
- */
96
- export function invalidateEnsuredFTSForRepo(repoId) {
97
- const prefix = `${repoId}:`;
98
- for (const key of ensuredPoolFTS) {
99
- if (key.startsWith(prefix))
100
- ensuredPoolFTS.delete(key);
101
- }
102
- }
103
- /**
104
- * Tracks whether we've already wired the pool-close listener for this
105
- * process. The pool adapter is dynamically imported, so registration
106
- * happens lazily on the first MCP-pool-backed FTS query.
107
- */
108
- let poolCloseListenerRegistered = false;
109
- function registerPoolCloseListenerOnce(addPoolCloseListener) {
110
- if (poolCloseListenerRegistered)
111
- return;
112
- poolCloseListenerRegistered = true;
113
- addPoolCloseListener((repoId) => invalidateEnsuredFTSForRepo(repoId));
114
- }
115
- async function ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties) {
116
- const key = `${repoId}:${table}:${indexName}`;
117
- if (ensuredPoolFTS.has(key))
118
- return;
119
- const propList = properties.map((p) => `'${p}'`).join(', ');
120
- try {
121
- await executor(`CALL CREATE_FTS_INDEX('${table}', '${indexName}', [${propList}], stemmer := 'porter')`);
122
- // Index was created successfully — safe to cache.
123
- ensuredPoolFTS.add(key);
124
- }
125
- catch (e) {
126
- // 'already exists' is the happy path (index persists on disk between
127
- // process invocations) — cache it. Anything else is treated as a
128
- // transient failure: surface a one-time warning and leave the key
129
- // unset so the NEXT query retries rather than silently using a
130
- // cached failure (which previously disabled BM25 for the whole
131
- // process for that repo).
132
- const msg = String(e?.message ?? '');
133
- if (msg.includes('already exists')) {
134
- ensuredPoolFTS.add(key);
135
- }
136
- else {
137
- console.warn(`[codragraph] FTS index ensure failed for repo "${repoId}" table "${table}" ` +
138
- `(index "${indexName}"): ${msg || e}. Will retry on next query.`);
139
- }
140
- }
141
- }
142
72
  /**
143
73
  * Execute a single FTS query via a custom executor (for MCP connection pool).
144
74
  * Returns the same shape as core queryFTS (from LadybugDB adapter).
@@ -254,23 +184,16 @@ export const searchFTSFromCgdb = async (query, limit = 20, repoId) => {
254
184
  // IMPORTANT: FTS queries run sequentially to avoid connection contention.
255
185
  // The MCP pool supports multiple connections, but FTS is best run serially.
256
186
  const poolMod = await import('../cgdb/pool-adapter.js');
257
- const { executeQuery, addPoolCloseListener } = poolMod;
258
- // Register the pool-close listener lazily on first use so a teardown of
259
- // the pool entry (LRU eviction, idle timeout, explicit close) drops the
260
- // matching `ensuredPoolFTS` entries. Without this, stale ensure-state
261
- // can outlive the pool that produced it.
262
- registerPoolCloseListenerOnce(addPoolCloseListener);
187
+ const { executeQuery } = poolMod;
263
188
  const executor = (cypher) => executeQuery(repoId, cypher);
264
- // Lazy-create FTS indexes on first query for this repo (analyze no longer
265
- // creates them up-front, so we ensure them here). Cached per-process.
266
- // RFC 0001 Phase 2.5: drop `content` from FTS properties for repos
267
- // analysed with --compress brotli|zstd the column holds encoded
268
- // bytes and would tokenise to garbage.
189
+ // The MCP/LocalBackend pool is opened read-only so it can safely coexist
190
+ // 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.
193
+ // RFC 0001 Phase 2.5: drop `content` from fallback scoring for repos
194
+ // analysed with --compress brotli|zstd — the column holds encoded bytes.
269
195
  const compress = await getCompressMode(repoId);
270
196
  const properties = ftsPropertiesFor(compress);
271
- for (const { table, indexName } of FTS_TABLES) {
272
- await ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties);
273
- }
274
197
  fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
275
198
  functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
276
199
  classResults = await queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit);
@@ -111,8 +111,16 @@ export const formatHybridResults = (results) => {
111
111
  * The semanticSearch function is injected to keep this module environment-agnostic.
112
112
  */
113
113
  export const hybridSearch = async (query, limit, executeQuery, semanticSearch) => {
114
- // Use LadybugDB FTS for always-fresh BM25 results
115
- const bm25Results = await searchFTSFromCgdb(query, limit);
116
- const semanticResults = await semanticSearch(executeQuery, query, limit);
114
+ const bm25Promise = searchFTSFromCgdb(query, limit);
115
+ // Start semantic work immediately, but gate its DB calls behind BM25.
116
+ // semanticSearch performs embedding before it calls executeQuery, so this
117
+ // overlaps CPU/model work with BM25 while avoiding concurrent queries on the
118
+ // singleton LadybugDB connection used by CLI/HTTP paths.
119
+ const executeAfterBm25 = async (cypher) => {
120
+ await bm25Promise;
121
+ return executeQuery(cypher);
122
+ };
123
+ const semanticPromise = semanticSearch(executeAfterBm25, query, limit);
124
+ const [bm25Results, semanticResults] = await Promise.all([bm25Promise, semanticPromise]);
117
125
  return mergeWithRRF(bm25Results, semanticResults, limit);
118
126
  };
@@ -186,7 +186,9 @@ export declare class LocalBackend {
186
186
  */
187
187
  private semanticSearch;
188
188
  executeCypher(repoName: string, query: string): Promise<any>;
189
+ private trySimpleGraphstoreNodeScan;
189
190
  private cypher;
191
+ private executeLabellessNodeScan;
190
192
  /**
191
193
  * Format raw Cypher result rows as a markdown table for LLM readability.
192
194
  * Falls back to raw result if rows aren't tabular objects.
@@ -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]),
@@ -2266,6 +2459,10 @@ export class LocalBackend {
2266
2459
  const impacted = [];
2267
2460
  const visited = new Set([symId]);
2268
2461
  let frontier = [symId];
2462
+ const frontierTypes = new Map();
2463
+ const symLabel = safeNodeLabelForCypher(symType, symId);
2464
+ if (symLabel)
2465
+ frontierTypes.set(symId, symLabel);
2269
2466
  let traversalComplete = true;
2270
2467
  // Fix #480: For Java (and other JVM) Class/Interface nodes, CALLS edges
2271
2468
  // point to Constructor nodes and IMPORTS edges point to File nodes — not
@@ -2279,16 +2476,16 @@ export class LocalBackend {
2279
2476
  // Run both seed queries in parallel — they are independent.
2280
2477
  const [ctorRows, fileRows] = await Promise.all([
2281
2478
  executeParameterized(repo.id, `
2282
- MATCH (n)-[hm:CodeRelation]->(c:Constructor)
2479
+ MATCH (n:${symLabel ?? symType})-[hm:CodeRelation]->(c:Constructor)
2283
2480
  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
2481
+ RETURN c.id AS id, c.name AS name, 'Constructor' AS type, c.filePath AS filePath
2285
2482
  `, { symId }),
2286
2483
  // Restrict to DEFINES edges only — other File->Class edge types (if
2287
2484
  // any) should not be treated as the owning file relationship.
2288
2485
  executeParameterized(repo.id, `
2289
- MATCH (f:File)-[rel:CodeRelation]->(n)
2486
+ MATCH (f:File)-[rel:CodeRelation]->(n:${symLabel ?? symType})
2290
2487
  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
2488
+ RETURN f.id AS id, f.name AS name, 'File' AS type, f.filePath AS filePath
2292
2489
  `, { symId }),
2293
2490
  ]);
2294
2491
  for (const r of ctorRows) {
@@ -2296,6 +2493,9 @@ export class LocalBackend {
2296
2493
  if (rid && !visited.has(rid)) {
2297
2494
  visited.add(rid);
2298
2495
  frontier.push(rid);
2496
+ const label = safeNodeLabelForCypher(r.type ?? r[2], rid);
2497
+ if (label)
2498
+ frontierTypes.set(rid, label);
2299
2499
  }
2300
2500
  }
2301
2501
  for (const r of fileRows) {
@@ -2303,6 +2503,9 @@ export class LocalBackend {
2303
2503
  if (rid && !visited.has(rid)) {
2304
2504
  visited.add(rid);
2305
2505
  frontier.push(rid);
2506
+ const label = safeNodeLabelForCypher(r.type ?? r[2], rid);
2507
+ if (label)
2508
+ frontierTypes.set(rid, label);
2306
2509
  }
2307
2510
  }
2308
2511
  }
@@ -2312,13 +2515,23 @@ export class LocalBackend {
2312
2515
  }
2313
2516
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
2314
2517
  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
2518
  try {
2321
- const related = await executeQuery(repo.id, query);
2519
+ const frontierByLabel = new Map();
2520
+ for (const id of frontier) {
2521
+ const label = frontierTypes.get(id) ?? safeNodeLabelForCypher(undefined, id) ?? '';
2522
+ const ids = frontierByLabel.get(label) ?? [];
2523
+ ids.push(id);
2524
+ frontierByLabel.set(label, ids);
2525
+ }
2526
+ const related = [];
2527
+ for (const [label, ids] of frontierByLabel) {
2528
+ const idList = ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(', ');
2529
+ const targetPattern = label ? `(n:${label})` : '(n)';
2530
+ const query = direction === 'upstream'
2531
+ ? `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`
2532
+ : `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`;
2533
+ related.push(...(await executeQuery(repo.id, query)));
2534
+ }
2322
2535
  for (const rel of related) {
2323
2536
  const relId = rel.id || rel[1];
2324
2537
  const filePath = rel.filePath || rel[4] || '';
@@ -2327,6 +2540,10 @@ export class LocalBackend {
2327
2540
  if (!visited.has(relId)) {
2328
2541
  visited.add(relId);
2329
2542
  nextFrontier.push(relId);
2543
+ const relTypeLabel = firstNonEmptyString(rel.type, rel.__cgLabel, rel[3]);
2544
+ const relLabel = safeNodeLabelForCypher(relTypeLabel, relId);
2545
+ if (relLabel)
2546
+ frontierTypes.set(relId, relLabel);
2330
2547
  const storedConfidence = rel.confidence ?? rel[6];
2331
2548
  const relationType = rel.relType || rel[5];
2332
2549
  // Prefer the stored confidence from the graph (set at analysis time);
@@ -2338,7 +2555,7 @@ export class LocalBackend {
2338
2555
  depth,
2339
2556
  id: relId,
2340
2557
  name: rel.name || rel[2],
2341
- type: rel.type || rel[3],
2558
+ type: relTypeLabel,
2342
2559
  filePath,
2343
2560
  relationType,
2344
2561
  confidence: effectiveConfidence,
@@ -685,8 +685,8 @@ async function getSetupResource(backend) {
685
685
  '',
686
686
  '## Cross-platform commands',
687
687
  '',
688
- '- Use `npx @codragraph/cli ...` or `codragraph ...` in Windows PowerShell, macOS bash/zsh, and Linux shells.',
689
- '- Prefer `npm --prefix <package> <script>` from repo root for package checks instead of shell-specific `cd dir && ...` chains.',
688
+ '- Use `npx @codragraph/cli ...`, `bunx @codragraph/cli ...`, or `codragraph ...` in Windows PowerShell, macOS bash/zsh, and Linux shells.',
689
+ '- Prefer `npm --prefix <package> <script>` or `bun run --filter <workspace> <script>` from repo root for package checks instead of shell-specific `cd dir && ...` chains.',
690
690
  ];
691
691
  sections.push(lines.join('\n'));
692
692
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import express from 'express';
11
11
  import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
12
+ import { type WebDashboardMode } from './web-dashboard.js';
12
13
  /**
13
14
  * Determine whether an HTTP Origin header value is allowed by CORS policy.
14
15
  *
@@ -20,13 +21,16 @@ import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
20
21
  * 10.0.0.0/8 → 10.x.x.x
21
22
  * 172.16.0.0/12 → 172.16.x.x – 172.31.x.x
22
23
  * 192.168.0.0/16 → 192.168.x.x
23
- * - https://codragraph.vercel.app — the deployed CodraGraph web UI
24
+ * - Hosted CodraGraph web UI — defaults to https://codragraph.vercel.app
24
25
  *
25
26
  * @param origin - The value of the HTTP `Origin` request header, or `undefined`
26
27
  * when the header is absent (non-browser request).
27
28
  * @returns `true` if the origin is allowed, `false` otherwise.
28
29
  */
29
30
  export declare const isAllowedOrigin: (origin: string | undefined) => boolean;
31
+ export interface CreateServerOptions {
32
+ web?: WebDashboardMode;
33
+ }
30
34
  type GraphStreamRecord = {
31
35
  type: 'node';
32
36
  data: GraphNode;
@@ -41,7 +45,15 @@ export declare class ClientDisconnectedError extends Error {
41
45
  constructor();
42
46
  }
43
47
  export declare const isIgnorableGraphQueryError: (err: unknown) => boolean;
48
+ export interface GraphStoreErrorResponse {
49
+ error: string;
50
+ code: 'GRAPHSTORE_CORRUPT';
51
+ operation: string;
52
+ recovery: string[];
53
+ }
54
+ export declare const isGraphStoreCorruptionError: (err: unknown) => boolean;
55
+ export declare const getGraphStoreErrorResponse: (err: unknown, operation: string) => GraphStoreErrorResponse | null;
44
56
  export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
45
57
  export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
46
- export declare const createServer: (port: number, host?: string) => Promise<void>;
58
+ export declare const createServer: (port: number, host?: string, options?: CreateServerOptions) => Promise<void>;
47
59
  export {};