fix(scan): attach change relationships in linear time

The third wall under the 13.3 replay, found by the streamed case log: the
evidence and context passes finished at 14:53, then nothing -- no engine
call, no log line -- for 82 minutes, until the case was killed 60 s past
its 7,200 s deadline. `affected_context` tested membership with
`relation not in relationships` on a list: 7,998,000 comparisons for 4,000
relationships (profiled, 83% of the time), and a push over a WordPress
plugin attaches tens of thousands. The deadline is checked once per
question, so a single late question could run for minutes, which is also
why the process overran its cancellation allowance.

Membership now goes through a set beside the ordered list, and each
distinct context path is resolved once instead of once per row. Measured:
4,000 relationships 7.4 s -> 0.44 s; 400,000 in 16 s. The control counts
comparisons rather than timing them, and fails on the old code with
7,998,000.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-23 18:33:26 +02:00
co-authored by Claude Opus 5.5
parent 982cb14115
commit e7e2941e37
2 changed files with 48 additions and 5 deletions
+21
View File
@@ -451,3 +451,24 @@ def test_the_evidence_pass_names_the_changed_files_to_the_engine(tmp_path):
assert asked and all("contextFilter" not in r for r in asked)
assert scan._changed_files(context("x\ny.php")) is None
assert scan._changed_files(context("source.php")) == ("source.php",)
def test_attaching_relationships_is_linear_in_what_it_attaches(tmp_path, monkeypatch):
"""Membership went through a list: 8 million comparisons for 4,000 relationships, and a push over a
WordPress plugin ran 82 minutes past its evidence pass and overran the deadline. Counted, not timed, so the
control cannot flake under load."""
from openultrasast.model.contracts import AffectedRelationship, QuestionIdentity
from openultrasast.model.regions import affected_context
(tmp_path / "source.php").write_text("x\n" * 400)
compared = []
original = AffectedRelationship.__eq__
monkeypatch.setattr(AffectedRelationship, "__eq__", lambda self, other: compared.append(1) or original(self, other))
questions = [QuestionIdentity("unit", "php", f"f{i}.php", "run", "injection") for i in range(200)]
rows = {
q: [{"kind": "context_summary"}] + [{**row("source.php", function=f"m{j}"), "startLine": 1, "endLine": 400} for j in range(20)]
for q in questions
}
enriched, _ = affected_context(context(), questions, rows, tmp_path)
assert len(enriched.relationships) == 4000
assert len(compared) < 4000, f"{len(compared)} comparisons for 4,000 relationships"