@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,7 @@ import * as fsSync from 'node:fs';
15
15
  import * as v8 from 'node:v8';
16
16
  import { getLanguageFromFilename } from '../_shared/index.js';
17
17
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
18
- import { initCgdb, loadGraphToCgdb, getCgdbStats, executeQuery, executeWithReusedStatement, closeCgdb, loadCachedEmbeddings, } from './cgdb/cgdb-adapter.js';
18
+ import { initCgdb, loadGraphToCgdb, getCgdbStats, executeQuery, executeWithReusedStatement, closeCgdb, loadCachedEmbeddings, ensureFTSIndex, applyFileGraphPatchToCgdb, replaceFileScopedGraphInCgdb, replaceGlobalGraphLayersInCgdb, loadKnowledgeGraphFromCgdb, fetchExistingEmbeddingHashes, } from './cgdb/cgdb-adapter.js';
19
19
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, INDEX_SCHEMA_VERSION, } from '../storage/repo-manager.js';
20
20
  import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName } from '../storage/git.js';
21
21
  import { shouldIgnorePath } from '../config/ignore-service.js';
@@ -23,8 +23,13 @@ import { recordAnalysisSnapshot } from './graphstore/index.js';
23
23
  import { generateAIContextFiles } from '../cli/ai-context.js';
24
24
  import { EMBEDDING_TABLE_NAME } from './cgdb/schema.js';
25
25
  import { STALE_HASH_SENTINEL } from './cgdb/schema.js';
26
- /** Threshold: auto-skip embeddings for repos with more nodes than this */
27
- const EMBEDDING_NODE_LIMIT = 50_000;
26
+ import { FTS_TABLES, ftsPropertiesFor } from './search/bm25-index.js';
27
+ import { processCommunities, } from './ingestion/community-processor.js';
28
+ import { processProcesses } from './ingestion/process-processor.js';
29
+ import { processFeatureClusters, } from './ingestion/feature-cluster-processor.js';
30
+ import { createKnowledgeGraph } from './graph/graph.js';
31
+ import { generateId } from '../lib/utils.js';
32
+ import { decideEmbeddingRun, formatAdaptiveAnalyzePlan, resolveAdaptiveAnalyzePlan, } from './adaptive-profile.js';
28
33
  const GENERATED_AGENT_CONTEXT_PATHS = new Set(['agents.md', 'claude.md']);
