feat: make MAX_CALLABLE_VALUE_TARGETS configurable via env (#2725)

* feat(scope-resolution): make MAX_CALLABLE_VALUE_TARGETS configurable via env

The branch's original commit was a whole-file snapshot taken at a stale base
and never touched the constant, so the env read was missing and the branch's
own test failed. Implemented here, matching the sibling
GITNEXUS_MAX_PROPERTY_DISPATCH_FANOUT knob.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(callable-value-flow): add env override tests

* docs(callable-value-flow): document GITNEXUS_MAX_CALLABLE_VALUE_TARGETS env

Adds a Troubleshooting subsection to README.md and a commented entry to
gitnexus/.env.example for the new per-callable-site dispatch-target cap
(default 32), following the maintainer's review request to document the
knob alongside its implementation.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
ChunxueLi
2026-08-01 12:42:36 +01:00
committed by GitHub
co-authored by Gergo Magyar Claude Opus 5 Ubuntu Gergő Magyar
parent bebb1d2367
commit 99291891b7
4 changed files with 97 additions and 1 deletions
+7
View File
@@ -38,3 +38,10 @@
# Positive integer only — non-integer or < 1 values fall back to 32.
# See README § "Scope-resolution property-key dispatch cap".
# GITNEXUS_MAX_PROPERTY_DISPATCH_FANOUT=32
# Per-callable-site dispatch-target cap (default 32). Raise this for repos whose
# wide dispatch tables overflow the default and lose a whole call chain; analyze
# then logs "callable-value-flow: candidate set exceeded the cap". Positive
# integer only — non-integer or < 1 values fall back to 32.
# See README § "Scope-resolution dispatch-target cap".
# GITNEXUS_MAX_CALLABLE_VALUE_TARGETS=64
+27
View File
@@ -711,6 +711,33 @@ export GITNEXUS_MAX_PROPERTY_DISPATCH_FANOUT=64
npx gitnexus analyze --force
```
### Scope-resolution dispatch-target cap
During scope resolution GitNexus resolves calls that flow through *callable
values* — function/method references bound to variables, passed as arguments,
or stored in maps/tables. To keep that inclusion-based resolution finite, each
callable site is capped at **32 dispatch targets**. When a site gathers more
candidates than the cap it is treated as **overflowed** and *all* of its call
edges are dropped — a cliff, not a tail, so a repository with a legitimately
wide dispatch table (a single callable site resolving to 33+ targets) loses
that site's whole call chain. In that case `analyze` logs
`callable-value-flow: candidate set exceeded the cap; no partial CALLS emitted`
alongside a warning carrying the language, the overflowing context, the
candidate count, and the cap (32).
Raise the cap for such repositories:
| Variable | Default | Effect |
| ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GITNEXUS_MAX_CALLABLE_VALUE_TARGETS` | `32` | Per-callable-site dispatch-target cap in the callable-value-flow scope-resolution pass. Set to a positive integer to raise it for repositories whose wide dispatch tables overflow the default and lose a whole call chain; non-integer or `< 1` values fall back to `32`. Lowering it tightens the overflow budget. |
```bash
# A callable site resolving to 48 targets overflows the default 32 and drops
# the chain — raise the cap for that repo and rebuild so scope resolution reruns.
export GITNEXUS_MAX_CALLABLE_VALUE_TARGETS=64
npx gitnexus analyze --force
```
### Hook augmentation and skip diagnostics
The Claude Code / Antigravity hooks keep their **stderr** silent on normal skip
@@ -29,7 +29,17 @@ import { resolveInheritanceBaseInScope } from '../scope/walkers.js';
import { definitionIdPosition } from '../utils/definition-id.js';
import { narrowOverloadCandidates } from './overload-narrowing.js';
export const MAX_CALLABLE_VALUE_TARGETS = 32;
/**
* Per-site dispatch-target cap. Above it the site is treated as overflowed and
* its edges are dropped — a cliff, so a repo with a legitimately wide dispatch
* table (33+ candidates on one callable site) loses the whole call chain.
*
* Override via `GITNEXUS_MAX_CALLABLE_VALUE_TARGETS` for such repos.
*/
export const MAX_CALLABLE_VALUE_TARGETS = (() => {
const env = Number(process.env.GITNEXUS_MAX_CALLABLE_VALUE_TARGETS);
return Number.isInteger(env) && env >= 1 ? env : 32;
})();
interface Target {
readonly id: string;
@@ -0,0 +1,52 @@
import { describe, expect, it, afterEach } from 'vitest';
/**
* Verifies that MAX_CALLABLE_VALUE_TARGETS reads the
* GITNEXUS_MAX_CALLABLE_VALUE_TARGETS environment variable at module load time
* and falls back to the default (32) when unset.
*
* Because the constant is evaluated once at import time, we use a dynamic
* import per test to get a fresh module instance under each env configuration.
*/
const MODULE_PATH = '../../../src/core/ingestion/scope-resolution/passes/callable-value-flow.js';
async function loadWithEnv(envValue: string | undefined): Promise<number> {
// vitest module cache is keyed by path; vi.resetModules is needed for re-import
const { vi } = await import('vitest');
vi.resetModules();
if (envValue === undefined) {
delete process.env.GITNEXUS_MAX_CALLABLE_VALUE_TARGETS;
} else {
process.env.GITNEXUS_MAX_CALLABLE_VALUE_TARGETS = envValue;
}
const mod = await import(MODULE_PATH);
return mod.MAX_CALLABLE_VALUE_TARGETS;
}
describe('MAX_CALLABLE_VALUE_TARGETS env override', () => {
afterEach(() => {
delete process.env.GITNEXUS_MAX_CALLABLE_VALUE_TARGETS;
});
it('defaults to 32 when the env var is not set', async () => {
const cap = await loadWithEnv(undefined);
expect(cap).toBe(32);
});
it('respects a valid positive integer override', async () => {
const cap = await loadWithEnv('48');
expect(cap).toBe(48);
});
it('falls back to default for non-integer values', async () => {
expect(await loadWithEnv('abc')).toBe(32);
});
it('falls back to default for values below 1', async () => {
expect(await loadWithEnv('0')).toBe(32);
expect(await loadWithEnv('-5')).toBe(32);
});
});