From c8f761031383e46cbf4467c71d348eea3520cb01 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:03:12 +0200 Subject: [PATCH] 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 --- pyproject.toml | 11 +++++++++++ src/openultrasast/hunt/ablation.py | 2 +- src/openultrasast/hunt/accept.py | 9 +++++---- src/openultrasast/hunt/artifact.py | 2 +- src/openultrasast/hunt/contamination.py | 5 +++-- src/openultrasast/hunt/producers.py | 2 +- src/openultrasast/hunt/program.py | 7 +++++-- src/openultrasast/hunt/propose.py | 16 ++++++++++++---- src/openultrasast/hunt/report.py | 12 ++++++------ src/openultrasast/hunt/rounds.py | 8 +++++--- src/openultrasast/hunter_tools.py | 15 ++++++++++++--- src/openultrasast/learn/labels.py | 7 +++++-- src/openultrasast/model/candidates.py | 5 +++-- src/openultrasast/plane/runner.py | 4 ++-- src/openultrasast/search/verify.py | 6 ++++-- src/openultrasast/source.py | 1 + src/openultrasast/stats.py | 2 +- 17 files changed, 78 insertions(+), 36 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 996147a..732ce96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,17 @@ ignore_missing_imports = true module = "yaml" 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] dev = [ "dagger-io>=0.21.7", diff --git a/src/openultrasast/hunt/ablation.py b/src/openultrasast/hunt/ablation.py index 07e4fc6..c56469c 100644 --- a/src/openultrasast/hunt/ablation.py +++ b/src/openultrasast/hunt/ablation.py @@ -190,7 +190,7 @@ def run( if table.digest() != manifest.table_digest: raise ValueError("table digest does not match registration") 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: key, value = row["change_id"], (row["repo"], row.get("fold")) if key in folds and folds[key] != value: diff --git a/src/openultrasast/hunt/accept.py b/src/openultrasast/hunt/accept.py index 0891f24..bfa5801 100644 --- a/src/openultrasast/hunt/accept.py +++ b/src/openultrasast/hunt/accept.py @@ -77,7 +77,7 @@ class Decision: 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.""" 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()] if any(not isinstance(text, str) or len(text) > rule.instruction_cap for text in instructions): 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 family, result in source.items(): record = _record(result) @@ -150,10 +151,10 @@ def decide( reasons.append("no_positive_recall_difference") if _valid_number(holdout_diff) and abs(holdout_diff) <= rule.tolerance: reasons.append("holdout_tie") - for family, result in heldout.items(): + for family, heldout_record in heldout.items(): if family == "pooled": continue - diff = result["recall"].get("diff") + diff = heldout_record["recall"].get("diff") floor = rule.regression_budget.get(family, rule.regression_budget.get("default", 0)) if _valid_number(diff) and diff < floor: reasons.append(f"family_regression:{family}") diff --git a/src/openultrasast/hunt/artifact.py b/src/openultrasast/hunt/artifact.py index bf6cb90..f25a1ed 100644 --- a/src/openultrasast/hunt/artifact.py +++ b/src/openultrasast/hunt/artifact.py @@ -28,7 +28,7 @@ def _exhausted(usage: Mapping[str, float], budget: Mapping[str, float]) -> bool: class RunArtifact: program_id: str schema_version: int - mask: tuple[str, ...] + mask: tuple[str, ...] | list[str] data_digests: dict[str, str] configurations: list[dict[str, Any]] usage: dict[str, float] diff --git a/src/openultrasast/hunt/contamination.py b/src/openultrasast/hunt/contamination.py index 1c56e6f..9b29260 100644 --- a/src/openultrasast/hunt/contamination.py +++ b/src/openultrasast/hunt/contamination.py @@ -4,13 +4,14 @@ from __future__ import annotations import hashlib import json +import re import tomllib -from collections.abc import Iterable +from collections.abc import Callable, Iterable from datetime import UTC, datetime 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.""" from benchmarks.unseen.eligibility import URL, normalize diff --git a/src/openultrasast/hunt/producers.py b/src/openultrasast/hunt/producers.py index 1899a02..4a0f037 100644 --- a/src/openultrasast/hunt/producers.py +++ b/src/openultrasast/hunt/producers.py @@ -234,7 +234,7 @@ def _roles(function: Any, facts: Any) -> dict[str, str]: for call in function.calls: for arguments in call.arg_names: names.update(arguments) - roles = {} + roles: dict[str, str] = {} for role, entries, attribute in ( ("source", facts.sources, "patterns"), ("sink", facts.sinks, "calls"), diff --git a/src/openultrasast/hunt/program.py b/src/openultrasast/hunt/program.py index 822efcf..aef1275 100644 --- a/src/openultrasast/hunt/program.py +++ b/src/openultrasast/hunt/program.py @@ -67,7 +67,8 @@ def signature_for(mask: Iterable[str] = ()) -> type[dspy.Signature]: continue fields[name] = (str, dspy.InputField(desc=desc)) 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: @@ -194,7 +195,9 @@ class _Loop(dspy.ReAct): 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: return super()._call_with_potential_trajectory_truncation(module, trajectory, **input_args) except dspy.utils.exceptions.AdapterParseError as error: diff --git a/src/openultrasast/hunt/propose.py b/src/openultrasast/hunt/propose.py index c650a1d..8f64a5c 100644 --- a/src/openultrasast/hunt/propose.py +++ b/src/openultrasast/hunt/propose.py @@ -20,7 +20,7 @@ from .metric import search_metric, search_outcome 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.""" names = {"gepa": "GEPA", "miprov2": "MIPROv2", "bootstrap": "BootstrapFewShot"} @@ -28,7 +28,8 @@ class _Optimisers(Mapping): def __getitem__(self, name: str) -> type: import dspy - return getattr(dspy, self.names[name]) + optimiser: type = getattr(dspy, self.names[name]) + return optimiser def __iter__(self) -> Iterator[str]: return iter(self.names) @@ -93,7 +94,8 @@ def compile_examples( from .program import INPUTS - repo_folds, ids = {}, set() + repo_folds: dict[str, int] = {} + ids: set[str] = set() for change in changes: if change["id"] in ids: 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: 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.""" import dspy diff --git a/src/openultrasast/hunt/report.py b/src/openultrasast/hunt/report.py index d70a50a..bc1819a 100644 --- a/src/openultrasast/hunt/report.py +++ b/src/openultrasast/hunt/report.py @@ -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.""" from benchmarks.unseen import score @@ -109,7 +109,7 @@ def _clean(records: Sequence[Mapping]) -> list[dict]: 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.""" from benchmarks.unseen import score @@ -127,7 +127,7 @@ def _rate(values: list[float], repos: list[str], *, binary: bool = False, scale: 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.""" result = [] 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.valid import metrics - units = [ + units: list[dict[str, Any]] = [ dict(change=after, repeat=repeat, a=_outcome(before, before, 10), b=_outcome(after, after, 10)) for repeat, baseline in enumerate(baselines) 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), 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: before = metrics.scoring_records(_family_records(baseline, family)) after = metrics.scoring_records(_family_records(records, family)) @@ -426,7 +426,7 @@ def pilot_gain( records = _records(directory / "records", changes) proof = _instrument(records) _write(directory / "instrument.json", proof) - analysis = dict( + analysis: dict[str, Any] = dict( program_id=candidate.program_id, comparison=decision.comparison, measurement_directory=directory.name, diff --git a/src/openultrasast/hunt/rounds.py b/src/openultrasast/hunt/rounds.py index ef0adaa..598ccb6 100644 --- a/src/openultrasast/hunt/rounds.py +++ b/src/openultrasast/hunt/rounds.py @@ -21,7 +21,8 @@ def _fold_changes(pool_manifest: Path, table: FeatureTable, load_changes: Any) - from benchmarks.unseen.replay import load_slice 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: key, repo, fold = row["change_id"], row["repo"], row.get("fold") 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): raise ValueError("change IDs must be unique") 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(): path = directory / "study.json" if path.exists(): @@ -291,7 +293,7 @@ def round_trip( ) ) 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) if not decision.accepted: diff --git a/src/openultrasast/hunter_tools.py b/src/openultrasast/hunter_tools.py index 1e2efa4..9673d36 100644 --- a/src/openultrasast/hunter_tools.py +++ b/src/openultrasast/hunter_tools.py @@ -4,6 +4,7 @@ import os import re from collections.abc import Iterator from pathlib import Path +from typing import NotRequired, TypedDict from .preprocess import enumerate_source_files from .source import callers_of, product_files @@ -67,11 +68,19 @@ def _mtime(path: Path) -> int: 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: return [] compiled = re.compile(pattern) - matches: list[dict[str, object]] = [] + matches: list[ToolMatch] = [] for relative, lines in _source_lines(root): for line_no, line in enumerate(lines, start=1): if compiled.search(line): @@ -81,7 +90,7 @@ def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[dict[str, o 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: return [] lines = dict(_source_lines(root)) diff --git a/src/openultrasast/learn/labels.py b/src/openultrasast/learn/labels.py index 1eb60bb..cf40188 100644 --- a/src/openultrasast/learn/labels.py +++ b/src/openultrasast/learn/labels.py @@ -752,8 +752,11 @@ class LabelBuilder: if not function: lines = self._clone(population, str(case["id"])).lines(str(case["vulnerable"]), site_path) language = detect_language(Path(site_path)) - covered = lines and language in DECLARATION and line <= len(lines) - function = enclosing(lines, line - 1, language) if covered else MODULE_FUNCTION + function = ( + enclosing(lines, line - 1, language) + if lines and language in DECLARATION and line <= len(lines) + else MODULE_FUNCTION + ) self._row( 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, diff --git a/src/openultrasast/model/candidates.py b/src/openultrasast/model/candidates.py index 5d141f2..d9025fb 100644 --- a/src/openultrasast/model/candidates.py +++ b/src/openultrasast/model/candidates.py @@ -20,7 +20,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Literal -from ..source import canonical_language +from ..source import FunctionRange, canonical_language if TYPE_CHECKING: from .taxonomy import FamilyTaxonomy @@ -368,7 +368,7 @@ def labeled_family(case: object, taxonomy: FamilyTaxonomy | None = None) -> str: 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], ...]: """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.discard("") found: list[tuple[str, int, int]] = [] + _kind: list[str] for path, entries in ranges.items(): for name, start, end, *_kind in entries: if name in wanted: diff --git a/src/openultrasast/plane/runner.py b/src/openultrasast/plane/runner.py index 0a71c2a..248e11c 100644 --- a/src/openultrasast/plane/runner.py +++ b/src/openultrasast/plane/runner.py @@ -704,7 +704,7 @@ def safe_url(url: str) -> str: def deliver( url: str, payload: bytes, - headers: Mapping[str, str], + headers: MutableMapping[str, str], *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0, @@ -732,7 +732,7 @@ def deliver( 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`. Returns the byte count; :class:`DeliveryError` names the last cause when every attempt failed.""" diff --git a/src/openultrasast/search/verify.py b/src/openultrasast/search/verify.py index 3ce2fe6..bef5a4b 100644 --- a/src/openultrasast/search/verify.py +++ b/src/openultrasast/search/verify.py @@ -409,9 +409,11 @@ def _side(side: Side, demo: Path, family: str, timeout: int, browser: BrowserExe if task_boundary: # Files are app data, so resolve their paths before writing; # the task runner only translates argv and environment. + def fixture_path(_: re.Match[str], path: str = str(canary.root)) -> str: + return path + file_bindings = { - key: re.sub(r"/(?:fixture|scratch)(?=/|$)", lambda _, path=str(canary.root): path, value) - for key, value in bindings.items() + key: re.sub(r"/(?:fixture|scratch)(?=/|$)", fixture_path, value) for key, value in bindings.items() } private_values.extend((*bindings.values(), *file_bindings.values())) write_start_files(checkout, start_spec, file_bindings) diff --git a/src/openultrasast/source.py b/src/openultrasast/source.py index 7dbee98..fc2af18 100644 --- a/src/openultrasast/source.py +++ b/src/openultrasast/source.py @@ -307,6 +307,7 @@ _HEAD = re.compile( def _regex_ranges(text: str, language: str) -> tuple[FunctionRange, ...]: lines, code = text.splitlines(), _code_lines(text) result = [] + end: int | None for index, line in enumerate(code): name = declarator_name(line, language) if language == "python": diff --git a/src/openultrasast/stats.py b/src/openultrasast/stats.py index 5543e3f..1447d00 100644 --- a/src/openultrasast/stats.py +++ b/src/openultrasast/stats.py @@ -167,7 +167,7 @@ def separation(frame: Any, label: str, columns: Iterable[str]) -> Any: import pandas as pd from sklearn.metrics import roc_auc_score - rows = [] + rows: list[dict[str, str | float | None]] = [] labelled = frame[frame[label].isin([0, 1])] for column in columns: series = pd.to_numeric(labelled[column], errors="coerce") if column in labelled else None