feat(pre-push-safety-net): preserve exact ranker scope and outcomes

This commit is contained in:
Marius Ciepluch
2026-09-13 10:16:22 +02:00
parent bd53b6dc9d
commit 546db24b8e
10 changed files with 931 additions and 37 deletions
+13 -3
View File
@@ -890,7 +890,19 @@ def _model_payload(
honesty requirement (Req 3.3); NAMING them is what makes an exclusion list for a large repository
something a maintainer can write from evidence rather than guesswork.
"""
scope = getattr(result, "scope", None)
outcomes = getattr(result, "question_outcomes", ())
if scope is not None:
unjudged_paths = tuple(
dict.fromkeys(
[q.identity.path for q in scope.deferred if q.reason != "tier_zero"]
+ [q.identity.path for q in outcomes if q.status != "completed"]
)
)[:10]
return {
"scope": scope.to_payload() if scope is not None else None,
"question_outcomes": [q.to_payload() for q in outcomes],
"family_coverage": [c.to_payload() for c in getattr(result, "family_coverage", ())],
"by_rung": dict(getattr(result, "by_rung", {}) or {}),
"regions_scanned": getattr(result, "regions_scanned", 0),
"regions_unjudged": getattr(result, "regions_unjudged", 0),
@@ -967,10 +979,8 @@ def _run_model_layer(
for degradation in result.degradations:
runtime.state["degradations"].append(dict(degradation))
# The regions the budget did not reach, weakest first -- the evidence an exclusion list is built from.
unjudged = tuple(region.path for region in regions[result.regions_scanned :][:10])
findings = [_finding_from_model(item) for item in result.findings]
return findings, _model_payload(result, unjudged_paths=unjudged, coverage=coverage)
return findings, _model_payload(result, coverage=coverage)
def _model_rationale(item: object, witness: str, family: str) -> str:
+48
View File
@@ -145,6 +145,8 @@ class RankedQuestion(Contract):
identity: QuestionIdentity
priority: float
evidence: tuple[str, ...]
tier: int | None = None
score: float | None = None
@dataclass(frozen=True)
@@ -169,3 +171,49 @@ class ScopeDecision(Contract):
identities = [item.identity for item in self.selected] + [item.identity for item in self.deferred]
if len(set(identities)) != len(identities):
raise ValueError("question identities must be unique across selected and deferred scope")
@dataclass(frozen=True)
class QuestionOutcome(Contract):
"""Execution, not vulnerability disposition; raw answers survive interrupted arbitration."""
identity: QuestionIdentity
status: Literal["completed", "unanswered", "unresolved", "not_arbitrated"]
reason: str
raw_rows_json: str | None = None
def __post_init__(self) -> None:
super().__post_init__()
if self.raw_rows_json is not None:
try:
rows = json.loads(self.raw_rows_json)
except (ValueError, TypeError) as error:
raise ValueError("raw_rows_json must contain a JSON row list") from error
if not isinstance(rows, list):
raise ValueError("raw_rows_json must contain a JSON row list")
if self.status in {"completed", "not_arbitrated"} and self.raw_rows_json is None:
raise ValueError("answered outcome requires raw rows")
if self.status == "unanswered" and self.raw_rows_json is not None:
raise ValueError("unanswered outcome cannot carry an answer")
@dataclass(frozen=True)
class FamilyCoverage(Contract):
"""Counts over the supplied population; completed never means safe or admitted."""
unit: str
language: str
family: str
selected: int
completed: int
unanswered: int
deferred: int
unsupported: int
def __post_init__(self) -> None:
super().__post_init__()
counts = (self.selected, self.completed, self.unanswered, self.deferred, self.unsupported)
if any(count < 0 for count in counts):
raise ValueError("coverage counts must be nonnegative")
if self.completed + self.unanswered != self.selected or self.unsupported > self.deferred:
raise ValueError("inconsistent family coverage counts")
+188 -29
View File
@@ -19,17 +19,18 @@ the region's risk score said beforehand.
from __future__ import annotations
import inspect
import json
import logging
import time
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field, replace
from dataclasses import asdict, dataclass, field, replace
from pathlib import Path
from typing import Any
from ..cpg.backend import CpgResult
from .candidates import enumerate_candidates
from .config_value import request_params as config_params
from .contracts import ExecutionBudget
from .contracts import DeferredQuestion, ExecutionBudget, FamilyCoverage, QuestionIdentity, QuestionOutcome, RankedQuestion, ScopeDecision
from .dominance import request_params as dominance_params
from .evidence import TIER_EXCLUDE, Evidence, evidence_from_rows
from .ladder import Rung
@@ -59,6 +60,7 @@ ENTRY_POINT_CALL_DEPTH = 3
logger = logging.getLogger(__name__)
_RUNG_ORDER = {Rung.ENTAILED: 0, Rung.CORROBORATED: 1, Rung.SUSPICION: 2, Rung.EXECUTION_CONFIRMED: -1}
@@ -115,6 +117,10 @@ class ModelScanResult:
# could have been found was skipped. Reported so the request count is legible.
requests_pruned: int = 0
scope: ScopeDecision | None = None
question_outcomes: tuple[QuestionOutcome, ...] = ()
family_coverage: tuple[FamilyCoverage, ...] = ()
@property
def arbitrate_seconds(self) -> float:
"""What is left once the CPG is built and queried: the judge calls and the arbiters themselves."""
@@ -130,8 +136,21 @@ def scan_repository(
model: str = "",
budget: ScanBudget | None = None,
execution_budget: ExecutionBudget | None = None,
ranking_mode: str | None = None,
unit: str = "repository",
population_complete: bool = False,
) -> ModelScanResult:
"""Run with an optional transaction deadline; graph ownership ends on every exit."""
if ranking_mode not in (None, "static", "evidence"):
raise ValueError("ranking_mode must be static or evidence")
limits = budget if budget is not None else ScanBudget()
if ranking_mode is not None:
limits = replace(limits, order_by_evidence=ranking_mode == "evidence")
mode = "evidence" if limits.order_by_evidence else "static"
# Reject duplicate canonical questions before any engine work or resource ownership.
identities = [_identity(unit, r, f) for r in regions for f in r.families]
if len(set(identities)) != len(identities):
raise ValueError("duplicate question identity")
started = time.monotonic()
owned: list[Any] = []
try:
@@ -141,7 +160,10 @@ def scan_repository(
backend=backend,
client=client if execution_budget is None else None,
model=model,
budget=budget,
budget=limits,
ranking_mode=mode,
unit=unit,
population_complete=population_complete,
execution_budget=execution_budget,
owned=owned,
)
@@ -172,6 +194,9 @@ def _scan_repository_impl(
budget: ScanBudget | None = None,
execution_budget: ExecutionBudget | None = None,
owned: list[Any],
ranking_mode: str,
unit: str,
population_complete: bool,
) -> ModelScanResult:
"""Arbitrate a repository's regions under one CPG and one budget."""
limits = budget if budget is not None else ScanBudget()
@@ -188,7 +213,10 @@ def _scan_repository_impl(
cpg = _build(backend, root, dominant, execution_budget=execution_budget)
build_seconds = round(time.monotonic() - build_started, 2)
if cpg is None:
scope, _ = _scope_work(regions, {}, limits, unit, ranking_mode, population_complete, failure="cpg_build_failed")
return ModelScanResult(
scope=scope,
family_coverage=_family_coverage(scope, ()),
by_rung=_empty_tally(),
regions_unjudged=len(regions),
seconds=round(time.monotonic() - started, 2),
@@ -252,7 +280,7 @@ def _scan_repository_impl(
else ordered_regions[: limits.max_regions]
)
tier_started = time.monotonic()
evidence_by_pair, failed = _evidence_pass(cpg, _collect(evidence_candidates), hooks)
evidence_by_pair, failed = _evidence_pass(cpg, _collect(evidence_candidates), hooks, degradations=degradations)
per_kind["evidence"] = round(time.monotonic() - tier_started, 2)
if failed:
degradations.append({"stage": "model", "reason": "query_failed", "kind": "evidence", "requests": failed})
@@ -260,11 +288,15 @@ def _scan_repository_impl(
if limits.order_by_evidence and evidence_by_pair:
ordered_regions = order_by_evidence(ordered_regions, evidence_by_pair)
# Phase 1: collect the whole scan's questions, for at most `max_regions` regions. Nothing is asked yet.
work = _collect(ordered_regions[: limits.max_regions])
tier_by_rid = {
rid: evidence_by_pair[_pair_key(region, spec)].tier for rid, region, spec in work if _pair_key(region, spec) in evidence_by_pair
}
# Authoritative selection is made once, from the actual final ranker order.
# Stable question IDs are the IDs sent to every execution request below.
scope_limits = (
replace(limits, prune_tier0=False)
if any(d.get("reason") in {"files_unparsed", "cpg_empty", "cpg_sharded"} for d in degradations)
else limits
)
scope, work = _scope_work(ordered_regions, evidence_by_pair, scope_limits, unit, ranking_mode, population_complete)
outcomes: dict[str, QuestionOutcome] = {}
# Phase 2: ONE invocation per query kind. JVM startup dominates a repository scan -- a call per region
# per family put a ten-line file at four minutes and a thousand regions at roughly fifty hours -- while
@@ -272,19 +304,10 @@ def _scan_repository_impl(
rows_by_id: dict[str, list[object]] = {}
census_reported = False
query_started = time.monotonic()
pruned = 0
pruned = sum(q.reason == "tier_zero" for q in scope.deferred)
for kind, requests in _grouped(work, hook_callbacks=hooks).items():
if not _within_deadline(execution_budget):
break
if kind == "taint" and limits.prune_tier0 and tier_by_rid:
# Tier 0 is exact: a family with no sink in reach cannot yield a flow. Measured on a 637-file
# plugin, that is 2,161 of 2,500 requests -- 86% of a query that timed out at forty minutes.
keep = {rid: params for rid, params in requests.items() if tier_by_rid.get(rid, TIER_EXCLUDE + 1) != TIER_EXCLUDE}
pruned = len(requests) - len(keep)
requests = keep
if not requests:
per_kind[kind] = 0.0
continue
kind_started = time.monotonic()
batch = getattr(cpg, "run_batch", None)
if callable(batch):
@@ -314,7 +337,11 @@ def _scan_repository_impl(
if shards > 1:
degradations.append({"stage": "model", "reason": "cpg_sharded", "shards": shards})
census_reported = True
rows_by_id.update(answered_batch)
valid = {rid: rows for rid, rows in answered_batch.items() if rid in requests and isinstance(rows, list)}
rows_by_id.update(valid)
missing = len(requests) - len(valid)
if missing:
degradations.append({"stage": "model", "reason": "query_failed", "kind": kind, "requests": missing})
else: # a backend without batching still works, one call at a time
for rid, params in requests.items():
if not _within_deadline(execution_budget):
@@ -322,8 +349,6 @@ def _scan_repository_impl(
answered = cpg.run(kind, params)
if isinstance(answered, list):
rows_by_id[rid] = list(answered)
elif execution_budget is None:
rows_by_id[rid] = []
else:
degradations.append({"stage": "model", "reason": "query_failed", "kind": kind, "requests": 1})
per_kind[kind] = round(time.monotonic() - kind_started, 2)
@@ -345,9 +370,8 @@ def _scan_repository_impl(
for rid, region, spec in work:
if not _within_deadline(execution_budget):
break
# Unanswered requests are deferred on the bounded path, never adjudicated
# from an invented empty response. Exact identities are added in task 3.2.
if execution_budget is not None and rid not in rows_by_id:
# Unanswered requests are never adjudicated from invented empty responses.
if rid not in rows_by_id:
continue
exhausted = counted is not None and counted.calls >= limits.max_model_calls
family = getattr(spec, "family", "")
@@ -379,7 +403,13 @@ def _scan_repository_impl(
except Exception as exc: # noqa: BLE001 -- one bad region must not end the scan
logger.warning("model scan failed for %s: %s", region.path, exc)
degradations.append({"stage": "model", "reason": "region_failed", "path": region.path})
outcomes[rid] = QuestionOutcome(
_identity(unit, region, family), "unresolved", "arbitration_failed", _rows_json(rows_by_id[rid])
)
continue
outcomes[rid] = QuestionOutcome(
_identity(unit, region, family), "completed", "query_answered_and_arbitrated", _rows_json(rows_by_id[rid])
)
judged.add((region.path, region.function))
if exhausted:
unasked.add((region.path, region.function))
@@ -388,6 +418,21 @@ def _scan_repository_impl(
if unasked:
degradations.append({"stage": "model", "reason": "budget_exhausted", "regions_unasked": len(unasked)})
graph_gaps = tuple(str(d["reason"]) for d in degradations if d.get("reason") in {"files_unparsed", "cpg_empty", "cpg_sharded"})
for question in scope.selected:
rid = question.identity.question_id
if rid not in outcomes:
answered = rid in rows_by_id
outcomes[rid] = QuestionOutcome(
question.identity,
"not_arbitrated" if answered else "unanswered",
"deadline_exhausted" if not _within_deadline(execution_budget) else "query_unanswered",
_rows_json(rows_by_id[rid]) if answered else None,
)
if graph_gaps and outcomes[rid].status == "completed":
outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="graph_incomplete")
scope = replace(scope, unresolved_boundaries=tuple(dict.fromkeys((*scope.unresolved_boundaries, *graph_gaps))))
ordered_outcomes = tuple(outcomes[q.identity.question_id] for q in scope.selected)
scanned = len(judged)
findings = _ordered(_deduplicated(collected))
@@ -409,6 +454,100 @@ def _scan_repository_impl(
tiers=tuple(tiers),
tier_counts={tier: sum(1 for t in tiers if t[3] == tier) for tier in sorted({t[3] for t in tiers})},
requests_pruned=pruned,
scope=scope,
question_outcomes=ordered_outcomes,
family_coverage=_family_coverage(scope, ordered_outcomes),
)
def _identity(unit: str, region: ScanRegion, family: str) -> QuestionIdentity:
return QuestionIdentity(unit, region.language, region.path, region.function, family)
def _rows_json(rows: object) -> str:
return json.dumps(rows, sort_keys=True, separators=(",", ":"))
def _scope_work(
regions: Sequence[ScanRegion],
evidence: Mapping[tuple[str, str, str], Evidence],
limits: ScanBudget,
unit: str,
mode: str,
population_complete: bool,
*,
failure: str | None = None,
) -> tuple[ScopeDecision, list[tuple[str, ScanRegion, ArbiterSpec]]]:
"""Consume the ranker's final order once; the returned work IS selected scope."""
selected: list[RankedQuestion] = []
deferred: list[DeferredQuestion] = []
work: list[tuple[str, ScanRegion, ArbiterSpec]] = []
boundaries: list[str] = [] if population_complete else ["population_not_asserted_complete"]
if failure:
boundaries.append(failure)
for index, region in enumerate(regions):
for family in region.families:
identity = _identity(unit, region, family)
spec = _spec_for(family, region.language)
vector = evidence.get((region.path, region.function or "", family))
facts: tuple[str, ...] = ("static_rank=" + str(region.rank),)
if vector is not None:
facts += ("normalized_evidence=" + _rows_json(asdict(vector)), "tier=" + str(vector.tier), "score=" + str(vector.score))
elif isinstance(spec, TaintSpec):
facts += ("evidence_unknown",)
boundaries.append("evidence_unknown:" + identity.question_id)
else:
facts += ("taint_evidence_not_applicable",)
reason = (
"unsupported_family"
if spec is None
else failure
or (
"region_budget"
if index >= limits.max_regions
else "tier_zero"
if limits.prune_tier0 and isinstance(spec, TaintSpec) and vector is not None and vector.tier == TIER_EXCLUDE
else None
)
)
if reason:
deferred.append(DeferredQuestion(identity, reason, facts))
if reason == "unsupported_family":
boundaries.append("unsupported_family:" + identity.question_id)
else:
assert spec is not None
selected.append(
RankedQuestion(
identity,
float(-len(selected)),
facts,
vector.tier if vector is not None else None,
vector.score if vector is not None else None,
)
)
work.append((identity.question_id, region, spec))
return ScopeDecision(
"existing-static-evidence-v1", mode, population_complete, tuple(selected), tuple(deferred), tuple(boundaries)
), work
def _family_coverage(scope: ScopeDecision, outcomes: tuple[QuestionOutcome, ...]) -> tuple[FamilyCoverage, ...]:
population: list[RankedQuestion | DeferredQuestion] = [*scope.selected, *scope.deferred]
keys = sorted({(q.identity.unit, q.identity.language, q.identity.family) for q in population})
def matches(identity: QuestionIdentity, key: tuple[str, str, str]) -> bool:
return (identity.unit, identity.language, identity.family) == key
return tuple(
FamilyCoverage(
*key,
selected=sum(matches(q.identity, key) for q in scope.selected),
completed=sum(matches(q.identity, key) and q.status == "completed" for q in outcomes),
unanswered=sum(matches(q.identity, key) and q.status != "completed" for q in outcomes),
deferred=sum(matches(q.identity, key) for q in scope.deferred),
unsupported=sum(matches(q.identity, key) and q.reason == "unsupported_family" for q in scope.deferred),
)
for key in keys
)
@@ -429,7 +568,7 @@ def _collect(regions: Sequence[ScanRegion]) -> list[tuple[str, ScanRegion, Arbit
def _evidence_pass(
cpg: Any, work: Sequence[tuple[str, ScanRegion, ArbiterSpec]], hooks: str
cpg: Any, work: Sequence[tuple[str, ScanRegion, ArbiterSpec]], hooks: str, *, degradations: list[Mapping[str, object]] | None = None
) -> tuple[dict[tuple[str, str, str], Evidence], int]:
"""The evidence vector of every taint pair in ``work``, keyed by (path, function, family).
@@ -443,12 +582,23 @@ def _evidence_pass(
answered = cpg.run_batch("taint", requests)
if answered is None:
return {}, len(requests)
answered.pop("__census__", None)
census = answered.pop("__census__", None)
if census and degradations is not None:
entry = census[0] if isinstance(census[0], Mapping) else {}
try:
methods = int(str(entry.get("methods", "0")))
shards = int(str(entry.get("shards", "1")))
except ValueError:
methods, shards = 0, 1
if methods <= 0:
degradations.append({"stage": "model", "reason": "cpg_empty"})
if shards > 1:
degradations.append({"stage": "model", "reason": "cpg_sharded", "shards": shards})
by_rid = {rid: (region, spec) for rid, region, spec in work}
out: dict[tuple[str, str, str], Evidence] = {}
for rid, rows in answered.items():
pair = by_rid.get(rid)
if pair is None:
if pair is None or rid not in requests or not isinstance(rows, list):
continue
region, spec = pair
evidence = evidence_from_rows(
@@ -463,7 +613,16 @@ def _evidence_pass(
)
if evidence is not None:
out[_pair_key(region, spec)] = evidence
return out, 0
# Legacy ranker keys omit language. An overlapping frontend population cannot
# borrow another language's vector; partitioned callers retain distinct IDs.
languages_by_pair: dict[tuple[str, str, str], set[str]] = {}
for rid, region, spec in work:
if rid in requests:
languages_by_pair.setdefault(_pair_key(region, spec), set()).add(region.language)
for key, languages in languages_by_pair.items():
if len(languages) > 1:
out.pop(key, None)
return out, len(requests) - len(out)
def order_by_evidence(regions: Sequence[ScanRegion], evidence: Mapping[tuple[str, str, str], Evidence]) -> list[ScanRegion]: