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:
Marius Ciepluch
2026-10-03 09:40:58 +02:00
co-authored by Claude Opus 5.5
parent 5b2bd9bb91
commit 2ad9c1db7a
5 changed files with 425 additions and 12 deletions
@@ -119,6 +119,19 @@ rule hits.
2. Without a trained model available the tools fall back to today's behaviour and say so in the report. 2. Without a trained model available the tools fall back to today's behaviour and say so in the report.
3. Hand-written rules remain as signal producers; changing their enabled/shadow status no longer changes what is 3. Hand-written rules remain as signal producers; changing their enabled/shadow status no longer changes what is
reported unless the engine's decision changes. reported unless the engine's decision changes.
4. *(Amended 2026-10-03, maintainer decision; refines the BLOCK rule of 7.3 and 7.4. Tested by exp-004.)* BLOCK
precision is demonstrated pooled across families, not per family:
- **Normalisation**, declared before any run: each family's raw score is put on one scale by a per-family Platt
map (`learn.calibrate.fit_map` with its floors) fitted on training folds only. A family the training folds
cannot map does not block on that fold.
- **t_block**: one threshold on the normalised score for all families, chosen on training folds as the lowest
one whose Wilson 95% lower bound on precision, over the training flags of all families together, is >= 0.95.
It is reported by the Wilson 95% lower bound over all held-out flags of all families, before the floor.
- **Family floor**: a family may block only if its own held-out flags have zero errors and number at least F
(F = 10, reasoned in the exp-004 registration); otherwise that family stays ADVISORY.
- **t_quiet**: per family, the highest threshold keeping >= 90% of the paired positives at or above it (chosen
on training folds, its held-out recall reported); findings below it are silent.
- No held-out label moves a threshold or a normalisation map; candidates whose verdict is `unsure` never block.
### Requirement 7: Built for unseen repositories -- a repo safety net ### Requirement 7: Built for unseen repositories -- a repo safety net
@@ -136,7 +149,8 @@ rule hits.
ADVISORY, trading precision for recall; both reported with out-of-repository intervals. The decision unit is a ADVISORY, trading precision for recall; both reported with out-of-repository intervals. The decision unit is a
candidate in changed code (`pre-push` delta), with whole-repository scans as a secondary mode. candidate in changed code (`pre-push` delta), with whole-repository scans as a secondary mode.
4. Calibration is checked on held-out repositories; where it does not hold, only ADVISORY is offered for that 4. Calibration is checked on held-out repositories; where it does not hold, only ADVISORY is offered for that
family. family. *(Amended 2026-10-03: once exp-004 adopts the pooled gate of 6.4, BLOCK precision is shown by its held-out
bound and family floor, and calibration is reported, not required.)*
5. Any adaptation to a user's own repository (facts reuse, their dismissals) is opt-in, separate from the 5. Any adaptation to a user's own repository (facts reuse, their dismissals) is opt-in, separate from the
generalisation model, and never contributes to its reported numbers. generalisation model, and never contributes to its reported numbers.
@@ -0,0 +1,90 @@
# Experiment exp-004 (learned-decision-engine Req 6.4, amended 2026-10-03 by maintainer decision): the pooled BLOCK
# gate with a family floor. Registered before it runs; `ousast learn experiment register` records this file's sha256,
# and a changed file is a new experiment. The procedure and the decision rule are fixed here; the units are NOT: they
# are frozen at run time (`ousast learn experiment units`) from the memory after the negatives harvest (merged in
# 5b2bd9b, 1133 static examples), committed, and registered again before the run. Until then the registration row
# says `units_status: pending` and the run refuses.
#
# Scoring: kind rule, zero cost by construction -- one scoring pass of the six slice-2 incumbent programs, replayed from
# the response cache behind a zero-ceiling meter; a unit with any uncached response is excluded and counted. The
# harvest changed the memory the programs retrieve from, so most prompts are new: a paid cache fill (separately
# approved, its own ceiling, as for exp-003's supplementary analysis) must precede the run, or the exclusions will be
# most of the units.
#
# Normalisation (declared before the run): per-family Platt scaling, learn.calibrate.fit_map with its existing floors
# (own slope and intercept per family at >= 20 positive and negative repository groups; a pooled slope with a
# per-family intercept at >= 10; below that no map, so the family cannot block on that fold). Fitted on the training
# folds' paired units only. Why Platt and not rank-to-quantile: a quantile map gives every family the same flag rate
# whatever its precision, so one pooled threshold would flag the top slice of a weak family as readily as of a strong
# one. Platt puts each family on the scale of its own training precision, which is what a pooled precision bound needs.
# The map is a normaliser only: exp-003 showed calibration (ECE <= 0.05) does not hold per family, and precision is
# demonstrated here by the bound on held-out flags, not by calibration.
#
# Thresholds, per outer fold j, from the other four folds only (fold j's labels never enter a threshold or a map;
# tests/test_learn_rules.py):
# - the training units 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: the lowest normalised score at which the training paired units of ALL families together with p >= t
# and a verdict other than `unsure` have a Wilson 95% lower bound of precision >= 0.95; none -> no BLOCK on fold j;
# - t_quiet, per family: the highest normalised score keeping >= 90% of that family's training paired positives at
# or above it;
# - fold j's units, normalised by the map fitted on all its training units: BLOCK when not `unsure` and p >= t_block;
# ADVISORY when p >= t_quiet (or the family has no map or no t_quiet: nothing is silenced blind); silent below.
#
# Family floor F = 10: a family may block only if its own held-out paired flags (all five folds) number >= 10 with zero
# errors; otherwise its flags are reported as ADVISORY. Why 10: zero errors in 10 flags is unlikely for a family that is
# much worse than the bar (a family at 0.75 precision produces 10 clean flags with probability 0.056, at 0.80 with
# 0.107); a larger F is out of reach of the smaller families (16-19 paired positives in exp-003's units, all folds); and
# 10 is exp-003 arm C's training minimum. The floor uses held-out labels to decide which families block; it moves no
# threshold. The primary metric is computed BEFORE the floor, over every family's held-out flags, so dropping a failing
# family cannot raise it.
#
# Power note: the pooled bound needs >= 73 flagged units with no error (110 with one, 142 with two). exp-003's
# instrument check found few positives above the first negative per family on the raw score (paired: 10, 1, 3, 0, 1);
# per-family normalisation keeps each family's order, so the pooled top set can reach 73 only if the harvest's pairs
# separate better. An inconclusive verdict is a plausible outcome and will be reported as such.
id: exp-004-pooled-block-gate
hypothesis: >
One BLOCK threshold pooled across families on a per-family Platt-normalised score, chosen on training folds only as
the lowest one whose Wilson 95% lower bound on flagged precision is >= 0.95, reaches that bound on held-out
repositories' paired units, where per-family thresholds (exp-003) could not for lack of flags; a per-family floor
(>= 10 held-out flags, none wrong) keeps a family that the pooling masks from blocking.
kind: rule
programs: # the slice-2 incumbents, as exp-003
access_control: 745cb1d76bbb09dc1574d333d2118489be62e39886517894d3699fa565b0e8c2
deserialization: 3c0c2831be7d78198a62351661c37867363c501cb40e3ed7b0119c155af09654
injection: b207a0710e0abf053b3b9397b25f9ee1cab4679144865e3a82c1b9be2c7b4982
output_encoding: 5d768f16724562c381c09949ff3df9cbb11d58cbb7a9c0515d0dc3f858d3818e
path: aea396680a4874dcb6de70e240ca0480c83424debdd3710bc3f1ea6a612661f3
untrusted_destination: 1242b3973fc89056aa08e00843043d68235df217d8e3ef8a0ea0e0b81c142753
arms:
A:
label: today's rule -- Platt map cross-fitted per family, BLOCK only when the calibration check holds, BLOCK point nested on the other folds' calibrated p
rule: {name: calibrated_block}
B:
label: pooled gate -- one t_block over all families on a per-family Platt score (training folds, inner cross-fit), t_quiet per family at 90% recall, family floor 10 clean held-out flags
rule: {name: pooled_block_gate, precision: 0.95, recall: 0.9, floor: 10, normalise: platt, select_on: paired}
unit: candidate
pairing: paired # every unit scored once; every arm decides every unit
cluster: repository
order: {randomise: none, seed: 4}
# primary population: paired units (both sides of the candidate present); pooled (every unit) is reported beside it
populations: {primary: paired, secondary: [pooled]}
metric:
primary:
- {name: flagged_precision_lb, target: 0.95} # held-out Wilson 95% LB of flagged precision, all families pooled, arm B, paired, before the floor
secondary: [flagged_precision, coverage, flagged, quiet_recall] # quiet_recall: held-out positives at or above t_quiet / positives
# Decision rule (decide_pooled), on arm B's held-out paired flags of all families together, before the floor:
# REJECT B if their point precision < 0.95;
# else ADOPT B if their Wilson 95% LB >= 0.95 and at least one family passes the floor (>= 10 held-out paired
# flags, zero errors) -- those families block, every other family stays advisory;
# else INCONCLUSIVE (the incumbent rule stays).
# Reported, not deciding: per-family held-out precision and LB, the floor table, quiet_recall per family against 0.90,
# the after-floor totals (what users would get), the repository-cluster bootstrap interval.
decision: {arm: B, target_precision: 0.95, pooled: true}
test: {resamples: 2000} # repository-cluster bootstrap of the held-out flagged precision, beside the Wilson bound
mde: 0.0 # not a paired difference test; see the power note above
budget_usd: {total: 0} # the run itself is zero cost; the cache fill before it is a separate, approved spend
families: [access_control, deserialization, injection, output_encoding, path, untrusted_destination]
folds: {kind: outer, k: 5, seed: 0, compile_fraction: 0.25} # the incumbent evaluation's folds (learn/compile.toml seed)
units_file: plane/experiments/exp-004-pooled-block-gate.units.jsonl # frozen at run time, after the negatives harvest
+95 -2
View File
@@ -25,7 +25,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from typing import Any 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 .examples import Example
from .folds import Fold from .folds import Fold
from .labels import PER_FAMILY_FLOOR, POOLED_FLOOR 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 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]: 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"))] 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) positives = sum(r.label for r in rows)
@@ -408,7 +500,8 @@ def candidate_from_example(
__all__ = [ __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", "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", "learning_curve", "nested_points", "nested_precision_block", "precision_threshold", "predict", "strata", "summary", "targets",
"unevaluable_families", "unevaluable_families",
+93 -8
View File
@@ -44,7 +44,20 @@ import yaml
from ..plane.budget import BudgetExhausted, MeteredClient from ..plane.budget import BudgetExhausted, MeteredClient
from ..plane.memory import MemoryStore, row_id from ..plane.memory import MemoryStore, row_id
from .compile import load_program, spec_of 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 .examples import EXAMPLE_KIND, Example
from .folds import Fold, compile_split, folds_digest, outer_folds from .folds import Fold, compile_split, folds_digest, outer_folds
from .program import Caller, Decision, Program, ProgramSpec from .program import Caller, Decision, Program, ProgramSpec
@@ -60,12 +73,17 @@ RUN_TASK = "run"
ARM_KINDS = ("program", "plane", "rule") ARM_KINDS = ("program", "plane", "rule")
# decision-rule experiments (kind: rule): one replay-only scoring pass of the incumbent programs, arms differ only in # 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 # 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 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) 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 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, 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") UNITS = ("candidate", "file", "repository")
PAIRINGS = ("paired", "assigned") PAIRINGS = ("paired", "assigned")
# O'Brien-Fleming boundaries for two equally spaced looks at alpha 0.05, two-sided (design section 6). # 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") name = arm.rule.get("name")
if name not in RULES: if name not in RULES:
raise ExperimentError(f"{path}: arm {arm.name} rule.name {name!r} (one of {', '.join(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: if set(arm.rule) - allowed:
raise ExperimentError(f"{path}: arm {arm.name} rule parameters {sorted(set(arm.rule) - allowed)} are not understood") 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: 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}") 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 {}) 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", [])): 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}") raise ExperimentError(f"{path}: populations.primary (and secondary) in {POPULATIONS}")
decision = dict(raw.get("decision") or {}) decision = dict(raw.get("decision") or {})
if not 0 < float(decision.get("target_precision", 0)) <= 1 or decision.get("arm") not in arms: 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") 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 ------------------------------------------------------------------------------------------------------------ # --- register ------------------------------------------------------------------------------------------------------------
@@ -1087,6 +1112,17 @@ def _apply_rule(arm: Arm, preds: Sequence[Prediction], seed: int) -> tuple[list[
if arm.rule["name"] == "calibrated_block": if arm.rule["name"] == "calibrated_block":
return calibrated_block(preds, seed=seed) return calibrated_block(preds, seed=seed)
select_on = str(arm.rule.get("select_on", "paired")) 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( decided, thresholds = nested_precision_block(
preds, eligible=(lambda r: bool(r.pair)) if select_on == "paired" else None, 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")), 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()} 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 """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 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"]) 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"]) arm, target = str(decision["arm"]), float(decision["target_precision"])
primary = str(manifest.raw["populations"]["primary"]) primary = str(manifest.raw["populations"]["primary"])
cells = {f: v[arm][primary] for f, v in families.items()} 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) 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()} 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]] = {} families: dict[str, dict[str, Any]] = {}
for family in manifest.families: for family in manifest.families:
cell: dict[str, Any] = {} cell: dict[str, Any] = {}
@@ -1149,6 +1225,9 @@ def analyse_rule(
mine = [r for r in rows if r.family == family] 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) cell[name] = {**info.get(family, {}), **{p: flag_summary(select(mine, p), target, resamples=n, seed=manifest.seed)
for p in populations}} # fmt: skip 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] mine = [r for r in preds if r.family == family]
folds = sorted({r.fold for r in mine}) folds = sorted({r.fold for r in mine})
cell["capacity"] = { 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} name: {p: flag_summary(select(rows, p), target, resamples=n, seed=manifest.seed) for p in populations}
for name, (rows, _) in decided.items() 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)} spend = {SCORE_ARM: round(sum(o.usd or 0.0 for o in outcomes if o.arm == SCORE_ARM), 6)}
return { return {
"experiment": manifest.id, "manifest_digest": manifest.digest, "hypothesis": manifest.hypothesis, "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, "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], "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, "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 } # fmt: skip
+132 -1
View File
@@ -4,13 +4,24 @@ today's calibrated BLOCK rule, scored once from the response cache at zero cost
from __future__ import annotations from __future__ import annotations
import json import json
from dataclasses import replace
from pathlib import Path from pathlib import Path
import pytest import pytest
from test_learn_experiments import CODE, EXAMPLES, PRICES, _json_shape, artifact, committed, example_rows, excerpt_text, paid, put_program from test_learn_experiments import CODE, EXAMPLES, PRICES, _json_shape, artifact, committed, example_rows, excerpt_text, paid, put_program
from openultrasast.learn import experiments as ex from openultrasast.learn import experiments as ex
from openultrasast.learn.evaluate import Prediction, calibrated_block, nested_precision_block, precision_threshold, wilson_lower from openultrasast.learn.evaluate import (
Prediction,
apply_floor,
calibrated_block,
family_floor,
nested_precision_block,
pooled_block_gate,
precision_threshold,
quiet_threshold,
wilson_lower,
) # fmt: skip
from openultrasast.learn.program import Candidate, Program from openultrasast.learn.program import Candidate, Program
from openultrasast.learn.retrieve import Target from openultrasast.learn.retrieve import Target
from openultrasast.plane.memory import FileStore from openultrasast.plane.memory import FileStore
@@ -245,3 +256,123 @@ def test_the_registered_exp003_manifest_loads() -> None:
assert manifest.kind == "rule" and manifest.budget_total_usd == 0 and manifest.raw["populations"]["primary"] == "paired" assert manifest.kind == "rule" and manifest.budget_total_usd == 0 and manifest.raw["populations"]["primary"] == "paired"
assert manifest.arms["A"].rule == {"name": "calibrated_block"} and manifest.arms["B"].rule["bound"] == "wilson" assert manifest.arms["A"].rule == {"name": "calibrated_block"} and manifest.arms["B"].rule["bound"] == "wilson"
assert len(manifest.families) == 6 and set(manifest.programs) == set(manifest.families) assert len(manifest.families) == 6 and set(manifest.programs) == set(manifest.families)
# --- the pooled BLOCK gate with a family floor (Req 6.4 amended 2026-10-03; exp-004) -------------------------------------------
LOW_FLOORS = {"per_family_floor": 2, "pooled_floor": 1} # small synthetic families still get a Platt map
def two_scales(per_fold: int = 12, *, error: tuple[int, str] | None = None) -> list[Prediction]:
"""Two families on different raw scales: injection positives at 0.9, negatives at 0.1; path at 0.7 and 0.3. With
``error = (fold, family)`` one negative of that fold and family scores like a positive and says vulnerable."""
rows = []
for f in range(5):
for family, hi, lo in (("injection", 0.9, 0.1), ("path", 0.7, 0.3)):
for i in range(per_fold):
key = f"{family}{f}-{i}"
bad = error == (f, family) and i == 0
rows.append(pred(f, 1, hi, family=family, group=f"org/{key}", verdict="vulnerable", pair=key))
verdict = "vulnerable" if bad else "not_vulnerable"
rows.append(pred(f, 0, hi if bad else lo, family=family, group=f"org/{key}", verdict=verdict, pair=key))
return rows
def test_one_pooled_threshold_blocks_where_no_family_alone_reaches_the_bound() -> None:
rows = two_scales() # 48 training positives per family: fewer than the 73 the bound needs, 96 together
_, alone = nested_precision_block(rows)
assert all(t is None for fam in alone.values() for t in fam.values())
decided, gates = pooled_block_gate(rows, **LOW_FLOORS)
assert all(g["t_block"] is not None for g in gates.values())
blocked = [r for r in decided if r.point == "BLOCK"]
assert {r.family for r in blocked} == {"injection", "path"} and all(r.label == 1 for r in blocked)
assert sum(r.label for r in blocked) == sum(r.label for r in rows) # every positive, on both raw scales
def test_no_held_out_label_moves_a_threshold_or_the_map() -> None:
"""Fold outer-0's labels are flipped (and its scores kept): its t_block, t_quiet and normalised scores must not move,
while the other folds, which train on outer-0, do see the change."""
rows = two_scales()
flipped = [replace(r, label=1 - r.label) if r.fold == "outer-0" else r for r in rows]
decided, gates = pooled_block_gate(rows, **LOW_FLOORS)
again, regates = pooled_block_gate(flipped, **LOW_FLOORS)
assert regates["outer-0"] == gates["outer-0"]
assert [r.p for r in decided if r.fold == "outer-0"] == [r.p for r in again if r.fold == "outer-0"]
assert [r.point for r in decided if r.fold == "outer-0"] == [r.point for r in again if r.fold == "outer-0"]
assert any(regates[f"outer-{f}"] != gates[f"outer-{f}"] for f in range(1, 5)) # the test has teeth
def test_a_family_with_one_held_out_error_cannot_block() -> None:
rows = two_scales(16, error=(2, "path"))
decided, _ = pooled_block_gate(rows, **LOW_FLOORS)
floors = family_floor(decided, floor=10)
assert floors["path"]["errors"] == 1 and floors["path"]["flagged"] >= 10 and floors["path"]["may_block"] is False
assert floors["injection"] == {"flagged": 80, "errors": 0, "floor": 10, "may_block": True}
shipped = apply_floor(decided, floors)
assert not any(r.point == "BLOCK" for r in shipped if r.family == "path")
assert all(r.point == "ADVISORY" for r, d in zip(shipped, decided, strict=True) if d.point == "BLOCK" and r.family == "path")
few = [r for r in decided if r.family == "injection"][:18] # 9 clean flags: below the floor
assert family_floor(few, floor=10)["injection"] == {"flagged": 9, "errors": 0, "floor": 10, "may_block": False}
def test_quiet_threshold_keeps_ninety_percent_of_positives_and_nothing_is_silenced_blind() -> None:
rows = [replace(pred(0, 1, 0.5), p=v / 10) for v in range(1, 11)]
assert quiet_threshold(rows) == 0.2 # 9 of 10 positives at or above 0.2; 0.3 keeps 8
assert quiet_threshold([replace(pred(0, 0, 0.5), p=0.9)]) is None
lone = [pred(0, 1, 0.9, family="deserialization", group="org/lone")] # a family with no training rows: no map, no t_quiet
decided, _ = pooled_block_gate([*two_scales(), *lone], **LOW_FLOORS)
assert decided[-1].p is None and decided[-1].point == "ADVISORY"
assert any(r.point == "DROP" for r in decided) # low scores below t_quiet are silent
POOLED_MANIFEST = RULE_MANIFEST.replace(
" B: {{label: bound, rule: {{name: precision_bound_block, bound: wilson, precision: 0.95, select_on: paired}}}}\n"
" C: {{label: point, rule: {{name: precision_bound_block, bound: point, precision: 0.95, min_flagged: 2, select_on: paired}}}}",
" B: {{label: gate, rule: {{name: pooled_block_gate, precision: 0.95, recall: 0.9, floor: 10, normalise: platt, select_on: paired}}}}",
).replace("decision: {{arm: B, target_precision: 0.95}}", "decision: {{arm: B, target_precision: 0.95, pooled: true}}")
def pooled_outcomes(error: tuple[int, str] | None) -> tuple[list[ex.Outcome], list[ex.Unit]]:
units, outcomes = [], []
for r in two_scales(16, error=error):
u = ex.Unit(f"{r.pair}-{r.label}", r.group, r.family, r.label, r.fold, r.pair)
units.append(u)
outcomes.append(ex.Outcome(u.unit, u.group, u.family, u.label, u.pair, ex.SCORE_ARM, 0, r.s, r.verdict, 0.0))
return outcomes, units
def test_the_pooled_verdict_follows_the_registered_rule(tmp_path: Path) -> None:
path = tmp_path / "exp-904-pooled.yaml"
path.write_text(POOLED_MANIFEST.format(id="exp-904-pooled", pid="0" * 64, units=tmp_path / "u.jsonl"))
manifest = ex.load_manifest(path)
assert manifest.arms["B"].rule["normalise"] == "platt"
outcomes, units = pooled_outcomes(None)
report = ex.analyse_rule(manifest, outcomes, units, resamples=20)
assert report["decision"] == "adopt" and report["overall"]["B"]["paired"]["meets_target_lb"]
assert all(report["families"][f]["B"]["floor"]["may_block"] for f in ("injection", "path"))
assert report["families"]["path"]["B"]["quiet_recall"]["paired"] == 1.0
outcomes, units = pooled_outcomes((2, "path"))
report = ex.analyse_rule(manifest, outcomes, units, resamples=20)
assert report["families"]["path"]["B"]["floor"]["may_block"] is False and report["decision"] == "adopt"
assert "injection" in report["reason"] and "path" not in report["reason"]
assert report["overall_after_floor"]["B"]["paired"]["flagged"] == report["families"]["injection"]["B"]["paired"]["flagged"]
text = path.read_text()
for bad, why in (
(text.replace("normalise: platt", "normalise: rank"), "normalise"),
(text.replace("floor: 10", "floor: 0"), "floor"),
(text.replace("name: pooled_block_gate", "name: precision_bound_block"), "not understood"),
):
path.write_text(bad)
with pytest.raises(ex.ExperimentError, match=why):
ex.load_manifest(path)
def test_the_exp004_manifest_registers_the_procedure_before_its_units() -> None:
path = ROOT / "plane/experiments/exp-004-pooled-block-gate.yaml"
if not path.exists():
pytest.skip("exp-004 is not written yet")
manifest = ex.load_manifest(path)
gate = manifest.arms["B"].rule
assert manifest.kind == "rule" and manifest.budget_total_usd == 0 and manifest.raw["decision"]["pooled"] is True
assert gate["name"] == "pooled_block_gate" and gate["normalise"] == "platt" and gate["floor"] == 10 and gate["precision"] == 0.95
assert manifest.raw["populations"]["primary"] == "paired" and set(manifest.programs) == set(manifest.families)