fix(types): mypy clean again over src; stub-less libraries and the benchmark imports handled in pyproject

66 errors had accumulated in src since the last green run (the hunt modules of groups 4-6, stats, candidates,
runner). Codex typed them without behaviour change; the 17 inline import-untyped ignores it added are replaced by
one override for dspy, pandas, statsmodels and scikit-learn, and the hunt package's lazy imports of benchmarks/
(never type-checked, not in files) are followed silently so src stays strict against their signatures.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-14 03:03:12 +02:00
co-authored by Claude Fable 5.1
parent 0d7d3f0dc9
commit c8f7610313
17 changed files with 78 additions and 36 deletions
+11
View File
@@ -90,6 +90,17 @@ ignore_missing_imports = true
module = "yaml" module = "yaml"
ignore_missing_imports = true ignore_missing_imports = true
# dspy, pandas, statsmodels and scikit-learn ship no py.typed marker and no stub packages are dev dependencies.
[[tool.mypy.overrides]]
module = ["dspy", "dspy.*", "pandas", "pandas.*", "statsmodels.*", "sklearn.*"]
ignore_missing_imports = true
# The hunt package reaches the benchmark scorers and manifests lazily. benchmarks/ is not in `files` and has never
# been type-checked; src stays strict against its signatures while errors inside benchmarks/ are not reported.
[[tool.mypy.overrides]]
module = "benchmarks.*"
follow_imports = "silent"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"dagger-io>=0.21.7", "dagger-io>=0.21.7",
+1 -1
View File
@@ -190,7 +190,7 @@ def run(
if table.digest() != manifest.table_digest: if table.digest() != manifest.table_digest:
raise ValueError("table digest does not match registration") raise ValueError("table digest does not match registration")
runner.refuse_pilot(Path(pool_manifest_path), pilot_report=False) runner.refuse_pilot(Path(pool_manifest_path), pilot_report=False)
folds: dict[str, tuple[str, int]] = {} folds: dict[str, tuple[str, int | None]] = {}
for row in table.rows: for row in table.rows:
key, value = row["change_id"], (row["repo"], row.get("fold")) key, value = row["change_id"], (row["repo"], row.get("fold"))
if key in folds and folds[key] != value: if key in folds and folds[key] != value:
+5 -4
View File
@@ -77,7 +77,7 @@ class Decision:
artifact.configurations.append(record) artifact.configurations.append(record)
def _record(value: Any) -> dict[str, Any]: def _record(value: Estimate | FoldResult | Mapping[str, Any]) -> dict[str, Any]:
"""Convert dataclass estimates and ordinary mappings to JSON records.""" """Convert dataclass estimates and ordinary mappings to JSON records."""
return asdict(value) if is_dataclass(value) else dict(value) return asdict(value) if is_dataclass(value) else dict(value)
@@ -124,7 +124,8 @@ def decide(
instructions = [state.get("signature", {}).get("instructions", "") for state in candidate.state.values()] instructions = [state.get("signature", {}).get("instructions", "") for state in candidate.state.values()]
if any(not isinstance(text, str) or len(text) > rule.instruction_cap for text in instructions): if any(not isinstance(text, str) or len(text) > rule.instruction_cap for text in instructions):
reasons.append("instruction_cap") reasons.append("instruction_cap")
compiled, heldout = {}, {} compiled: dict[str, dict[str, Any]] = {}
heldout: dict[str, dict[str, Any]] = {}
for scope, source, target in (("compile", compile_results, compiled), ("holdout", holdout_results, heldout)): for scope, source, target in (("compile", compile_results, compiled), ("holdout", holdout_results, heldout)):
for family, result in source.items(): for family, result in source.items():
record = _record(result) record = _record(result)
@@ -150,10 +151,10 @@ def decide(
reasons.append("no_positive_recall_difference") reasons.append("no_positive_recall_difference")
if _valid_number(holdout_diff) and abs(holdout_diff) <= rule.tolerance: if _valid_number(holdout_diff) and abs(holdout_diff) <= rule.tolerance:
reasons.append("holdout_tie") reasons.append("holdout_tie")
for family, result in heldout.items(): for family, heldout_record in heldout.items():
if family == "pooled": if family == "pooled":
continue continue
diff = result["recall"].get("diff") diff = heldout_record["recall"].get("diff")
floor = rule.regression_budget.get(family, rule.regression_budget.get("default", 0)) floor = rule.regression_budget.get(family, rule.regression_budget.get("default", 0))
if _valid_number(diff) and diff < floor: if _valid_number(diff) and diff < floor:
reasons.append(f"family_regression:{family}") reasons.append(f"family_regression:{family}")
+1 -1
View File
@@ -28,7 +28,7 @@ def _exhausted(usage: Mapping[str, float], budget: Mapping[str, float]) -> bool:
class RunArtifact: class RunArtifact:
program_id: str program_id: str
schema_version: int schema_version: int
mask: tuple[str, ...] mask: tuple[str, ...] | list[str]
data_digests: dict[str, str] data_digests: dict[str, str]
configurations: list[dict[str, Any]] configurations: list[dict[str, Any]]
usage: dict[str, float] usage: dict[str, float]
+3 -2
View File
@@ -4,13 +4,14 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import re
import tomllib import tomllib
from collections.abc import Iterable from collections.abc import Callable, Iterable
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
def _eligibility(): def _eligibility() -> tuple[re.Pattern[str], Callable[[str], str]]:
"""The benchmark's name normaliser and GitHub URL pattern, imported late so the package imports without the benchmarks tree.""" """The benchmark's name normaliser and GitHub URL pattern, imported late so the package imports without the benchmarks tree."""
from benchmarks.unseen.eligibility import URL, normalize from benchmarks.unseen.eligibility import URL, normalize
+1 -1
View File
@@ -234,7 +234,7 @@ def _roles(function: Any, facts: Any) -> dict[str, str]:
for call in function.calls: for call in function.calls:
for arguments in call.arg_names: for arguments in call.arg_names:
names.update(arguments) names.update(arguments)
roles = {} roles: dict[str, str] = {}
for role, entries, attribute in ( for role, entries, attribute in (
("source", facts.sources, "patterns"), ("source", facts.sources, "patterns"),
("sink", facts.sinks, "calls"), ("sink", facts.sinks, "calls"),
+5 -2
View File
@@ -67,7 +67,8 @@ def signature_for(mask: Iterable[str] = ()) -> type[dspy.Signature]:
continue continue
fields[name] = (str, dspy.InputField(desc=desc)) fields[name] = (str, dspy.InputField(desc=desc))
fields["findings"] = (list[Finding], dspy.OutputField(desc="JSON list; empty when the change is ordinary")) fields["findings"] = (list[Finding], dspy.OutputField(desc="JSON list; empty when the change is ordinary"))
return dspy.Signature(fields, BASELINE_INSTRUCTION) signature: type[dspy.Signature] = dspy.Signature(fields, BASELINE_INSTRUCTION)
return signature
def numbered(lines: Sequence[str], start: int, end: int) -> str: def numbered(lines: Sequence[str], start: int, end: int) -> str:
@@ -194,7 +195,9 @@ class _Loop(dspy.ReAct):
loop_parse_failed = False loop_parse_failed = False
def _call_with_potential_trajectory_truncation(self, module, trajectory, **input_args): def _call_with_potential_trajectory_truncation(
self, module: dspy.Module, trajectory: dict[str, Any], **input_args: str
) -> dspy.Prediction:
try: try:
return super()._call_with_potential_trajectory_truncation(module, trajectory, **input_args) return super()._call_with_potential_trajectory_truncation(module, trajectory, **input_args)
except dspy.utils.exceptions.AdapterParseError as error: except dspy.utils.exceptions.AdapterParseError as error:
+12 -4
View File
@@ -20,7 +20,7 @@ from .metric import search_metric, search_outcome
from .table import FEATURE_COLUMNS, canonical_json from .table import FEATURE_COLUMNS, canonical_json
class _Optimisers(Mapping): class _Optimisers(Mapping[str, type]):
"""Load a DSPy optimiser class only when its registry entry is requested.""" """Load a DSPy optimiser class only when its registry entry is requested."""
names = {"gepa": "GEPA", "miprov2": "MIPROv2", "bootstrap": "BootstrapFewShot"} names = {"gepa": "GEPA", "miprov2": "MIPROv2", "bootstrap": "BootstrapFewShot"}
@@ -28,7 +28,8 @@ class _Optimisers(Mapping):
def __getitem__(self, name: str) -> type: def __getitem__(self, name: str) -> type:
import dspy import dspy
return getattr(dspy, self.names[name]) optimiser: type = getattr(dspy, self.names[name])
return optimiser
def __iter__(self) -> Iterator[str]: def __iter__(self) -> Iterator[str]:
return iter(self.names) return iter(self.names)
@@ -93,7 +94,8 @@ def compile_examples(
from .program import INPUTS from .program import INPUTS
repo_folds, ids = {}, set() repo_folds: dict[str, int] = {}
ids: set[str] = set()
for change in changes: for change in changes:
if change["id"] in ids: if change["id"] in ids:
raise ValueError("change IDs must be unique") raise ValueError("change IDs must be unique")
@@ -230,7 +232,13 @@ def _compile(round: Round, program: Any, examples: list, optimiser: Any, metric:
if dspy.settings.lm is None: if dspy.settings.lm is None:
raise ValueError("GEPA needs a configured reflection model in dspy.context") raise ValueError("GEPA needs a configured reflection model in dspy.context")
def feedback(example, prediction, trace=None, pred_name=None, pred_trace=None): def feedback(
example: dspy.Example,
prediction: dspy.Prediction,
trace: Any = None,
pred_name: str | None = None,
pred_trace: Any = None,
) -> dspy.Prediction:
"""Give GEPA the same score and the metric's compile-only feedback.""" """Give GEPA the same score and the metric's compile-only feedback."""
import dspy import dspy
+6 -6
View File
@@ -90,7 +90,7 @@ def _instrument(records: Sequence[Mapping]) -> dict:
) )
def _clean(records: Sequence[Mapping]) -> list[dict]: def _clean(records: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
"""Remove stale findings on failures while preserving the scorer's coverage checks.""" """Remove stale findings on failures while preserving the scorer's coverage checks."""
from benchmarks.unseen import score from benchmarks.unseen import score
@@ -109,7 +109,7 @@ def _clean(records: Sequence[Mapping]) -> list[dict]:
return result return result
def _rate(values: list[float], repos: list[str], *, binary: bool = False, scale: float = 1) -> dict: def _rate(values: Sequence[float], repos: list[str], *, binary: bool = False, scale: float = 1) -> dict:
"""Report a mean with its count and a repository-clustered interval, plus Wilson for proportions.""" """Report a mean with its count and a repository-clustered interval, plus Wilson for proportions."""
from benchmarks.unseen import score from benchmarks.unseen import score
@@ -127,7 +127,7 @@ def _rate(values: list[float], repos: list[str], *, binary: bool = False, scale:
return result return result
def _family_records(records: Sequence[Mapping], family: str | None) -> list[dict]: def _family_records(records: Sequence[dict[str, Any]], family: str | None) -> list[dict[str, Any]]:
"""Keep a family's positives and count only its findings over all ordinary pushes.""" """Keep a family's positives and count only its findings over all ordinary pushes."""
result = [] result = []
for original in records: for original in records:
@@ -217,7 +217,7 @@ def _baseline_comparison(baselines: list[list[dict]], records: list[dict], famil
from benchmarks.unseen import score from benchmarks.unseen import score
from benchmarks.valid import metrics from benchmarks.valid import metrics
units = [ units: list[dict[str, Any]] = [
dict(change=after, repeat=repeat, a=_outcome(before, before, 10), b=_outcome(after, after, 10)) dict(change=after, repeat=repeat, a=_outcome(before, before, 10), b=_outcome(after, after, 10))
for repeat, baseline in enumerate(baselines) for repeat, baseline in enumerate(baselines)
for before, after in zip(baseline, records, strict=True) for before, after in zip(baseline, records, strict=True)
@@ -232,7 +232,7 @@ def _baseline_comparison(baselines: list[list[dict]], records: list[dict], famil
suspicions_per_push=asdict(suspicions), suspicions_per_push=asdict(suspicions),
mcnemar=asdict(stats.mcnemar_exact([u["a"]["hit"] for u in positives], [u["b"]["hit"] for u in positives])), mcnemar=asdict(stats.mcnemar_exact([u["a"]["hit"] for u in positives], [u["b"]["hit"] for u in positives])),
) )
rows = {"recall": [], "false_alarms_per_100_pushes": [], "precision": []} rows: dict[str, list[dict[str, Any]]] = {"recall": [], "false_alarms_per_100_pushes": [], "precision": []}
for baseline in baselines: for baseline in baselines:
before = metrics.scoring_records(_family_records(baseline, family)) before = metrics.scoring_records(_family_records(baseline, family))
after = metrics.scoring_records(_family_records(records, family)) after = metrics.scoring_records(_family_records(records, family))
@@ -426,7 +426,7 @@ def pilot_gain(
records = _records(directory / "records", changes) records = _records(directory / "records", changes)
proof = _instrument(records) proof = _instrument(records)
_write(directory / "instrument.json", proof) _write(directory / "instrument.json", proof)
analysis = dict( analysis: dict[str, Any] = dict(
program_id=candidate.program_id, program_id=candidate.program_id,
comparison=decision.comparison, comparison=decision.comparison,
measurement_directory=directory.name, measurement_directory=directory.name,
+5 -3
View File
@@ -21,7 +21,8 @@ def _fold_changes(pool_manifest: Path, table: FeatureTable, load_changes: Any) -
from benchmarks.unseen.replay import load_slice from benchmarks.unseen.replay import load_slice
runner.refuse_pilot(pool_manifest, pilot_report=False) runner.refuse_pilot(pool_manifest, pilot_report=False)
folds, repo_folds = {}, {} folds: dict[str, tuple[str, int]] = {}
repo_folds: dict[str, int] = {}
for row in table.rows: for row in table.rows:
key, repo, fold = row["change_id"], row["repo"], row.get("fold") key, repo, fold = row["change_id"], row["repo"], row.get("fold")
if type(fold) is not int or fold < 0: if type(fold) is not int or fold < 0:
@@ -183,7 +184,8 @@ def fold_results(
if len({c["id"] for c in changes}) != len(changes): if len({c["id"] for c in changes}) != len(changes):
raise ValueError("change IDs must be unique") raise ValueError("change IDs must be unique")
directories = {"a": Path(current_dir), "b": Path(candidate_dir)} directories = {"a": Path(current_dir), "b": Path(candidate_dir)}
studies, repeats = {}, set() studies: dict[str, Any] = {}
repeats: set[int] = set()
for arm, directory in directories.items(): for arm, directory in directories.items():
path = directory / "study.json" path = directory / "study.json"
if path.exists(): if path.exists():
@@ -291,7 +293,7 @@ def round_trip(
) )
) )
results[scope] = fold_results( results[scope] = fold_results(
*paths, [c for c in changes if c["fold"] in folds], scope=scope, tolerance=tolerance, seed=seed, resamples=resamples paths[0], paths[1], [c for c in changes if c["fold"] in folds], scope=scope, tolerance=tolerance, seed=seed, resamples=resamples
) )
decision = accept.decide(current_id, candidate, results["compile"], results["holdout"], rule) decision = accept.decide(current_id, candidate, results["compile"], results["holdout"], rule)
if not decision.accepted: if not decision.accepted:
+12 -3
View File
@@ -4,6 +4,7 @@ import os
import re import re
from collections.abc import Iterator from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from typing import NotRequired, TypedDict
from .preprocess import enumerate_source_files from .preprocess import enumerate_source_files
from .source import callers_of, product_files from .source import callers_of, product_files
@@ -67,11 +68,19 @@ def _mtime(path: Path) -> int:
return -1 return -1
def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[dict[str, object]]: class ToolMatch(TypedDict):
path: str
line: int
text: str
name: NotRequired[str]
source: NotRequired[str]
def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[ToolMatch]:
if max_matches <= 0: if max_matches <= 0:
return [] return []
compiled = re.compile(pattern) compiled = re.compile(pattern)
matches: list[dict[str, object]] = [] matches: list[ToolMatch] = []
for relative, lines in _source_lines(root): for relative, lines in _source_lines(root):
for line_no, line in enumerate(lines, start=1): for line_no, line in enumerate(lines, start=1):
if compiled.search(line): if compiled.search(line):
@@ -81,7 +90,7 @@ def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[dict[str, o
return matches return matches
def find_refs(root: Path, symbol: str) -> list[dict[str, object]]: def find_refs(root: Path, symbol: str) -> list[ToolMatch]:
if not symbol: if not symbol:
return [] return []
lines = dict(_source_lines(root)) lines = dict(_source_lines(root))
+5 -2
View File
@@ -752,8 +752,11 @@ class LabelBuilder:
if not function: if not function:
lines = self._clone(population, str(case["id"])).lines(str(case["vulnerable"]), site_path) lines = self._clone(population, str(case["id"])).lines(str(case["vulnerable"]), site_path)
language = detect_language(Path(site_path)) language = detect_language(Path(site_path))
covered = lines and language in DECLARATION and line <= len(lines) function = (
function = enclosing(lines, line - 1, language) if covered else MODULE_FUNCTION enclosing(lines, line - 1, language)
if lines and language in DECLARATION and line <= len(lines)
else MODULE_FUNCTION
)
self._row( self._row(
candidate=f"{site_path}::{function}", family=str(row.get("family") or case["family"]), label=label, source=source.id, candidate=f"{site_path}::{function}", family=str(row.get("family") or case["family"]), label=label, source=source.id,
source_ref=f"{Path(path).name}#{key}[{index}]", unit="pin", weight=1.0, split=population.id, source_ref=f"{Path(path).name}#{key}[{index}]", unit="pin", weight=1.0, split=population.id,
+3 -2
View File
@@ -20,7 +20,7 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from ..source import canonical_language from ..source import FunctionRange, canonical_language
if TYPE_CHECKING: if TYPE_CHECKING:
from .taxonomy import FamilyTaxonomy from .taxonomy import FamilyTaxonomy
@@ -368,7 +368,7 @@ def labeled_family(case: object, taxonomy: FamilyTaxonomy | None = None) -> str:
def labeled_spans( def labeled_spans(
case: object, ranges: Mapping[str, Sequence[tuple[str, int, int]]], *, function: str = "" case: object, ranges: Mapping[str, Sequence[tuple[str, int, int] | FunctionRange]], *, function: str = ""
) -> tuple[tuple[str, int, int], ...]: ) -> tuple[tuple[str, int, int], ...]:
"""The (path, start, end) spans of the labeled functions; ``function`` overrides the label for the fixed side. """The (path, start, end) spans of the labeled functions; ``function`` overrides the label for the fixed side.
@@ -378,6 +378,7 @@ def labeled_spans(
wanted = {function} if function else {str(getattr(row, "function", "") or "") for row in getattr(case, "expected", ()) or ()} wanted = {function} if function else {str(getattr(row, "function", "") or "") for row in getattr(case, "expected", ()) or ()}
wanted.discard("") wanted.discard("")
found: list[tuple[str, int, int]] = [] found: list[tuple[str, int, int]] = []
_kind: list[str]
for path, entries in ranges.items(): for path, entries in ranges.items():
for name, start, end, *_kind in entries: for name, start, end, *_kind in entries:
if name in wanted: if name in wanted:
+2 -2
View File
@@ -704,7 +704,7 @@ def safe_url(url: str) -> str:
def deliver( def deliver(
url: str, url: str,
payload: bytes, payload: bytes,
headers: Mapping[str, str], headers: MutableMapping[str, str],
*, *,
attempts: int = DELIVERY_ATTEMPTS, attempts: int = DELIVERY_ATTEMPTS,
backoff: float = 1.0, backoff: float = 1.0,
@@ -732,7 +732,7 @@ def deliver(
raise DeliveryError(f"{last} after {attempts} attempts to {safe_url(url)}") raise DeliveryError(f"{last} after {attempts} attempts to {safe_url(url)}")
def fetch(url: str, headers: Mapping[str, str], dest: Path, *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0) -> int: def fetch(url: str, headers: MutableMapping[str, str], dest: Path, *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0) -> int:
"""GET ``url`` into ``dest`` (streamed to a sibling temp file, then renamed); retried like :func:`deliver`. """GET ``url`` into ``dest`` (streamed to a sibling temp file, then renamed); retried like :func:`deliver`.
Returns the byte count; :class:`DeliveryError` names the last cause when every attempt failed.""" Returns the byte count; :class:`DeliveryError` names the last cause when every attempt failed."""
+4 -2
View File
@@ -409,9 +409,11 @@ def _side(side: Side, demo: Path, family: str, timeout: int, browser: BrowserExe
if task_boundary: if task_boundary:
# Files are app data, so resolve their paths before writing; # Files are app data, so resolve their paths before writing;
# the task runner only translates argv and environment. # the task runner only translates argv and environment.
def fixture_path(_: re.Match[str], path: str = str(canary.root)) -> str:
return path
file_bindings = { file_bindings = {
key: re.sub(r"/(?:fixture|scratch)(?=/|$)", lambda _, path=str(canary.root): path, value) key: re.sub(r"/(?:fixture|scratch)(?=/|$)", fixture_path, value) for key, value in bindings.items()
for key, value in bindings.items()
} }
private_values.extend((*bindings.values(), *file_bindings.values())) private_values.extend((*bindings.values(), *file_bindings.values()))
write_start_files(checkout, start_spec, file_bindings) write_start_files(checkout, start_spec, file_bindings)
+1
View File
@@ -307,6 +307,7 @@ _HEAD = re.compile(
def _regex_ranges(text: str, language: str) -> tuple[FunctionRange, ...]: def _regex_ranges(text: str, language: str) -> tuple[FunctionRange, ...]:
lines, code = text.splitlines(), _code_lines(text) lines, code = text.splitlines(), _code_lines(text)
result = [] result = []
end: int | None
for index, line in enumerate(code): for index, line in enumerate(code):
name = declarator_name(line, language) name = declarator_name(line, language)
if language == "python": if language == "python":
+1 -1
View File
@@ -167,7 +167,7 @@ def separation(frame: Any, label: str, columns: Iterable[str]) -> Any:
import pandas as pd import pandas as pd
from sklearn.metrics import roc_auc_score from sklearn.metrics import roc_auc_score
rows = [] rows: list[dict[str, str | float | None]] = []
labelled = frame[frame[label].isin([0, 1])] labelled = frame[frame[label].isin([0, 1])]
for column in columns: for column in columns:
series = pd.to_numeric(labelled[column], errors="coerce") if column in labelled else None series = pd.to_numeric(labelled[column], errors="coerce") if column in labelled else None