mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
learn(exp-004): Req 6.4 pooled BLOCK gate with a family floor; manifest before units
Maintainer decision 2026-10-03: BLOCK precision is shown pooled across families. Req 6.4 (amended; 7.4 annotated): one t_block on a per-family Platt-normalised score (calibrate.fit_map, training folds, inner cross-fit), Wilson 95% LB >= 0.95 over all families' training flags; a family blocks only with >= F = 10 held-out flags and zero errors, else advisory; t_quiet per family keeps >= 90% of training paired positives. evaluate: normalised, pooled_threshold, quiet_threshold, pooled_block_gate, family_floor, apply_floor. experiments: rule pooled_block_gate (normalise: platt, floor, recall), decide_pooled on the pre-floor pooled held-out LB, floor table, quiet_recall and after-floor totals in analyse_rule; exp-003's analysis is unchanged. plane/experiments/exp-004-pooled-block-gate.yaml: procedure and decision rule registered now; units frozen at run time from the post-harvest memory (units_status pending until then). Tests (tests/test_learn_rules.py): pooled threshold where no family alone reaches the bound, no held-out label moves a threshold or the map, a family with one held-out error cannot block, t_quiet and no blind silencing, the pooled verdict, the exp-004 manifest. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5b2bd9bb91
commit
2ad9c1db7a
@@ -25,7 +25,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
|
||||
from .calibrate import Row, check, cross_fit, offered
|
||||
from .calibrate import Row, check, cross_fit, fit_map, offered
|
||||
from .examples import Example
|
||||
from .folds import Fold
|
||||
from .labels import PER_FAMILY_FLOOR, POOLED_FLOOR
|
||||
@@ -210,6 +210,98 @@ def calibrated_block(preds: Sequence[Prediction], *, seed: int = 0, **floors: in
|
||||
return out, info
|
||||
|
||||
|
||||
# --- the pooled BLOCK gate (Req 6.4, amended 2026-10-03; exp-004) ---------------------------------------------------------
|
||||
|
||||
FAMILY_FLOOR = 10 # a family blocks only with at least this many held-out flags and none of them wrong
|
||||
|
||||
|
||||
def normalised(train: Sequence[Prediction], rows: Sequence[Prediction], **floors: int) -> list[float | None]:
|
||||
"""``rows``' raw scores on one scale across families: the per-family Platt map of :func:`.calibrate.fit_map`
|
||||
(pooled slope with a per-family intercept between the floors) fitted on ``train`` only. ``None`` for a family the
|
||||
training rows cannot map (below the pooled floor): it cannot block on that fold."""
|
||||
cmap = fit_map([Row(r.fold, r.family, r.group, r.label, r.s) for r in train], **floors)
|
||||
return [cmap.apply(r.family, r.s) for r in rows]
|
||||
|
||||
|
||||
def pooled_threshold(rows: Sequence[Prediction], *, precision: float = TARGET_PRECISION) -> float | None:
|
||||
"""The lowest normalised score ``t`` at which the blockable rows of every family together with ``p >= t`` have a
|
||||
Wilson 95% lower bound of precision >= ``precision``; candidate thresholds are the rows' own ``p``."""
|
||||
blockable = [r for r in rows if r.blockable and r.p is not None]
|
||||
for t in sorted({float(r.p) for r in blockable if r.p is not None}):
|
||||
chosen = [r for r in blockable if (r.p or 0.0) >= t]
|
||||
if wilson_lower(sum(r.label for r in chosen), len(chosen)) >= precision:
|
||||
return t
|
||||
return None
|
||||
|
||||
|
||||
def quiet_threshold(rows: Sequence[Prediction], *, recall: float = TARGET_RECALL) -> float | None:
|
||||
"""The highest normalised score ``t`` that keeps at least ``recall`` of the rows' positives at or above it."""
|
||||
positives = sorted((float(r.p) for r in rows if r.label == 1 and r.p is not None), reverse=True)
|
||||
if not positives:
|
||||
return None
|
||||
return positives[math.ceil(recall * len(positives) - 1e-9) - 1]
|
||||
|
||||
|
||||
def pooled_block_gate(
|
||||
preds: Sequence[Prediction],
|
||||
*,
|
||||
eligible: Callable[[Prediction], bool] | None = None,
|
||||
precision: float = TARGET_PRECISION,
|
||||
recall: float = TARGET_RECALL,
|
||||
**floors: int,
|
||||
) -> tuple[list[Prediction], dict[str, dict[str, Any]]]:
|
||||
"""One BLOCK threshold for every family, on a normalised score, chosen per outer fold j on the other folds only.
|
||||
|
||||
The training rows (``eligible`` rows of the other folds) are normalised by an inner cross-fit: each training fold
|
||||
k by a map fitted on the training folds other than k, so ``t_block`` is chosen on out-of-sample scores. ``t_block``
|
||||
is the lowest normalised score whose pooled Wilson 95% lower bound of precision is >= ``precision``; ``t_quiet``
|
||||
per family is the highest one keeping >= ``recall`` of that family's training positives. Fold j's rows are then
|
||||
normalised by the map fitted on all its training rows and decided: ``BLOCK`` when blockable and ``p >= t_block``;
|
||||
``ADVISORY`` when ``p >= t_quiet`` (or the family has no map, or no ``t_quiet``: nothing is silenced blind);
|
||||
``DROP`` (silent) below ``t_quiet``. Fold j's labels never enter its thresholds or its map."""
|
||||
chosen = [r for r in preds if eligible is None or eligible(r)]
|
||||
families = sorted({r.family for r in preds})
|
||||
decided: dict[int, Prediction] = {}
|
||||
gates: dict[str, dict[str, Any]] = {}
|
||||
for fold in sorted({r.fold for r in preds}):
|
||||
train = [r for r in chosen if r.fold != fold]
|
||||
inner: list[Prediction] = []
|
||||
for k in sorted({r.fold for r in train}):
|
||||
mine = [r for r in train if r.fold == k]
|
||||
inner += [replace(r, p=p) for r, p in zip(mine, normalised([r for r in train if r.fold != k], mine, **floors), strict=True)
|
||||
if p is not None] # fmt: skip
|
||||
t_block = pooled_threshold(inner, precision=precision)
|
||||
t_quiet = {f: quiet_threshold([r for r in inner if r.family == f], recall=recall) for f in families}
|
||||
held = [(i, r) for i, r in enumerate(preds) if r.fold == fold]
|
||||
for (i, r), p in zip(held, normalised(train, [r for _, r in held], **floors), strict=True):
|
||||
quiet = t_quiet[r.family]
|
||||
if p is not None and t_block is not None and r.blockable and p >= t_block:
|
||||
point = "BLOCK"
|
||||
elif p is None or quiet is None or p >= quiet:
|
||||
point = "ADVISORY"
|
||||
else:
|
||||
point = "DROP"
|
||||
decided[i] = replace(r, p=p, point=point)
|
||||
gates[fold] = {"t_block": t_block, "t_quiet": t_quiet, "training_rows": len(train), "normalised_training_rows": len(inner)}
|
||||
return [decided[i] for i in range(len(preds))], gates
|
||||
|
||||
|
||||
def family_floor(rows: Sequence[Prediction], *, floor: int = FAMILY_FLOOR) -> dict[str, dict[str, Any]]:
|
||||
"""Per family, its held-out ``BLOCK`` flags among ``rows`` (the primary population, every fold): it may block only
|
||||
when none is wrong and there are at least ``floor``; otherwise its flags are reported as ADVISORY."""
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for family in sorted({r.family for r in rows}):
|
||||
flagged = [r for r in rows if r.family == family and r.point == "BLOCK"]
|
||||
errors = sum(1 for r in flagged if r.label == 0)
|
||||
out[family] = {"flagged": len(flagged), "errors": errors, "floor": floor, "may_block": errors == 0 and len(flagged) >= floor}
|
||||
return out
|
||||
|
||||
|
||||
def apply_floor(rows: Sequence[Prediction], floors: Mapping[str, Mapping[str, Any]]) -> list[Prediction]:
|
||||
"""``BLOCK`` becomes ``ADVISORY`` in every family that does not pass :func:`family_floor`."""
|
||||
return [replace(r, point="ADVISORY") if r.point == "BLOCK" and not floors.get(r.family, {}).get("may_block") else r for r in rows]
|
||||
|
||||
|
||||
def _at(rows: Sequence[Prediction], level: str) -> dict[str, float | None]:
|
||||
flagged = [r for r in rows if r.point in (("BLOCK",) if level == "block" else ("BLOCK", "ADVISORY"))]
|
||||
positives = sum(r.label for r in rows)
|
||||
@@ -408,7 +500,8 @@ def candidate_from_example(
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BOUNDS", "PARSE_FAILURE_LIMIT", "TARGET_PRECISION", "TARGET_RECALL", "Points", "Prediction", "calibrated_block",
|
||||
"BOUNDS", "FAMILY_FLOOR", "PARSE_FAILURE_LIMIT", "TARGET_PRECISION", "TARGET_RECALL", "Points", "Prediction", "apply_floor",
|
||||
"calibrated_block", "family_floor", "normalised", "pooled_block_gate", "pooled_threshold", "quiet_threshold",
|
||||
"candidate_from_example", "choose_points", "cluster_bootstrap", "evaluate", "explain", "floor_status", "incompatible",
|
||||
"learning_curve", "nested_points", "nested_precision_block", "precision_threshold", "predict", "strata", "summary", "targets",
|
||||
"unevaluable_families",
|
||||
|
||||
@@ -44,7 +44,20 @@ import yaml
|
||||
from ..plane.budget import BudgetExhausted, MeteredClient
|
||||
from ..plane.memory import MemoryStore, row_id
|
||||
from .compile import load_program, spec_of
|
||||
from .evaluate import BOUNDS, TARGET_PRECISION, Prediction, calibrated_block, candidate_from_example, nested_precision_block, wilson_lower
|
||||
from .evaluate import (
|
||||
BOUNDS,
|
||||
FAMILY_FLOOR,
|
||||
TARGET_PRECISION,
|
||||
TARGET_RECALL,
|
||||
Prediction,
|
||||
apply_floor,
|
||||
calibrated_block,
|
||||
candidate_from_example,
|
||||
family_floor,
|
||||
nested_precision_block,
|
||||
pooled_block_gate,
|
||||
wilson_lower,
|
||||
)
|
||||
from .examples import EXAMPLE_KIND, Example
|
||||
from .folds import Fold, compile_split, folds_digest, outer_folds
|
||||
from .program import Caller, Decision, Program, ProgramSpec
|
||||
@@ -60,12 +73,17 @@ RUN_TASK = "run"
|
||||
ARM_KINDS = ("program", "plane", "rule")
|
||||
# decision-rule experiments (kind: rule): one replay-only scoring pass of the incumbent programs, arms differ only in
|
||||
# the BLOCK rule applied to the same scores
|
||||
RULES = ("calibrated_block", "precision_bound_block")
|
||||
RULES = ("calibrated_block", "precision_bound_block", "pooled_block_gate")
|
||||
NORMALISATIONS = ("platt",) # pooled_block_gate: how the scores of different families are put on one scale (Req 6.4)
|
||||
RULE_PARAMETERS: Mapping[str, set[str]] = {
|
||||
"precision_bound_block": {"precision", "bound", "min_flagged", "select_on"},
|
||||
"pooled_block_gate": {"precision", "recall", "floor", "normalise", "select_on"},
|
||||
}
|
||||
POPULATIONS = ("paired", "pooled") # paired: units whose candidate has both sides present; pooled: every unit
|
||||
SCORE_ARM = "score" # the arm name of a rule experiment's outcome rows (the shared scores)
|
||||
MISS_VERDICT = "replay_miss" # an outcome row of a unit excluded because a response was not cached
|
||||
# rule metrics, per family and population of held-out (outer-fold) units: direction only (no paired test)
|
||||
RULE_METRICS: Mapping[str, int] = {"flagged_precision_lb": 1, "flagged_precision": 1, "coverage": 1, "flagged": 1}
|
||||
RULE_METRICS: Mapping[str, int] = {"flagged_precision_lb": 1, "flagged_precision": 1, "coverage": 1, "flagged": 1, "quiet_recall": 1}
|
||||
UNITS = ("candidate", "file", "repository")
|
||||
PAIRINGS = ("paired", "assigned")
|
||||
# O'Brien-Fleming boundaries for two equally spaced looks at alpha 0.05, two-sided (design section 6).
|
||||
@@ -277,17 +295,24 @@ def _check_rule_manifest(path: Path, raw: Mapping[str, Any], arms: Mapping[str,
|
||||
name = arm.rule.get("name")
|
||||
if name not in RULES:
|
||||
raise ExperimentError(f"{path}: arm {arm.name} rule.name {name!r} (one of {', '.join(RULES)})")
|
||||
allowed = {"name"} | ({"precision", "bound", "min_flagged", "select_on"} if name == "precision_bound_block" else set())
|
||||
allowed = {"name"} | RULE_PARAMETERS.get(str(name), set())
|
||||
if set(arm.rule) - allowed:
|
||||
raise ExperimentError(f"{path}: arm {arm.name} rule parameters {sorted(set(arm.rule) - allowed)} are not understood")
|
||||
if arm.rule.get("bound", "wilson") not in BOUNDS or arm.rule.get("select_on", "paired") not in POPULATIONS:
|
||||
raise ExperimentError(f"{path}: arm {arm.name} bound in {BOUNDS} and select_on in {POPULATIONS}")
|
||||
if name == "pooled_block_gate":
|
||||
if arm.rule.get("normalise") not in NORMALISATIONS:
|
||||
raise ExperimentError(f"{path}: arm {arm.name} normalise in {NORMALISATIONS} (declared before the run)")
|
||||
if int(arm.rule.get("floor", FAMILY_FLOOR)) < 1 or not 0 < float(arm.rule.get("recall", TARGET_RECALL)) <= 1:
|
||||
raise ExperimentError(f"{path}: arm {arm.name} floor >= 1 and recall in (0, 1]")
|
||||
populations = dict(raw.get("populations") or {})
|
||||
if populations.get("primary") not in POPULATIONS or any(p not in POPULATIONS for p in populations.get("secondary", [])):
|
||||
raise ExperimentError(f"{path}: populations.primary (and secondary) in {POPULATIONS}")
|
||||
decision = dict(raw.get("decision") or {})
|
||||
if not 0 < float(decision.get("target_precision", 0)) <= 1 or decision.get("arm") not in arms:
|
||||
raise ExperimentError(f"{path}: decision.target_precision in (0, 1] and decision.arm naming an arm")
|
||||
if decision.get("pooled") and arms[str(decision["arm"])].rule.get("name") != "pooled_block_gate":
|
||||
raise ExperimentError(f"{path}: decision.pooled judges a pooled_block_gate arm only")
|
||||
|
||||
|
||||
# --- register ------------------------------------------------------------------------------------------------------------
|
||||
@@ -1087,6 +1112,17 @@ def _apply_rule(arm: Arm, preds: Sequence[Prediction], seed: int) -> tuple[list[
|
||||
if arm.rule["name"] == "calibrated_block":
|
||||
return calibrated_block(preds, seed=seed)
|
||||
select_on = str(arm.rule.get("select_on", "paired"))
|
||||
if arm.rule["name"] == "pooled_block_gate": # the family floor is applied by analyse_rule, on held-out flags
|
||||
rows, gates = pooled_block_gate(
|
||||
preds, eligible=(lambda r: bool(r.pair)) if select_on == "paired" else None,
|
||||
precision=float(arm.rule.get("precision", TARGET_PRECISION)), recall=float(arm.rule.get("recall", TARGET_RECALL)),
|
||||
) # fmt: skip
|
||||
info = {
|
||||
f: {"t_block_by_fold": {j: g["t_block"] for j, g in gates.items()},
|
||||
"t_quiet_by_fold": {j: g["t_quiet"].get(f) for j, g in gates.items()}}
|
||||
for f in sorted({r.family for r in preds})
|
||||
} # fmt: skip
|
||||
return rows, info
|
||||
decided, thresholds = nested_precision_block(
|
||||
preds, eligible=(lambda r: bool(r.pair)) if select_on == "paired" else None,
|
||||
precision=float(arm.rule.get("precision", TARGET_PRECISION)), bound=str(arm.rule.get("bound", "wilson")),
|
||||
@@ -1095,11 +1131,38 @@ def _apply_rule(arm: Arm, preds: Sequence[Prediction], seed: int) -> tuple[list[
|
||||
return decided, {f: {"thresholds_by_fold": t} for f, t in thresholds.items()}
|
||||
|
||||
|
||||
def decide_rule(manifest: Manifest, families: Mapping[str, Mapping[str, Any]]) -> tuple[str, str]:
|
||||
def decide_pooled(manifest: Manifest, families: Mapping[str, Mapping[str, Any]], overall: Mapping[str, Any]) -> tuple[str, str]:
|
||||
"""The pre-registered rule of a pooled gate (Req 6.4, exp-004), on the primary population's held-out flags of
|
||||
every family together, before the family floor: **reject** when they flag at a point precision below the target;
|
||||
**adopt** when their Wilson 95% lower bound reaches the target and at least one family passes the floor (no
|
||||
held-out error, at least ``floor`` flags) -- those families block, the others stay advisory; otherwise
|
||||
**inconclusive** (the incumbent rule stays)."""
|
||||
decision = dict(manifest.raw["decision"])
|
||||
arm, target = str(decision["arm"]), float(decision["target_precision"])
|
||||
primary = str(manifest.raw["populations"]["primary"])
|
||||
cell = overall[arm][primary]
|
||||
if cell["flagged"] and not cell["meets_target_point"]:
|
||||
return "reject", f"{arm} flags held-out {primary} units of all families together below precision {target}"
|
||||
passing = sorted(f for f, v in families.items() if v[arm]["floor"]["may_block"])
|
||||
if cell["meets_target_lb"] and passing:
|
||||
return "adopt", f"{arm}'s pooled held-out Wilson lower bound reaches {target}; {', '.join(passing)} pass the family floor and block"
|
||||
if not cell["flagged"]:
|
||||
return "inconclusive", f"{arm} offers no BLOCK on held-out {primary} units; the incumbent stays"
|
||||
if not cell["meets_target_lb"]:
|
||||
return "inconclusive", f"{arm}'s pooled held-out Wilson lower bound is below {target}; the incumbent stays"
|
||||
return "inconclusive", f"{arm} reaches {target} pooled but no family passes the floor; every family stays advisory"
|
||||
|
||||
|
||||
def decide_rule(manifest: Manifest, families: Mapping[str, Mapping[str, Any]], overall: Mapping[str, Any] | None = None) -> tuple[str, str]:
|
||||
"""The pre-registered rule: **reject** the decision arm when it flags held-out units of the primary population in
|
||||
some family at a point precision below the target; **adopt** it when it does not and some family's held-out
|
||||
Wilson lower bound reaches the target; otherwise **inconclusive** (the incumbent rule stays)."""
|
||||
Wilson lower bound reaches the target; otherwise **inconclusive** (the incumbent rule stays). A manifest whose
|
||||
``decision.pooled`` is set is judged by :func:`decide_pooled` instead."""
|
||||
decision = dict(manifest.raw["decision"])
|
||||
if decision.get("pooled"):
|
||||
if overall is None:
|
||||
raise ExperimentError(f"{manifest.id}: a pooled decision needs the pooled-over-families totals")
|
||||
return decide_pooled(manifest, families, overall)
|
||||
arm, target = str(decision["arm"]), float(decision["target_precision"])
|
||||
primary = str(manifest.raw["populations"]["primary"])
|
||||
cells = {f: v[arm][primary] for f, v in families.items()}
|
||||
@@ -1142,6 +1205,19 @@ def analyse_rule(
|
||||
return [r for r in rows if r.pair] if population == "paired" else list(rows)
|
||||
|
||||
decided = {name: _apply_rule(arm, preds, manifest.seed) for name, arm in manifest.arms.items()}
|
||||
gated = {name for name, arm in manifest.arms.items() if arm.rule.get("name") == "pooled_block_gate"}
|
||||
floors = { # held-out flags of the primary population decide which families may block; no threshold moves
|
||||
name: {f: family_floor([r for r in select(decided[name][0], populations[0]) if r.family == f],
|
||||
floor=int(manifest.arms[name].rule.get("floor", FAMILY_FLOOR))).get(f)
|
||||
or {"flagged": 0, "errors": 0, "floor": int(manifest.arms[name].rule.get("floor", FAMILY_FLOOR)), "may_block": False}
|
||||
for f in manifest.families}
|
||||
for name in gated
|
||||
} # fmt: skip
|
||||
|
||||
def quiet_recall(rows: Sequence[Prediction]) -> float | None:
|
||||
positives = [r for r in rows if r.label == 1]
|
||||
return round(sum(1 for r in positives if r.point in ("BLOCK", "ADVISORY")) / len(positives), 4) if positives else None
|
||||
|
||||
families: dict[str, dict[str, Any]] = {}
|
||||
for family in manifest.families:
|
||||
cell: dict[str, Any] = {}
|
||||
@@ -1149,6 +1225,9 @@ def analyse_rule(
|
||||
mine = [r for r in rows if r.family == family]
|
||||
cell[name] = {**info.get(family, {}), **{p: flag_summary(select(mine, p), target, resamples=n, seed=manifest.seed)
|
||||
for p in populations}} # fmt: skip
|
||||
if name in gated:
|
||||
cell[name]["floor"] = floors[name][family]
|
||||
cell[name]["quiet_recall"] = {p: quiet_recall(select(mine, p)) for p in populations}
|
||||
mine = [r for r in preds if r.family == family]
|
||||
folds = sorted({r.fold for r in mine})
|
||||
cell["capacity"] = {
|
||||
@@ -1160,7 +1239,12 @@ def analyse_rule(
|
||||
name: {p: flag_summary(select(rows, p), target, resamples=n, seed=manifest.seed) for p in populations}
|
||||
for name, (rows, _) in decided.items()
|
||||
}
|
||||
verdict, reason = decide_rule(manifest, families)
|
||||
after_floor = { # what users would get: BLOCK only in the families that pass the floor (secondary, not deciding)
|
||||
name: {p: flag_summary(select(apply_floor(decided[name][0], floors[name]), p), target, resamples=n, seed=manifest.seed)
|
||||
for p in populations}
|
||||
for name in sorted(gated)
|
||||
} # fmt: skip
|
||||
verdict, reason = decide_rule(manifest, families, overall)
|
||||
spend = {SCORE_ARM: round(sum(o.usd or 0.0 for o in outcomes if o.arm == SCORE_ARM), 6)}
|
||||
return {
|
||||
"experiment": manifest.id, "manifest_digest": manifest.digest, "hypothesis": manifest.hypothesis,
|
||||
@@ -1172,7 +1256,8 @@ def analyse_rule(
|
||||
"populations": {"primary": populations[0], "secondary": populations[1:]}, "target_precision": target,
|
||||
"min_flagged_for_bound": min_flagged_for_bound(target), "primary": [m.as_dict() for m in manifest.primary],
|
||||
"secondary": [m.as_dict() for m in manifest.secondary], "families": families, "overall": overall, "resamples": n,
|
||||
"decision": verdict, "reason": reason, "spend_usd": spend, **dict(provenance or {}),
|
||||
**({"overall_after_floor": after_floor} if after_floor else {}), "decision": verdict, "reason": reason, "spend_usd": spend,
|
||||
**dict(provenance or {}),
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user