gitnexus 1.6.11-rc.2 → 1.6.11-rc.21
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 +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- package/web/index.html +1 -1
package/dist/core/run-analyze.js
CHANGED
|
@@ -12,6 +12,7 @@ import { detectGraphWriteCollapse } from './index-freshness.js';
|
|
|
12
12
|
import { PDG_EDGE_TYPES } from './lbug/pdg-emit-sink.js';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import fs from 'fs/promises';
|
|
15
|
+
import { constants as fsConstants } from 'node:fs';
|
|
15
16
|
import { randomUUID } from 'node:crypto';
|
|
16
17
|
import { retryRename } from '../storage/fs-atomic.js';
|
|
17
18
|
import { acquireIndexLock } from '../storage/index-lock.js';
|
|
@@ -20,11 +21,11 @@ import { logUnresolvedReceiverFiles, summarizeUnresolvedReceivers, } from './ing
|
|
|
20
21
|
import { summarizeUndecidedSatisfaction } from './ingestion/scope-resolution/undecided-satisfaction.js';
|
|
21
22
|
import { summarizeScopeExtractionFailures } from './ingestion/scope-resolution/scope-extraction-failures.js';
|
|
22
23
|
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
23
|
-
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, ensureEmbeddingRowDmlSafe, ensureFtsRowDmlSafe, readIndexCatalogSnapshot, INDEX_CATALOG_UNREADABLE, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, deleteAllAdvisedBy, deleteSpringAopEvidenceNodes, deleteSpringAutoConfigurationDeclarations, deleteSpringAutoConfigurationSyntheticClasses, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
|
|
24
|
+
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, nodeTablesWithRowsForFiles, snapshotDerivedRelsForFiles, restoreDerivedRels, ensureEmbeddingRowDmlSafe, ensureFtsRowDmlSafe, readIndexCatalogSnapshot, INDEX_CATALOG_UNREADABLE, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, deleteAllAdvisedBy, deleteSpringAopEvidenceNodes, deleteSpringAutoConfigurationDeclarations, deleteSpringAutoConfigurationSyntheticClasses, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
|
|
24
25
|
import { estimateBufferPool, setBufferPoolSizeHint, resolveNativeSafeStorageDir, } from './lbug/lbug-config.js';
|
|
25
26
|
import { escapeCypherString } from './lbug/cypher-escape.js';
|
|
26
27
|
import { chunk } from '../lib/utils.js';
|
|
27
|
-
import { buildSearchIndexesOrDegrade, ftsFailureIsFatal, createSearchFTSIndexes, summarizeFtsIndexBuildFailures, dropSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
28
|
+
import { buildSearchIndexesOrDegrade, ftsFailureIsFatal, createSearchFTSIndexes, summarizeFtsIndexBuildFailures, dropSearchFTSIndexes, missingSearchFTSIndexTables, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
28
29
|
import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSearchFTSCjkSegmentation, } from './search/cjk-segmentation.js';
|
|
29
30
|
import { getExtensionCapability, getExtensionCapabilities, getFtsCapability, resolveAnalyzeInstallPolicy, } from './lbug/extension-loader.js';
|
|
30
31
|
import { diagnoseExtensionLoad } from './lbug/extension-load-error.js';
|
|
@@ -42,9 +43,13 @@ import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
|
|
|
42
43
|
import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
|
|
43
44
|
import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
|
|
44
45
|
import { shouldEscalateIncrementalWrite } from './incremental/escalation-gate.js';
|
|
46
|
+
import { ftsTablesAmong, incrementalFtsTablesFromGraph, nodeTablesForIncrementalDelete, shouldPreservePersistedDerivedGraph, } from './incremental/derived-writeback.js';
|
|
47
|
+
import { NODE_TABLES } from './lbug/schema.js';
|
|
45
48
|
import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, } from '../storage/parse-cache.js';
|
|
46
49
|
import { getDurableParsedFileDir, pruneAndSaveDurableParsedFileStore, } from '../storage/parsedfile-store.js';
|
|
47
|
-
import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, isWorkingTreeDirty, resolveRepoIdentityRoot, } from '../storage/git.js';
|
|
50
|
+
import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, isWorkingTreeDirty, listWorkingTreeDirtyPaths, resolveRepoIdentityRoot, } from '../storage/git.js';
|
|
51
|
+
import { isGitNexusManagedPath } from '../storage/gitnexus-managed-paths.js';
|
|
52
|
+
import { getMaxFileSizeBytes } from './ingestion/utils/max-file-size.js';
|
|
48
53
|
import { generateAIContextFiles } from '../cli/ai-context.js';
|
|
49
54
|
import { sanitizeDetectedBranch } from '../cli/analyze-config.js';
|
|
50
55
|
import { EMBEDDING_TABLE_NAME, EMBEDDING_DIMS, STALE_HASH_SENTINEL, SCHEMA_FINGERPRINT, schemaFingerprintMismatch, isSchemaFingerprintShaped, embeddingDimsMismatch, } from './lbug/schema.js';
|
|
@@ -149,6 +154,17 @@ function collectSpringBeanDeclarationDriftFiles(graph, persistedRows) {
|
|
|
149
154
|
}
|
|
150
155
|
return driftFiles;
|
|
151
156
|
}
|
|
157
|
+
const liveIndexMutationRisks = new WeakSet();
|
|
158
|
+
function recordLiveIndexMutationRisk(error) {
|
|
159
|
+
if ((typeof error === 'object' && error !== null) || typeof error === 'function') {
|
|
160
|
+
liveIndexMutationRisks.add(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Whether a failed analyze may already have changed the live DB. */
|
|
164
|
+
export function analyzeFailureMayHaveMutatedLiveIndex(error) {
|
|
165
|
+
return (((typeof error === 'object' && error !== null) || typeof error === 'function') &&
|
|
166
|
+
liveIndexMutationRisks.has(error));
|
|
167
|
+
}
|
|
152
168
|
/**
|
|
153
169
|
* Logged when the optional FTS extension cannot be loaded or installed during
|
|
154
170
|
* a full analyze. Kept as a named constant so the env-var/command guidance
|
|
@@ -784,6 +800,13 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
784
800
|
}
|
|
785
801
|
progress('fts', 90, 'Search indexes ready');
|
|
786
802
|
progress('done', 100, 'Done');
|
|
803
|
+
if (options.registryName) {
|
|
804
|
+
await registerRepo(repoPath, existingMeta, {
|
|
805
|
+
name: options.registryName,
|
|
806
|
+
allowDuplicateName: options.allowDuplicateName,
|
|
807
|
+
branch: placement.branch,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
787
810
|
return {
|
|
788
811
|
repoName: options.registryName ??
|
|
789
812
|
getInferredRepoName(repoPath) ??
|
|
@@ -1118,6 +1141,29 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1118
1141
|
// later read on a host where it loads — which is a legitimate, common
|
|
1119
1142
|
// state, and the invariant `analyzer-identity-cli.test.ts` pins.
|
|
1120
1143
|
if (!dirty && !healUnregistered) {
|
|
1144
|
+
if (options.registryName) {
|
|
1145
|
+
await registerRepo(repoPath, existingMeta, {
|
|
1146
|
+
name: options.registryName,
|
|
1147
|
+
allowDuplicateName: options.allowDuplicateName,
|
|
1148
|
+
branch: placement.branch,
|
|
1149
|
+
});
|
|
1150
|
+
if (!placement.branch) {
|
|
1151
|
+
try {
|
|
1152
|
+
await generateAIContextFiles(repoPath, storagePath, options.registryName, existingMeta.stats ?? {}, undefined, {
|
|
1153
|
+
skipAgentsMd: options.skipAgentsMd,
|
|
1154
|
+
skipSkills: options.skipSkills,
|
|
1155
|
+
noStats: options.noStats,
|
|
1156
|
+
defaultBranch: options.defaultBranch,
|
|
1157
|
+
// Fast path does not re-run PDG. Using `options.pdg` would
|
|
1158
|
+
// strip PDG bullets from AGENTS.md on a rename-only analyze.
|
|
1159
|
+
hasPdg: existingMeta.pdg != null,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
/* best-effort — never fail the fast path over a context refresh */
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1121
1167
|
// ── #2354: restamp the workspace label on a same-commit branch flip ──
|
|
1122
1168
|
// The flat slot follows the checked-out working tree; a branch switch
|
|
1123
1169
|
// at the SAME commit with a clean tree changes nothing the pipeline
|
|
@@ -1262,6 +1308,23 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1262
1308
|
// pipeline has already run by then; that run emits non-streamed, precisely as
|
|
1263
1309
|
// `resolveStreamPdgEmit` — read fresh at the same point — behaves.)
|
|
1264
1310
|
const streamGraphEmitActive = resolveStreamGraphEmit(options);
|
|
1311
|
+
// #3016: hold back Leiden and flow extraction when the persisted metadata
|
|
1312
|
+
// says this run is a candidate for a surgical incremental write, whose
|
|
1313
|
+
// derived layer is reused rather than recomputed. Deliberately the same
|
|
1314
|
+
// conditions as the `isIncremental` decision below MINUS the two that only
|
|
1315
|
+
// the pipeline can answer (the analysis-feature re-check and a non-empty
|
|
1316
|
+
// file list), so this is a superset: every run that turns out incremental
|
|
1317
|
+
// had the phases skipped, and the runs that do not are caught by
|
|
1318
|
+
// `runDeferredDerivedPhases` once the write plan is known. Excluded on the
|
|
1319
|
+
// streaming path because that is a full rebuild by construction, and the
|
|
1320
|
+
// deferred phases must not write into a finalized emit sink.
|
|
1321
|
+
const skipDerivedGraphPhases = !streamGraphEmitActive &&
|
|
1322
|
+
!options.force &&
|
|
1323
|
+
!!existingMeta &&
|
|
1324
|
+
!!existingMeta.fileHashes &&
|
|
1325
|
+
Object.keys(existingMeta.fileHashes).length > 0 &&
|
|
1326
|
+
repoHasGit &&
|
|
1327
|
+
!schemaFingerprintMismatch(existingMeta.schemaFingerprint);
|
|
1265
1328
|
// ── Phase 1: Full Pipeline (0–60%) ────────────────────────────────
|
|
1266
1329
|
const pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
|
|
1267
1330
|
const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
|
|
@@ -1301,6 +1364,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1301
1364
|
? resolveNativeSafeStorageDir(storagePath, 'graph-csv')
|
|
1302
1365
|
: undefined,
|
|
1303
1366
|
fetchWrappers: options.fetchWrappers,
|
|
1367
|
+
skipDerivedGraphPhases,
|
|
1304
1368
|
});
|
|
1305
1369
|
// ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
|
|
1306
1370
|
progress('lbug', 60, 'Loading into LadybugDB...');
|
|
@@ -1350,6 +1414,26 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1350
1414
|
const hashDiff = isIncremental
|
|
1351
1415
|
? diffFileHashes(newFileHashes, existingMeta.fileHashes)
|
|
1352
1416
|
: undefined;
|
|
1417
|
+
// #3016: `skipDerivedGraphPhases` was decided BEFORE the pipeline, from the
|
|
1418
|
+
// persisted metadata alone, so it can only ever be a bet that this run stays
|
|
1419
|
+
// surgical. Settle the bet here, where `isIncremental` and the deletion set
|
|
1420
|
+
// are both known, and pay it off by running the held-back phases whenever the
|
|
1421
|
+
// write plan needs a freshly derived layer:
|
|
1422
|
+
// - not incremental → full rebuild writes the whole graph, and a graph
|
|
1423
|
+
// with no Community/Process nodes would publish an
|
|
1424
|
+
// index with no communities and no flows;
|
|
1425
|
+
// - added/changed/deleted files → the persisted derived layer can miss new
|
|
1426
|
+
// symbols, keep stale memberships, or reference
|
|
1427
|
+
// removed ids. Only an empty file-hash diff is a
|
|
1428
|
+
// proof that Leiden/flows still match.
|
|
1429
|
+
const preserveDerivedLayer = skipDerivedGraphPhases &&
|
|
1430
|
+
isIncremental &&
|
|
1431
|
+
!!hashDiff &&
|
|
1432
|
+
shouldPreservePersistedDerivedGraph(hashDiff);
|
|
1433
|
+
if (skipDerivedGraphPhases && !preserveDerivedLayer) {
|
|
1434
|
+
progress('communities', 58, 'Detecting code communities and flows...');
|
|
1435
|
+
await pipelineResult.runDeferredDerivedPhases?.();
|
|
1436
|
+
}
|
|
1353
1437
|
// #2 atomic index publish: on a full rebuild, build the fresh DB at a temp
|
|
1354
1438
|
// path and swap it over the live index in one rename at the very end, so a
|
|
1355
1439
|
// concurrent MCP reader opening mid-build only ever sees the previous
|
|
@@ -1378,11 +1462,14 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1378
1462
|
const windowsSwapOk = process.platform === 'win32' &&
|
|
1379
1463
|
options.pdg !== true &&
|
|
1380
1464
|
process.env.GITNEXUS_ATOMIC_WINDOWS_SWAP === '1';
|
|
1381
|
-
// Incremental atomicity
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
const wantAtomicIncremental = isIncremental &&
|
|
1465
|
+
// Incremental atomicity stages the whole index before mutation. It remains
|
|
1466
|
+
// opt-in for ordinary analyze runs; watch mode requests it for failure
|
|
1467
|
+
// preservation. The copy requests a filesystem clone and records its actual
|
|
1468
|
+
// duration, while Node falls back to a normal copy where reflinks are absent.
|
|
1469
|
+
const wantAtomicIncremental = isIncremental &&
|
|
1470
|
+
!!hashDiff &&
|
|
1471
|
+
process.platform !== 'win32' &&
|
|
1472
|
+
(options.atomicIncremental === true || process.env.GITNEXUS_ATOMIC_INCREMENTAL === '1');
|
|
1386
1473
|
// #2614 F3: the copy-then-swap stages ONLY the main lbug file, so a live index
|
|
1387
1474
|
// carrying an orphan .wal/.shadow (a silently-failed prior checkpoint) would
|
|
1388
1475
|
// be copied incompletely and lose that delta. Only take the atomic path when
|
|
@@ -1399,6 +1486,10 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1399
1486
|
// valve. Nothing between here and there reads either binding except
|
|
1400
1487
|
// `initLbug(buildPath)`, which the upgrade re-runs against the staging path.
|
|
1401
1488
|
let useAtomicSwap = (isFullRebuild || atomicIncremental) && (posixSwap || windowsSwapOk);
|
|
1489
|
+
// Set only at the first operation that can mutate the live graph store.
|
|
1490
|
+
// Pre-write failures (config, lock, parsing, metadata, importer expansion)
|
|
1491
|
+
// remain retryable even when this platform cannot use an atomic swap.
|
|
1492
|
+
let liveIndexMutationStarted = false;
|
|
1402
1493
|
// #2658: a per-run staging name (was the fixed `lbug.new`). Even under the
|
|
1403
1494
|
// single-writer lock, a unique name means a crashed run's half-built staging
|
|
1404
1495
|
// file can never be mistaken for — or clobber — a live run's; the lock's
|
|
@@ -1426,10 +1517,13 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1426
1517
|
if (atomicIncremental) {
|
|
1427
1518
|
// Stage the live index into the temp so the in-place delete/writeback
|
|
1428
1519
|
// below mutates the COPY, and the end-of-run swap publishes it atomically.
|
|
1429
|
-
// Clear any stale temp first (a crashed run), then copy the
|
|
1430
|
-
// single-file
|
|
1520
|
+
// Clear any stale temp first (a crashed run), then clone/copy the
|
|
1521
|
+
// consolidated single-file live index.
|
|
1431
1522
|
await wipeLbugDbFiles(buildPath);
|
|
1432
|
-
|
|
1523
|
+
const copyStartedAt = Date.now();
|
|
1524
|
+
await fs.copyFile(lbugPath, buildPath, fsConstants.COPYFILE_FICLONE);
|
|
1525
|
+
log(`atomic-incremental: staged ${lbugPath} in ${Date.now() - copyStartedAt}ms ` +
|
|
1526
|
+
'(copy-on-write requested; filesystem fallback is allowed)');
|
|
1433
1527
|
}
|
|
1434
1528
|
}
|
|
1435
1529
|
else {
|
|
@@ -1466,7 +1560,16 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1466
1560
|
// (`buildPath` = `<lbugPath>.new`, clearing any stragglers from a crashed
|
|
1467
1561
|
// run) and leaves the live index untouched until the end-of-run swap. On
|
|
1468
1562
|
// Windows buildPath === lbugPath, so this is the original in-place wipe.
|
|
1469
|
-
|
|
1563
|
+
if (buildPath === lbugPath)
|
|
1564
|
+
liveIndexMutationStarted = true;
|
|
1565
|
+
try {
|
|
1566
|
+
await wipeLbugDbFiles(buildPath);
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
if (liveIndexMutationStarted)
|
|
1570
|
+
recordLiveIndexMutationRisk(error);
|
|
1571
|
+
throw error;
|
|
1572
|
+
}
|
|
1470
1573
|
}
|
|
1471
1574
|
// Size the buffer pool to the graph just built by the pipeline (a page cache
|
|
1472
1575
|
// over the on-disk index, which scales with node/edge count) instead of the
|
|
@@ -1483,7 +1586,14 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1483
1586
|
(pipelineResult.graphEmitManifest?.totalRows ?? 0)));
|
|
1484
1587
|
// Full rebuild (POSIX) builds into the temp `buildPath`; incremental and
|
|
1485
1588
|
// Windows use `buildPath === lbugPath` in place.
|
|
1486
|
-
|
|
1589
|
+
try {
|
|
1590
|
+
await initLbug(buildPath);
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
if (liveIndexMutationStarted)
|
|
1594
|
+
recordLiveIndexMutationRisk(error);
|
|
1595
|
+
throw error;
|
|
1596
|
+
}
|
|
1487
1597
|
// Manual WAL checkpoint driver (#1741): periodically drain the WAL
|
|
1488
1598
|
// from JS so the un-retriable native auto-checkpoint almost never
|
|
1489
1599
|
// has work left to do. Failures of the manual CHECKPOINT are absorbed
|
|
@@ -1506,6 +1616,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1506
1616
|
// "escalated full write" (DB wiped, index destroyed) — tri-review
|
|
1507
1617
|
// 4669518496 P1.
|
|
1508
1618
|
let escalatedFullWrite = false;
|
|
1619
|
+
let incrementalStats;
|
|
1509
1620
|
// Phase 3.5's restore scope (FIX 3 of this shipping review): on the
|
|
1510
1621
|
// SURGICAL write plan this is the exact file set whose rows
|
|
1511
1622
|
// deleteNodesForFiles just removed — only THOSE files' cached embedding
|
|
@@ -1521,6 +1632,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1521
1632
|
// collapse check compares the whole in-memory graph against the whole DB,
|
|
1522
1633
|
// which is only a like-for-like comparison on a full rebuild.
|
|
1523
1634
|
let wroteChangedSubgraphOnly = false;
|
|
1635
|
+
let incrementalFtsRebuildTables;
|
|
1524
1636
|
if (isIncremental && hashDiff) {
|
|
1525
1637
|
// ── Incremental DB writeback ───────────────────────────────────
|
|
1526
1638
|
// 0. Expand the writable set with transitive importers of
|
|
@@ -1624,6 +1736,13 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1624
1736
|
}
|
|
1625
1737
|
}
|
|
1626
1738
|
const importerExpansion = writableFiles.size - directlyChangedCount;
|
|
1739
|
+
incrementalStats = {
|
|
1740
|
+
changedFiles: hashDiff.changed.length + hashDiff.added.length + hashDiff.deleted.length,
|
|
1741
|
+
reparsedFiles: pipelineResult.reparsedFileCount,
|
|
1742
|
+
affectedDependents: importerExpansion,
|
|
1743
|
+
deletedFiles: hashDiff.deleted.length,
|
|
1744
|
+
writeMode: 'incremental',
|
|
1745
|
+
};
|
|
1627
1746
|
await saveIncrementalDirtyState('importer-bfs', {
|
|
1628
1747
|
importerExpansion,
|
|
1629
1748
|
shadowSeedCount: shadowSeed.length,
|
|
@@ -1776,6 +1895,14 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1776
1895
|
const sizeForcedRebuild = shouldEscalateIncrementalWrite(filesToDelete.length, effectiveWriteSet.size, allFilePaths.length);
|
|
1777
1896
|
if (extensionForcedRebuild || sizeForcedRebuild) {
|
|
1778
1897
|
escalatedFullWrite = true;
|
|
1898
|
+
// #3016: escalation converts this run into a wipe + full bulk COPY of
|
|
1899
|
+
// the in-memory graph, so the derived layer the skip was betting on
|
|
1900
|
+
// preserving has to exist in that graph after all. Same reasoning as
|
|
1901
|
+
// the not-incremental branch above, just discovered later.
|
|
1902
|
+
if (preserveDerivedLayer) {
|
|
1903
|
+
progress('communities', 63, 'Detecting code communities and flows...');
|
|
1904
|
+
await pipelineResult.runDeferredDerivedPhases?.();
|
|
1905
|
+
}
|
|
1779
1906
|
// Every live cause is named, not just the first: a DB can carry BOTH a
|
|
1780
1907
|
// vector index and FTS indexes, and reporting one cause while the other
|
|
1781
1908
|
// is equally fatal is how #2841 stayed mis-diagnosed for so long. §5.D:
|
|
@@ -1946,6 +2073,8 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1946
2073
|
}
|
|
1947
2074
|
await walCheckpointDriver.stop();
|
|
1948
2075
|
await closeLbug();
|
|
2076
|
+
if (buildPath === lbugPath)
|
|
2077
|
+
liveIndexMutationStarted = true;
|
|
1949
2078
|
await wipeLbugDbFiles(buildPath);
|
|
1950
2079
|
await initLbug(buildPath);
|
|
1951
2080
|
walCheckpointDriver = startWalCheckpointDriver();
|
|
@@ -1972,7 +2101,55 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1972
2101
|
// same connection, and nothing on this branch creates or drops an index
|
|
1973
2102
|
// in between — so re-reading would only weaken the one-read invariant
|
|
1974
2103
|
// the snapshot type exists to enforce.
|
|
1975
|
-
|
|
2104
|
+
if (buildPath === lbugPath)
|
|
2105
|
+
liveIndexMutationStarted = true;
|
|
2106
|
+
// FTS narrowing is independent of Leiden/flow reuse: even when this
|
|
2107
|
+
// run re-derives communities, Ladybug still cannot DML a live FTS
|
|
2108
|
+
// index (#2589), so only the tables this write set touches should
|
|
2109
|
+
// lose their index. The probe is a question about the DB rather than
|
|
2110
|
+
// the fresh graph — a symbol the edit DELETED is in no fresh graph
|
|
2111
|
+
// but is still a row that has to go.
|
|
2112
|
+
const tablesWithRows = await nodeTablesWithRowsForFiles(filesToDelete, NODE_TABLES);
|
|
2113
|
+
// Narrowing 1 — the FTS sweep, from "every configured index" to "the
|
|
2114
|
+
// indexes this run must touch". Three sources, and dropping any one of
|
|
2115
|
+
// them strands something:
|
|
2116
|
+
// - what the writeback DELETES (the probe above), because a symbol
|
|
2117
|
+
// the edit removed is in no fresh graph but is still a row;
|
|
2118
|
+
// - what it INSERTS (the fresh graph), because inserting under a live
|
|
2119
|
+
// FTS index is the same #2589 hazard as deleting under one;
|
|
2120
|
+
// - what is MISSING right now, because narrowing to the written
|
|
2121
|
+
// tables would otherwise leave keyword search degraded forever on
|
|
2122
|
+
// tables whose index a previous escalation dropped — the next full
|
|
2123
|
+
// rebuild would be the only thing that ever restored them.
|
|
2124
|
+
// An unreadable catalog proves nothing about that third set, so it
|
|
2125
|
+
// withdraws the narrowing entirely rather than guess.
|
|
2126
|
+
const missingFts = await missingSearchFTSIndexTables(indexCatalogRows);
|
|
2127
|
+
const touchedFts = missingFts
|
|
2128
|
+
? new Set([
|
|
2129
|
+
...ftsTablesAmong(tablesWithRows),
|
|
2130
|
+
...incrementalFtsTablesFromGraph(pipelineResult.graph, new Set(filesToDelete)),
|
|
2131
|
+
...missingFts,
|
|
2132
|
+
])
|
|
2133
|
+
: undefined;
|
|
2134
|
+
// Graph-wide Spring synthetic Class nodes are DETACH DELETEd on this
|
|
2135
|
+
// branch even when Class is not in the write set
|
|
2136
|
+
// (`deleteSpringAutoConfigurationSyntheticClasses`). Always include
|
|
2137
|
+
// Class so class_fts is not live across that DML (#2589), including
|
|
2138
|
+
// when the fresh graph no longer materializes the synthetics but the
|
|
2139
|
+
// DB still holds them.
|
|
2140
|
+
if (touchedFts) {
|
|
2141
|
+
touchedFts.add('Class');
|
|
2142
|
+
}
|
|
2143
|
+
incrementalFtsRebuildTables = touchedFts;
|
|
2144
|
+
// MEMBER_OF / STEP_IN_PROCESS / ENTRY_POINT_OF edges hang off the nodes
|
|
2145
|
+
// the DETACH DELETE below removes, so preserving the Community/Process
|
|
2146
|
+
// nodes preserves only half the layer unless these are reattached after
|
|
2147
|
+
// the subgraph write puts the member nodes back. Only the probed tables
|
|
2148
|
+
// can own such an edge, so they are the only ones worth scanning.
|
|
2149
|
+
const derivedSnapshot = preserveDerivedLayer
|
|
2150
|
+
? await snapshotDerivedRelsForFiles(filesToDelete, [...tablesWithRows])
|
|
2151
|
+
: [];
|
|
2152
|
+
await dropSearchFTSIndexes(indexCatalogRows, incrementalFtsRebuildTables);
|
|
1976
2153
|
// 1b. Remove the write set's existing rows — batched (#2409): one
|
|
1977
2154
|
// DETACH DELETE per table per 200-file chunk. The former per-file
|
|
1978
2155
|
// loop issued a count + delete per table per FILE — ~13k
|
|
@@ -1986,6 +2163,9 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1986
2163
|
progress('lbug', 62, `Removing rows for changed files (0/${filesToDelete.length})...`);
|
|
1987
2164
|
await deleteNodesForFiles(filesToDelete, {
|
|
1988
2165
|
onChunk: (done, total) => progress('lbug', 62, `Removing rows for changed files (${done}/${total})...`),
|
|
2166
|
+
nodeTables: incrementalFtsRebuildTables
|
|
2167
|
+
? nodeTablesForIncrementalDelete(NODE_TABLES, incrementalFtsRebuildTables)
|
|
2168
|
+
: undefined,
|
|
1989
2169
|
});
|
|
1990
2170
|
// Surgical path: Phase 3.5 restores exactly these files' embedding
|
|
1991
2171
|
// rows (FIX 3). Sound because deleteNodesForFiles propagates errors
|
|
@@ -1993,10 +2173,12 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
1993
2173
|
// deterministically — and this process holds the exclusive DB lock,
|
|
1994
2174
|
// so no concurrent writer can disturb the derivation.
|
|
1995
2175
|
deletedFilePathsForRestore = new Set(filesToDelete);
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2176
|
+
if (!preserveDerivedLayer) {
|
|
2177
|
+
// 2. Drop graph-wide nodes (Community, Process). They'll be re-inserted
|
|
2178
|
+
// from the fresh pipeline output below. Required for the
|
|
2179
|
+
// "Leiden runs on the FULL graph" correctness invariant.
|
|
2180
|
+
await deleteAllCommunitiesAndProcesses();
|
|
2181
|
+
}
|
|
2000
2182
|
// 2a. Drop INJECTS edges (DI collection injection, #2200) — their
|
|
2001
2183
|
// validity is a whole-program property (a third-file change to the
|
|
2002
2184
|
// interface or an implementer creates/invalidates edges between two
|
|
@@ -2043,7 +2225,9 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
2043
2225
|
// only that. Unchanged-file rows in the DB stay untouched. Pass
|
|
2044
2226
|
// the SAME effectiveWriteSet so the subgraph and the deletes
|
|
2045
2227
|
// cover identical files (asymmetry would silently corrupt).
|
|
2046
|
-
const subgraph = extractChangedSubgraph(pipelineResult.graph, effectiveWriteSet
|
|
2228
|
+
const subgraph = extractChangedSubgraph(pipelineResult.graph, effectiveWriteSet, {
|
|
2229
|
+
includeDerivedGraphWide: !preserveDerivedLayer,
|
|
2230
|
+
});
|
|
2047
2231
|
wroteChangedSubgraphOnly = true;
|
|
2048
2232
|
await saveIncrementalDirtyState('load-graph', {
|
|
2049
2233
|
importerExpansion,
|
|
@@ -2056,6 +2240,9 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
2056
2240
|
const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
|
|
2057
2241
|
progress('lbug', pct, msg);
|
|
2058
2242
|
});
|
|
2243
|
+
if (preserveDerivedLayer && derivedSnapshot.length > 0) {
|
|
2244
|
+
await restoreDerivedRels(derivedSnapshot);
|
|
2245
|
+
}
|
|
2059
2246
|
}
|
|
2060
2247
|
// Boundary drain (#2409): checkpoint at the end of the incremental
|
|
2061
2248
|
// writeback so the WAL it accumulated never lingers into the FTS and
|
|
@@ -2107,6 +2294,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
2107
2294
|
// pre-existing row (#2544/#2546) must not discard this run's otherwise-
|
|
2108
2295
|
// successful graph/embeddings work — only keyword search degrades.
|
|
2109
2296
|
const ftsResult = await buildSearchIndexesOrDegrade(executeQuery, {
|
|
2297
|
+
tables: incrementalFtsRebuildTables,
|
|
2110
2298
|
onIndexStart: options.verbose
|
|
2111
2299
|
? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
|
|
2112
2300
|
: undefined,
|
|
@@ -2751,8 +2939,9 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
2751
2939
|
files: pipelineResult.totalFileCount,
|
|
2752
2940
|
nodes: stats.nodes,
|
|
2753
2941
|
edges: stats.edges,
|
|
2754
|
-
communities: pipelineResult.communityResult?.stats.totalCommunities
|
|
2755
|
-
|
|
2942
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities ??
|
|
2943
|
+
existingMeta?.stats?.communities,
|
|
2944
|
+
processes: pipelineResult.processResult?.stats.totalProcesses ?? existingMeta?.stats?.processes,
|
|
2756
2945
|
embeddings: persistedEmbeddingCount,
|
|
2757
2946
|
},
|
|
2758
2947
|
capabilities: {
|
|
@@ -2828,6 +3017,12 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
2828
3017
|
// absence has exactly one meaning — an index older than the field.
|
|
2829
3018
|
embeddingDims: EMBEDDING_DIMS,
|
|
2830
3019
|
fileHashes: hasGitDir(repoPath) ? newFileHashesRecord : undefined,
|
|
3020
|
+
indexCoverage: hasGitDir(repoPath)
|
|
3021
|
+
? {
|
|
3022
|
+
maxFileSizeBytes: getMaxFileSizeBytes(),
|
|
3023
|
+
dirtyPaths: (listWorkingTreeDirtyPaths(repoPath) ?? Object.keys(newFileHashesRecord)).filter((rel) => newFileHashesRecord[rel] !== undefined && !isGitNexusManagedPath(rel)),
|
|
3024
|
+
}
|
|
3025
|
+
: undefined,
|
|
2831
3026
|
// This branch's full live chunk-key set (#2106 R6). `usedKeys` is every
|
|
2832
3027
|
// chunk hash touched in this scan — cache HITS included (see parse-impl
|
|
2833
3028
|
// usedKeys.add) — so it's complete even on an incremental run. Persisted
|
|
@@ -2956,9 +3151,10 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
2956
3151
|
files: pipelineResult.totalFileCount,
|
|
2957
3152
|
nodes: stats.nodes,
|
|
2958
3153
|
edges: stats.edges,
|
|
2959
|
-
communities: pipelineResult.communityResult?.stats.totalCommunities
|
|
3154
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities ??
|
|
3155
|
+
existingMeta?.stats?.communities,
|
|
2960
3156
|
clusters: aggregatedClusterCount,
|
|
2961
|
-
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
3157
|
+
processes: pipelineResult.processResult?.stats.totalProcesses ?? existingMeta?.stats?.processes,
|
|
2962
3158
|
}, undefined, {
|
|
2963
3159
|
skipAgentsMd: options.skipAgentsMd,
|
|
2964
3160
|
skipSkills: options.skipSkills,
|
|
@@ -3006,6 +3202,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
3006
3202
|
: false;
|
|
3007
3203
|
if (useAtomicSwap && builtDbExists) {
|
|
3008
3204
|
await retryRename(buildPath, lbugPath);
|
|
3205
|
+
liveIndexMutationStarted = true;
|
|
3009
3206
|
// Clear any sidecars orphaned beside the replaced file. A cleanly-closed
|
|
3010
3207
|
// prior index has none; a crashed one could, and it would be replay
|
|
3011
3208
|
// poison next to the freshly published index. Best-effort.
|
|
@@ -3037,6 +3234,12 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
3037
3234
|
ftsSkipped: !ftsReady,
|
|
3038
3235
|
ftsSkipReason: ftsReady ? undefined : ftsSkipReason,
|
|
3039
3236
|
isPrimaryBranch: !placement.branch,
|
|
3237
|
+
incrementalStats: incrementalStats
|
|
3238
|
+
? {
|
|
3239
|
+
...incrementalStats,
|
|
3240
|
+
writeMode: escalatedFullWrite ? 'full' : 'incremental',
|
|
3241
|
+
}
|
|
3242
|
+
: undefined,
|
|
3040
3243
|
};
|
|
3041
3244
|
}
|
|
3042
3245
|
catch (err) {
|
|
@@ -3079,6 +3282,11 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
3079
3282
|
/* swallow — orphan reclamation must never mask the real failure */
|
|
3080
3283
|
}
|
|
3081
3284
|
}
|
|
3285
|
+
if (liveIndexMutationStarted) {
|
|
3286
|
+
// Preserve the original error identity/prototype: callers distinguish
|
|
3287
|
+
// IndexLockTimeoutError and other domain failures with `instanceof`.
|
|
3288
|
+
recordLiveIndexMutationRisk(err);
|
|
3289
|
+
}
|
|
3082
3290
|
throw err;
|
|
3083
3291
|
}
|
|
3084
3292
|
}
|
|
@@ -59,6 +59,12 @@ export declare const SUPPORTED_FTS_STEMMERS: ReadonlySet<string>;
|
|
|
59
59
|
export interface CreateSearchFTSIndexesOptions {
|
|
60
60
|
onIndexStart?: (table: string, indexName: string) => void;
|
|
61
61
|
onIndexReady?: (table: string, indexName: string) => void;
|
|
62
|
+
/**
|
|
63
|
+
* When set, only these node-table names are dropped/rebuilt (#3016).
|
|
64
|
+
* Omit to rebuild every configured FTS index (full analyze / deleted-file
|
|
65
|
+
* incremental / `--repair-fts`).
|
|
66
|
+
*/
|
|
67
|
+
tables?: ReadonlySet<string>;
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Resolve + validate `GITNEXUS_FTS_STEMMER` once, up front at analyze startup,
|
|
@@ -98,7 +104,19 @@ export declare function getSearchFTSStemmer(): string;
|
|
|
98
104
|
* contract, and the same one-shared-`SHOW_INDEXES`-read purpose, as the gates in
|
|
99
105
|
* `lbug-adapter.ts`. Omit it to have the sweep read the catalog itself.
|
|
100
106
|
*/
|
|
101
|
-
export declare function dropSearchFTSIndexes(indexRows?: IndexCatalogSnapshot): Promise<void>;
|
|
107
|
+
export declare function dropSearchFTSIndexes(indexRows?: IndexCatalogSnapshot, tables?: ReadonlySet<string>): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* The configured FTS tables whose index the catalog proves is ABSENT right now.
|
|
110
|
+
*
|
|
111
|
+
* `undefined` means the catalog could not be read, which proves nothing — the
|
|
112
|
+
* same fail-closed reading the sweep above applies. Callers narrowing a rebuild
|
|
113
|
+
* to a subset of tables (#3016) must union this in, or must not narrow at all
|
|
114
|
+
* when it is `undefined`: a run that rebuilds only the tables it wrote leaves
|
|
115
|
+
* keyword search permanently degraded on every table whose index went missing
|
|
116
|
+
* earlier (a prior escalation drops all of them, and only the next full rebuild
|
|
117
|
+
* would ever put them back).
|
|
118
|
+
*/
|
|
119
|
+
export declare function missingSearchFTSIndexTables(indexRows?: IndexCatalogSnapshot): Promise<Set<string> | undefined>;
|
|
102
120
|
/** One configured index that could not be (re)built, and why. */
|
|
103
121
|
export interface FtsIndexBuildFailure {
|
|
104
122
|
table: string;
|
|
@@ -172,7 +172,7 @@ export function getSearchFTSStemmer() {
|
|
|
172
172
|
* contract, and the same one-shared-`SHOW_INDEXES`-read purpose, as the gates in
|
|
173
173
|
* `lbug-adapter.ts`. Omit it to have the sweep read the catalog itself.
|
|
174
174
|
*/
|
|
175
|
-
export async function dropSearchFTSIndexes(indexRows) {
|
|
175
|
+
export async function dropSearchFTSIndexes(indexRows, tables) {
|
|
176
176
|
// One catalog read for the whole sweep, decided PER CONFIGURED INDEX on
|
|
177
177
|
// IDENTITY (#2841 cleanup review). `undefined` = the catalog could not be
|
|
178
178
|
// read, which proves nothing — attempt every drop rather than skip a real one,
|
|
@@ -193,6 +193,8 @@ export async function dropSearchFTSIndexes(indexRows) {
|
|
|
193
193
|
// whether the sweep ran or not.
|
|
194
194
|
const rows = await resolveGateRows(indexRows);
|
|
195
195
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
196
|
+
if (tables && !tables.has(table))
|
|
197
|
+
continue;
|
|
196
198
|
// Skip only what the catalog POSITIVELY proves absent. Without this, a
|
|
197
199
|
// machine whose FTS extension cannot load, analyzing a DB that never carried
|
|
198
200
|
// an FTS index, pays one failed `CALL DROP_FTS_INDEX` per configured table on
|
|
@@ -209,6 +211,29 @@ export async function dropSearchFTSIndexes(indexRows) {
|
|
|
209
211
|
await dropFTSIndex(table, indexName);
|
|
210
212
|
}
|
|
211
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The configured FTS tables whose index the catalog proves is ABSENT right now.
|
|
216
|
+
*
|
|
217
|
+
* `undefined` means the catalog could not be read, which proves nothing — the
|
|
218
|
+
* same fail-closed reading the sweep above applies. Callers narrowing a rebuild
|
|
219
|
+
* to a subset of tables (#3016) must union this in, or must not narrow at all
|
|
220
|
+
* when it is `undefined`: a run that rebuilds only the tables it wrote leaves
|
|
221
|
+
* keyword search permanently degraded on every table whose index went missing
|
|
222
|
+
* earlier (a prior escalation drops all of them, and only the next full rebuild
|
|
223
|
+
* would ever put them back).
|
|
224
|
+
*/
|
|
225
|
+
export async function missingSearchFTSIndexTables(indexRows) {
|
|
226
|
+
const rows = await resolveGateRows(indexRows);
|
|
227
|
+
if (rows === undefined)
|
|
228
|
+
return undefined;
|
|
229
|
+
const missing = new Set();
|
|
230
|
+
for (const { table, indexName } of FTS_INDEXES) {
|
|
231
|
+
const present = rows.some((row) => indexRowTable(row) === table && indexRowName(row) === indexName);
|
|
232
|
+
if (!present)
|
|
233
|
+
missing.add(table);
|
|
234
|
+
}
|
|
235
|
+
return missing;
|
|
236
|
+
}
|
|
212
237
|
/**
|
|
213
238
|
* Build every configured FTS index, and keep going when one of them fails
|
|
214
239
|
* (#2889).
|
|
@@ -232,6 +257,8 @@ export async function createSearchFTSIndexes(options) {
|
|
|
232
257
|
const stemmer = getSearchFTSStemmer();
|
|
233
258
|
const failures = [];
|
|
234
259
|
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
260
|
+
if (options?.tables && !options.tables.has(table))
|
|
261
|
+
continue;
|
|
235
262
|
options?.onIndexStart?.(table, indexName);
|
|
236
263
|
// Drop first so the live `properties` always win. `createFTSIndex` is
|
|
237
264
|
// idempotent-by-name (skips when the index already exists), so without the
|
|
@@ -17,6 +17,7 @@ import { generateHTMLViewer } from './html-viewer.js';
|
|
|
17
17
|
import { sanitizeMermaidMarkdown } from './mermaid-sanitizer.js';
|
|
18
18
|
import { callLLM, estimateTokens, } from './llm-client.js';
|
|
19
19
|
import { callCursorLLM, resolveCursorConfig } from './cursor-client.js';
|
|
20
|
+
import { callGrokLLM, resolveGrokConfig } from './grok-client.js';
|
|
20
21
|
import { callClaudeLLM, callCodexLLM, callOpenCodeLLM, resolveLocalCLIConfig, } from './local-cli-client.js';
|
|
21
22
|
import { GROUPING_SYSTEM_PROMPT, GROUPING_USER_PROMPT, MODULE_SYSTEM_PROMPT, MODULE_USER_PROMPT, PARENT_SYSTEM_PROMPT, PARENT_USER_PROMPT, OVERVIEW_SYSTEM_PROMPT, OVERVIEW_USER_PROMPT, fillTemplate, formatFileListForGrouping, formatDirectoryTree, formatCallEdges, formatProcesses, } from './prompts.js';
|
|
22
23
|
import { shouldIgnorePath } from '../../config/ignore-service.js';
|
|
@@ -123,6 +124,13 @@ export class WikiGenerator {
|
|
|
123
124
|
});
|
|
124
125
|
return callCursorLLM(prompt, cursorConfig, systemPrompt, options);
|
|
125
126
|
}
|
|
127
|
+
if (this.llmConfig.provider === 'grok') {
|
|
128
|
+
const grokConfig = resolveGrokConfig({
|
|
129
|
+
model: this.llmConfig.model,
|
|
130
|
+
requestTimeoutMs: this.llmConfig.requestTimeoutMs,
|
|
131
|
+
});
|
|
132
|
+
return callGrokLLM(prompt, grokConfig, systemPrompt, options);
|
|
133
|
+
}
|
|
126
134
|
if (this.llmConfig.provider === 'claude' ||
|
|
127
135
|
this.llmConfig.provider === 'codex' ||
|
|
128
136
|
this.llmConfig.provider === 'opencode') {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build CLI client for wiki generation.
|
|
3
|
+
*
|
|
4
|
+
* Uses headless `grok --prompt-file` so large wiki prompts are not placed on
|
|
5
|
+
* argv or stdin (Grok does not read stdin as the prompt).
|
|
6
|
+
*/
|
|
7
|
+
import type { CallLLMOptions, LLMResponse } from './llm-client.js';
|
|
8
|
+
export interface GrokConfig {
|
|
9
|
+
model?: string;
|
|
10
|
+
requestTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Returns `'grok'` when the CLI is on PATH, else null. Cached. */
|
|
13
|
+
export declare function detectGrokCLI(): string | null;
|
|
14
|
+
export declare function resolveGrokConfig(overrides?: Partial<GrokConfig>): GrokConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Call Grok Build in headless mode and return the assistant text.
|
|
17
|
+
*
|
|
18
|
+
* `--cwd` is an empty temp directory (not the repo) so project AGENTS.md
|
|
19
|
+
* files are not injected into wiki generation.
|
|
20
|
+
*/
|
|
21
|
+
export declare function callGrokLLM(prompt: string, config: GrokConfig, systemPrompt?: string, options?: CallLLMOptions): Promise<LLMResponse>;
|