@codragraph/cli 2.1.4 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -7
- package/dist/cli/ai-context.js +297 -0
- package/dist/cli/analyze.d.ts +9 -4
- package/dist/cli/analyze.js +37 -13
- package/dist/cli/index.js +40 -14
- package/dist/cli/status.d.ts +1 -1
- package/dist/cli/status.js +8 -0
- package/dist/cli/tool.d.ts +10 -2
- package/dist/cli/tool.js +100 -39
- package/dist/config/ignore-service.js +1 -0
- 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 +130 -20
- package/dist/core/ingestion/parsing-processor.js +7 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +7 -1
- package/dist/core/ingestion/pipeline-phases/structure.js +19 -3
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/workers/parse-worker.js +1 -1
- package/dist/core/ingestion/workers/worker-pool.d.ts +14 -1
- package/dist/core/ingestion/workers/worker-pool.js +33 -17
- package/dist/core/run-analyze.d.ts +27 -2
- package/dist/core/run-analyze.js +626 -32
- package/dist/core/search/bm25-index.d.ts +16 -8
- package/dist/core/search/bm25-index.js +72 -110
- package/dist/mcp/local/local-backend.d.ts +2 -0
- package/dist/mcp/local/local-backend.js +241 -21
- package/dist/storage/repo-manager.d.ts +29 -0
- package/dist/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist/web/assets/agent-DcdaQnmu.js +1104 -0
- package/dist/web/assets/architectureDiagram-UL44E2DR-DFSpa3Hb.js +36 -0
- package/dist/web/assets/blockDiagram-7IZFK4PR-DlFaxH1b.js +132 -0
- package/dist/web/assets/{c4Diagram-DFAF54RM-C4Hl3J2U.js → c4Diagram-Y2BXMSZH-BjJ_Yrim.js} +1 -1
- package/dist/web/assets/{chunk-7RZVMHOQ-BitYcNVR.js → chunk-3SSMPTDK-KGZSzG3Y.js} +1 -1
- package/dist/web/assets/{chunk-TBF5ZNIQ-DL5stGM1.js → chunk-6764PJDD-p1sGJgVm.js} +1 -1
- package/dist/web/assets/{chunk-KSICW3F5-BYzvDLNI.js → chunk-AZZRMDJM-DIDkQA4V.js} +1 -1
- package/dist/web/assets/{chunk-AEOMTBSW-BgTIXPsY.js → chunk-JQRUD6KW-DAwg-yCU.js} +1 -1
- package/dist/web/assets/chunk-KRXBNO2N-ChVO_XdS.js +1 -0
- package/dist/web/assets/chunk-LCXTWHL2-DGYdb_Eh.js +231 -0
- package/dist/web/assets/{chunk-O5ABG6QK-dHwHzA6n.js → chunk-LII3EMHJ-Bzh9SNgD.js} +1 -1
- package/dist/web/assets/chunk-RG4AUYOV-Bcl7U_IV.js +206 -0
- package/dist/web/assets/{chunk-TU3PZOEN-RLyvLcv-.js → chunk-T5OCTHI4-CZYMg5sc.js} +1 -1
- package/dist/web/assets/chunk-W44A43WB-REOI67PN.js +13 -0
- package/dist/web/assets/{chunk-RWUO3TPN-BgRTY0_k.js → chunk-ZXARS5L4-BfFdV1tf.js} +1 -1
- package/dist/web/assets/classDiagram-KGZ6W3CR-B-qkKMYi.js +1 -0
- package/dist/web/assets/classDiagram-v2-72OJOZXJ-B-qkKMYi.js +1 -0
- package/dist/web/assets/{cose-bilkent-PNC4W37J-DVhePRYg.js → cose-bilkent-UX7MHV2Q-D6vANJGG.js} +1 -1
- package/dist/web/assets/dagre-ND4H6XIP-BiHe5Lal.js +4 -0
- package/dist/web/assets/diagram-3NCE3AQN-CEutBCOW.js +43 -0
- package/dist/web/assets/diagram-GF46GFSD-CZns6HPQ.js +24 -0
- package/dist/web/assets/diagram-HNR7UZ2L-Vz8fE5of.js +3 -0
- package/dist/web/assets/diagram-QXG6HAR7-D60HKZ_y.js +24 -0
- package/dist/web/assets/diagram-WEQXMOUZ-vGAf1p3E.js +10 -0
- package/dist/web/assets/{erDiagram-GCSMX5X6-C3dhDFA8.js → erDiagram-L5TCEMPS-DZaplJA6.js} +5 -5
- package/dist/web/assets/{flowDiagram-OTCZ4VVT-CWSFWmhr.js → flowDiagram-H6V6AXG4-BqUqeAsI.js} +9 -9
- package/dist/web/assets/ganttDiagram-JCBTUEKG-XEB6H-0G.js +292 -0
- package/dist/web/assets/gitGraphDiagram-S2ZK5IYY-7G50u1Cd.js +106 -0
- package/dist/web/assets/index-B5WxtMpv.js +1415 -0
- package/dist/web/assets/infoDiagram-3YFTVSEB-Cut_rzaf.js +2 -0
- package/dist/web/assets/{ishikawaDiagram-YMYX4NHK-DUoJvNP2.js → ishikawaDiagram-BNXS4ZKH-B4DGfGi3.js} +3 -3
- package/dist/web/assets/{journeyDiagram-SO5T7YLQ-RMFPNNqz.js → journeyDiagram-M6C3CM3L-BBFhsL3E.js} +1 -1
- package/dist/web/assets/{kanban-definition-LJHFXRCJ-BzpDs1K9.js → kanban-definition-75IXJCU3-DarGRyn3.js} +4 -4
- package/dist/web/assets/{katex-GD7MH7QM-DBQvrix-.js → katex-K3KEBU37-W5XTYMhr.js} +1 -1
- package/dist/web/assets/mindmap-definition-2TDM6QVE-BgeczIJM.js +96 -0
- package/dist/web/assets/pieDiagram-CU6KROY3-Kkoo-Noq.js +30 -0
- package/dist/web/assets/quadrantDiagram-VICAPDV7-CDQFeRWN.js +7 -0
- package/dist/web/assets/{requirementDiagram-M5DCFWZL-DLHOVTSv.js → requirementDiagram-JXO7QTGE-Cz9-XnkA.js} +2 -2
- package/dist/web/assets/sankeyDiagram-URQDO5SZ-CU26z0n7.js +40 -0
- package/dist/web/assets/sequenceDiagram-VS2MUI6T-OGK1FLOt.js +162 -0
- package/dist/web/assets/stateDiagram-7D4R322I-DJ9brq0U.js +1 -0
- package/dist/web/assets/stateDiagram-v2-36443NZ5-DhJ4Ky-7.js +1 -0
- package/dist/web/assets/{timeline-definition-5SPVSISX-TRSDRgPw.js → timeline-definition-O6YCAMPW-XZvnjqTT.js} +4 -4
- package/dist/web/assets/{vennDiagram-IE5QUKF5-DNy7HRBM.js → vennDiagram-MWXL3ELB-CJUssEjA.js} +6 -6
- package/dist/web/assets/wardley-L42UT6IY-5TKZOOLJ-DZr11zBG.js +173 -0
- package/dist/web/assets/wardleyDiagram-CUQ6CDDI-C276iqrN.js +78 -0
- package/dist/web/assets/{xychartDiagram-ZHJ5623Y-Dr9r7a35.js → xychartDiagram-N2JHSOCM-B9-uCZyP.js} +4 -4
- package/dist/web/index.html +1 -1
- package/hooks/claude/codragraph-hook.cjs +15 -122
- package/package.json +1 -1
- 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/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',
|
|
@@ -144,8 +157,10 @@ export const changedPathAffectsGraph = (change) => {
|
|
|
144
157
|
const paths = [change.path, change.previousPath].filter((p) => Boolean(p));
|
|
145
158
|
if (paths.some(isGraphContentPath))
|
|
146
159
|
return true;
|
|
147
|
-
// Add/delete/rename/copy
|
|
148
|
-
// is not
|
|
160
|
+
// Add/delete/rename/copy affect the graph's File/Folder topology even when
|
|
161
|
+
// the path is not source code. Ignore only generated agent context and
|
|
162
|
+
// configured ignored paths; staying conservative here prevents stale file
|
|
163
|
+
// and documentation surfaces after path-only commits.
|
|
149
164
|
if (statusCode === 'A' || statusCode === 'D' || statusCode === 'R' || statusCode === 'C') {
|
|
150
165
|
return paths.some((p) => !isGeneratedAgentContextPath(p) && !shouldIgnorePath(p));
|
|
151
166
|
}
|
|
@@ -157,11 +172,119 @@ export const changedPathAffectsGraph = (change) => {
|
|
|
157
172
|
return true;
|
|
158
173
|
};
|
|
159
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
|
+
};
|
|
160
280
|
export const getAnalyzeConfigRebuildReason = (existingMeta, options) => {
|
|
161
281
|
const existingCompress = existingMeta.compress ?? 'none';
|
|
162
282
|
if (options.compress && options.compress !== existingCompress) {
|
|
163
283
|
return `requested compression changed from ${existingCompress} to ${options.compress}`;
|
|
164
284
|
}
|
|
285
|
+
if (existingMeta.searchIndexes?.fts !== true) {
|
|
286
|
+
return 'search indexes are missing';
|
|
287
|
+
}
|
|
165
288
|
if (options.embeddings && (existingMeta.stats?.embeddings ?? 0) === 0) {
|
|
166
289
|
return 'embeddings were requested but the existing index has no vectors';
|
|
167
290
|
}
|
|
@@ -176,6 +299,39 @@ const buildReusedMeta = (existingMeta, repoPath, currentCommit) => ({
|
|
|
176
299
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
177
300
|
remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : existingMeta.remoteUrl,
|
|
178
301
|
});
|
|
302
|
+
const metaStatsForAIContext = (stats = {}) => ({
|
|
303
|
+
files: stats.files,
|
|
304
|
+
nodes: stats.nodes,
|
|
305
|
+
edges: stats.edges,
|
|
306
|
+
communities: stats.communities,
|
|
307
|
+
clusters: stats.featureClusters,
|
|
308
|
+
processes: stats.processes,
|
|
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
|
+
};
|
|
179
335
|
const pathExists = async (targetPath) => {
|
|
180
336
|
try {
|
|
181
337
|
await fs.stat(targetPath);
|
|
@@ -185,6 +341,387 @@ const pathExists = async (targetPath) => {
|
|
|
185
341
|
return false;
|
|
186
342
|
}
|
|
187
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
|
+
};
|
|
188
725
|
// ---------------------------------------------------------------------------
|
|
189
726
|
// Main orchestrator
|
|
190
727
|
// ---------------------------------------------------------------------------
|
|
@@ -274,6 +811,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
274
811
|
const repoHasGit = hasGitDir(repoPath);
|
|
275
812
|
const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
|
|
276
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));
|
|
277
823
|
// ── Early-return: already up to date ──────────────────────────────
|
|
278
824
|
// Schema-version mismatch forces a full re-analyze regardless of commit
|
|
279
825
|
// equality: existing 1.7.x indexes have no `schemaVersion` field at all,
|
|
@@ -288,7 +834,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
288
834
|
: null;
|
|
289
835
|
const configRebuildReason = storageRebuildReason ??
|
|
290
836
|
(existingMeta && schemaUpToDate && !options.force
|
|
291
|
-
? getAnalyzeConfigRebuildReason(existingMeta,
|
|
837
|
+
? getAnalyzeConfigRebuildReason(existingMeta, {
|
|
838
|
+
compress: adaptivePlan.compress,
|
|
839
|
+
embeddings: existingEmbeddingDecision.enabled,
|
|
840
|
+
})
|
|
292
841
|
: null);
|
|
293
842
|
if (existingMeta &&
|
|
294
843
|
schemaUpToDate &&
|
|
@@ -297,8 +846,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
297
846
|
existingMeta.lastCommit === currentCommit) {
|
|
298
847
|
// Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
|
|
299
848
|
if (currentCommit !== '') {
|
|
849
|
+
const repoName = options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath);
|
|
850
|
+
try {
|
|
851
|
+
await generateAIContextFiles(repoPath, storagePath, repoName, metaStatsForAIContext(existingMeta.stats), undefined, { skipAgentsMd: options.skipAgentsMd, noStats: options.noStats });
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
// Best-effort only.
|
|
855
|
+
}
|
|
300
856
|
return {
|
|
301
|
-
repoName
|
|
857
|
+
repoName,
|
|
302
858
|
repoPath,
|
|
303
859
|
stats: existingMeta.stats ?? {},
|
|
304
860
|
alreadyUpToDate: true,
|
|
@@ -327,8 +883,14 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
327
883
|
if (hasGitDir(repoPath)) {
|
|
328
884
|
await addToGitignore(repoPath);
|
|
329
885
|
}
|
|
886
|
+
try {
|
|
887
|
+
await generateAIContextFiles(repoPath, storagePath, projectName, metaStatsForAIContext(reusedMeta.stats), undefined, { skipAgentsMd: options.skipAgentsMd, noStats: options.noStats });
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
// Best-effort only.
|
|
891
|
+
}
|
|
330
892
|
const reuseReason = `Smart analyze reused the existing graph; ${changedPaths.length} changed ` +
|
|
331
|
-
`file(s) did not affect indexed
|
|
893
|
+
`file(s) did not affect indexed graph inputs.`;
|
|
332
894
|
log(reuseReason);
|
|
333
895
|
progress('done', 100, 'Existing graph reused');
|
|
334
896
|
return {
|
|
@@ -342,9 +904,39 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
342
904
|
}
|
|
343
905
|
const preview = graphRelevantChanges.slice(0, 5).map(formatChangeForLog).join(', ');
|
|
344
906
|
const suffix = graphRelevantChanges.length > 5 ? ', ...' : '';
|
|
345
|
-
log(`Smart analyze: ${graphRelevantChanges.length} indexed change(s) require rebuild` +
|
|
907
|
+
log(`Smart analyze: ${graphRelevantChanges.length} indexed graph input change(s) require rebuild` +
|
|
346
908
|
(preview ? ` (${preview}${suffix})` : '') +
|
|
347
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
|
+
}
|
|
348
940
|
}
|
|
349
941
|
else {
|
|
350
942
|
log('Smart analyze: could not inspect git diff; rebuilding.');
|
|
@@ -358,7 +950,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
358
950
|
// ── Cache embeddings from existing index before rebuild ────────────
|
|
359
951
|
let cachedEmbeddingNodeIds = new Set();
|
|
360
952
|
let cachedEmbeddings = [];
|
|
361
|
-
if (
|
|
953
|
+
if (existingEmbeddingDecision.enabled && existingMeta && !options.force) {
|
|
362
954
|
try {
|
|
363
955
|
progress('embeddings', 0, 'Caching embeddings...');
|
|
364
956
|
await initCgdb(cgdbPath);
|
|
@@ -385,6 +977,8 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
385
977
|
}, {
|
|
386
978
|
featureClusterRepo: repoNameForFeatureClusters,
|
|
387
979
|
lastIndexedCommit: currentCommit || undefined,
|
|
980
|
+
workerPoolSize: adaptivePlan.workerPoolSize,
|
|
981
|
+
workerSubBatchSize: adaptivePlan.workerSubBatchSize,
|
|
388
982
|
});
|
|
389
983
|
// ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
|
|
390
984
|
progress('cgdb', 60, 'Loading into LadybugDB...');
|
|
@@ -413,7 +1007,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
413
1007
|
// through encodeContent before hitting the CSV. Default 'none' is
|
|
414
1008
|
// a true passthrough, so the on-disk layout is byte-identical to
|
|
415
1009
|
// pre-Phase-2 indexes when no compression flag is passed.
|
|
416
|
-
{ compress:
|
|
1010
|
+
{ compress: adaptivePlan.compress });
|
|
417
1011
|
// ── Phase 2.5: Versioned-graph snapshot (best-effort) ────────────
|
|
418
1012
|
// Phase 4 hook: snapshot the freshly-loaded graph into the
|
|
419
1013
|
// content-addressed `.codragraph/graphstore/`. Failures here do NOT
|
|
@@ -441,12 +1035,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
441
1035
|
log(`graphstore: snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
442
1036
|
}
|
|
443
1037
|
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
1047
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
451
1048
|
if (cachedEmbeddings.length > 0) {
|
|
452
1049
|
const cachedDims = cachedEmbeddings[0].embedding.length;
|
|
@@ -474,12 +1071,14 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
474
1071
|
}
|
|
475
1072
|
// ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
|
|
476
1073
|
const stats = await getCgdbStats();
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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}.`);
|
|
483
1082
|
if (!embeddingSkipped) {
|
|
484
1083
|
const { isHttpMode } = await import('./embeddings/http-client.js');
|
|
485
1084
|
const httpMode = isHttpMode();
|
|
@@ -509,20 +1108,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
509
1108
|
// ── Phase 5: Finalize (98–100%) ───────────────────────────────────
|
|
510
1109
|
progress('done', 98, 'Saving metadata...');
|
|
511
1110
|
// Count embeddings in the index (cached + newly generated)
|
|
512
|
-
|
|
513
|
-
try {
|
|
514
|
-
const embResult = await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS cnt`);
|
|
515
|
-
embeddingCount = embResult?.[0]?.cnt ?? 0;
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
/* table may not exist if embeddings never ran */
|
|
519
|
-
}
|
|
1111
|
+
const embeddingCount = await countEmbeddings();
|
|
520
1112
|
const meta = {
|
|
521
1113
|
repoPath,
|
|
522
1114
|
lastCommit: currentCommit,
|
|
523
1115
|
indexedAt: new Date().toISOString(),
|
|
524
1116
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
525
|
-
compress:
|
|
1117
|
+
compress: adaptivePlan.compress,
|
|
1118
|
+
searchIndexes: { fts: true },
|
|
1119
|
+
adaptiveProfile: buildAdaptiveProfileMeta(adaptivePlan, embeddingDecision),
|
|
526
1120
|
// Captured here (not at registration) so it travels with the
|
|
527
1121
|
// on-disk meta.json — sibling-clone fingerprinting works for
|
|
528
1122
|
// out-of-tree consumers (group-status, future tooling) without
|