gitnexus 1.6.13-rc.16 → 1.6.13-rc.17

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 CHANGED
@@ -245,6 +245,8 @@ gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexu
245
245
  gitnexus analyze --skip-skills # Skip installing standard .claude/skills/gitnexus-* skill files
246
246
  gitnexus analyze --skip-git # Index folders that are not Git repositories
247
247
  gitnexus analyze --workers <n> # Parse worker pool size (>=1; default: cores-1, capped at 16)
248
+ gitnexus analyze --max-processes <n> # Process-detection process cap (replaces dynamic max(20, round(symbols/10)))
249
+ gitnexus analyze --max-entry-point-candidates <n> # Ranked entry-point pool (default 200; raise when the warning names it)
248
250
  gitnexus analyze --spring-actuator ./actuator # Enrich with local Spring Boot Actuator JSON snapshots
249
251
  gitnexus analyze --verbose # Log skipped files when parsers are unavailable
250
252
  gitnexus analyze --max-file-size 1024 # Skip files larger than N KB (default: 512, cap: 32768)
@@ -298,7 +300,8 @@ installation. Run a one-shot `gitnexus analyze` when those generated files need
298
300
  updating. Stop watch mode with Ctrl+C.
299
301
 
300
302
  Watch mode accepts `--debounce`, `--workers`, `--worker-timeout`,
301
- `--max-file-size`, `--branch`, `--pdg`, `--name`, `--allow-duplicate-name`, and
303
+ `--max-file-size`, `--max-processes`, `--max-process-branching`,
304
+ `--max-process-trace-depth`, `--max-entry-point-candidates`, `--branch`, `--pdg`, `--name`, `--allow-duplicate-name`, and
302
305
  `--verbose`. Explicit one-shot options such as `--force`, `--repair-fts`,
303
306
  embedding flags, `--skills`, `--default-branch`, `--skip-agents-md`,
304
307
  `--skip-skills`, `--no-stats`, `--self-commit`, `--index-only`, and `--skip-git`
@@ -764,6 +767,22 @@ npx gitnexus analyze
764
767
 
765
768
  Values above **32768 KB (32 MB)** are clamped to the tree-sitter parser ceiling; invalid values fall back to the 512 KB default with a one-time warning. When an override is active, `analyze` prints the effective threshold in its startup banner (e.g. `GITNEXUS_MAX_FILE_SIZE: effective threshold 2048KB (default 512KB)`).
766
769
 
770
+ ### Process detection reports missing flows
771
+
772
+ On a large repository, `analyze` may warn that `[processes] … whole flows are MISSING`. That means ranked entry points or completed flows were sampled away by the analyze-time detection budget — not that the code path is absent, and not the query-time `IMPACT_MAX_CHUNKS` cap.
773
+
774
+ Defaults stay in place when nothing is set: dynamic `maxProcesses = max(20, round(non-File symbols / 10))`, branching `4`, trace depth `10`, entry-point candidate pool `200`. Raise a knob only when the warning names it:
775
+
776
+ ```bash
777
+ # Usual first move when entryPointCandidatesDropped is the loud counter
778
+ npx gitnexus analyze --max-entry-point-candidates 400
779
+
780
+ # When ranked entry points were never traced, or flows were dropped at maxProcesses
781
+ npx gitnexus analyze --max-processes 80
782
+ ```
783
+
784
+ Equivalent `.gitnexusrc` keys: `maxProcesses`, `maxProcessBranching`, `maxProcessTraceDepth`, `maxEntryPointCandidates`. Equivalent env vars: `GITNEXUS_MAX_PROCESSES`, `GITNEXUS_MAX_PROCESS_BRANCHING`, `GITNEXUS_MAX_PROCESS_TRACE_DEPTH`, `GITNEXUS_MAX_ENTRY_POINT_CANDIDATES`. Precedence is CLI > `.gitnexusrc` > env > default. `0` is invalid, not unlimited. Changing these knobs re-runs process detection on the next `analyze` without `--force`. Raising them increases CPU and memory; this is not a heap-OOM fix.
785
+
767
786
  ### Analyze reports a worker timeout
768
787
 
769
788
  Worker parse timeouts are recoverable. GitNexus retries stalled worker jobs with backoff, splits large jobs to isolate slow files, and quarantines a file that repeatedly crashes its worker (respawning the slot so the pool keeps going). If a large repository needs more time per worker job, use either:
