Skip to content

feat(analyzer): name the paths that filled the fingerprint byte budget - #506

Open
L4XB wants to merge 1 commit into
clay-good:mainfrom
L4XB:fix/504-fingerprint-budget-diagnostics
Open

L4XB wants to merge 1 commit into
clay-good:mainfrom
L4XB:fix/504-fingerprint-budget-diagnostics

Conversation

@L4XB

@L4XB L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

What & why

✗ Analysis failed: Project fingerprint byte budget exceeded (1073741824) names no path. The one question it raises, which files did this, is the one it does not answer, so the only way forward was to scan the filesystem by hand until the weight turned up. That is what #504 reports having to do.

The information was already in memory and being thrown away. By the time the read loop trips the budget, FileWalker has stat'd every admitted file, so the sizes are sitting in walk.files. The error now spends them:

Project fingerprint byte budget exceeded: the files selected for indexing total 4.2 GB,
over the 1 GB cap on how much one analysis will read (1073741824 bytes). This is a safety
cap in openlore, not a limit on your machine.

Largest contributors:
    3.4 GB  tmp/vecheck_data
  703.8 MB  docs/graph_vectors/index.bin
   38.1 MB  assets/demo.mp4
       2 KB  src/app.ts

Add the paths you do not want indexed to excludePatterns in .openlore/config.json, then re-run.

Three things about that list are decisions rather than mechanics:

  • The paths never overlap. Every file's size is rolled into each of its ancestor directories, then the totals are read from largest down, keeping an entry only when no ancestor or descendant of it has already been kept. That is what makes the list usable as input: each line is its own bytes, and no two entries claim the same weight.
  • Files compete with directories, and ties break toward the deeper path. The name that comes back is the most specific one that still accounts for the bytes. A vector store whose weight sits under tmp/vecheck_data is reported there rather than at tmp, and a lone oversized archive is reported as the file rather than as the directory that happens to hold it.
  • The repository root is never named. It is the largest subtree by construction, and naming it tells you only that your repository is large.

The totals cover the whole corpus rather than the prefix that had been read when the budget tripped, because they come from the walk and not from the read. Files are read in sorted path order, so the file that trips the budget is just the one that happens to cross the line; a list built from the read so far would be an accident of the alphabet. Where the walk hit its own maxFiles cap first the message says so, since the totals are then partial.

Refs #504.

Scope

The issue proposes four things. This PR does the two that are diagnostics:

  • Explicit error messaging (proposal 1) and automatic culprit reporting (proposal 2): done, above.
  • Expanding SKIP_EXTENSIONS with .lance, .parquet, .arrow (proposal 3): deliberately not here. It is not a diagnostic, it is a change to what every project indexes by default, and it fails silently in the direction that hides data rather than reports it. Someone whose repository legitimately contains .parquet would quietly stop having it analyzed with no message saying so. That deserves its own PR and a maintainer's call, and the diagnostic above makes the need for it visible in the meantime. Happy to open it separately if you want it.
  • openlore doctor integration (proposal 4): also left out, for the same reason plus size. doctor inspects infrastructure and would need to run a walk to say anything here.

So this is not a full Fixes #504, which is why the commit says Refs. Close the issue on merge if you consider the reporting half sufficient, or keep it open for proposals 3 and 4.

Checklist

  • npm run typecheck passes
  • npm run lint passes
  • npm run test:run passes
  • Added/updated tests for the change
  • If I touched src/core/analyzer/, src/core/generator/stages/, or src/core/services/mcp-handlers/: ran npm run test:e2e
  • If I added/changed an MCP tool or Pi-extension behavior: kept the two surfaces in parity (see CLAUDE.md)
  • Updated docs (README / docs/ / specs) if behavior or counts changed

Notes on the boxes above, because two of them need qualifying:

npm run test:run. 10134 passed, 60 failed, 12 skipped. The 60 sit in 8 files (blast-radius, enforce, impact-certificate, git-hooks, pi-surface, file-walker-corpus-boundary, atomic-store, progress) and all of them fail the same way on main: the base run in a clean worktree gives 10125 passed, 61 failed, 12 skipped in the same 8 files. Total tests differ by exactly the 8 added here (10198 → 10206). Running that set of 8 files alone gives byte-identical results on both trees (219 passed, 58 failed), so the 60-vs-61 wobble is test-ordering flakiness in the full run, not this diff. They look environmental to my machine: npm install skipped install scripts here, which several of those suites depend on (git hooks, native modules). I have not tried to fix them.

npm run test:e2e. Not run. It needs a built semantic index (openlore embed --local), which I do not have set up, and the suite auto-skips rather than fails without one, so running it would have proved nothing. Flagging it rather than ticking it, since this does touch src/core/services/mcp-handlers/. Worth a maintainer run before merge.

The last box is unticked because no behavior or counts changed for a passing analysis: the only difference is the text of an error that was already being thrown.

Notes for reviewers

Tests. The pair the change turns on is in describe('fingerprint byte budget diagnostics'): a tight cap over a tree whose weight sits in one subdirectory must come back naming that subdirectory, and the same tree under a cap it fits must produce no diagnostic at all. The first fails on main with expected 'Project fingerprint byte budget exceeded (1024)' to contain 'Largest contributors:'; the second passes on main too, which is the point of having it, since it pins that the diagnostic is a failure-path thing only. describe('largestCorpusPaths') covers the selection rules directly: deeper-path-wins, file-over-directory, no overlapping entries, the root never named, and the limit respected.

