From 499f6c447426f029ba481712b872074ae69c9806 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:32:43 +0200 Subject: [PATCH] fix(hunt): label every verified target of a change; undo an inherited sparse-checkout in the worktree label_rows honoured only a change's primary site, so 43 of the 48 accepted access-control locations were unlabelled; every entry of targets is now a positive with its own label_ref and span. The training clones were emptied with sparse-checkout to save disk and a new worktree inherits it: every file read failed and the source and semantic producers reported an unread tree on all 599 rows. The runner now disables sparse-checkout in the worktree it adds. Co-Authored-By: Claude Fable 5.1 --- src/openultrasast/hunt/labels.py | 27 ++++++++++++++++++--------- src/openultrasast/hunt/runner.py | 14 ++++++++++++++ tests/test_hunt_labels.py | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/openultrasast/hunt/labels.py b/src/openultrasast/hunt/labels.py index a944c6c..5ba5968 100644 --- a/src/openultrasast/hunt/labels.py +++ b/src/openultrasast/hunt/labels.py @@ -16,21 +16,30 @@ def label_rows(rows: Sequence[Mapping[str, Any]], change: Mapping[str, Any]) -> raise ValueError("a label needs a traceable case_id") if kind == "vulnerability" and not all(change.get(k) for k in ("path", "function", "function_lines", "family")): raise ValueError("a vulnerability label needs its path, function, span and family") + # Every verified site of the change is a positive: the primary one and, when several accepted + # locations share the fix commit, each entry of ``targets``. + sites = [] + if kind == "vulnerability": + sites.append((change["path"], change["function"], list(change["function_lines"]), change["case_id"])) + for target in change.get("targets", []): + sites.append((target["path"], target["function"], list(target["function_lines"]), target.get("case_id", change["case_id"]))) result = [] for row in rows: data = deepcopy(dict(row)) - span = change.get("function_lines") function = row.get("function", row.get("feature.source.function")) - site = ( - kind == "vulnerability" - and row.get("path") == change["path"] - and (function == change["function"] or (span[0] <= row.get("line", 0) <= span[1])) + matched = next( + ( + site + for site in sites + if row.get("path") == site[0] and (function == site[1] or (site[2][0] <= row.get("line", 0) <= site[2][1])) + ), + None, ) data.update( - label=1 if site else 0 if kind == "ordinary" else None, - family=change["family"] if site else None, - fix_span=list(span) if site else None, - label_ref=change["case_id"] if site or kind == "ordinary" else None, + label=1 if matched else 0 if kind == "ordinary" else None, + family=change["family"] if matched else None, + fix_span=matched[2] if matched else None, + label_ref=matched[3] if matched else change["case_id"] if kind == "ordinary" else None, verification=None, ) result.append(data) diff --git a/src/openultrasast/hunt/runner.py b/src/openultrasast/hunt/runner.py index 603aaad..ce7d2e0 100644 --- a/src/openultrasast/hunt/runner.py +++ b/src/openultrasast/hunt/runner.py @@ -118,6 +118,7 @@ def hunt_change( try: _git(repo_root, "worktree", "add", "--detach", str(root), change["head"]) added = True + _full_checkout(root) if _git(root, "rev-parse", "HEAD").strip() != change["head"]: raise InstrumentFailure("head_mismatch") instrument["head_verified"] = True @@ -160,6 +161,19 @@ def hunt_change( return record +def _full_checkout(root: Path) -> None: + """Undo a sparse-checkout a worktree inherits from a clone whose working tree was emptied to save disk. + + Without this every file read fails and the producers report an unread tree, which must never pass as a + clean result.""" + try: + sparse = _git(root, "config", "--get", "core.sparseCheckout").strip() + except subprocess.CalledProcessError: + return + if sparse == "true": + _git(root, "sparse-checkout", "disable") + + def _function_at(root: Path, finding: Mapping[str, Any]) -> str | None: try: path = str(finding["path"]) diff --git a/tests/test_hunt_labels.py b/tests/test_hunt_labels.py index bdf6c93..5f47f47 100644 --- a/tests/test_hunt_labels.py +++ b/tests/test_hunt_labels.py @@ -45,3 +45,22 @@ def test_positive_requires_traceable_reference(): del change["case_id"] with pytest.raises(ValueError, match="case_id"): label_rows([], change) + + +def test_every_verified_target_of_a_change_is_a_positive(): + change = { + **positive(), + "case_id": "c1", + "targets": [ + {"case_id": "c1", "path": "app.py", "function": "handle", "function_lines": [10, 20]}, + {"case_id": "c2", "path": "lib.py", "function": "check", "function_lines": [3, 8]}, + ], + } + rows = [ + {"path": "app.py", "line": 12, "function": "handle"}, + {"path": "lib.py", "line": 5, "function": "check"}, + {"path": "lib.py", "line": 40, "function": "other"}, + ] + out = label_rows(rows, change) + assert [r["label"] for r in out] == [1, 1, None] + assert out[1]["label_ref"] == "c2" and out[1]["fix_span"] == [3, 8] and out[2]["label_ref"] is None