@@ -76,6 +76,10 @@ const KEY_SPECS = {
76
76
  workerTimeout: { target: 'workerTimeout', kind: 'numeric-string' },
77
77
  walCheckpointThreshold: { target: 'walCheckpointThreshold', kind: 'numeric-string' },
78
78
  workers: { target: 'workers', kind: 'numeric-string' },
79
+ maxProcesses: { target: 'maxProcesses', kind: 'numeric-string' },
80
+ maxProcessBranching: { target: 'maxProcessBranching', kind: 'numeric-string' },
81
+ maxProcessTraceDepth: { target: 'maxProcessTraceDepth', kind: 'numeric-string' },
82
+ maxEntryPointCandidates: { target: 'maxEntryPointCandidates', kind: 'numeric-string' },
79
83
  embeddingThreads: { target: 'embeddingThreads', kind: 'numeric-string' },
80
84
  embeddingBatchSize: { target: 'embeddingBatchSize', kind: 'numeric-string' },
81
85
  embeddingSubBatchSize: { target: 'embeddingSubBatchSize', kind: 'numeric-string' },
@@ -115,6 +115,14 @@ export interface AnalyzeOptions {
115
115
  walCheckpointThreshold?: string;
116
116
  /** Parse worker pool size (>=1); 0 is rejected (no sequential mode). */
117
117
  workers?: string;
118
+ /** Process-detection process cap. Positive integer string; `0` is invalid. */
119
+ maxProcesses?: string;
120
+ /** Process-detection per-node branching cap. Positive integer string. */
121
+ maxProcessBranching?: string;
122
+ /** Process-detection DFS depth cap. Positive integer string. */
123
+ maxProcessTraceDepth?: string;
124
+ /** Ranked entry-point candidate pool. Positive integer string. */
125
+ maxEntryPointCandidates?: string;
118
126
  embeddingThreads?: string;
119
127
  embeddingBatchSize?: string;
120
128
  embeddingSubBatchSize?: string;
@@ -11,6 +11,7 @@ import { GITNEXUS_DIR } from '../storage/repo-meta.js';
11
11
  import { loadAnalyzeConfigStrict, mergeAnalyzeOptions, validateBranchName, } from './analyze-config.js';
12
12
  import { ensureHeap } from './analyze.js';
13
13
  import { cliError, cliInfo, cliWarn } from './cli-message.js';
14
+ import { formatInvalidProcessDetectionOverride, parseProcessDetectionBudgetStrings, } from '../core/ingestion/process-detection-budget.js';
14
15
  import { WATCH_FULL_REFRESH_PATH, WatchRefreshQueue, } from './watch-queue.js';
15
16
  const DEFAULT_DEBOUNCE_MS = 300;
16
17
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
@@ -119,6 +120,14 @@ export async function resolveWatchOptions(repoPath, cli, baseline, reportIgnored
119
120
  const workerPoolSize = positiveInteger(merged.workers, '--workers');
120
121
  const workerTimeoutSeconds = positiveInteger(merged.workerTimeout, 'workerTimeout');
121
122
  const maxFileSize = positiveInteger(merged.maxFileSize, 'maxFileSize', MAX_FILE_SIZE_KB);
123
+ const processDetection = parseProcessDetectionBudgetStrings({
124
+ maxProcesses: merged.maxProcesses,
125
+ maxProcessBranching: merged.maxProcessBranching,
126
+ maxProcessTraceDepth: merged.maxProcessTraceDepth,
127
+ maxEntryPointCandidates: merged.maxEntryPointCandidates,
128
+ }, (flag, raw) => {
129
+ cliWarn(formatInvalidProcessDetectionOverride(flag, raw));
130
+ });
122
131
  setEnvironment('GITNEXUS_MAX_FILE_SIZE', maxFileSize === undefined ? baseline.maxFileSize : String(maxFileSize));
123
132
  if (workerTimeoutSeconds !== undefined) {
124
133
  process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS = String(workerTimeoutSeconds * 1000);
@@ -134,6 +143,10 @@ export async function resolveWatchOptions(repoPath, cli, baseline, reportIgnored
134
143
  registryName: merged.name,
135
144
  allowDuplicateName: merged.allowDuplicateName,
136
145
  workerPoolSize,
146
+ maxProcesses: processDetection.maxProcesses,
147
+ maxProcessBranching: processDetection.maxProcessBranching,
148
+ maxProcessTraceDepth: processDetection.maxProcessTraceDepth,
149
+ maxEntryPointCandidates: processDetection.maxEntryPointCandidates,
137
150
  fetchWrappers: merged.fetchWrappers,
138
151
  skipAgentsMd: true,
139
152
  skipSkills: true,
@@ -25,6 +25,7 @@ import { loadAnalyzeConfig, mergeAnalyzeOptions, resolveDefaultBranch, validateB
25
25
  import { runFullAnalysis } from '../core/run-analyze.js';
26
26
  import { getRuntimeFingerprint } from '../core/platform/capabilities.js';
27
27
  import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
28
+ import { formatInvalidProcessDetectionOverride, formatProcessDetectionBudgetBanner, parseProcessDetectionBudgetStrings, resolveProcessDetectionBudget, } from '../core/ingestion/process-detection-budget.js';
28
29
  import { warnMissingOptionalGrammars, getOptionalGrammarExtensions } from './optional-grammars.js';
29
30
  import { glob } from 'glob';
30
31
  import fs from 'fs/promises';
@@ -753,6 +754,14 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
753
754
  }
754
755
  workerPoolSize = parsedWorkers;
755
756
  }
757
+ const processDetectionFromFlags = parseProcessDetectionBudgetStrings({
758
+ maxProcesses: options.maxProcesses,
759
+ maxProcessBranching: options.maxProcessBranching,
760
+ maxProcessTraceDepth: options.maxProcessTraceDepth,
761
+ maxEntryPointCandidates: options.maxEntryPointCandidates,
762
+ }, (flag, raw) => {
763
+ cliWarn(` ${formatInvalidProcessDetectionOverride(flag, raw)}\n`);
764
+ });
756
765
  // Parse `--embeddings [limit]`: `true` → default cap, string → numeric cap
757
766
  // (0 disables the cap entirely). Validated up here so failures match the
758
767
  // sibling-validation pattern (exit before bar.start() — otherwise
@@ -987,6 +996,10 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
987
996
  if (maxFileSizeBanner) {
988
997
  console.log(`${maxFileSizeBanner}\n`);
989
998
  }
999
+ const processDetectionBanner = formatProcessDetectionBudgetBanner(resolveProcessDetectionBudget(processDetectionFromFlags));
1000
+ if (processDetectionBanner) {
1001
+ console.log(`${processDetectionBanner}\n`);
1002
+ }
990
1003
  // ── CLI progress bar setup ─────────────────────────────────────────
991
1004
  const barOptions = {
992
1005
  format: ' {bar} {percentage}% | {phase}',
@@ -1119,6 +1132,10 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
1119
1132
  // GITNEXUS_WORKER_POOL_SIZE env mutation. `undefined` defers to the
1120
1133
  // env / auto-formula fallback inside the pipeline.
1121
1134
  workerPoolSize,
1135
+ maxProcesses: processDetectionFromFlags.maxProcesses,
1136
+ maxProcessBranching: processDetectionFromFlags.maxProcessBranching,
1137
+ maxProcessTraceDepth: processDetectionFromFlags.maxProcessTraceDepth,
1138
+ maxEntryPointCandidates: processDetectionFromFlags.maxEntryPointCandidates,
1122
1139
  // Extra fetch-wrapper names from `.gitnexusrc` (#1589/#1852 residual);
1123
1140
  // forwarded to the routes phase consumer scan.
1124
1141
  fetchWrappers: options.fetchWrappers,
@@ -68,6 +68,10 @@ const OPTION_DESCRIPTION_KEYS = {
68
68
  'analyze|--worker-timeout <seconds>': 'help.option.analyze.workerTimeout',
69
69
  'analyze|--wal-checkpoint-threshold <bytes>': 'help.option.analyze.walCheckpointThreshold',
70
70
  'analyze|--workers <n>': 'help.option.analyze.workers',
71
+ 'analyze|--max-processes <n>': 'help.option.analyze.maxProcesses',
72
+ 'analyze|--max-process-branching <n>': 'help.option.analyze.maxProcessBranching',
73
+ 'analyze|--max-process-trace-depth <n>': 'help.option.analyze.maxProcessTraceDepth',
74
+ 'analyze|--max-entry-point-candidates <n>': 'help.option.analyze.maxEntryPointCandidates',
71
75
  'analyze|--embedding-threads <n>': 'help.option.analyze.embeddingThreads',
72
76
  'analyze|--embedding-batch-size <n>': 'help.option.analyze.embeddingBatchSize',
73
77
  'analyze|--embedding-sub-batch-size <n>': 'help.option.analyze.embeddingSubBatchSize',
@@ -193,6 +193,10 @@ export declare const en: {
193
193
  readonly 'help.option.analyze.workerTimeout': 'Worker sub-batch idle timeout before retry/fallback. Default: 30.';
194
194
  readonly 'help.option.analyze.walCheckpointThreshold': 'LadybugDB WAL auto-checkpoint threshold in bytes during analyze (integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).';
195
195
  readonly 'help.option.analyze.workers': 'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.';
196
+ readonly 'help.option.analyze.maxProcesses': 'Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Default: dynamic.';
197
+ readonly 'help.option.analyze.maxProcessBranching': 'Process-detection per-node branching cap (positive integer). Default: 4.';
198
+ readonly 'help.option.analyze.maxProcessTraceDepth': 'Process-detection DFS depth cap (positive integer). Default: 10.';
199
+ readonly 'help.option.analyze.maxEntryPointCandidates': 'Ranked entry-point candidate pool (positive integer). Default: 200. Raise when the warning names this knob; doubling is the usual first raise.';
196
200
  readonly 'help.option.analyze.embeddingThreads': 'Limit local ONNX embedding CPU threads';
197
201
  readonly 'help.option.analyze.embeddingBatchSize': 'Number of nodes per embedding batch';
198
202
  readonly 'help.option.analyze.embeddingSubBatchSize': 'Number of chunks per embedding model call';
@@ -278,5 +282,5 @@ export declare const en: {
278
282
  readonly 'help.option.group.contracts.repo': 'Filter by repo';
279
283
  readonly 'help.option.group.contracts.unmatched': 'Show only unmatched contracts';
280
284
  readonly 'help.identityCache.environment': '\nAnalyzer identity cache:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n Operator-trusted persistent cache for warm cross-process status. The directory must pre-exist, be outside the GitNexus package/build roots, and contain no symlink or junction components. Defaults remain fail-closed on platforms without POSIX ownership APIs.';
281
- readonly 'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_STORAGE_PATH=/absolute/index Complete external index directory. Preserves the existing configuration semantics and overrides GITNEXUS_STORAGE_ROOT when both are set.\n GITNEXUS_STORAGE_ROOT=/absolute/root External index root; each repository uses an isolated <repo-basename>-<canonical-path-hash>/ slot.\n GITNEXUS_CONTENT_RETENTION=full Source-text retention profile: full, symbol, or none. Default full.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).';
285
+ readonly 'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_STORAGE_PATH=/absolute/index Complete external index directory. Preserves the existing configuration semantics and overrides GITNEXUS_STORAGE_ROOT when both are set.\n GITNEXUS_STORAGE_ROOT=/absolute/root External index root; each repository uses an isolated <repo-basename>-<canonical-path-hash>/ slot.\n GITNEXUS_CONTENT_RETENTION=full Source-text retention profile: full, symbol, or none. Default full.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n GITNEXUS_MAX_PROCESSES=N Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Distinct from query-time IMPACT_MAX_CHUNKS.\n GITNEXUS_MAX_PROCESS_BRANCHING=N Process-detection per-node branching cap. Default 4.\n GITNEXUS_MAX_PROCESS_TRACE_DEPTH=N Process-detection DFS depth cap. Default 10.\n GITNEXUS_MAX_ENTRY_POINT_CANDIDATES=N Ranked entry-point candidate pool. Default 200. Raise when the warning names this knob; doubling is the usual first raise.\n\nCLI flags take precedence over `.gitnexusrc`, which takes precedence over env vars, which take precedence over built-in defaults.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).';
282
286
  };
@@ -195,6 +195,10 @@ export const en = {
195
195
  'help.option.analyze.workerTimeout': 'Worker sub-batch idle timeout before retry/fallback. Default: 30.',
196
196
  'help.option.analyze.walCheckpointThreshold': 'LadybugDB WAL auto-checkpoint threshold in bytes during analyze (integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).',
197
197
  'help.option.analyze.workers': 'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.',
198
+ 'help.option.analyze.maxProcesses': 'Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Default: dynamic.',
199
+ 'help.option.analyze.maxProcessBranching': 'Process-detection per-node branching cap (positive integer). Default: 4.',
200
+ 'help.option.analyze.maxProcessTraceDepth': 'Process-detection DFS depth cap (positive integer). Default: 10.',
201
+ 'help.option.analyze.maxEntryPointCandidates': 'Ranked entry-point candidate pool (positive integer). Default: 200. Raise when the warning names this knob; doubling is the usual first raise.',
198
202
  'help.option.analyze.embeddingThreads': 'Limit local ONNX embedding CPU threads',
199
203
  'help.option.analyze.embeddingBatchSize': 'Number of nodes per embedding batch',
200
204
  'help.option.analyze.embeddingSubBatchSize': 'Number of chunks per embedding model call',
@@ -280,5 +284,5 @@ export const en = {
280
284
  'help.option.group.contracts.repo': 'Filter by repo',
281
285
  'help.option.group.contracts.unmatched': 'Show only unmatched contracts',
282
286
  'help.identityCache.environment': '\nAnalyzer identity cache:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n Operator-trusted persistent cache for warm cross-process status. The directory must pre-exist, be outside the GitNexus package/build roots, and contain no symlink or junction components. Defaults remain fail-closed on platforms without POSIX ownership APIs.',
283
- 'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_STORAGE_PATH=/absolute/index Complete external index directory. Preserves the existing configuration semantics and overrides GITNEXUS_STORAGE_ROOT when both are set.\n GITNEXUS_STORAGE_ROOT=/absolute/root External index root; each repository uses an isolated <repo-basename>-<canonical-path-hash>/ slot.\n GITNEXUS_CONTENT_RETENTION=full Source-text retention profile: full, symbol, or none. Default full.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).',
287
+ 'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_STORAGE_PATH=/absolute/index Complete external index directory. Preserves the existing configuration semantics and overrides GITNEXUS_STORAGE_ROOT when both are set.\n GITNEXUS_STORAGE_ROOT=/absolute/root External index root; each repository uses an isolated <repo-basename>-<canonical-path-hash>/ slot.\n GITNEXUS_CONTENT_RETENTION=full Source-text retention profile: full, symbol, or none. Default full.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n GITNEXUS_MAX_PROCESSES=N Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Distinct from query-time IMPACT_MAX_CHUNKS.\n GITNEXUS_MAX_PROCESS_BRANCHING=N Process-detection per-node branching cap. Default 4.\n GITNEXUS_MAX_PROCESS_TRACE_DEPTH=N Process-detection DFS depth cap. Default 10.\n GITNEXUS_MAX_ENTRY_POINT_CANDIDATES=N Ranked entry-point candidate pool. Default 200. Raise when the warning names this knob; doubling is the usual first raise.\n\nCLI flags take precedence over `.gitnexusrc`, which takes precedence over env vars, which take precedence over built-in defaults.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).',
284
288
  };
@@ -194,6 +194,10 @@ export declare const cliResources: {
194
194
  readonly 'help.option.analyze.workerTimeout': 'Worker sub-batch idle timeout before retry/fallback. Default: 30.';
195
195
  readonly 'help.option.analyze.walCheckpointThreshold': 'LadybugDB WAL auto-checkpoint threshold in bytes during analyze (integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).';
196
196
  readonly 'help.option.analyze.workers': 'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.';
197
+ readonly 'help.option.analyze.maxProcesses': 'Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Default: dynamic.';
198
+ readonly 'help.option.analyze.maxProcessBranching': 'Process-detection per-node branching cap (positive integer). Default: 4.';
199
+ readonly 'help.option.analyze.maxProcessTraceDepth': 'Process-detection DFS depth cap (positive integer). Default: 10.';
200
+ readonly 'help.option.analyze.maxEntryPointCandidates': 'Ranked entry-point candidate pool (positive integer). Default: 200. Raise when the warning names this knob; doubling is the usual first raise.';
197
201
  readonly 'help.option.analyze.embeddingThreads': 'Limit local ONNX embedding CPU threads';
198
202
  readonly 'help.option.analyze.embeddingBatchSize': 'Number of nodes per embedding batch';
199
203
  readonly 'help.option.analyze.embeddingSubBatchSize': 'Number of chunks per embedding model call';
@@ -279,7 +283,7 @@ export declare const cliResources: {
279
283
  readonly 'help.option.group.contracts.repo': 'Filter by repo';
280
284
  readonly 'help.option.group.contracts.unmatched': 'Show only unmatched contracts';
281
285
  readonly 'help.identityCache.environment': '\nAnalyzer identity cache:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n Operator-trusted persistent cache for warm cross-process status. The directory must pre-exist, be outside the GitNexus package/build roots, and contain no symlink or junction components. Defaults remain fail-closed on platforms without POSIX ownership APIs.';
282
- readonly 'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_STORAGE_PATH=/absolute/index Complete external index directory. Preserves the existing configuration semantics and overrides GITNEXUS_STORAGE_ROOT when both are set.\n GITNEXUS_STORAGE_ROOT=/absolute/root External index root; each repository uses an isolated <repo-basename>-<canonical-path-hash>/ slot.\n GITNEXUS_CONTENT_RETENTION=full Source-text retention profile: full, symbol, or none. Default full.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).';
286
+ readonly 'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_STORAGE_PATH=/absolute/index Complete external index directory. Preserves the existing configuration semantics and overrides GITNEXUS_STORAGE_ROOT when both are set.\n GITNEXUS_STORAGE_ROOT=/absolute/root External index root; each repository uses an isolated <repo-basename>-<canonical-path-hash>/ slot.\n GITNEXUS_CONTENT_RETENTION=full Source-text retention profile: full, symbol, or none. Default full.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n GITNEXUS_MAX_PROCESSES=N Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Distinct from query-time IMPACT_MAX_CHUNKS.\n GITNEXUS_MAX_PROCESS_BRANCHING=N Process-detection per-node branching cap. Default 4.\n GITNEXUS_MAX_PROCESS_TRACE_DEPTH=N Process-detection DFS depth cap. Default 10.\n GITNEXUS_MAX_ENTRY_POINT_CANDIDATES=N Ranked entry-point candidate pool. Default 200. Raise when the warning names this knob; doubling is the usual first raise.\n\nCLI flags take precedence over `.gitnexusrc`, which takes precedence over env vars, which take precedence over built-in defaults.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).';
283
287
  };
284
288
  readonly 'zh-CN': {
285
289
  'common.notIndexed': string;
@@ -476,6 +480,10 @@ export declare const cliResources: {
476
480
  'help.option.analyze.workerTimeout': string;
477
481
  'help.option.analyze.walCheckpointThreshold': string;
478
482
  'help.option.analyze.workers': string;
483
+ 'help.option.analyze.maxProcesses': string;
484
+ 'help.option.analyze.maxProcessBranching': string;
485
+ 'help.option.analyze.maxProcessTraceDepth': string;
486
+ 'help.option.analyze.maxEntryPointCandidates': string;
479
487
  'help.option.analyze.embeddingThreads': string;
480
488
  'help.option.analyze.embeddingBatchSize': string;
481
489
  'help.option.analyze.embeddingSubBatchSize': string;
@@ -193,6 +193,10 @@ export declare const zhCN: {
193
193
  'help.option.analyze.workerTimeout': string;
194
194
  'help.option.analyze.walCheckpointThreshold': string;
195
195
  'help.option.analyze.workers': string;
196
+ 'help.option.analyze.maxProcesses': string;
197
+ 'help.option.analyze.maxProcessBranching': string;
198
+ 'help.option.analyze.maxProcessTraceDepth': string;
199
+ 'help.option.analyze.maxEntryPointCandidates': string;
196
200
  'help.option.analyze.embeddingThreads': string;
197
201
  'help.option.analyze.embeddingBatchSize': string;
198
202
  'help.option.analyze.embeddingSubBatchSize': string;
@@ -193,6 +193,10 @@ export const zhCN = {
193
193
  'help.option.analyze.workerTimeout': 'Worker 子批次空闲超时,超时后重试/回退。默认:30。',
194
194
  'help.option.analyze.walCheckpointThreshold': 'analyze 期间 LadybugDB WAL 自动 checkpoint 阈值(字节,整数 >= -1;默认:67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。',
195
195
  'help.option.analyze.workers': '解析 worker 池大小(>=1)。默认:cores-1,最多 16,按仓库规模自适应。',
196
+ 'help.option.analyze.maxProcesses': '流程检测的流程数量上限(正整数)。覆盖动态的 max(20, round(symbols/10)) 公式。默认:动态。',
197
+ 'help.option.analyze.maxProcessBranching': '流程检测的单节点分支上限(正整数)。默认:4。',
198
+ 'help.option.analyze.maxProcessTraceDepth': '流程检测的 DFS 深度上限(正整数)。默认:10。',
199
+ 'help.option.analyze.maxEntryPointCandidates': '排序后的入口点候选池(正整数)。默认:200。仅在警告点名该上限时提高;那时通常先翻倍。',
196
200
  'help.option.analyze.embeddingThreads': '限制本地 ONNX 嵌入 CPU 线程数',
197
201
  'help.option.analyze.embeddingBatchSize': '每个嵌入批次的节点数',
198
202
  'help.option.analyze.embeddingSubBatchSize': '每次嵌入模型调用的分块数',
@@ -278,5 +282,5 @@ export const zhCN = {
278
282
  'help.option.group.contracts.repo': '按仓库过滤',
279
283
  'help.option.group.contracts.unmatched': '仅显示未匹配契约',
280
284
  'help.identityCache.environment': '\n分析器身份缓存:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n 由操作员明确信任的持久缓存,用于跨进程快速查询状态。目录必须预先存在、位于 GitNexus 包/构建根目录之外,且路径中不得包含符号链接或 junction。缺少 POSIX 所有权 API 的平台默认保持故障关闭。',
281
- 'help.analyze.environment': '\n环境变量:\n GITNEXUS_NO_GITIGNORE=1 跳过 .gitignore 解析(仍读取 .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N 覆盖大文件跳过阈值(KB)。默认 512,最大 32768。\n GITNEXUS_STORAGE_PATH=/absolute/index 完整外部索引目录。保留既有配置语义;与 GITNEXUS_STORAGE_ROOT 同时设置时优先使用。\n GITNEXUS_STORAGE_ROOT=/absolute/root 外部索引根目录;每个仓库使用独立的 <仓库名>-<规范路径哈希>/ 子目录。\n GITNEXUS_CONTENT_RETENTION=full 源码文本保留策略:full、symbol 或 none。默认 full。\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir 由操作员明确信任的持久分析器身份缓存;目录必须预先存在、位于包/构建根目录之外,且路径中不得包含符号链接或 junction。\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker 空闲超时(毫秒)。默认 30000。\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL 自动 checkpoint 阈值(字节,默认 67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker 作业字节预算。默认 8388608。\n GITNEXUS_WORKER_POOL_SIZE=N 解析 worker 数量覆盖值。默认 cores-1,最多 16。\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N 并发进行中的解析分块数。默认 2。\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N 每个 slot 丢弃前允许的最大替换进程数。默认 3。\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N 每个作业的总重试墙钟时间。默认 5 倍子批次超时。\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N 每个 slot 触发熔断的死亡次数。默认 max(3, poolSize)。\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N 线程池关闭时等待仍在原生代码中的已退役 worker 的最长时间(到达安全点后再终止,避免进程级 abort)。默认 30000。\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N C++ 捕获提取的每文件墙钟预算;超出后该文件保留部分捕获并输出警告。默认 20000。\n GITNEXUS_EMBEDDING_THREADS=N 限制 --embeddings 的本地 ONNX CPU 线程数。\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 将单次 HTTP 嵌入超时纳入 GITNEXUS_EMBEDDING_MAX_ATTEMPTS 重试(默认关闭,超时仍为终止错误)。\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N exact-scan 回退的最大嵌入分块数。默认 10000。\n GITNEXUS_VECTOR_MAX_DISTANCE=N 语义/向量搜索接受的最大余弦距离(0 < N <= 2;超出则钳制为 2)。MCP 默认 0.6,其他路径默认 0.5。\n\n当参数和对应环境变量同时提供时,参数优先。\n\n提示:`.gitnexusignore` 支持 `.gitignore` 风格的取反。比如添加\n `!__tests__/` 可以索引默认自动过滤的目录(#771)。',
285
+ 'help.analyze.environment': '\n环境变量:\n GITNEXUS_NO_GITIGNORE=1 跳过 .gitignore 解析(仍读取 .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N 覆盖大文件跳过阈值(KB)。默认 512,最大 32768。\n GITNEXUS_STORAGE_PATH=/absolute/index 完整外部索引目录。保留既有配置语义;与 GITNEXUS_STORAGE_ROOT 同时设置时优先使用。\n GITNEXUS_STORAGE_ROOT=/absolute/root 外部索引根目录;每个仓库使用独立的 <仓库名>-<规范路径哈希>/ 子目录。\n GITNEXUS_CONTENT_RETENTION=full 源码文本保留策略:full、symbol 或 none。默认 full。\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir 由操作员明确信任的持久分析器身份缓存;目录必须预先存在、位于包/构建根目录之外,且路径中不得包含符号链接或 junction。\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker 空闲超时(毫秒)。默认 30000。\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL 自动 checkpoint 阈值(字节,默认 67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker 作业字节预算。默认 8388608。\n GITNEXUS_WORKER_POOL_SIZE=N 解析 worker 数量覆盖值。默认 cores-1,最多 16。\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N 并发进行中的解析分块数。默认 2。\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N 每个 slot 丢弃前允许的最大替换进程数。默认 3。\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N 每个作业的总重试墙钟时间。默认 5 倍子批次超时。\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N 每个 slot 触发熔断的死亡次数。默认 max(3, poolSize)。\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N 线程池关闭时等待仍在原生代码中的已退役 worker 的最长时间(到达安全点后再终止,避免进程级 abort)。默认 30000。\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N C++ 捕获提取的每文件墙钟预算;超出后该文件保留部分捕获并输出警告。默认 20000。\n GITNEXUS_EMBEDDING_THREADS=N 限制 --embeddings 的本地 ONNX CPU 线程数。\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 将单次 HTTP 嵌入超时纳入 GITNEXUS_EMBEDDING_MAX_ATTEMPTS 重试(默认关闭,超时仍为终止错误)。\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N exact-scan 回退的最大嵌入分块数。默认 10000。\n GITNEXUS_VECTOR_MAX_DISTANCE=N 语义/向量搜索接受的最大余弦距离(0 < N <= 2;超出则钳制为 2)。MCP 默认 0.6,其他路径默认 0.5。\n GITNEXUS_MAX_PROCESSES=N 流程检测的流程数量上限(正整数)。覆盖动态的 max(20, round(symbols/10)) 公式。与查询时的 IMPACT_MAX_CHUNKS 无关。\n GITNEXUS_MAX_PROCESS_BRANCHING=N 流程检测的单节点分支上限。默认 4。\n GITNEXUS_MAX_PROCESS_TRACE_DEPTH=N 流程检测的 DFS 深度上限。默认 10。\n GITNEXUS_MAX_ENTRY_POINT_CANDIDATES=N 排序后的入口点候选池。默认 200。仅在警告点名该上限时提高;那时通常先翻倍。\n\nCLI 参数优先于 `.gitnexusrc`,后者优先于环境变量,环境变量优先于内置默认值。\n\n提示:`.gitnexusignore` 支持 `.gitignore` 风格的取反。比如添加\n `!__tests__/` 可以索引默认自动过滤的目录(#771)。',
282
286
  };
package/dist/cli/index.js CHANGED
@@ -85,6 +85,10 @@ program
85
85
  .option('--wal-checkpoint-threshold <bytes>', 'LadybugDB WAL auto-checkpoint threshold in bytes during analyze ' +
86
86
  '(integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).')
87
87
  .option('--workers <n>', 'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.')
88
+ .option('--max-processes <n>', 'Process-detection process cap (positive integer). Replaces the dynamic max(20, round(symbols/10)) formula. Default: dynamic.')
89
+ .option('--max-process-branching <n>', 'Process-detection per-node branching cap (positive integer). Default: 4.')
90
+ .option('--max-process-trace-depth <n>', 'Process-detection DFS depth cap (positive integer). Default: 10.')
91
+ .option('--max-entry-point-candidates <n>', 'Ranked entry-point candidate pool (positive integer). Default: 200. Raise when the warning names this knob; doubling is the usual first raise.')
88
92
  .option('--spring-actuator <path>', 'Import local Spring Boot Actuator JSON snapshots (mappings, beans, conditions, ' +
89
93
  'configprops, env). Explicit opt-in; disabled by default.')
90
94
  .option('--asyncapi-spec <path>', 'Read AsyncAPI 3.x documents from this directory or file and resolve broker ' +
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { getPhaseOutput } from './types.js';
14
14
  import { processProcesses } from '../process-processor.js';
15
+ import { buildProcessDetectionPhaseConfig, formatWholeFlowsMissingRemedies, processDetectionEffectiveLimits, resolveProcessDetectionBudget, } from '../process-detection-budget.js';
15
16
  import { generateId } from '../../../lib/utils.js';
16
17
  import { routeNodeKey } from '../route-extractors/route-path.js';
17
18
  import { isDev } from '../utils/env.js';
@@ -60,12 +61,24 @@ export const processesPhase = {
60
61
  message: 'Detecting execution flows...',
61
62
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: ctx.graph.nodeCount },
62
63
  });
64
+ const resolvedBudget = resolveProcessDetectionBudget({
65
+ maxProcesses: ctx.options?.maxProcesses,
66
+ maxProcessBranching: ctx.options?.maxProcessBranching,
67
+ maxProcessTraceDepth: ctx.options?.maxProcessTraceDepth,
68
+ maxEntryPointCandidates: ctx.options?.maxEntryPointCandidates,
69
+ },
70
+ // Env is resolved in `runFullAnalysis` and threaded on PipelineOptions.
71
+ // The phase reads only those fields so unit tests stay isolated from
72
+ // the host environment.
73
+ {});
63
74
  let symbolCount = 0;
64
- ctx.graph.forEachNode((n) => {
65
- if (n.label !== 'File')
66
- symbolCount++;
67
- });
68
- const dynamicMaxProcesses = computeDynamicMaxProcesses(symbolCount);
75
+ if (resolvedBudget.maxProcesses === undefined) {
76
+ ctx.graph.forEachNode((n) => {
77
+ if (n.label !== 'File')
78
+ symbolCount++;
79
+ });
80
+ }
81
+ const detectionConfig = buildProcessDetectionPhaseConfig(resolvedBudget, symbolCount, computeDynamicMaxProcesses);
69
82
  // R3-6: where the program reaches outward. Already collected by the parse
70
83
  // phase for FILE-level FETCHES/QUERIES edges; reused here at function
71
84
  // granularity so a trace can end somewhere meaningful instead of only at a
@@ -100,7 +113,7 @@ export const processesPhase = {
100
113
  message,
101
114
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: ctx.graph.nodeCount },
102
115
  });
103
- }, { maxProcesses: dynamicMaxProcesses, minSteps: 3 }, outwardActionSites);
116
+ }, detectionConfig, outwardActionSites);
104
117
  if (isDev) {
105
118
  logger.info(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
106
119
  }
@@ -114,8 +127,8 @@ export const processesPhase = {
114
127
  // "unexplored entry points mean whole flows are missing, while a
115
128
  // depth-capped trace means a flow is present but shorter than it really is"
116
129
  // — and it is what keeps the line worth reading. Warning on every counter
117
- // meant warning on every run: this phase overrides only `maxProcesses`, so
118
- // at the shipped defaults (`maxBranching: 4`, `maxTraceDepth: 10`,
130
+ // meant warning on every run: at the shipped defaults (`maxBranching: 4`,
131
+ // `maxTraceDepth: 10`,
119
132
  // per-entry trace budget 12) `calleesDropped` fires for any function with
120
133
  // five callees, `tracesDepthCapped` for any chain deeper than ten, and
121
134
  // `walksCutByBudget` for any entry point with twelve paths under it. All
@@ -140,6 +153,8 @@ export const processesPhase = {
140
153
  const flowsMissing = truncation.entryPointCandidatesDropped > 0 ||
141
154
  truncation.entryPointsUnexplored > 0 ||
142
155
  truncation.processesDropped > 0;
156
+ const effectiveLimits = processDetectionEffectiveLimits(detectionConfig.maxProcesses, resolvedBudget);
157
+ const remedies = formatWholeFlowsMissingRemedies(truncation, effectiveLimits, entryPointCandidates);
143
158
  const shape = `${truncation.entryPointCandidatesDropped} of ${entryPointCandidates} candidate entry point(s) never ranked in, ` +
144
159
  `${truncation.entryPointsUnexplored} ranked entry point(s) never traced, ` +
145
160
  `${truncation.processesDropped} deduplicated flow(s) dropped at maxProcesses, ` +
@@ -147,11 +162,11 @@ export const processesPhase = {
147
162
  `${truncation.calleesDropped} callee(s) skipped at maxBranching, ` +
148
163
  `${truncation.walksCutByBudget} walk(s) cut by the per-entry trace budget.`;
149
164
  if (flowsMissing) {
150
- logger.warn({ truncation }, `[processes] ${processResult.stats.totalProcesses} flows reported, but whole flows are MISSING: ` +
151
- `${shape} An absent flow does NOT mean the code path does not exist.`);
165
+ logger.warn({ truncation, effectiveLimits }, `[processes] ${processResult.stats.totalProcesses} flows reported, but whole flows are MISSING: ` +
166
+ `${shape}${remedies} An absent flow does NOT mean the code path does not exist.`);
152
167
  }
153
168
  else if (truncation.truncated) {
154
- logger.debug({ truncation }, `[processes] ${processResult.stats.totalProcesses} flows reported; every flow found is present, ` +
169
+ logger.debug({ truncation, effectiveLimits }, `[processes] ${processResult.stats.totalProcesses} flows reported; every flow found is present, ` +
155
170
  `but some are shorter than the code path they describe: ${shape}`);
156
171
  }
157
172
  processResult.processes.forEach((proc) => {
@@ -204,6 +204,15 @@ export interface PipelineOptions {
204
204
  * analyze invocations.
205
205
  */
206
206
  workerPoolSize?: number;
207
+ /**
208
+ * Process-detection budget (#3313). Explicit `maxProcesses` replaces the
209
+ * dynamic `symbols / 10` formula; the other three replace compiled defaults.
210
+ * Unset fields keep shipped behavior. `0` is rejected upstream — not unlimited.
211
+ */
212
+ maxProcesses?: number;
213
+ maxProcessBranching?: number;
214
+ maxProcessTraceDepth?: number;
215
+ maxEntryPointCandidates?: number;
207
216
  /**
208
217
  * Number of chunks whose file contents may be read into memory in
209
218
  * parallel while the worker pool is busy dispatching the current
@@ -136,8 +136,8 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
136
136
  const pdgEmitManifest = scopeResolutionOutput.pdgEmitManifest;
137
137
  const propertyInference = scopeResolutionOutput.propertyInference;
138
138
  // Presence check, not `!skipGraphPhases`: phases can now be filtered out by
139
- // any `enabledWhen` predicate (streamGraphEmit disables communities/processes
140
- // too), and `getPhaseOutput` THROWS on a phase that was never resolved. Keying
139
+ // any `enabledWhen` predicate (`skipGraphPhases` drops communities/processes),
140
+ // and `getPhaseOutput` THROWS on a phase that was never resolved. Keying
141
141
  // off the options flag alone made every filtered-out combination crash here
142
142
  // rather than return undefined results.
143
143
  if (results.has('communities') && results.has('processes')) {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Analyze-time process-detection budget (#3313).
3
+ *
4
+ * Four knobs control how many execution flows `processProcesses` keeps:
5
+ * process count, per-node branching, trace depth, and the ranked entry-point
6
+ * pool. Precedence is CLI / explicit `AnalyzeOptions` > `.gitnexusrc` (already
7
+ * merged into those fields) > `GITNEXUS_*` env > built-in defaults. Unset
8
+ * `maxProcesses` keeps the dynamic `symbols / 10` formula. `0` is invalid, not
9
+ * unlimited.
10
+ */
11
+ import type { ProcessDetectionConfig, ProcessTruncationStats } from './process-processor.js';
12
+ export declare const PROCESS_DETECTION_BUDGET_DEFAULTS: {
13
+ readonly maxProcessBranching: 4;
14
+ readonly maxProcessTraceDepth: 10;
15
+ readonly maxEntryPointCandidates: 200;
16
+ readonly minSteps: 3;
17
+ };
18
+ export declare const PROCESS_DETECTION_ENV: {
19
+ readonly maxProcesses: 'GITNEXUS_MAX_PROCESSES';
20
+ readonly maxProcessBranching: 'GITNEXUS_MAX_PROCESS_BRANCHING';
21
+ readonly maxProcessTraceDepth: 'GITNEXUS_MAX_PROCESS_TRACE_DEPTH';
22
+ readonly maxEntryPointCandidates: 'GITNEXUS_MAX_ENTRY_POINT_CANDIDATES';
23
+ };
24
+ export declare const PROCESS_DETECTION_CLI_FLAGS: {
25
+ readonly maxProcesses: '--max-processes';
26
+ readonly maxProcessBranching: '--max-process-branching';
27
+ readonly maxProcessTraceDepth: '--max-process-trace-depth';
28
+ readonly maxEntryPointCandidates: '--max-entry-point-candidates';
29
+ };
30
+ declare const BUDGET_KEYS: readonly ['maxProcesses', 'maxProcessBranching', 'maxProcessTraceDepth', 'maxEntryPointCandidates'];
31
+ export type ProcessDetectionBudgetKey = (typeof BUDGET_KEYS)[number];
32
+ export type ProcessDetectionBudgetFields = {
33
+ maxProcesses?: number;
34
+ maxProcessBranching?: number;
35
+ maxProcessTraceDepth?: number;
36
+ maxEntryPointCandidates?: number;
37
+ };
38
+ export type ProcessDetectionBudgetStrings = {
39
+ maxProcesses?: string;
40
+ maxProcessBranching?: string;
41
+ maxProcessTraceDepth?: string;
42
+ maxEntryPointCandidates?: string;
43
+ };
44
+ export type ProcessDetectionStamp = {
45
+ /** Explicit override, or `null` when this run used the dynamic formula. */
46
+ maxProcesses: number | null;
47
+ maxProcessBranching: number;
48
+ maxProcessTraceDepth: number;
49
+ maxEntryPointCandidates: number;
50
+ /**
51
+ * In-place FTS park after a derived-layer rewrite (#3322). Missing stamp +
52
+ * defaults is a match, so recovery must persist a complete stamp that still
53
+ * mismatches until a successful analyze certifies the live Community/Process
54
+ * rows. Success writes omit this flag.
55
+ */
56
+ uncertified?: true;
57
+ };
58
+ export type ResolvedProcessDetectionBudget = {
59
+ /** Set only when an explicit override won. */
60
+ maxProcesses?: number;
61
+ maxProcessBranching: number;
62
+ maxProcessTraceDepth: number;
63
+ maxEntryPointCandidates: number;
64
+ overridden: {
65
+ maxProcesses: boolean;
66
+ maxProcessBranching: boolean;
67
+ maxProcessTraceDepth: boolean;
68
+ maxEntryPointCandidates: boolean;
69
+ };
70
+ };
71
+ export type ProcessDetectionEffectiveLimits = {
72
+ maxProcesses: number;
73
+ maxProcessBranching: number;
74
+ maxProcessTraceDepth: number;
75
+ maxEntryPointCandidates: number;
76
+ /**
77
+ * Pre-entry gate: `processProcesses` does not start the next entry once
78
+ * collected traces already reach `maxProcesses * 2`. One started entry can
79
+ * still append every trace `traceFromEntryPoint` returns.
80
+ */
81
+ maxProcessTraces: number;
82
+ };
83
+ export type InvalidBudgetHandler = (knob: string, raw: string) => void;
84
+ /** Operator copy when a CLI/rc/env token is rejected. Next precedence still applies. */
85
+ export declare const formatInvalidProcessDetectionOverride: (knob: string, raw: string) => string;
86
+ export declare const parsePositiveIntegerOverride: (raw: string | number | undefined | null, onInvalid?: (raw: string) => void) => number | undefined;
87
+ export declare const parseProcessDetectionBudgetStrings: (raw: ProcessDetectionBudgetStrings, onInvalid?: InvalidBudgetHandler) => ProcessDetectionBudgetFields;
88
+ export declare const resolveProcessDetectionBudget: (options?: ProcessDetectionBudgetFields, env?: NodeJS.ProcessEnv, onInvalid?: InvalidBudgetHandler) => ResolvedProcessDetectionBudget;
89
+ export declare const hasProcessDetectionOverride: (resolved: ResolvedProcessDetectionBudget) => boolean;
90
+ export declare const toProcessDetectionStamp: (resolved: ResolvedProcessDetectionBudget) => ProcessDetectionStamp;
91
+ /** Complete stamp that always mismatches until the next successful analyze. */
92
+ export declare const uncertifyProcessDetectionStamp: (recorded: ProcessDetectionStamp | undefined) => ProcessDetectionStamp;
93
+ export declare const processDetectionBudgetMismatch: (recorded: ProcessDetectionStamp | undefined, resolved: ResolvedProcessDetectionBudget) => boolean;
94
+ export declare const buildProcessDetectionPhaseConfig: (resolved: ResolvedProcessDetectionBudget, symbolCount: number, computeDynamicMaxProcesses: (n: number) => number) => Pick<ProcessDetectionConfig, 'maxProcesses' | 'maxBranching' | 'maxTraceDepth' | 'maxEntryPointCandidates' | 'minSteps'>;
95
+ export declare const processDetectionEffectiveLimits: (maxProcesses: number, resolved: ResolvedProcessDetectionBudget) => ProcessDetectionEffectiveLimits;
96
+ export declare const formatWholeFlowsMissingRemedies: (truncation: Pick<ProcessTruncationStats, 'entryPointCandidatesDropped' | 'entryPointsUnexplored' | 'processesDropped'>, limits: ProcessDetectionEffectiveLimits, observedEntryPointCandidates: number) => string;
97
+ export declare const formatProcessDetectionBudgetBanner: (resolved: ResolvedProcessDetectionBudget) => string | null;
98
+ export {};
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Analyze-time process-detection budget (#3313).
3
+ *
4
+ * Four knobs control how many execution flows `processProcesses` keeps:
5
+ * process count, per-node branching, trace depth, and the ranked entry-point
6
+ * pool. Precedence is CLI / explicit `AnalyzeOptions` > `.gitnexusrc` (already
7
+ * merged into those fields) > `GITNEXUS_*` env > built-in defaults. Unset
8
+ * `maxProcesses` keeps the dynamic `symbols / 10` formula. `0` is invalid, not
9
+ * unlimited.
10
+ */
11
+ import { parsePositiveIntEnv } from './utils/env.js';
12
+ export const PROCESS_DETECTION_BUDGET_DEFAULTS = {
13
+ maxProcessBranching: 4,
14
+ maxProcessTraceDepth: 10,
15
+ maxEntryPointCandidates: 200,
16
+ minSteps: 3,
17
+ };
18
+ export const PROCESS_DETECTION_ENV = {
19
+ maxProcesses: 'GITNEXUS_MAX_PROCESSES',
20
+ maxProcessBranching: 'GITNEXUS_MAX_PROCESS_BRANCHING',
21
+ maxProcessTraceDepth: 'GITNEXUS_MAX_PROCESS_TRACE_DEPTH',
22
+ maxEntryPointCandidates: 'GITNEXUS_MAX_ENTRY_POINT_CANDIDATES',
23
+ };
24
+ export const PROCESS_DETECTION_CLI_FLAGS = {
25
+ maxProcesses: '--max-processes',
26
+ maxProcessBranching: '--max-process-branching',
27
+ maxProcessTraceDepth: '--max-process-trace-depth',
28
+ maxEntryPointCandidates: '--max-entry-point-candidates',
29
+ };
30
+ const BUDGET_KEYS = [
31
+ 'maxProcesses',
32
+ 'maxProcessBranching',
33
+ 'maxProcessTraceDepth',
34
+ 'maxEntryPointCandidates',
35
+ ];
36
+ /** Operator copy when a CLI/rc/env token is rejected. Next precedence still applies. */
37
+ export const formatInvalidProcessDetectionOverride = (knob, raw) => {
38
+ const next = knob.startsWith('GITNEXUS_')
39
+ ? 'the built-in default'
40
+ : 'the next source (env, then the built-in default)';
41
+ return `${knob} must be a positive integer (got ${JSON.stringify(raw)}); ignoring it so ${next} applies.`;
42
+ };
43
+ export const parsePositiveIntegerOverride = (raw, onInvalid) => {
44
+ if (raw === undefined || raw === null)
45
+ return undefined;
46
+ const text = typeof raw === 'number' ? String(raw) : raw;
47
+ const parsed = parsePositiveIntEnv(text);
48
+ if (parsed === undefined)
49
+ onInvalid?.(typeof raw === 'number' ? text : text.trim());
50
+ return parsed;
51
+ };
52
+ export const parseProcessDetectionBudgetStrings = (raw, onInvalid) => {
53
+ const out = {};
54
+ for (const key of BUDGET_KEYS) {
55
+ if (raw[key] === undefined)
56
+ continue;
57
+ const parsed = parsePositiveIntegerOverride(raw[key], (invalid) => onInvalid?.(PROCESS_DETECTION_CLI_FLAGS[key], invalid));
58
+ if (parsed !== undefined)
59
+ out[key] = parsed;
60
+ }
61
+ return out;
62
+ };
63
+ const readOverride = (options, env, key, onInvalid) => {
64
+ const fromOptions = options[key];
65
+ if (fromOptions !== undefined) {
66
+ const parsed = parsePositiveIntegerOverride(fromOptions, (invalid) => onInvalid?.(PROCESS_DETECTION_CLI_FLAGS[key], invalid));
67
+ if (parsed !== undefined)
68
+ return parsed;
69
+ }
70
+ const envName = PROCESS_DETECTION_ENV[key];
71
+ const fromEnv = env[envName];
72
+ if (fromEnv === undefined)
73
+ return undefined;
74
+ return parsePositiveIntegerOverride(fromEnv, (invalid) => onInvalid?.(envName, invalid));
75
+ };
76
+ export const resolveProcessDetectionBudget = (options = {}, env = process.env, onInvalid) => {
77
+ const maxProcesses = readOverride(options, env, 'maxProcesses', onInvalid);
78
+ const branching = readOverride(options, env, 'maxProcessBranching', onInvalid);
79
+ const depth = readOverride(options, env, 'maxProcessTraceDepth', onInvalid);
80
+ const entryPoints = readOverride(options, env, 'maxEntryPointCandidates', onInvalid);
81
+ return {
82
+ ...(maxProcesses === undefined ? {} : { maxProcesses }),
83
+ maxProcessBranching: branching ?? PROCESS_DETECTION_BUDGET_DEFAULTS.maxProcessBranching,
84
+ maxProcessTraceDepth: depth ?? PROCESS_DETECTION_BUDGET_DEFAULTS.maxProcessTraceDepth,
85
+ maxEntryPointCandidates: entryPoints ?? PROCESS_DETECTION_BUDGET_DEFAULTS.maxEntryPointCandidates,
86
+ overridden: {
87
+ maxProcesses: maxProcesses !== undefined,
88
+ maxProcessBranching: branching !== undefined,
89
+ maxProcessTraceDepth: depth !== undefined,
90
+ maxEntryPointCandidates: entryPoints !== undefined,
91
+ },
92
+ };
93
+ };
94
+ export const hasProcessDetectionOverride = (resolved) => resolved.overridden.maxProcesses ||
95
+ resolved.overridden.maxProcessBranching ||
96
+ resolved.overridden.maxProcessTraceDepth ||
97
+ resolved.overridden.maxEntryPointCandidates;
98
+ export const toProcessDetectionStamp = (resolved) => ({
99
+ maxProcesses: resolved.maxProcesses ?? null,
100
+ maxProcessBranching: resolved.maxProcessBranching,
101
+ maxProcessTraceDepth: resolved.maxProcessTraceDepth,
102
+ maxEntryPointCandidates: resolved.maxEntryPointCandidates,
103
+ });
104
+ /** Complete stamp that always mismatches until the next successful analyze. */
105
+ export const uncertifyProcessDetectionStamp = (recorded) => ({
106
+ maxProcesses: recorded?.maxProcesses ?? null,
107
+ maxProcessBranching: recorded?.maxProcessBranching ?? PROCESS_DETECTION_BUDGET_DEFAULTS.maxProcessBranching,
108
+ maxProcessTraceDepth: recorded?.maxProcessTraceDepth ?? PROCESS_DETECTION_BUDGET_DEFAULTS.maxProcessTraceDepth,
109
+ maxEntryPointCandidates: recorded?.maxEntryPointCandidates ?? PROCESS_DETECTION_BUDGET_DEFAULTS.maxEntryPointCandidates,
110
+ uncertified: true,
111
+ });
112
+ const isCompleteStamp = (recorded) => recorded !== undefined &&
113
+ (recorded.maxProcesses === null ||
114
+ (typeof recorded.maxProcesses === 'number' && Number.isInteger(recorded.maxProcesses))) &&
115
+ Number.isInteger(recorded.maxProcessBranching) &&
116
+ Number.isInteger(recorded.maxProcessTraceDepth) &&
117
+ Number.isInteger(recorded.maxEntryPointCandidates);
118
+ export const processDetectionBudgetMismatch = (recorded, resolved) => {
119
+ if (recorded?.uncertified === true)
120
+ return true;
121
+ if (!isCompleteStamp(recorded)) {
122
+ // Legacy meta: same defaults as today's shipped behavior stay a match so
123
+ // an upgrade backfills the stamp instead of re-detecting. Any explicit
124
+ // override is a mismatch — otherwise a budget-only raise on a pre-#3313
125
+ // index would preserve the sampled Community/Process layer.
126
+ return hasProcessDetectionOverride(resolved);
127
+ }
128
+ const stamp = toProcessDetectionStamp(resolved);
129
+ return (recorded.maxProcesses !== stamp.maxProcesses ||
130
+ recorded.maxProcessBranching !== stamp.maxProcessBranching ||
131
+ recorded.maxProcessTraceDepth !== stamp.maxProcessTraceDepth ||
132
+ recorded.maxEntryPointCandidates !== stamp.maxEntryPointCandidates);
133
+ };
134
+ export const buildProcessDetectionPhaseConfig = (resolved, symbolCount, computeDynamicMaxProcesses) => ({
135
+ maxProcesses: resolved.maxProcesses ?? computeDynamicMaxProcesses(symbolCount),
136
+ maxBranching: resolved.maxProcessBranching,
137
+ maxTraceDepth: resolved.maxProcessTraceDepth,
138
+ maxEntryPointCandidates: resolved.maxEntryPointCandidates,
139
+ minSteps: PROCESS_DETECTION_BUDGET_DEFAULTS.minSteps,
140
+ });
141
+ export const processDetectionEffectiveLimits = (maxProcesses, resolved) => ({
142
+ maxProcesses,
143
+ maxProcessBranching: resolved.maxProcessBranching,
144
+ maxProcessTraceDepth: resolved.maxProcessTraceDepth,
145
+ maxEntryPointCandidates: resolved.maxEntryPointCandidates,
146
+ maxProcessTraces: maxProcesses * 2,
147
+ });
148
+ export const formatWholeFlowsMissingRemedies = (truncation, limits, observedEntryPointCandidates) => {
149
+ const parts = [];
150
+ if (truncation.entryPointCandidatesDropped > 0) {
151
+ parts.push(`${PROCESS_DETECTION_CLI_FLAGS.maxEntryPointCandidates} ` +
152
+ `(this run ranked ${observedEntryPointCandidates} candidate(s))`);
153
+ }
154
+ if (truncation.entryPointsUnexplored > 0 || truncation.processesDropped > 0) {
155
+ parts.push(`${PROCESS_DETECTION_CLI_FLAGS.maxProcesses} ` +
156
+ `(this run used ${limits.maxProcesses}; next entry is skipped once ` +
157
+ `collected traces reach ${limits.maxProcessTraces})`);
158
+ }
159
+ return parts.length === 0 ? '' : ` Raise ${parts.join('; ')}.`;
160
+ };
161
+ export const formatProcessDetectionBudgetBanner = (resolved) => {
162
+ if (!hasProcessDetectionOverride(resolved))
163
+ return null;
164
+ const maxProcesses = resolved.overridden.maxProcesses
165
+ ? String(resolved.maxProcesses)
166
+ : 'dynamic (max(20, round(symbols/10)))';
167
+ return (` Process-detection budget: maxProcesses=${maxProcesses}, ` +
168
+ `maxProcessBranching=${resolved.maxProcessBranching}, ` +
169
+ `maxProcessTraceDepth=${resolved.maxProcessTraceDepth}, ` +
170
+ `maxEntryPointCandidates=${resolved.maxEntryPointCandidates}`);
171
+ };
@@ -16,7 +16,9 @@ export interface ProcessDetectionConfig {
16
16
  maxBranching: number;
17
17
  maxProcesses: number;
18
18
  minSteps: number;
19
+ maxEntryPointCandidates: number;
19
20
  }
21
+ export declare const DEFAULT_CONFIG: ProcessDetectionConfig;
20
22
  export interface ProcessNode {
21
23
  id: string;
22
24
  label: string;
@@ -63,7 +65,7 @@ export interface ProcessTruncationStats {
63
65
  truncated: boolean;
64
66
  /**
65
67
  * Scoring candidates that never reached the trace loop because
66
- * `findEntryPoints` keeps only the top `ENTRY_POINT_CANDIDATE_LIMIT`.
68
+ * `findEntryPoints` keeps only the top `maxEntryPointCandidates`.
67
69
  * Counted BEFORE the slice, so it sees what `entryPointsFound` cannot.
68
70
  */
69
71
  entryPointCandidatesDropped: number;
@@ -13,12 +13,14 @@ import { isHeuristicEdgeReason } from '../graph/edge-reasons.js';
13
13
  import { calculateEntryPointScore, isTestFile } from './entry-point-scoring.js';
14
14
  import { SupportedLanguages } from '../../_shared/index.js';
15
15
  import { isDev } from './utils/env.js';
16
+ import { PROCESS_DETECTION_BUDGET_DEFAULTS } from './process-detection-budget.js';
16
17
  import { logger } from '../logger.js';
17
- const DEFAULT_CONFIG = {
18
- maxTraceDepth: 10,
19
- maxBranching: 4,
18
+ export const DEFAULT_CONFIG = {
19
+ maxTraceDepth: PROCESS_DETECTION_BUDGET_DEFAULTS.maxProcessTraceDepth,
20
+ maxBranching: PROCESS_DETECTION_BUDGET_DEFAULTS.maxProcessBranching,
20
21
  maxProcesses: 75,
21
- minSteps: 3, // 3+ steps = genuine multi-hop flow (2-step is just "A calls B")
22
+ minSteps: PROCESS_DETECTION_BUDGET_DEFAULTS.minSteps, // 3+ steps = genuine multi-hop flow (2-step is just "A calls B")
23
+ maxEntryPointCandidates: PROCESS_DETECTION_BUDGET_DEFAULTS.maxEntryPointCandidates,
22
24
  };
23
25
  /** Zeroed counters, mutated in place by the walk. */
24
26
  const emptyTruncation = () => ({
@@ -58,10 +60,10 @@ outwardActionSites = []) => {
58
60
  for (const n of knowledgeGraph.iterNodes())
59
61
  nodeMap.set(n.id, n);
60
62
  // Declared before Step 1 because `findEntryPoints` has a ceiling of its own
61
- // (see `ENTRY_POINT_CANDIDATE_LIMIT`) and reports it through the same record.
63
+ // (`cfg.maxEntryPointCandidates`, default 200) and reports it through the same record.
62
64
  const truncation = emptyTruncation();
63
65
  // Step 1: Find entry points (functions that call others but have few callers)
64
- const entryPoints = findEntryPoints(knowledgeGraph, reverseCallsEdges, callsEdges, truncation);
66
+ const entryPoints = findEntryPoints(knowledgeGraph, reverseCallsEdges, callsEdges, truncation, cfg.maxEntryPointCandidates);
65
67
  onProgress?.(`Found ${entryPoints.length} entry points, tracing flows...`, 20);
66
68
  onProgress?.(`Found ${entryPoints.length} entry points, tracing flows...`, 20);
67
69
  // Step 2: Trace processes from each entry point
@@ -81,7 +83,7 @@ outwardActionSites = []) => {
81
83
  // the remainder are not "no flows found" — they were never looked at.
82
84
  //
83
85
  // Counted over the list `findEntryPoints` RETURNS, which is already capped at
84
- // `ENTRY_POINT_CANDIDATE_LIMIT`; candidates beyond that cap are invisible here
86
+ // `maxEntryPointCandidates`; candidates beyond that cap are invisible here
85
87
  // by construction and are reported separately as
86
88
  // `entryPointCandidatesDropped`.
87
89
  truncation.entryPointsUnexplored = entryPoints.length - tracedEntryPoints;
@@ -327,12 +329,6 @@ const buildReverseCallsGraph = (graph) => {
327
329
  });
328
330
  return adj;
329
331
  };
330
- /**
331
- * How many ranked candidates survive to be traced. Everything below this line
332
- * is discarded — see `ProcessTruncationStats.entryPointCandidatesDropped`, the
333
- * counter that exists because this cap spent a release being invisible.
334
- */
335
- const ENTRY_POINT_CANDIDATE_LIMIT = 200;
336
332
  /**
337
333
  * Find functions/methods that are good entry points for tracing.
338
334
  *
@@ -341,7 +337,9 @@ const ENTRY_POINT_CANDIDATE_LIMIT = 200;
341
337
  * 2. Export status (exported/public functions rank higher)
342
338
  * 3. Name patterns (handle*, on*, *Controller, etc.)
343
339
  *
344
- * Test files are excluded entirely.
340
+ * Test files are excluded entirely. How many ranked candidates survive to be
341
+ * traced is `maxEntryPointCandidates` (default 200) — see
342
+ * `ProcessTruncationStats.entryPointCandidatesDropped`.
345
343
  */
346
344
  const findEntryPoints = (graph, reverseCallsEdges, callsEdges,
347
345
  /**
@@ -351,7 +349,7 @@ const findEntryPoints = (graph, reverseCallsEdges, callsEdges,
351
349
  * unchanged, and a caller that does not care about completeness does not have
352
350
  * to unwrap a counter to ask for entry points.
353
351
  */
354
- truncation) => {
352
+ truncation, maxEntryPointCandidates = DEFAULT_CONFIG.maxEntryPointCandidates) => {
355
353
  const symbolTypes = new Set(['Function', 'Method']);
356
354
  const entryPointCandidates = [];
357
355
  for (const node of graph.iterNodes()) {
@@ -397,14 +395,14 @@ truncation) => {
397
395
  }
398
396
  // Limit to prevent explosion — and SAY SO. This is the ceiling that decides
399
397
  // how much of a repository process detection ever looks at: on anything with
400
- // more than 200 scoring candidates the reported flows are a sample of the
401
- // top-ranked ones, and every downstream count (`entryPointsFound`,
402
- // `entryPointsUnexplored`) is computed over the survivors, so none of them can
403
- // see what was cut here.
404
- if (truncation !== undefined && sorted.length > ENTRY_POINT_CANDIDATE_LIMIT) {
405
- truncation.entryPointCandidatesDropped = sorted.length - ENTRY_POINT_CANDIDATE_LIMIT;
398
+ // more scoring candidates than `maxEntryPointCandidates` the reported flows
399
+ // are a sample of the top-ranked ones, and every downstream count
400
+ // (`entryPointsFound`, `entryPointsUnexplored`) is computed over the
401
+ // survivors, so none of them can see what was cut here.
402
+ if (truncation !== undefined && sorted.length > maxEntryPointCandidates) {
403
+ truncation.entryPointCandidatesDropped = sorted.length - maxEntryPointCandidates;
406
404
  }
407
- return sorted.slice(0, ENTRY_POINT_CANDIDATE_LIMIT).map((c) => c.id);
405
+ return sorted.slice(0, maxEntryPointCandidates).map((c) => c.id);
408
406
  };
409
407
  // ============================================================================
410
408
  // HELPER: Trace from entry point (DFS)
@@ -618,7 +616,7 @@ const compareOrderKeys = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
618
616
  * joined strings, so an O(n log n) sort performs O(n log n) joins of
619
617
  * O(depth x id-length) characters each.
620
618
  *
621
- * `n` is bounded here (`ENTRY_POINT_CANDIDATE_LIMIT` entry points x the
619
+ * `n` is bounded here (`maxEntryPointCandidates` entry points x the
622
620
  * per-entry trace budget), so the cost is small and once-per-analyze: measured
623
621
  * at the ceiling, 23,851 comparisons performed 70,524 joins, and end-to-end
624
622
  * `processProcesses` at 80,000 functions / 640k CALLS went 456 -> 555 ms. It is
@@ -113,8 +113,9 @@ export interface AnalyzeOptions {
113
113
  pdgEmitChunkSize?: number;
114
114
  /** Streamed structural graph emit (#2680). Honored only on a full rebuild
115
115
  * (`force === true`). May also be enabled via `GITNEXUS_STREAM_GRAPH_EMIT`.
116
- * Trades community detection, process extraction and PDG taint summaries for
117
- * a ~2.9x reduction of in-memory graph heap. */
116
+ * The sink answers a complete relationship read, so community detection,
117
+ * process extraction, and PDG taint summaries still run; streaming reduces
118
+ * in-memory graph heap (~2.9x) by keeping those edges on disk. */
118
119
  streamGraphEmit?: boolean;
119
120
  /**
120
121
  * Default branch threaded into generated AGENTS.md / CLAUDE.md so the
@@ -156,6 +157,16 @@ export interface AnalyzeOptions {
156
157
  * removed); `undefined` defers to the env / auto-formula fallback.
157
158
  */
158
159
  workerPoolSize?: number;
160
+ /**
161
+ * Process-detection budget overrides (#3313). Threaded to
162
+ * `PipelineOptions` without mutating `process.env`. Unset fields fall
163
+ * back to `GITNEXUS_*` env, then shipped defaults / the dynamic
164
+ * `maxProcesses` formula.
165
+ */
166
+ maxProcesses?: number;
167
+ maxProcessBranching?: number;
168
+ maxProcessTraceDepth?: number;
169
+ maxEntryPointCandidates?: number;
159
170
  /**
160
171
  * Extra fetch-wrapper function names to treat as HTTP consumers, forwarded to
161
172
  * `PipelineOptions.fetchWrappers` (#1589/#1852 residual). Sourced from the CLI
@@ -53,6 +53,7 @@ import { collectSpringConfigConsumerDriftFiles, } from './incremental/spring-con
53
53
  import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
54
54
  import { shouldEscalateIncrementalWrite } from './incremental/escalation-gate.js';
55
55
  import { ftsTablesAmong, incrementalFtsTablesFromGraph, nodeTablesForIncrementalDelete, shouldPreservePersistedDerivedGraph, } from './incremental/derived-writeback.js';
56
+ import { formatInvalidProcessDetectionOverride, processDetectionBudgetMismatch, resolveProcessDetectionBudget, toProcessDetectionStamp, uncertifyProcessDetectionStamp, } from './ingestion/process-detection-budget.js';
56
57
  import { NODE_TABLES } from './lbug/schema.js';
57
58
  import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, createColdParseRebuildDir, emptyParseCache, forgetCreatedParseCacheDir, } from '../storage/parse-cache.js';
58
59
  import { mergeStagedDurableParsedFileStore } from '../storage/parsedfile-store.js';
@@ -1370,12 +1371,28 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1370
1371
  log('Parser cache bypass requested; forcing a full rebuild so unchanged files are re-parsed.');
1371
1372
  options = { ...options, force: true };
1372
1373
  }
1374
+ // Process-detection budget (#3313). Resolve CLI/options then env here so
1375
+ // MCP/server jobs honor GITNEXUS_* without a CLI merge. Compare against
1376
+ // the persisted stamp BEFORE the already-up-to-date fast path: a clean
1377
+ // same-commit raise must re-detect flows rather than return the sampled
1378
+ // index. Does NOT set force — incremental empty-diff + skip derived
1379
+ // preserve is enough.
1380
+ const processDetectionBudget = resolveProcessDetectionBudget({
1381
+ maxProcesses: options.maxProcesses,
1382
+ maxProcessBranching: options.maxProcessBranching,
1383
+ maxProcessTraceDepth: options.maxProcessTraceDepth,
1384
+ maxEntryPointCandidates: options.maxEntryPointCandidates,
1385
+ }, process.env, (knob, raw) => {
1386
+ log(formatInvalidProcessDetectionOverride(knob, raw));
1387
+ });
1388
+ const processDetectionMismatch = processDetectionBudgetMismatch(existingMeta?.processDetection, processDetectionBudget);
1373
1389
  // ── Early-return: already up to date ──────────────────────────────
1374
1390
  if (existingMeta &&
1375
1391
  !existingMeta.embeddingCheckpoint &&
1376
1392
  !options.force &&
1377
1393
  existingMeta.lastCommit === currentCommit &&
1378
- !ftsModeChanged) {
1394
+ !ftsModeChanged &&
1395
+ !processDetectionMismatch) {
1379
1396
  // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
1380
1397
  if (currentCommit !== '') {
1381
1398
  // For git repos, even if HEAD matches lastCommit, the working tree
@@ -1420,6 +1437,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1420
1437
  // later read on a host where it loads — which is a legitimate, common
1421
1438
  // state, and the invariant `analyzer-identity-cli.test.ts` pins.
1422
1439
  if (!dirty && !healUnregistered) {
1440
+ const processDetectionStamp = existingMeta.processDetection ?? toProcessDetectionStamp(processDetectionBudget);
1423
1441
  if (options.registryName) {
1424
1442
  await registerRepo(repoPath, existingMeta, {
1425
1443
  name: options.registryName,
@@ -1465,7 +1483,11 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1465
1483
  // documented Docker :ro workflow (#1549) — degrades to a warning.
1466
1484
  try {
1467
1485
  await adoptFlatBranchLabel(repoPath, branchLabel, storagePath);
1468
- await saveMeta(metaDir, { ...existingMeta, branch: branchLabel });
1486
+ await saveMeta(metaDir, {
1487
+ ...existingMeta,
1488
+ branch: branchLabel,
1489
+ processDetection: processDetectionStamp,
1490
+ });
1469
1491
  }
1470
1492
  catch (err) {
1471
1493
  // EACCES/EPERM also arise from ownership problems and transient
@@ -1478,12 +1500,26 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1478
1500
  // Discriminator-only restamp (flag↔env). `existingMeta` already
1479
1501
  // carries the folded skipReason; persist it without a write plan.
1480
1502
  try {
1481
- await saveMeta(metaDir, existingMeta);
1503
+ await saveMeta(metaDir, {
1504
+ ...existingMeta,
1505
+ processDetection: processDetectionStamp,
1506
+ });
1482
1507
  }
1483
1508
  catch (err) {
1484
1509
  log(`Warning: could not restamp the FTS skip reason (${formatMetaWriteFailureReason(err)}); will retry on the next run.`);
1485
1510
  }
1486
1511
  }
1512
+ else if (!existingMeta.processDetection) {
1513
+ try {
1514
+ await saveMeta(metaDir, {
1515
+ ...existingMeta,
1516
+ processDetection: processDetectionStamp,
1517
+ });
1518
+ }
1519
+ catch (err) {
1520
+ log(`Warning: could not backfill the process-detection stamp (${formatMetaWriteFailureReason(err)}); will retry on the next run.`);
1521
+ }
1522
+ }
1487
1523
  await ensureGitNexusIgnored(repoPath, storagePath);
1488
1524
  return {
1489
1525
  // `resolveRepoIdentityRoot` collapses worktree roots to the
@@ -1645,6 +1681,16 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1645
1681
  }, {
1646
1682
  parseCache,
1647
1683
  workerPoolSize: options.workerPoolSize,
1684
+ maxProcesses: processDetectionBudget.maxProcesses,
1685
+ maxProcessBranching: processDetectionBudget.overridden.maxProcessBranching
1686
+ ? processDetectionBudget.maxProcessBranching
1687
+ : undefined,
1688
+ maxProcessTraceDepth: processDetectionBudget.overridden.maxProcessTraceDepth
1689
+ ? processDetectionBudget.maxProcessTraceDepth
1690
+ : undefined,
1691
+ maxEntryPointCandidates: processDetectionBudget.overridden.maxEntryPointCandidates
1692
+ ? processDetectionBudget.maxEntryPointCandidates
1693
+ : undefined,
1648
1694
  // CFG/PDG opt-in (#2081 M1). PipelineOptions.pdg fans out to the worker
1649
1695
  // build gate (workerData.pdg) and the scope-resolution emit gate.
1650
1696
  pdg: options.pdg === true,
@@ -1762,7 +1808,8 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1762
1808
  const preserveDerivedLayer = skipDerivedGraphPhases &&
1763
1809
  isIncremental &&
1764
1810
  !!hashDiff &&
1765
- shouldPreservePersistedDerivedGraph(hashDiff);
1811
+ shouldPreservePersistedDerivedGraph(hashDiff) &&
1812
+ !processDetectionMismatch;
1766
1813
  if (skipDerivedGraphPhases && !preserveDerivedLayer) {
1767
1814
  progress('communities', 58, 'Detecting code communities and flows...');
1768
1815
  await pipelineResult.runDeferredDerivedPhases?.();
@@ -1836,17 +1883,21 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1836
1883
  `(skipping wipe + ${allFilePaths.length - hashDiff.toWrite.length} unchanged file rows preserved)`);
1837
1884
  // Set the dirty flag BEFORE any destructive DB mutation. Cleared on
1838
1885
  // success at the meta-save step. Scoped to this branch's meta.json.
1839
- const now = Date.now();
1840
- await saveMeta(metaDir, {
1841
- ...existingMeta,
1842
- incrementalInProgress: {
1843
- startedAt: now,
1844
- updatedAt: now,
1845
- phase: 'pre-write',
1846
- toWriteCount: hashDiff.toWrite.length,
1847
- directWriteCount: hashDiff.toWrite.length,
1848
- },
1849
- });
1886
+ // POSIX atomic incremental mutates the copy, so a live dirty stamp would
1887
+ // force-rebuild a healthy index after a crash before swap.
1888
+ if (!atomicIncremental) {
1889
+ const now = Date.now();
1890
+ await saveMeta(metaDir, {
1891
+ ...existingMeta,
1892
+ incrementalInProgress: {
1893
+ startedAt: now,
1894
+ updatedAt: now,
1895
+ phase: 'pre-write',
1896
+ toWriteCount: hashDiff.toWrite.length,
1897
+ directWriteCount: hashDiff.toWrite.length,
1898
+ },
1899
+ });
1900
+ }
1850
1901
  if (atomicIncremental) {
1851
1902
  // Stage the live index into the temp so the in-place delete/writeback
1852
1903
  // below mutates the COPY, and the end-of-run swap publishes it atomically.
@@ -2672,6 +2723,11 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
2672
2723
  lastCommit: '',
2673
2724
  indexedAt: new Date().toISOString(),
2674
2725
  };
2726
+ // #3322: persist uncertified *before* CREATE_FTS_INDEX. Park keeps this
2727
+ // stamp; it must not invent one on every FTS-only crash. Missing stamp +
2728
+ // shipped defaults is a match, so a budget-mismatch derived rewrite that
2729
+ // dies in FTS would otherwise recertify the rewritten Community/Process
2730
+ // rows on a flagless retry.
2675
2731
  await saveMeta(metaDir, {
2676
2732
  ...base,
2677
2733
  incrementalInProgress: buildFtsDirtyStamp({
@@ -2679,6 +2735,11 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
2679
2735
  writePlan: 'in-place',
2680
2736
  checkpointSucceeded: boundaryCheckpointSucceeded,
2681
2737
  }),
2738
+ ...(processDetectionMismatch
2739
+ ? {
2740
+ processDetection: uncertifyProcessDetectionStamp(base.processDetection),
2741
+ }
2742
+ : {}),
2682
2743
  });
2683
2744
  }
2684
2745
  // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
@@ -3550,6 +3611,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
3550
3611
  // stamp after an on→off flip; the next pdgModeMismatch then compares
3551
3612
  // off==off and incremental eligibility is restored.
3552
3613
  pdg: resolvePdgConfig(options),
3614
+ processDetection: toProcessDetectionStamp(processDetectionBudget),
3553
3615
  };
3554
3616
  // Re-resolve at the commit boundary. Long analyses can overlap an npm
3555
3617
  // upgrade, rebuilt dist tree, or native dependency replacement; stamping
@@ -607,6 +607,22 @@ export interface RepoMeta {
607
607
  */
608
608
  hasCallSummary?: boolean;
609
609
  };
610
+ /**
611
+ * The process-detection budget this index's Community/Process rows were
612
+ * built under (#3313). Compared on the next analyze so a budget-only
613
+ * config change re-detects flows without `--force`. `maxProcesses: null`
614
+ * means the dynamic `symbols / 10` formula. Absent on pre-#3313 metas:
615
+ * that absence matches default/dynamic knobs (backfill) and mismatches
616
+ * any explicit override.
617
+ */
618
+ processDetection?: {
619
+ maxProcesses: number | null;
620
+ maxProcessBranching: number;
621
+ maxProcessTraceDepth: number;
622
+ maxEntryPointCandidates: number;
623
+ /** Live Community/Process rows are not certified under this stamp (#3322). */
624
+ uncertified?: true;
625
+ };
610
626
  }
611
627
  /**
612
628
  * Resolve the configured storage path for a repository.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.16",
3
+ "version": "1.6.13-rc.17",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",