Where the constant lives. FINGERPRINT_BUDGET_TOP_OFFENDERS went into src/constants.ts per the convention in CONTRIBUTING. Note that DEFAULT_FINGERPRINT_MAX_FILES and DEFAULT_FINGERPRINT_MAX_BYTES next to it are still local to utils.ts; I left them alone rather than widen this diff, but they look like they belong in constants.ts too.

Plain Error, not errors.*. CONTRIBUTING points at the errors.* factories for user-facing errors, and I kept new Error(...) here to match the two sibling throws in the same function (path escaped repository, source changed while fingerprinting) and to avoid changing what callers catch. An OpenLoreError with a suggestion field would arguably render better; say the word and I will switch all three.

Multi-line messages do survive. I checked the path this actually takes to a user: analyze.ts interpolates error.message into logger.error, which passes newlines through to console.error, and formatError interpolates the message verbatim too. Nothing truncates at the first line.

A follow-up I did not take. The corpus total is known before a single byte is read, so the budget could fail immediately instead of after reading up to a gigabyte. I left the check where it is because moving it changes which error wins for a repository that is both over budget and has, say, a path escaping the root, and that is a behavior change rather than a diagnostic. Easy to do separately if you want the speed.

@L4XB
L4XB requested a review from clay-good as a code owner September 15, 2026 11:27
@L4XB

L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

The Windows gate is red on this branch, and from the run's own JSON report the two files it flags as new are timeouts on a slow runner rather than anything this diff reaches:

src/core/analyzer/index-bundle.test.ts
  refuses publication when a tracked non-corpus file dirties the source tree
  Error: Test timed out in 30000ms.

src/core/services/mcp-watcher-parity.test.ts
  path rename: post-barrier graph and served answer equal a fresh full oracle
  Error: Test timed out in 30000ms.

Both are timeouts, not assertion failures, and neither file imports anything this branch changes: the diff adds one constant to src/constants.ts and two exported helpers to src/core/services/mcp-handlers/utils.ts, all additive.

The same run shows the runner was slow throughout. src/core/decisions/atomic-store.test.ts, which the report already tracks as tolerated flaky, timed out at the same 30s in two places and once hit store lock: timed out after 30000ms ... (sustained write contention). src/api/analysis-status.test.ts, the deny-listed file, failed as well. And src/core/analyzer/parse-budget.test.ts took 179s in the same job. In the console output for the shard that ran both flagged files, vitest reported Test Files 13 passed (13), so both passed on a later attempt in the same job.

I have left .github/windows-unit-exclusions.json alone: adding entries to the flake deny-list from an unrelated PR seems like your call rather than mine. Happy to rebase or push again if you would rather see a clean run before reviewing.

`Project fingerprint byte budget exceeded (1073741824)` named no path, so the
one question it raised, which files did this, was the one it did not answer.
Users were left scanning the filesystem by hand to find the weight before they
could write an excludePatterns entry.

The walk has already stat'd every admitted file by the time the read loop trips
the budget, so the sizes needed to answer that are in memory and were being
discarded. The error now reports the largest non-overlapping paths in the
corpus, the corpus total against the cap, and what to do next.

Non-overlapping is what makes the list usable: every file's size is rolled into
each of its ancestor directories, then the totals are read from largest down,
keeping an entry only when no ancestor or descendant of it has been kept
already. Each line is therefore its own bytes and can go straight into
excludePatterns without two entries claiming the same weight. Files compete
with directories and ties break toward the deeper path, so the name that comes
back is the most specific one that still accounts for the bytes: a vector store
is reported at its own directory rather than at the repository root, and a lone
oversized archive as the file rather than the directory holding it.

The totals cover the whole corpus, not the prefix that had been read when the
budget tripped, because they come from the walk rather than from the read. Where
the walk hit its own maxFiles cap the message says so, since the totals are then
partial.

Refs clay-good#504.
@L4XB
L4XB force-pushed the fix/504-fingerprint-budget-diagnostics branch from 586320a to 7865ba3 Compare September 16, 2026 03:10
@L4XB

L4XB commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

The red Windows job is two 30-second timeouts, not assertion failures, and neither test touches what this PR changes.

From the run's own windows-unit-report artifact:

index-bundle: runImport trust boundary refuses publication when a tracked non-corpus file dirties the source tree
  Error: Test timed out in 30000ms.

incremental-full-repair semantic-answer parity gate path rename: post-barrier graph and served answer equal a fresh full oracle
  Error: Test timed out in 30000ms.

Both are long filesystem-heavy tests — a full import and a watcher parity gate — and the same run already reports src/core/decisions/atomic-store.test.ts as tolerated flaky, so the job is known to be timing-sensitive on that runner. The run also passed 10201 of 10206.

This PR adds one constant to src/constants.ts and a pure function plus its caller in mcp-handlers/utils.ts; no existing value changes and nothing in either failing test's path is touched. Locally both files pass in full:

npx vitest run src/core/services/mcp-watcher-parity.test.ts src/core/analyzer/index-bundle.test.ts
Test Files  2 passed (2)
     Tests  112 passed (112)

I cannot re-run the job from a fork, so I have pushed the same tree under a fresh SHA to get a clean result. If it comes back red on the same two files I will dig into them properly rather than assume flake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant