@codragraph/cli 2.1.5 → 2.2.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -13
- package/dist/cli/analyze.d.ts +9 -4
- package/dist/cli/analyze.js +37 -13
- package/dist/cli/graphpack.d.ts +48 -0
- package/dist/cli/graphpack.js +217 -0
- package/dist/cli/index.js +81 -3
- package/dist/cli/status.d.ts +1 -1
- package/dist/cli/status.js +8 -0
- package/dist/cli/tool.d.ts +11 -2
- package/dist/cli/tool.js +138 -8
- package/dist/core/adaptive-profile.d.ts +52 -0
- package/dist/core/adaptive-profile.js +180 -0
- package/dist/core/cgdb/cgdb-adapter.d.ts +34 -5
- package/dist/core/cgdb/cgdb-adapter.js +418 -5
- package/dist/core/cgdb/pool-adapter.js +1 -1
- package/dist/core/graphpack/index.d.ts +14 -0
- package/dist/core/graphpack/index.js +474 -0
- package/dist/core/graphpack/types.d.ts +129 -0
- package/dist/core/graphpack/types.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +3 -1
- package/dist/core/ingestion/pipeline-phases/structure.js +19 -3
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/run-analyze.d.ts +27 -2
- package/dist/core/run-analyze.js +598 -27
- package/dist/core/search/bm25-index.d.ts +19 -0
- package/dist/core/search/bm25-index.js +68 -29
- package/dist/core/semantic/relationships.d.ts +36 -0
- package/dist/core/semantic/relationships.js +261 -0
- package/dist/mcp/local/local-backend.js +48 -3
- package/dist/mcp/resources.js +125 -0
- package/dist/mcp/tools.js +105 -0
- package/dist/server/api.js +112 -0
- package/dist/storage/repo-manager.d.ts +29 -0
- package/dist/web/assets/agent-CQNZQ-hg.js +1139 -0
- package/dist/web/assets/architectureDiagram-UL44E2DR-B5_goS_i.js +36 -0
- package/dist/web/assets/blockDiagram-7IZFK4PR-D7ZAlDyv.js +132 -0
- package/dist/web/assets/{c4Diagram-DFAF54RM-C4Hl3J2U.js → c4Diagram-Y2BXMSZH-Djcgm_54.js} +1 -1
- package/dist/web/assets/{chunk-7RZVMHOQ-BitYcNVR.js → chunk-3SSMPTDK-Cv2Zy2FO.js} +1 -1
- package/dist/web/assets/{chunk-TBF5ZNIQ-DL5stGM1.js → chunk-6764PJDD-Cppb-jH-.js} +1 -1
- package/dist/web/assets/{chunk-KSICW3F5-BYzvDLNI.js → chunk-AZZRMDJM-BHlLC7p3.js} +1 -1
- package/dist/web/assets/{chunk-AEOMTBSW-BgTIXPsY.js → chunk-JQRUD6KW-3F8Zg-1N.js} +1 -1
- package/dist/web/assets/chunk-KRXBNO2N-C0mbN9a7.js +1 -0
- package/dist/web/assets/chunk-LCXTWHL2-BoiuJpIF.js +231 -0
- package/dist/web/assets/{chunk-O5ABG6QK-dHwHzA6n.js → chunk-LII3EMHJ-Dqq0Qguw.js} +1 -1
- package/dist/web/assets/chunk-RG4AUYOV-Bl5F_gDs.js +206 -0
- package/dist/web/assets/{chunk-TU3PZOEN-RLyvLcv-.js → chunk-T5OCTHI4-B2tIcggA.js} +1 -1
- package/dist/web/assets/chunk-W44A43WB-BHe37iN7.js +13 -0
- package/dist/web/assets/{chunk-RWUO3TPN-BgRTY0_k.js → chunk-ZXARS5L4-wcrIaQvY.js} +1 -1
- package/dist/web/assets/classDiagram-KGZ6W3CR-IbI6v_24.js +1 -0
- package/dist/web/assets/classDiagram-v2-72OJOZXJ-IbI6v_24.js +1 -0
- package/dist/web/assets/{cose-bilkent-PNC4W37J-DVhePRYg.js → cose-bilkent-UX7MHV2Q-BWr7v0Wr.js} +1 -1
- package/dist/web/assets/dagre-ND4H6XIP-De5LIh1B.js +4 -0
- package/dist/web/assets/diagram-3NCE3AQN-Dd22FSHy.js +43 -0
- package/dist/web/assets/diagram-GF46GFSD-Cev3THY8.js +24 -0
- package/dist/web/assets/diagram-HNR7UZ2L-D8Z8RQGs.js +3 -0
- package/dist/web/assets/diagram-QXG6HAR7-B8VOJOiE.js +24 -0
- package/dist/web/assets/diagram-WEQXMOUZ-va1bLoMD.js +10 -0
- package/dist/web/assets/{erDiagram-GCSMX5X6-C3dhDFA8.js → erDiagram-L5TCEMPS-B3_9uAoP.js} +5 -5
- package/dist/web/assets/{flowDiagram-OTCZ4VVT-CWSFWmhr.js → flowDiagram-H6V6AXG4-98m6maI1.js} +9 -9
- package/dist/web/assets/ganttDiagram-JCBTUEKG-vE2nzETb.js +292 -0
- package/dist/web/assets/gitGraphDiagram-S2ZK5IYY-DKc8uUg_.js +106 -0
- package/dist/web/assets/index-BAhe1HSk.css +1 -0
- package/dist/web/assets/index-VTKdaklA.js +1415 -0
- package/dist/web/assets/infoDiagram-3YFTVSEB-DYP-Srzx.js +2 -0
- package/dist/web/assets/{ishikawaDiagram-YMYX4NHK-DUoJvNP2.js → ishikawaDiagram-BNXS4ZKH-QZnkpmmb.js} +3 -3
- package/dist/web/assets/{journeyDiagram-SO5T7YLQ-RMFPNNqz.js → journeyDiagram-M6C3CM3L-B5ojIuqu.js} +1 -1
- package/dist/web/assets/{kanban-definition-LJHFXRCJ-BzpDs1K9.js → kanban-definition-75IXJCU3-BJA8liRR.js} +4 -4
- package/dist/web/assets/{katex-GD7MH7QM-DBQvrix-.js → katex-K3KEBU37-DUqZiCRL.js} +1 -1
- package/dist/web/assets/mindmap-definition-2TDM6QVE-BQj5yylD.js +96 -0
- package/dist/web/assets/pieDiagram-CU6KROY3-4eSrPiQz.js +30 -0
- package/dist/web/assets/quadrantDiagram-VICAPDV7-PzxN8j55.js +7 -0
- package/dist/web/assets/{requirementDiagram-M5DCFWZL-DLHOVTSv.js → requirementDiagram-JXO7QTGE-CtplTc5y.js} +2 -2
- package/dist/web/assets/sankeyDiagram-URQDO5SZ-CoSgvkxv.js +40 -0
- package/dist/web/assets/sequenceDiagram-VS2MUI6T-D7ygyXvJ.js +162 -0
- package/dist/web/assets/stateDiagram-7D4R322I-v01gvwji.js +1 -0
- package/dist/web/assets/stateDiagram-v2-36443NZ5-DFD2b8_x.js +1 -0
- package/dist/web/assets/{timeline-definition-5SPVSISX-TRSDRgPw.js → timeline-definition-O6YCAMPW-CTI3M65J.js} +4 -4
- package/dist/web/assets/{vennDiagram-IE5QUKF5-DNy7HRBM.js → vennDiagram-MWXL3ELB-RnB0XMP7.js} +6 -6
- package/dist/web/assets/wardley-L42UT6IY-5TKZOOLJ-C-ZcgEBb.js +173 -0
- package/dist/web/assets/wardleyDiagram-CUQ6CDDI-EwRi4kwo.js +78 -0
- package/dist/web/assets/{xychartDiagram-ZHJ5623Y-Dr9r7a35.js → xychartDiagram-N2JHSOCM-DA38II6y.js} +4 -4
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/vendor/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/dist/web/assets/agent-D5lb0zXz.js +0 -1089
- package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +0 -36
- package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +0 -132
- package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +0 -231
- package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +0 -1
- package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +0 -13
- package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +0 -1
- package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +0 -206
- package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +0 -1
- package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +0 -1
- package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +0 -4
- package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +0 -43
- package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +0 -24
- package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +0 -10
- package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +0 -24
- package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +0 -292
- package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +0 -106
- package/dist/web/assets/index-BgeqpYgd.js +0 -1415
- package/dist/web/assets/index-CT0GtFLZ.css +0 -1
- package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +0 -2
- package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +0 -96
- package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +0 -30
- package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +0 -7
- package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +0 -10
- package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +0 -157
- package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +0 -1
- package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +0 -1
- package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +0 -161
- package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +0 -20
package/dist/core/run-analyze.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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,
|
|
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 (
|
|
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:
|
|
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
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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:
|
|
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
|