From 9feb8f1e33e45632e8ac0983d5b3e6471cb483cb Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:47:08 +0200 Subject: [PATCH] fix(hunt): a row's entry points are the ones in its file or a caller's file, not the whole repository's list The DSPy arm over the second pool failed with LMInvalidRequestError on every change with more than two rows, at no cost: `source.facts_for` attached every entry point of the product tree to every row, 8190 records and 1.5 MB per row on apache/seatunnel (the first pool's arm lost 11 changes the same way on trigger.dev and shopper), and the rendered prompt was refused. The screen table of the second pool weighed 814 MB for the same reason. A row now carries the entry points declared in its own file or in a file that calls the function, which is what the input has described all along ("in-repository entry points that reach the file"). `source.entry_point_count` follows that definition. Co-Authored-By: Claude Fable 5.1 --- src/openultrasast/source.py | 9 ++++++++- tests/test_source.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/openultrasast/source.py b/src/openultrasast/source.py index 46d0c9a..bba06f7 100644 --- a/src/openultrasast/source.py +++ b/src/openultrasast/source.py @@ -483,6 +483,8 @@ def facts_for( ) -> SourceFacts: """Compute repository facts at a line; supplied entry points, product files and texts avoid repeated work. + The facts carry only the entry points declared in the file itself or in a file that calls the function. + A caller computing facts for many lines of one checkout (the hunter's feature table) walks the product tree and reads it once; without ``files`` and ``texts`` every call walked and read the whole tree again.""" root = Path(root) @@ -495,8 +497,13 @@ def facts_for( from .mapping import analyze_entry_points entry_points = analyze_entry_points(root, [build_file_target(root, root / file) for file in files]) + callers = callers_of(root, function, files=files, texts=texts, index=index) + # The entry points that reach the file: those declared in it or in a file that calls this function. The whole + # repository's list was attached before; on an application with 8000 entry points every row carried 1.5 MB + # and the hunter's prompt for any change with more than two rows was refused by the model. + reaching = {path, *(caller_path for caller_path, _ in callers)} return SourceFacts( - language, function, bounds(spans, function), callers_of(root, function, files=files, texts=texts, index=index), tuple(entry_points) + language, function, bounds(spans, function), callers, tuple(entry for entry in entry_points if entry.path in reaching) ) diff --git a/tests/test_source.py b/tests/test_source.py index f3772f7..8447a89 100644 --- a/tests/test_source.py +++ b/tests/test_source.py @@ -189,3 +189,19 @@ def test_generated_reason_names_build_output_only(tmp_path): assert source.generated_reason(tmp_path, "big.js") == "too_large" assert source.generated_reason(tmp_path, "app.js") is None assert source.generated_reason(tmp_path, "missing.js") is None + + +def test_facts_keep_only_the_entry_points_in_the_file_or_a_caller_file(tmp_path): + from openultrasast.mapping import EntryPointRecord + + (tmp_path / "app.py").write_text("def run(x):\n return x\n") + (tmp_path / "web.py").write_text("from app import run\n\ndef view():\n return run(1)\n") + (tmp_path / "far.py").write_text("def other():\n return 2\n") + + def entry(path): + return EntryPointRecord(path, 1, 2, None, path, "route", "public", "external", [], [], "test", "fixture") + + entries = [entry("app.py"), entry("web.py"), entry("far.py")] + facts = source.facts_for(tmp_path, "app.py", 2, entry_points=entries, files=["app.py", "web.py", "far.py"]) + assert facts.callers == (("web.py", 4),) + assert [e.path for e in facts.entry_points] == ["app.py", "web.py"]