29
34
  const GENERATED_AGENT_CONTEXT_PREFIXES = [
30
35
  '.claude/skills/generated/',
@@ -56,6 +61,14 @@ const GRAPH_CONFIG_BASENAMES = new Set([
56
61
  ]);
57
62
  const GRAPH_CONFIG_PATTERNS = [/^tsconfig\..+\.json$/i, /^jsconfig\..+\.json$/i];
58
63
  const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
64
+ const GLOBAL_LAYER_NODE_LABELS = new Set(['Community', 'Process', 'FeatureCluster']);
65
+ const GLOBAL_LAYER_REL_TYPES = new Set([
66
+ 'MEMBER_OF',
67
+ 'STEP_IN_PROCESS',
68
+ 'ENTRY_POINT_OF',
69
+ 'FEATURE_MEMBER_OF',
70
+ 'FEATURE_DEPENDS_ON',
71
+ ]);
59
72
  export const PHASE_LABELS = {
60
73
  extracting: 'Scanning files',
61
74
  structure: 'Building structure',
@@ -159,11 +172,119 @@ export const changedPathAffectsGraph = (change) => {
159
172
  return true;
160
173
  };
161
174
  export const getGraphRelevantChangedPaths = (changes) => changes.filter(changedPathAffectsGraph);
175
+ export const isPatchableIncrementalPath = (filePath) => {
176
+ const normalized = normalizeGitPath(filePath);
177
+ if (isGeneratedAgentContextPath(normalized) || shouldIgnorePath(normalized))
178
+ return false;
179
+ if (getLanguageFromFilename(normalized) !== null)
180
+ return true;
181
+ return MARKDOWN_EXTENSIONS.has(path.posix.extname(normalized.toLowerCase()));
182
+ };
183
+ const isTopologyPatchablePath = (filePath) => {
184
+ const normalized = normalizeGitPath(filePath);
185
+ return !isGeneratedAgentContextPath(normalized) && !shouldIgnorePath(normalized);
186
+ };
187
+ const isGlobalGraphInputPath = (filePath) => {
188
+ const normalized = normalizeGitPath(filePath);
189
+ const basename = path.posix.basename(normalized);
190
+ const lowerBasename = basename.toLowerCase();
191
+ return (IGNORE_CONTROL_FILES.has(lowerBasename) ||
192
+ GRAPH_CONFIG_BASENAMES.has(lowerBasename) ||
193
+ GRAPH_CONFIG_PATTERNS.some((pattern) => pattern.test(basename)));
194
+ };
195
+ export const buildIncrementalFilePatchPlan = (changes, _options = {}) => {
196
+ if (changes.length === 0) {
197
+ return {
198
+ eligible: false,
199
+ reason: 'no indexed graph input changes',
200
+ replacePaths: [],
201
+ currentPaths: [],
202
+ fileCountDelta: 0,
203
+ replaceAllFileScoped: false,
204
+ pathAliases: {},
205
+ };
206
+ }
207
+ const replacePaths = new Set();
208
+ const currentPaths = new Set();
209
+ const pathAliases = {};
210
+ let fileCountDelta = 0;
211
+ let replaceAllFileScoped = false;
212
+ const reasons = new Set();
213
+ for (const change of changes) {
214
+ const statusCode = change.status[0]?.toUpperCase();
215
+ const paths = statusCode === 'C'
216
+ ? [change.path]
217
+ : [change.path, change.previousPath].filter((p) => Boolean(p));
218
+ if (paths.some(isGlobalGraphInputPath)) {
219
+ replaceAllFileScoped = true;
220
+ reasons.add('global config/input changed');
221
+ continue;
222
+ }
223
+ const contentPatchable = paths.every(isPatchableIncrementalPath);
224
+ const topologyPatchable = paths.every(isTopologyPatchablePath);
225
+ if (!contentPatchable && !topologyPatchable) {
226
+ return {
227
+ eligible: false,
228
+ reason: `change ${change.status} ${formatChangeForLog(change)} touches a global or unsupported graph input`,
229
+ replacePaths: [],
230
+ currentPaths: [],
231
+ fileCountDelta: 0,
232
+ replaceAllFileScoped: false,
233
+ pathAliases: {},
234
+ };
235
+ }
236
+ if (statusCode === 'D') {
237
+ replacePaths.add(change.path);
238
+ fileCountDelta -= 1;
239
+ continue;
240
+ }
241
+ if (statusCode === 'R') {
242
+ if (change.previousPath) {
243
+ replacePaths.add(change.previousPath);
244
+ pathAliases[change.previousPath] = change.path;
245
+ }
246
+ replacePaths.add(change.path);
247
+ currentPaths.add(change.path);
248
+ reasons.add('rename remapped');
249
+ continue;
250
+ }
251
+ if (statusCode === 'M' || statusCode === 'A' || statusCode === 'T' || statusCode === 'C') {
252
+ replacePaths.add(change.path);
253
+ currentPaths.add(change.path);
254
+ if (statusCode === 'A' || statusCode === 'C') {
255
+ fileCountDelta += 1;
256
+ }
257
+ if (!contentPatchable) {
258
+ reasons.add('topology-only file change');
259
+ }
260
+ continue;
261
+ }
262
+ replaceAllFileScoped = true;
263
+ reasons.add(`unsupported git status ${change.status}`);
264
+ }
265
+ const strategy = replaceAllFileScoped
266
+ ? 'all file-scoped graph rows will be refreshed'
267
+ : `${replacePaths.size} file path(s) will be patched`;
268
+ if (changes.length > 20)
269
+ reasons.add(`${changes.length} graph input changes`);
270
+ return {
271
+ eligible: true,
272
+ reason: [...reasons, strategy].filter(Boolean).join('; '),
273
+ replacePaths: [...replacePaths].sort(),
274
+ currentPaths: [...currentPaths].sort(),
275
+ fileCountDelta,
276
+ replaceAllFileScoped,
277
+ pathAliases,
278
+ };
279
+ };
162
280
  export const getAnalyzeConfigRebuildReason = (existingMeta, options) => {
163
281
  const existingCompress = existingMeta.compress ?? 'none';
164
282
  if (options.compress && options.compress !== existingCompress) {
165
283
  return `requested compression changed from ${existingCompress} to ${options.compress}`;
166
284
  }
285
+ if (existingMeta.searchIndexes?.fts !== true) {
286
+ return 'search indexes are missing';
287
+ }
167
288
  if (options.embeddings && (existingMeta.stats?.embeddings ?? 0) === 0) {
168
289
  return 'embeddings were requested but the existing index has no vectors';
169
290
  }
@@ -186,6 +307,31 @@ const metaStatsForAIContext = (stats = {}) => ({
186
307
  clusters: stats.featureClusters,
187
308
  processes: stats.processes,
188
309
  });
310
+ const buildAdaptiveProfileMeta = (adaptivePlan, embeddingDecision) => ({
311
+ requested: adaptivePlan.requestedProfile,
312
+ resolved: adaptivePlan.profile,
313
+ platform: adaptivePlan.machine.platform,
314
+ arch: adaptivePlan.machine.arch,
315
+ cpuCount: adaptivePlan.machine.availableParallelism,
316
+ totalMemoryBytes: adaptivePlan.machine.totalMemoryBytes,
317
+ heapLimitBytes: adaptivePlan.machine.heapLimitBytes,
318
+ compression: adaptivePlan.compress,
319
+ embeddingMode: adaptivePlan.embeddingMode,
320
+ embeddingNodeLimit: adaptivePlan.embeddingNodeLimit,
321
+ embeddingDecision: embeddingDecision.enabled ? 'enabled' : 'skipped',
322
+ embeddingReason: embeddingDecision.reason,
323
+ workerPoolSize: adaptivePlan.workerPoolSize,
324
+ workerSubBatchSize: adaptivePlan.workerSubBatchSize,
325
+ });
326
+ const countEmbeddings = async () => {
327
+ try {
328
+ const embResult = await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS cnt`);
329
+ return Number(embResult?.[0]?.cnt ?? embResult?.[0]?.[0] ?? 0);
330
+ }
331
+ catch {
332
+ return 0;
333
+ }
334
+ };
189
335
  const pathExists = async (targetPath) => {
190
336
  try {
191
337
  await fs.stat(targetPath);
@@ -195,6 +341,387 @@ const pathExists = async (targetPath) => {
195
341
  return false;
196
342
  }
197
343
  };
344
+ const addCommunityLayerToGraph = (graph, communityResult) => {
345
+ communityResult.communities.forEach((comm) => {
346
+ graph.addNode({
347
+ id: comm.id,
348
+ label: 'Community',
349
+ properties: {
350
+ name: comm.label,
351
+ filePath: '',
352
+ heuristicLabel: comm.heuristicLabel,
353
+ cohesion: comm.cohesion,
354
+ symbolCount: comm.symbolCount,
355
+ },
356
+ });
357
+ });
358
+ communityResult.memberships.forEach((membership) => {
359
+ graph.addRelationship({
360
+ id: `${membership.nodeId}_member_of_${membership.communityId}`,
361
+ type: 'MEMBER_OF',
362
+ sourceId: membership.nodeId,
363
+ targetId: membership.communityId,
364
+ confidence: 1.0,
365
+ reason: 'leiden-algorithm',
366
+ });
367
+ });
368
+ };
369
+ const addProcessLayerToGraph = (graph, processResult) => {
370
+ processResult.processes.forEach((proc) => {
371
+ graph.addNode({
372
+ id: proc.id,
373
+ label: 'Process',
374
+ properties: {
375
+ name: proc.label,
376
+ filePath: '',
377
+ heuristicLabel: proc.heuristicLabel,
378
+ processType: proc.processType,
379
+ stepCount: proc.stepCount,
380
+ communities: proc.communities,
381
+ entryPointId: proc.entryPointId,
382
+ terminalId: proc.terminalId,
383
+ },
384
+ });
385
+ });
386
+ processResult.steps.forEach((step) => {
387
+ graph.addRelationship({
388
+ id: `${step.nodeId}_step_${step.step}_${step.processId}`,
389
+ type: 'STEP_IN_PROCESS',
390
+ sourceId: step.nodeId,
391
+ targetId: step.processId,
392
+ confidence: 1.0,
393
+ reason: 'trace-detection',
394
+ step: step.step,
395
+ });
396
+ });
397
+ };
398
+ const addRouteToolProcessLinks = (graph, processResult) => {
399
+ const routesByFile = new Map();
400
+ const toolsByFile = new Map();
401
+ for (const node of graph.iterNodes()) {
402
+ const filePath = typeof node.properties?.filePath === 'string' ? node.properties.filePath : undefined;
403
+ const name = typeof node.properties?.name === 'string' ? node.properties.name : undefined;
404
+ if (!filePath || !name)
405
+ continue;
406
+ if (node.label === 'Route') {
407
+ let routes = routesByFile.get(filePath);
408
+ if (!routes) {
409
+ routes = [];
410
+ routesByFile.set(filePath, routes);
411
+ }
412
+ routes.push(name);
413
+ }
414
+ else if (node.label === 'Tool') {
415
+ let tools = toolsByFile.get(filePath);
416
+ if (!tools) {
417
+ tools = [];
418
+ toolsByFile.set(filePath, tools);
419
+ }
420
+ tools.push(name);
421
+ }
422
+ }
423
+ if (routesByFile.size === 0 && toolsByFile.size === 0)
424
+ return;
425
+ for (const proc of processResult.processes) {
426
+ if (!proc.entryPointId)
427
+ continue;
428
+ const entryNode = graph.getNode(proc.entryPointId);
429
+ const entryFile = typeof entryNode?.properties?.filePath === 'string' ? entryNode.properties.filePath : '';
430
+ if (!entryFile)
431
+ continue;
432
+ for (const routeURL of routesByFile.get(entryFile) ?? []) {
433
+ const routeNodeId = generateId('Route', routeURL);
434
+ graph.addRelationship({
435
+ id: generateId('ENTRY_POINT_OF', `${routeNodeId}->${proc.id}`),
436
+ sourceId: routeNodeId,
437
+ targetId: proc.id,
438
+ type: 'ENTRY_POINT_OF',
439
+ confidence: 0.85,
440
+ reason: 'route-handler-entry-point',
441
+ });
442
+ }
443
+ for (const toolName of toolsByFile.get(entryFile) ?? []) {
444
+ const toolNodeId = generateId('Tool', toolName);
445
+ graph.addRelationship({
446
+ id: generateId('ENTRY_POINT_OF', `${toolNodeId}->${proc.id}`),
447
+ sourceId: toolNodeId,
448
+ targetId: proc.id,
449
+ type: 'ENTRY_POINT_OF',
450
+ confidence: 0.85,
451
+ reason: 'tool-handler-entry-point',
452
+ });
453
+ }
454
+ }
455
+ };
456
+ const addFeatureClusterLayerToGraph = (graph, featureClusterResult) => {
457
+ featureClusterResult.clusters.forEach((cluster) => {
458
+ graph.addNode({
459
+ id: cluster.id,
460
+ label: 'FeatureCluster',
461
+ properties: {
462
+ name: cluster.name,
463
+ filePath: '',
464
+ slug: cluster.slug,
465
+ featureKind: cluster.featureKind,
466
+ summary: cluster.summary,
467
+ description: cluster.description,
468
+ repo: cluster.repo,
469
+ service: cluster.service,
470
+ signals: cluster.signals,
471
+ memberCount: cluster.memberCount,
472
+ entryPointIds: cluster.entryPointIds,
473
+ routes: cluster.routes,
474
+ tools: cluster.tools,
475
+ testCoverageHints: cluster.testCoverageHints,
476
+ lastIndexedCommit: cluster.lastIndexedCommit,
477
+ confidence: cluster.confidence,
478
+ source: 'heuristic',
479
+ },
480
+ });
481
+ });
482
+ featureClusterResult.memberships.forEach((membership) => {
483
+ graph.addRelationship({
484
+ id: generateId('FEATURE_MEMBER_OF', `${membership.nodeId}->${membership.clusterId}`),
485
+ sourceId: membership.nodeId,
486
+ targetId: membership.clusterId,
487
+ type: 'FEATURE_MEMBER_OF',
488
+ confidence: membership.confidence,
489
+ reason: membership.signals.join('|'),
490
+ });
491
+ });
492
+ featureClusterResult.dependencies.forEach((dependency) => {
493
+ graph.addRelationship({
494
+ id: generateId('FEATURE_DEPENDS_ON', `${dependency.sourceClusterId}->${dependency.targetClusterId}`),
495
+ sourceId: dependency.sourceClusterId,
496
+ targetId: dependency.targetClusterId,
497
+ type: 'FEATURE_DEPENDS_ON',
498
+ confidence: dependency.confidence,
499
+ reason: `member-dependency|edges:${dependency.edgeCount}|types:${dependency.relationshipTypes.join(',')}`,
500
+ });
501
+ });
502
+ };
503
+ const extractGlobalLayerGraph = (graph) => {
504
+ const globalGraph = createKnowledgeGraph();
505
+ const globalNodeIds = new Set();
506
+ for (const node of graph.iterNodes()) {
507
+ if (GLOBAL_LAYER_NODE_LABELS.has(node.label)) {
508
+ globalNodeIds.add(node.id);
509
+ globalGraph.addNode(node);
510
+ }
511
+ }
512
+ for (const rel of graph.iterRelationships()) {
513
+ if (GLOBAL_LAYER_REL_TYPES.has(rel.type) ||
514
+ globalNodeIds.has(rel.sourceId) ||
515
+ globalNodeIds.has(rel.targetId)) {
516
+ globalGraph.addRelationship(rel);
517
+ }
518
+ }
519
+ return globalGraph;
520
+ };
521
+ const scaleAnalyzerProgress = (start, span, progress) => {
522
+ const normalized = Math.max(0, Math.min(100, progress)) / 100;
523
+ return Math.round(start + normalized * span);
524
+ };
525
+ const recomputeGlobalGraphLayers = async (input) => {
526
+ const { repoPath, storagePath, currentCommit, repoNameForFeatureClusters, compress, progress } = input;
527
+ progress('communities', 82, 'Loading patched graph for global recompute...');
528
+ const graph = await loadKnowledgeGraphFromCgdb({ includeGlobal: false });
529
+ const communityResult = await processCommunities(graph, (message, phaseProgress) => {
530
+ progress('communities', scaleAnalyzerProgress(83, 2, phaseProgress), message);
531
+ });
532
+ addCommunityLayerToGraph(graph, communityResult);
533
+ let symbolCount = 0;
534
+ graph.forEachNode((n) => {
535
+ if (n.label !== 'File')
536
+ symbolCount++;
537
+ });
538
+ const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
539
+ const processResult = await processProcesses(graph, communityResult.memberships, (message, phaseProgress) => {
540
+ progress('processes', scaleAnalyzerProgress(85, 2, phaseProgress), message);
541
+ }, { maxProcesses: dynamicMaxProcesses, minSteps: 3 });
542
+ addProcessLayerToGraph(graph, processResult);
543
+ addRouteToolProcessLinks(graph, processResult);
544
+ const featureClusterResult = await processFeatureClusters(graph, (message, phaseProgress) => {
545
+ progress('feature_clusters', scaleAnalyzerProgress(87, 1, phaseProgress), message);
546
+ }, {
547
+ repo: repoNameForFeatureClusters,
548
+ lastIndexedCommit: currentCommit || undefined,
549
+ });
550
+ addFeatureClusterLayerToGraph(graph, featureClusterResult);
551
+ progress('cgdb', 88, 'Replacing global graph layers...');
552
+ const globalGraph = extractGlobalLayerGraph(graph);
553
+ const replaceResult = await replaceGlobalGraphLayersInCgdb(globalGraph, repoPath, storagePath, undefined, { compress });
554
+ return {
555
+ communityResult,
556
+ processResult,
557
+ featureClusterResult,
558
+ deletedGlobalNodes: replaceResult.deletedGlobalNodes,
559
+ insertedGlobalRels: replaceResult.insertedRels,
560
+ };
561
+ };
562
+ const runIncrementalFilePatchAnalysis = async (input) => {
563
+ const { repoPath, storagePath, cgdbPath, currentCommit, existingMeta, adaptivePlan, patchPlan, options, progress, log, } = input;
564
+ const repoNameForFeatureClusters = options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath);
565
+ progress('extracting', 5, patchPlan.replaceAllFileScoped
566
+ ? 'Incremental full graph scan for global input change'
567
+ : `Incremental scan: ${patchPlan.currentPaths.length} current file(s)`);
568
+ const pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
569
+ const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
570
+ const scaled = Math.min(59, 5 + Math.round((p.percent / 100) * 54));
571
+ progress(p.phase, scaled, phaseLabel);
572
+ }, {
573
+ skipGraphPhases: true,
574
+ featureClusterRepo: repoNameForFeatureClusters,
575
+ lastIndexedCommit: currentCommit || undefined,
576
+ workerPoolSize: adaptivePlan.workerPoolSize,
577
+ workerSubBatchSize: adaptivePlan.workerSubBatchSize,
578
+ focusPaths: patchPlan.replaceAllFileScoped ? undefined : patchPlan.currentPaths,
579
+ });
580
+ progress('cgdb', 60, patchPlan.replaceAllFileScoped
581
+ ? 'Replacing file-scoped graph rows...'
582
+ : `Patching ${patchPlan.replacePaths.length} file path(s)...`);
583
+ await initCgdb(cgdbPath);
584
+ try {
585
+ let cgdbMsgCount = 0;
586
+ const fileGraphProgress = (msg) => {
587
+ cgdbMsgCount++;
588
+ const pct = Math.min(82, 60 + Math.round((cgdbMsgCount / (cgdbMsgCount + 8)) * 22));
589
+ progress('cgdb', pct, msg);
590
+ };
591
+ if (patchPlan.replaceAllFileScoped) {
592
+ const replacementResult = await replaceFileScopedGraphInCgdb(pipelineResult.graph, repoPath, storagePath, fileGraphProgress, { compress: adaptivePlan.compress });
593
+ log(`Smart analyze: refreshed all file-scoped graph rows, deleted ${replacementResult.deletedNodes} node(s), ` +
594
+ `inserted ${replacementResult.insertedRels} edge(s).`);
595
+ }
596
+ else {
597
+ const patchResult = await applyFileGraphPatchToCgdb(pipelineResult.graph, repoPath, storagePath, patchPlan.replacePaths, fileGraphProgress, { compress: adaptivePlan.compress, pathAliases: patchPlan.pathAliases });
598
+ log(`Smart analyze: incrementally patched ${patchResult.replacedFiles} path(s), ` +
599
+ `deleted ${patchResult.deletedNodeIds} stale node(s), inserted ${patchResult.insertedRels} edge(s), ` +
600
+ `restored ${patchResult.restoredRels} preserved edge(s), pruned ${patchResult.prunedFolders} folder(s).`);
601
+ }
602
+ const globalResult = await recomputeGlobalGraphLayers({
603
+ repoPath,
604
+ storagePath,
605
+ currentCommit,
606
+ repoNameForFeatureClusters,
607
+ compress: adaptivePlan.compress,
608
+ progress,
609
+ });
610
+ log(`Smart analyze: recomputed global layers (${globalResult.communityResult.stats.totalCommunities} communities, ` +
611
+ `${globalResult.processResult.stats.totalProcesses} processes, ` +
612
+ `${globalResult.featureClusterResult.stats.totalClusters} feature clusters).`);
613
+ progress('fts', 89, 'Refreshing search indexes...');
614
+ const ftsProperties = [...ftsPropertiesFor(adaptivePlan.compress)];
615
+ for (const { table, indexName } of FTS_TABLES) {
616
+ await ensureFTSIndex(table, indexName, ftsProperties);
617
+ }
618
+ const stats = await getCgdbStats();
619
+ const embeddingDecision = decideEmbeddingRun(adaptivePlan, {
620
+ nodes: stats.nodes,
621
+ embeddings: existingMeta.stats?.embeddings ?? 0,
622
+ });
623
+ log(embeddingDecision.enabled
624
+ ? `Embeddings enabled: ${embeddingDecision.reason}.`
625
+ : `Embeddings skipped: ${embeddingDecision.reason}.`);
626
+ if (embeddingDecision.enabled) {
627
+ const { isHttpMode } = await import('./embeddings/http-client.js');
628
+ const httpMode = isHttpMode();
629
+ progress('embeddings', 90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
630
+ const { runEmbeddingPipeline } = await import('./embeddings/embedding-pipeline.js');
631
+ const existingEmbeddings = await fetchExistingEmbeddingHashes(executeQuery);
632
+ const { readServerMapping } = await import('./embeddings/server-mapping.js');
633
+ const projectName = path.basename(repoPath);
634
+ const serverName = await readServerMapping(projectName);
635
+ await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
636
+ const scaled = 90 + Math.round((p.percent / 100) * 7);
637
+ const label = p.phase === 'loading-model'
638
+ ? httpMode
639
+ ? 'Connecting to embedding endpoint...'
640
+ : 'Loading embedding model...'
641
+ : `Embedding ${p.nodesProcessed || 0}/${p.totalNodes || '?'}`;
642
+ progress('embeddings', scaled, label);
643
+ }, {}, undefined, { repoName: projectName, serverName }, existingEmbeddings);
644
+ }
645
+ progress('done', 97, 'Recording graph snapshot...');
646
+ let graphstoreCurrentBranch;
647
+ let graphstoreHeadCommit;
648
+ try {
649
+ const snapshotResult = await recordAnalysisSnapshot({
650
+ storagePath,
651
+ indexedRepoCommit: currentCommit || undefined,
652
+ onSkipTable: (tableName, err) => {
653
+ log(`graphstore: skipped table "${tableName}": ${err instanceof Error ? err.message : String(err)}`);
654
+ },
655
+ });
656
+ if (snapshotResult) {
657
+ graphstoreCurrentBranch = snapshotResult.branch;
658
+ graphstoreHeadCommit = snapshotResult.commitId;
659
+ log(`graphstore: snapshot ${snapshotResult.snapshotId.slice(0, 19)}... ` +
660
+ `commit ${snapshotResult.commitId.slice(0, 19)}... ` +
661
+ `branch ${snapshotResult.branch}`);
662
+ }
663
+ }
664
+ catch (err) {
665
+ log(`graphstore: snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
666
+ }
667
+ progress('done', 98, 'Saving metadata...');
668
+ const embeddingCount = await countEmbeddings();
669
+ const previousFileCount = existingMeta.stats?.files ?? 0;
670
+ const fileCount = Math.max(0, previousFileCount + patchPlan.fileCountDelta);
671
+ const meta = {
672
+ repoPath,
673
+ lastCommit: currentCommit,
674
+ indexedAt: new Date().toISOString(),
675
+ schemaVersion: INDEX_SCHEMA_VERSION,
676
+ compress: adaptivePlan.compress,
677
+ searchIndexes: { fts: true },
678
+ adaptiveProfile: buildAdaptiveProfileMeta(adaptivePlan, embeddingDecision),
679
+ remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : undefined,
680
+ currentBranch: graphstoreCurrentBranch,
681
+ headCommit: graphstoreHeadCommit,
682
+ stats: {
683
+ files: patchPlan.replaceAllFileScoped ? pipelineResult.totalFileCount : fileCount,
684
+ nodes: stats.nodes,
685
+ edges: stats.edges,
686
+ communities: globalResult.communityResult.stats.totalCommunities,
687
+ featureClusters: globalResult.featureClusterResult.stats.totalClusters,
688
+ processes: globalResult.processResult.stats.totalProcesses,
689
+ embeddings: embeddingCount,
690
+ },
691
+ };
692
+ await saveMeta(storagePath, meta);
693
+ const projectName = await registerRepo(repoPath, meta, {
694
+ name: options.registryName,
695
+ allowDuplicateName: options.allowDuplicateName,
696
+ });
697
+ if (hasGitDir(repoPath)) {
698
+ await addToGitignore(repoPath);
699
+ }
700
+ try {
701
+ await generateAIContextFiles(repoPath, storagePath, projectName, metaStatsForAIContext(meta.stats), undefined, { skipAgentsMd: options.skipAgentsMd, noStats: options.noStats });
702
+ }
703
+ catch {
704
+ // Best-effort only.
705
+ }
706
+ await closeCgdb();
707
+ progress('done', 100, 'Done');
708
+ return {
709
+ repoName: projectName,
710
+ repoPath,
711
+ stats: meta.stats ?? {},
712
+ pipelineResult,
713
+ };
714
+ }
715
+ catch (err) {
716
+ try {
717
+ await closeCgdb();
718
+ }
719
+ catch {
720
+ /* swallow */
721
+ }
722
+ throw err;
723
+ }
724
+ };
198
725
  // ---------------------------------------------------------------------------
199
726
  // Main orchestrator
200
727
  // ---------------------------------------------------------------------------
@@ -284,6 +811,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
284
811
  const repoHasGit = hasGitDir(repoPath);
285
812
  const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
286
813
  const existingMeta = await loadMeta(storagePath);
814
+ const adaptivePlan = resolveAdaptiveAnalyzePlan({
815
+ profile: options.profile,
816
+ embeddingMode: options.embeddingMode,
817
+ embeddings: options.embeddings,
818
+ compress: options.compress,
819
+ existingMeta,
820
+ });
821
+ const existingEmbeddingDecision = decideEmbeddingRun(adaptivePlan, existingMeta?.stats);
822
+ log(formatAdaptiveAnalyzePlan(adaptivePlan));
287
823
  // ── Early-return: already up to date ──────────────────────────────
288
824
  // Schema-version mismatch forces a full re-analyze regardless of commit
289
825
  // equality: existing 1.7.x indexes have no `schemaVersion` field at all,
@@ -298,7 +834,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
298
834
  : null;
299
835
  const configRebuildReason = storageRebuildReason ??
300
836
  (existingMeta && schemaUpToDate && !options.force
301
- ? getAnalyzeConfigRebuildReason(existingMeta, options)
837
+ ? getAnalyzeConfigRebuildReason(existingMeta, {
838
+ compress: adaptivePlan.compress,
839
+ embeddings: existingEmbeddingDecision.enabled,
840
+ })
302
841
  : null);
303
842
  if (existingMeta &&
304
843
  schemaUpToDate &&
@@ -368,6 +907,36 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
368
907
  log(`Smart analyze: ${graphRelevantChanges.length} indexed graph input change(s) require rebuild` +
369
908
  (preview ? ` (${preview}${suffix})` : '') +
370
909
  '.');
910
+ const patchPlan = buildIncrementalFilePatchPlan(graphRelevantChanges);
911
+ if (patchPlan.eligible) {
912
+ log(`Smart analyze: ${patchPlan.reason}.`);
913
+ try {
914
+ return await runIncrementalFilePatchAnalysis({
915
+ repoPath,
916
+ storagePath,
917
+ cgdbPath,
918
+ currentCommit,
919
+ existingMeta,
920
+ adaptivePlan,
921
+ patchPlan,
922
+ options,
923
+ progress,
924
+ log,
925
+ });
926
+ }
927
+ catch (err) {
928
+ log(`Smart analyze: incremental patch failed (${err instanceof Error ? err.message : String(err)}); rebuilding.`);
929
+ try {
930
+ await closeCgdb();
931
+ }
932
+ catch {
933
+ /* swallow */
934
+ }
935
+ }
936
+ }
937
+ else {
938
+ log(`Smart analyze: incremental patch unavailable: ${patchPlan.reason}; rebuilding.`);
939
+ }
371
940
  }
372
941
  else {
373
942
  log('Smart analyze: could not inspect git diff; rebuilding.');
@@ -381,7 +950,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
381
950
  // ── Cache embeddings from existing index before rebuild ────────────
382
951
  let cachedEmbeddingNodeIds = new Set();
383
952
  let cachedEmbeddings = [];
384
- if (options.embeddings && existingMeta && !options.force) {
953
+ if (existingEmbeddingDecision.enabled && existingMeta && !options.force) {
385
954
  try {
386
955
  progress('embeddings', 0, 'Caching embeddings...');
387
956
  await initCgdb(cgdbPath);
@@ -408,6 +977,8 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
408
977
  }, {
409
978
  featureClusterRepo: repoNameForFeatureClusters,
410
979
  lastIndexedCommit: currentCommit || undefined,
980
+ workerPoolSize: adaptivePlan.workerPoolSize,
981
+ workerSubBatchSize: adaptivePlan.workerSubBatchSize,
411
982
  });
412
983
  // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
413
984
  progress('cgdb', 60, 'Loading into LadybugDB...');
@@ -436,7 +1007,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
436
1007
  // through encodeContent before hitting the CSV. Default 'none' is
437
1008
  // a true passthrough, so the on-disk layout is byte-identical to
438
1009
  // pre-Phase-2 indexes when no compression flag is passed.
439
- { compress: options.compress });
1010
+ { compress: adaptivePlan.compress });
440
1011
  // ── Phase 2.5: Versioned-graph snapshot (best-effort) ────────────
441
1012
  // Phase 4 hook: snapshot the freshly-loaded graph into the
442
1013
  // content-addressed `.codragraph/graphstore/`. Failures here do NOT
@@ -464,12 +1035,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
464
1035
  log(`graphstore: snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
465
1036
  }
466
1037
  // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
467
- // FTS indexes are created lazily on first `query`/`context` call instead
468
- // of eagerly here. On small repos / CI runners the LadybugDB
469
- // CREATE_FTS_INDEX cost is ~440 ms × 5 (≈2 s) regardless of table size,
470
- // which dominated `analyze` runtime and pushed Windows CI past its
471
- // 30 s test budget. Lazy creation is implemented in
472
- // `core/search/bm25-index.ts` via `ensureFTSIndex`.
1038
+ // Build persisted keyword indexes while the analyzer still owns a writable
1039
+ // LadybugDB handle. MCP/local query paths intentionally open read-only so
1040
+ // they can coexist with editors and servers; if FTS is not warmed here,
1041
+ // the first agent `query` degrades to a bounded table scan.
1042
+ progress('fts', 85, 'Creating search indexes...');
1043
+ const ftsProperties = [...ftsPropertiesFor(adaptivePlan.compress)];
1044
+ for (const { table, indexName } of FTS_TABLES) {
1045
+ await ensureFTSIndex(table, indexName, ftsProperties);
1046
+ }
473
1047
  // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
474
1048
  if (cachedEmbeddings.length > 0) {
475
1049
  const cachedDims = cachedEmbeddings[0].embedding.length;
@@ -497,12 +1071,14 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
497
1071
  }
498
1072
  // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
499
1073
  const stats = await getCgdbStats();
500
- let embeddingSkipped = true;
501
- if (options.embeddings) {
502
- if (stats.nodes <= EMBEDDING_NODE_LIMIT) {
503
- embeddingSkipped = false;
504
- }
505
- }
1074
+ const embeddingDecision = decideEmbeddingRun(adaptivePlan, {
1075
+ nodes: stats.nodes,
1076
+ embeddings: existingMeta?.stats?.embeddings ?? cachedEmbeddings.length,
1077
+ });
1078
+ const embeddingSkipped = !embeddingDecision.enabled;
1079
+ log(embeddingDecision.enabled
1080
+ ? `Embeddings enabled: ${embeddingDecision.reason}.`
1081
+ : `Embeddings skipped: ${embeddingDecision.reason}.`);
506
1082
  if (!embeddingSkipped) {
507
1083
  const { isHttpMode } = await import('./embeddings/http-client.js');
508
1084
  const httpMode = isHttpMode();
@@ -532,20 +1108,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
532
1108
  // ── Phase 5: Finalize (98–100%) ───────────────────────────────────
533
1109
  progress('done', 98, 'Saving metadata...');
534
1110
  // Count embeddings in the index (cached + newly generated)
535
- let embeddingCount = 0;
536
- try {
537
- const embResult = await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS cnt`);
538
- embeddingCount = embResult?.[0]?.cnt ?? 0;
539
- }
540
- catch {
541
- /* table may not exist if embeddings never ran */
542
- }
1111
+ const embeddingCount = await countEmbeddings();
543
1112
  const meta = {
544
1113
  repoPath,
545
1114
  lastCommit: currentCommit,
546
1115
  indexedAt: new Date().toISOString(),
547
1116
  schemaVersion: INDEX_SCHEMA_VERSION,
548
- compress: options.compress ?? 'none',
1117
+ compress: adaptivePlan.compress,
1118
+ searchIndexes: { fts: true },
1119
+ adaptiveProfile: buildAdaptiveProfileMeta(adaptivePlan, embeddingDecision),
549
1120
  // Captured here (not at registration) so it travels with the
550
1121
  // on-disk meta.json — sibling-clone fingerprinting works for
551
1122
  // out-of-tree consumers (group-status, future tooling) without