fix(ingestion): surface skipped large-file paths by default (#1659) (#1661)

* fix(ingestion): surface skipped large-file paths by default (#1659)

The 512 KB skip threshold in filesystem-walker is necessary, but the
existing warning only said "Skipped N large files" with no paths unless
GITNEXUS_VERBOSE=1 was set. In a repo with one or two oversized first-
party source files (e.g. a 17K-line cron handler), every IMPORTS/CALLS
edge from that file silently disappeared and the surface looked like a
Python resolver bug. Issue #1659 was filed against the resolver for
exactly that reason, but the resolver was fine; the file was being
dropped before parse.

Changes:
  * Always print up to 5 skipped paths after the count line.
  * If more than 5 were skipped, append "...and N more" with a hint to
    set GITNEXUS_VERBOSE=1 for the full list.
  * When running at the default threshold, emit a one-line hint about
    GITNEXUS_MAX_FILE_SIZE=<KB> so operators know how to widen it.
  * Cover the new behavior with three additional tests in the existing
    filesystem-walker integration suite, plus a new describe block for
    the >5 preview-cap case.

Verified end-to-end on a 680-file Python repo that hit #1659: before
the patch, "Skipped 3 large files (>512KB, ...)" was the only signal
and impact upstream of a function called from cron.py returned 1 of 5
real callers; after the patch the cron file is listed by name with the
hint, and running with GITNEXUS_MAX_FILE_SIZE=1024 brings the missing
callers back (impactedCount 1 -> 9).

* fix(ingestion): address #1661 adversarial review follow-ups (F1/F2/F3)

Three non-blocking nits flagged by the adversarial review on #1661:

F1 (output stability) — skippedLargePaths was populated by concurrent
fs.stat callbacks in batches of 32, so push order within a batch was
completion-order rather than input-order. The default preview's "first
5" could vary across runs on the same repo. Fix: sort the array before
slicing. New test asserts the verbose output is in sorted order.

F2 (boundary coverage) — the preview-cap describe block created 8
large files, so the SKIPPED_PREVIEW_CAP = 5 comparison was never
exercised at the exact <= boundary. A future off-by-one (<= → <) would
not fail the suite. Fix: add two tests, one with exactly 5 files (all
listed, no truncation) and one with exactly 6 files (5 listed plus
"...and 1 more").

F3 (hint accuracy) — isDefault compared effective bytes, so an
operator who explicitly set GITNEXUS_MAX_FILE_SIZE=512 (the same KB as
the default) would still see the "Set GITNEXUS_MAX_FILE_SIZE=<KB>..."
hint. Fix: gate the hint on whether the env var is unset, not on the
resulting byte value. New test pins the explicit-default-value case.

All 34 filesystem-walker tests pass (was 30; +4 new). Prettier clean,
typecheck clean for the changed files.

---------

Co-authored-by: scotjelinski <58397194+scotjelinski@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
jelsco 2026-05-20 07:37:58 -06:00 • committed by GitHub
parent b9f7b39850
commit b61015693e
2 changed files with 190 additions and 4 deletions

View file

@ -74,12 +74,31 @@ export const walkRepositoryPaths = async (
if (skippedLarge > 0) {
const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES;
const isOverrideUnset = !process.env.GITNEXUS_MAX_FILE_SIZE;
const suffix = isDefault ? ', likely generated/vendored' : '';
logger.warn(` Skipped ${skippedLarge} large files (>${maxFileSizeBytes / 1024}KB${suffix})`);
if (isVerboseIngestionEnabled()) {
for (const p of skippedLargePaths) {
logger.warn(` - ${p}`);
}
// Always show at least the first few paths so users can diagnose why
// edges are missing from a specific file (issue #1659). The full list is
// gated behind GITNEXUS_VERBOSE=1 to avoid flooding output on repos with
// many generated/vendored blobs. Sort before slicing so the preview is
// stable across runs (fs.stat callbacks race within each batch).
skippedLargePaths.sort();
const SKIPPED_PREVIEW_CAP = 5;
const showAll = isVerboseIngestionEnabled() || skippedLargePaths.length <= SKIPPED_PREVIEW_CAP;
const preview = showAll ? skippedLargePaths : skippedLargePaths.slice(0, SKIPPED_PREVIEW_CAP);
for (const p of preview) {
logger.warn(` - ${p}`);
}
if (!showAll) {
const remaining = skippedLargePaths.length - SKIPPED_PREVIEW_CAP;
logger.warn(` ...and ${remaining} more (set GITNEXUS_VERBOSE=1 to list them all)`);
}
// Only hint about the env var when the user has not set it at all. An
// explicit GITNEXUS_MAX_FILE_SIZE=512 happens to resolve to the same
// bytes as the default but the operator clearly already knows the knob.
if (isDefault && isOverrideUnset) {
logger.warn(` Set GITNEXUS_MAX_FILE_SIZE=<KB> to include files above the default cap.`);
}
}

View file

@ -398,5 +398,172 @@ describe('filesystem-walker', () => {
expect(skipWarnings.length).toBeGreaterThan(0);
expect(String(skipWarnings[0].msg ?? '')).toContain('generated/vendored');
});
// Regression: issue #1659. The skipped-paths list and the
// GITNEXUS_MAX_FILE_SIZE hint must appear by default, otherwise users
// see "Skipped N large files" with no actionable detail and misdiagnose
// missing IMPORTS/CALLS edges as a resolver bug.
it('lists the skipped path by default (not gated behind GITNEXUS_VERBOSE)', async () => {
await walkRepositoryPaths(sizeDir);
const pathWarnings = cap.records().filter((r) => String(r.msg ?? '').includes(BIG_FILE));
expect(pathWarnings.length).toBeGreaterThan(0);
});
it('emits a GITNEXUS_MAX_FILE_SIZE hint when running with the default cap', async () => {
await walkRepositoryPaths(sizeDir);
const hint = cap
.records()
.filter((r) => String(r.msg ?? '').includes('GITNEXUS_MAX_FILE_SIZE=<KB>'));
expect(hint.length).toBe(1);
});
it('omits the GITNEXUS_MAX_FILE_SIZE hint when an override is active', async () => {
process.env.GITNEXUS_MAX_FILE_SIZE = '1';
await walkRepositoryPaths(sizeDir);
const hint = cap
.records()
.filter((r) => String(r.msg ?? '').includes('GITNEXUS_MAX_FILE_SIZE=<KB>'));
expect(hint.length).toBe(0);
});
// Edge case from the #1661 adversarial review: setting GITNEXUS_MAX_FILE_SIZE
// to the same value as the default (512KB) used to still print the hint
// because the byte comparison resolved to equal. The hint should care
// about whether the operator set the env var, not what value they chose.
it('omits the GITNEXUS_MAX_FILE_SIZE hint when the override equals the default value', async () => {
process.env.GITNEXUS_MAX_FILE_SIZE = '512';
await walkRepositoryPaths(sizeDir);
const hint = cap
.records()
.filter((r) => String(r.msg ?? '').includes('GITNEXUS_MAX_FILE_SIZE=<KB>'));
expect(hint.length).toBe(0);
});
});
describe('large file skip preview cap (#1659)', () => {
let manyDir: string;
const ORIGINAL_ENV = process.env.GITNEXUS_MAX_FILE_SIZE;
const ORIGINAL_VERBOSE = process.env.GITNEXUS_VERBOSE;
let cap: ReturnType<typeof _captureLogger>;
beforeAll(async () => {
manyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-size-many-'));
await fs.mkdir(path.join(manyDir, 'src'), { recursive: true });
// 8 files >512KB so the preview-cap path (5) is exercised.
for (let i = 0; i < 8; i++) {
await fs.writeFile(path.join(manyDir, 'src', `big${i}.ts`), 'x'.repeat(600 * 1024));
}
});
afterAll(async () => {
await fs.rm(manyDir, { recursive: true, force: true });
});
beforeEach(() => {
delete process.env.GITNEXUS_MAX_FILE_SIZE;
delete process.env.GITNEXUS_VERBOSE;
_resetMaxFileSizeWarnings();
cap = _captureLogger();
});
afterEach(() => {
if (ORIGINAL_ENV === undefined) {
delete process.env.GITNEXUS_MAX_FILE_SIZE;
} else {
process.env.GITNEXUS_MAX_FILE_SIZE = ORIGINAL_ENV;
}
if (ORIGINAL_VERBOSE === undefined) {
delete process.env.GITNEXUS_VERBOSE;
} else {
process.env.GITNEXUS_VERBOSE = ORIGINAL_VERBOSE;
}
cap.restore();
});
it('truncates the path list to 5 and mentions GITNEXUS_VERBOSE when over the cap', async () => {
await walkRepositoryPaths(manyDir);
const pathLines = cap.records().filter((r) => /^\s*-\s/.test(String(r.msg ?? '')));
expect(pathLines.length).toBe(5);
const more = cap
.records()
.filter((r) => String(r.msg ?? '').includes('and 3 more (set GITNEXUS_VERBOSE=1'));
expect(more.length).toBe(1);
});
// Boundary check from the #1661 adversarial review: the SKIPPED_PREVIEW_CAP
// comparison is `<=`, so 5 paths should list all five without a truncation
// line and 6 paths should list exactly five plus "...and 1 more". Tested
// explicitly so a future off-by-one refactor (`<=` → `<`) fails fast.
it('lists all paths and omits the truncation line at exactly 5 skipped files', async () => {
const fiveDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-size-five-'));
try {
await fs.mkdir(path.join(fiveDir, 'src'), { recursive: true });
for (let i = 0; i < 5; i++) {
await fs.writeFile(path.join(fiveDir, 'src', `big${i}.ts`), 'x'.repeat(600 * 1024));
}
await walkRepositoryPaths(fiveDir);
const pathLines = cap.records().filter((r) => /^\s*-\s/.test(String(r.msg ?? '')));
expect(pathLines.length).toBe(5);
const more = cap.records().filter((r) => String(r.msg ?? '').includes('...and '));
expect(more.length).toBe(0);
} finally {
await fs.rm(fiveDir, { recursive: true, force: true });
}
});
it('lists exactly 5 paths plus "...and 1 more" at exactly 6 skipped files', async () => {
const sixDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-size-six-'));
try {
await fs.mkdir(path.join(sixDir, 'src'), { recursive: true });
for (let i = 0; i < 6; i++) {
await fs.writeFile(path.join(sixDir, 'src', `big${i}.ts`), 'x'.repeat(600 * 1024));
}
await walkRepositoryPaths(sixDir);
const pathLines = cap.records().filter((r) => /^\s*-\s/.test(String(r.msg ?? '')));
expect(pathLines.length).toBe(5);
const more = cap
.records()
.filter((r) => String(r.msg ?? '').includes('and 1 more (set GITNEXUS_VERBOSE=1'));
expect(more.length).toBe(1);
} finally {
await fs.rm(sixDir, { recursive: true, force: true });
}
});
it('lists every skipped path when GITNEXUS_VERBOSE=1', async () => {
process.env.GITNEXUS_VERBOSE = '1';
await walkRepositoryPaths(manyDir);
const pathLines = cap.records().filter((r) => /^\s*-\s/.test(String(r.msg ?? '')));
expect(pathLines.length).toBe(8);
const more = cap.records().filter((r) => String(r.msg ?? '').includes('and '));
expect(more.length).toBe(0);
});
// Issue #1659 follow-up (PR #1661 review): paths were pushed in fs.stat
// completion order, so the default preview could vary between runs on
// the same repo. The implementation sorts skippedLargePaths before
// slicing, so the listed paths come out in sorted order, which is the
// stable contract operators can rely on.
it('lists skipped paths in sorted order (deterministic preview)', async () => {
process.env.GITNEXUS_VERBOSE = '1';
await walkRepositoryPaths(manyDir);
const pathLines = cap
.records()
.map((r) => String(r.msg ?? ''))
.filter((m) => /^\s*-\s/.test(m))
.map((m) => m.replace(/^\s*-\s*/, ''));
expect(pathLines).toEqual([...pathLines].sort());
// sanity-check we actually saw all 8 of the manyDir fixture
expect(pathLines).toEqual([
'src/big0.ts',
'src/big1.ts',
'src/big2.ts',
'src/big3.ts',
'src/big4.ts',
'src/big5.ts',
'src/big6.ts',
'src/big7.ts',
]);
});
});
});