fix(learn): withhold label-tracking signals from the v1 classifier; leak audit

Maintainer decision "Neutralise both for v1".

1. Phantom candidates. A pair side's candidates are now only the functions that
   side declares (plane/harvest.py `declares`, the excerpt builder's
   function_bounds); a labelled function absent from a side is listed under
   `absent_on_side` and never asked, and a side declaring none of its verify
   family's functions is no verify unit. The recorded harvest units held 58
   phantom verify candidates (53 fixed, 5 vulnerable sides). build_features.py
   skips label rows whose side does not declare the function and drops their
   earlier rows (MemoryStore.drop_rows); rebuilt the pair records from the
   recorded signals, no model call: 126 label rows absent_on_side (117 fixed,
   9 vulnerable) -- access_control 13, config_secrets 4, deserialization 20,
   injection 28, memory 4, output_encoding 16, path 15, untrusted_destination
   26 -- so 252 feature rows dropped (1712 -> 1460); no other row changed.

2. Engine withheld. Input profiles in learn/schema.py (`v1` withholds the
   engine instrument, `full` withholds nothing); ProgramSpec.inputs defaults to
   v1 and compile.toml records it. Neither the engine's state nor its values
   reach the candidate, examples, demonstrations or the instruction proposer's
   error summary, and the Gower retrieval distance leaves the engine block out.
   Stored records are unchanged. design.md Risks records the decision and that
   the engine returns only through a pre-registered A/B on assumed-benign and
   benign-control negatives.

3. `ousast learn audit-leaks` (learn/leaks.py): per feature and per instrument
   state, separation = (pairs the signal alone puts on the right side - on the
   wrong side) / pairs, over all pairs and per family; flagged at >= 0.20 over
   >= 10 pairs. Counts under
   benchmarks/measurements/2026-10-01-decision-engine-leak-audit/ (before and
   after the rebuild). After: facts.function_lines (0.80 untrusted_destination,
   0.75 config_secrets, 0.26 injection; higher on fixed), verify.turns_mean
   0.67 config_secrets, eng.* 0.35-0.40 output_encoding, verify.flag_a/votes/
   site_offset and agree.final 0.20-0.30 on 10-12 pair families. Before the
   rebuild the phantom fixed sides drove state.source/state.roles/ms.flagged
   to 1.0 and state.engine to 0.95 on deserialization.

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-01 19:40:58 +02:00
co-authored by Claude Opus 5.5
parent 4c96a59f8f
commit 9e3c83cfdc
20 changed files with 12062 additions and 35 deletions
+3
View File
@@ -1418,6 +1418,9 @@ def _plane_harvest(args: argparse.Namespace) -> int:
) # fmt: skip
for relative in sorted(k for k in files if k.startswith("runs/")):
print(f"{args.plane / relative}: {files[relative].splitlines()[1].lstrip('# ')}")
index = json.loads(files["units.json"])
absent = sum(len(u.get("absent_on_side") or ()) for u in index.values())
print(f"absent_on_side: {absent} labelled candidates not declared on their side (not asked there)")
return 0
+38
View File
@@ -53,8 +53,19 @@ def add_commands(learn_sub: Any) -> None:
if name == "curve":
sub.add_argument("--subset", type=int, default=200)
leaks = learn_sub.add_parser("audit-leaks", help="how well each signal alone separates the sides of a pair (no model)")
leaks.add_argument("--memory", help="the store (default OUSAST_MEMORY, then results_root()/memory)")
leaks.add_argument("--units", type=Path, help="the harvest units.json (unit -> case and side) for feature rows without a side")
leaks.add_argument("--profile", choices=("static", "plane"), default="plane")
leaks.add_argument("--kind", default="features", help="the row kind to audit (features, or example)")
leaks.add_argument("--threshold", type=float, help="flag a separation at least this (default 0.20)")
leaks.add_argument("--min-pairs", type=int, help="over at least this many pairs (default 10)")
leaks.add_argument("--out", type=Path, help="write the counts-only report here")
def run(args: argparse.Namespace) -> int:
if args.learn_command == "audit-leaks":
return _audit_leaks(args)
if args.learn_command == "memory" and args.memory_command == "build":
return _memory_build(args)
if args.learn_command == "memory" and args.memory_command == "embed":
@@ -225,4 +236,31 @@ def _memory_build(args: argparse.Namespace) -> int:
return 0
def _audit_leaks(args: argparse.Namespace) -> int:
"""Exit 2 when the store holds no pair to audit (an unread store is not a clean one)."""
from ..plane.memory import open_store
from .leaks import MIN_PAIRS, THRESHOLD, audit, pair_rows, sides_from_units
store = open_store(args.memory)
rows = [r.row for r in store.rows(kind=args.kind)]
units = json.loads(args.units.read_text(encoding="utf-8")) if args.units is not None else {}
pairs, skipped = pair_rows(rows, sides_from_units(units), args.profile)
if not pairs:
print(f"learn audit-leaks: {len(rows)} {args.kind} rows in {store.describe()}, no pair to audit {skipped}", file=sys.stderr)
return 2
threshold = THRESHOLD if args.threshold is None else args.threshold
report = audit(pairs, args.profile, threshold=threshold, min_pairs=MIN_PAIRS if args.min_pairs is None else args.min_pairs)
report = {"rows_read": len(rows), "kind": args.kind, "skipped": skipped, **report}
text = json.dumps(report, indent=1, sort_keys=True) + "\n"
if args.out is not None:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(text, encoding="utf-8")
print(json.dumps({"rows_read": len(rows), "pairs": report["pairs"], "flagged_signals": report["flagged_signals"]}, sort_keys=True))
for cell in report["flagged"]:
print(
f" {cell['separation']:.3f} {cell['signal']} [{cell['scope']}] {cell['correct']}-{cell['wrong']} of {cell['pairs']} pairs"
)
return 0
__all__ = ["add_commands", "run"]
+13 -4
View File
@@ -52,7 +52,7 @@ from .program import (
ProgramSpec,
label_word,
)
from .schema import SCHEMA_VERSION, feature_set_digest
from .schema import BY_NAME, DEFAULT_INPUTS, SCHEMA_VERSION, feature_set_digest, withheld
DEFAULT_SPEC = Path(__file__).resolve().parent / "compile.toml"
KIND = "llm-program"
@@ -152,8 +152,13 @@ class Bootstrap:
self.rejected[why] = self.rejected.get(why, 0) + 1
def _signal_summary(example: Example) -> dict[str, Any]:
ran = {k: v for k, v in example.x.items() if v not in (None, 0, False) and not k.startswith("lang.")}
def _signal_summary(example: Example, inputs: str = DEFAULT_INPUTS) -> dict[str, Any]:
"""The non-zero signals of a mistake for the instruction proposer, without the instruments ``inputs`` withholds."""
hidden = withheld(inputs)
ran = {
k: v for k, v in example.x.items()
if v not in (None, 0, False) and not k.startswith("lang.") and not (k in BY_NAME and BY_NAME[k].instrument in hidden)
} # fmt: skip
return dict(sorted(ran.items())[:12])
@@ -179,7 +184,7 @@ def bootstrap(
right = decision.verdict == label_word(example.label)
if not right:
out.errors.append({"family": example.family, "label": label_word(example.label), "verdict": decision.verdict,
"confidence": answer.confidence, "signals": _signal_summary(example)}) # fmt: skip
"confidence": answer.confidence, "signals": _signal_summary(example, program.spec.inputs)}) # fmt: skip
out.reject("wrong or unsure")
continue
if answer.parse_failed or answer.confidence < min_confidence:
@@ -370,6 +375,7 @@ def compile_program(
base = ProgramSpec(
profile=profile, model=str(classify.get("model", "deepseek-flash")), k=int(classify.get("k", 1)), k_ret=int(retrieval.get("k", 6)),
lam=float(retrieval.get("lam", 0.5)), n1=int(retrieval.get("n1", 40)), priors=str(retrieval.get("priors", "off")),
inputs=str(classify.get("inputs", DEFAULT_INPUTS)),
max_output_tokens=int(classify.get("max_output_tokens", 300)),
) # fmt: skip
from .evaluate import candidate_from_example
@@ -434,6 +440,8 @@ def compile_program(
"temperature": [0.0, 0.7],
"confidence": "vote_mean",
"max_output_tokens": base.max_output_tokens,
"inputs": base.inputs,
"withheld": sorted(withheld(base.inputs)),
},
"calibration": None,
"operating_points": None,
@@ -491,6 +499,7 @@ def spec_of(artifact: Mapping[str, Any]) -> ProgramSpec:
lam=float(retrieval["lam"]),
n1=int(retrieval["n1"]),
priors=str(retrieval.get("priors", "off")),
inputs=str(classify.get("inputs", DEFAULT_INPUTS)),
max_output_tokens=int(classify.get("max_output_tokens", 300)),
)
+1
View File
@@ -31,6 +31,7 @@ val_candidates = 60
model = "deepseek-flash"
k = 1 # k and lam are A/B variants (design section 6), not searched here
max_output_tokens = 300
inputs = "v1" # the input profile: the engine instrument is withheld from prompts and retrieval (2026-10-01)
[retrieval]
n1 = 40
+151
View File
@@ -0,0 +1,151 @@
"""The leakage audit: how well each signal *alone* tells the sides of a pair apart (``ousast learn audit-leaks``).
No model. A pair is one candidate (``path::function`` and family) on its vulnerable side and on its fixed side -- a
pair-corpus case, or a population case's vulnerable and fixed pins. Within a pair the code differs by the fix, so a
signal that differs between the sides *and whose difference names the side* is either real evidence about the fix or
an instrument artifact that tracks the label (the engine's "read the file, asked no question" state on a fixed side
that lost its sink). The audit cannot tell which; it ranks every signal so a person can.
For each feature of the profile, and for each instrument's state (``state.<instrument>``), over the ``N`` pairs:
- an ordered feature (``int``, ``float``, ``bool``; ``null`` ranks below every value) predicts the side by one global
direction: ``correct`` = pairs where the vulnerable side is higher (or lower, whichever is more), ``wrong`` = the
other direction;
- an unordered one (``enum``, an instrument state) by the majority side of each unordered pair of values;
- **separation** = ``(correct - wrong) / N``: the share of all pairs the signal alone puts on the right side, net of
the pairs it puts on the wrong one. 0 is no information (or no difference), 1 is every pair.
A signal is **flagged** when its separation is at least :data:`THRESHOLD` (0.20) over at least :data:`MIN_PAIRS`
(10) pairs, over all pairs or within one family. The output is counts only: feature, instrument and family names.
"""
from __future__ import annotations
from collections import Counter
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from typing import Any
from .schema import FeatureSpec, features_for, instruments_for
THRESHOLD = 0.20
MIN_PAIRS = 10
ALL = "all"
Record = Mapping[str, Any]
@dataclass(frozen=True)
class Pair:
family: str
vulnerable: Record # a feature record: x and instruments
fixed: Record
@dataclass(frozen=True)
class Signal:
"""A feature (``spec``) or an instrument's state (``instrument``)."""
name: str
instrument: str
ordered: bool
spec: FeatureSpec | None = None
def value(self, record: Record) -> Any:
if self.spec is None:
return str((record["instruments"].get(self.instrument) or {}).get("state", "none"))
return record["x"].get(self.name)
def signals(profile: str) -> list[Signal]:
out = [Signal(f"state.{name}", name, False) for name in instruments_for(profile) if name != "language"]
out += [Signal(s.name, s.instrument, s.type != "enum", s) for s in features_for(profile)]
return out
def _rank(value: Any) -> float:
return -1.0 if value is None else float(value)
def separation(signal: Signal, pairs: Sequence[Pair]) -> dict[str, Any]:
"""``pairs``, ``differ``, ``correct``, ``wrong``, ``separation`` and (ordered) the ``direction`` of one signal."""
n = len(pairs)
values = [(signal.value(p.vulnerable), signal.value(p.fixed)) for p in pairs]
out: dict[str, Any] = {"pairs": n}
if signal.ordered:
higher = sum(_rank(v) > _rank(f) for v, f in values)
lower = sum(_rank(v) < _rank(f) for v, f in values)
correct, wrong = max(higher, lower), min(higher, lower)
if correct:
out["direction"] = "higher on vulnerable" if higher >= lower else "higher on fixed"
else:
counts: Counter[tuple[str, str]] = Counter((repr(v), repr(f)) for v, f in values if v != f)
correct = wrong = 0
for a, b in {tuple(sorted(k)) for k in counts}:
one, other = counts[(a, b)], counts[(b, a)]
correct, wrong = correct + max(one, other), wrong + min(one, other)
out.update({"differ": correct + wrong, "correct": correct, "wrong": wrong, "separation": round((correct - wrong) / n, 4) if n else 0.0})
return out
def audit(pairs: Sequence[Pair], profile: str, *, threshold: float = THRESHOLD, min_pairs: int = MIN_PAIRS) -> dict[str, Any]:
"""Every signal's separation over all pairs and per family, and the flagged (signal, scope) cells, strongest first."""
families = sorted({p.family for p in pairs})
scopes = {ALL: list(pairs), **{f: [p for p in pairs if p.family == f] for f in families}}
table: dict[str, dict[str, Any]] = {}
flagged: list[dict[str, Any]] = []
for signal in signals(profile):
row = {scope: separation(signal, members) for scope, members in scopes.items() if members}
table[signal.name] = row
for scope, cell in row.items():
if cell["pairs"] >= min_pairs and cell["separation"] >= threshold:
flagged.append({"signal": signal.name, "instrument": signal.instrument, "scope": scope, **cell})
flagged.sort(key=lambda c: (-c["separation"], c["signal"], c["scope"]))
return {
"profile": profile,
"threshold": threshold,
"min_pairs": min_pairs,
"pairs": {scope: len(members) for scope, members in scopes.items()},
"flagged": flagged,
"flagged_signals": sorted({c["signal"] for c in flagged}),
"signals": table,
}
def pair_rows(rows: Iterable[Record], side_of: Mapping[str, tuple[str, str]], profile: str) -> tuple[list[Pair], dict[str, int]]:
"""Pairs from stored feature rows of ``profile``. ``side_of`` maps a row's ``task`` (the harvest unit) to
``(case reference, side)``; a row carrying ``pin_role`` and ``source_ref`` needs no entry. A candidate pairs when it
has exactly one vulnerable and one fixed record in the same case; the rest are counted."""
groups: dict[tuple[str, str, str], dict[str, list[Record]]] = {}
skipped: Counter[str] = Counter()
for row in rows:
if row.get("profile") != profile:
continue
if row.get("pin_role") and row.get("source_ref"):
ref, side = str(row["source_ref"]), str(row["pin_role"])
elif str(row.get("task")) in side_of:
ref, side = side_of[str(row["task"])]
else:
skipped["no side"] += 1
continue
if side not in ("vulnerable", "fixed"):
skipped[f"side {side}"] += 1
continue
groups.setdefault((ref, str(row["candidate"]), str(row["family"])), {}).setdefault(side, []).append(row)
pairs: list[Pair] = []
for (_, _, family), sides in sorted(groups.items()):
vulnerable, fixed = sides.get("vulnerable", []), sides.get("fixed", [])
if len(vulnerable) == 1 and len(fixed) == 1:
pairs.append(Pair(family, vulnerable[0], fixed[0]))
elif not vulnerable or not fixed:
skipped["one side only"] += 1
else:
skipped["ambiguous"] += 1
return pairs, dict(sorted(skipped.items()))
def sides_from_units(units: Mapping[str, Mapping[str, Any]]) -> dict[str, tuple[str, str]]:
"""Harvest ``units.json`` -> unit name: (case reference, side)."""
return {name: (str(unit["ref"]), str(unit["side"])) for name, unit in units.items()}
__all__ = ["ALL", "MIN_PAIRS", "THRESHOLD", "Pair", "Signal", "audit", "pair_rows", "separation", "sides_from_units", "signals"]
+25 -11
View File
@@ -13,7 +13,9 @@ sentences citing at least one line of the code. The **modules** are ``Retrieve -
``not_vulnerable``, 0.5 for ``unsure``); the verdict is the majority (ties are ``unsure``), and a majority
``unsure`` can never BLOCK.
Signals reach the prompt only through :func:`.schema.validate_x` (no ``EXCLUDED_FIELDS``, no free text); a missing
Signals reach the prompt only through :func:`.schema.validate_x` (no ``EXCLUDED_FIELDS``, no free text) and the input
profile (``ProgramSpec.inputs``, default ``v1``: the engine instrument is withheld from prompts and from the retrieval
distance, see :data:`.schema.INPUT_PROFILES`); a missing
instrument is shown in words. Every model call goes through :class:`Caller`: a response cache keyed by
``sha256(model, Model parameters digest, messages, temperature, sample, json_object)`` stored as
``responses/<sha>.json``, so a rerun on the same inputs replays at $0 and reproduces byte for byte. Before a call the
@@ -35,7 +37,7 @@ from ..plane.memory import MemoryStore
from .examples import Example
from .folds import Fold
from .retrieve import Retrieval, Target, assert_boundary, demonstrations_for, retrieve
from .schema import features_for, instruments_for, validate_x
from .schema import DEFAULT_INPUTS, features_for, instruments_for, validate_x, withheld
VERDICTS = ("vulnerable", "not_vulnerable", "unsure")
UNSURE = "unsure"
@@ -70,7 +72,12 @@ class Signature:
SIGNATURE = Signature(
(
Field("code", "in", "excerpt", "numbered lines, <= 80 lines and 4,000 characters; a delta adds the diff (<= 40 lines)"),
Field("signals", "in", "record", "the allow-listed features as name: value lines; a missing instrument in words"),
Field(
"signals",
"in",
"record",
"the allow-listed features the input profile shows, as name: value lines; a missing instrument in words",
),
Field("roles", "in", "roles", "inferred roles of the function: role, operation, line, origin"),
Field("family", "in", "family", "the candidate's family, or unknown"),
Field("verdict", "out", "enum", "vulnerable | not_vulnerable | unsure"),
@@ -95,13 +102,16 @@ BASELINE_INSTRUCTION = (
# --- rendering ---------------------------------------------------------------------------------------------------------
def render_signals(x: Mapping[str, Any], instruments: Mapping[str, Mapping[str, Any]], profile: str) -> str:
def render_signals(x: Mapping[str, Any], instruments: Mapping[str, Mapping[str, Any]], profile: str, inputs: str = DEFAULT_INPUTS) -> str:
"""``name: value`` per feature of an instrument that ran; ``<instrument>: no coverage | failed | not applicable``
otherwise. The record is validated first: an unknown, label or identity field never reaches a prompt."""
otherwise. The record is validated first: an unknown, label or identity field never reaches a prompt. An
instrument the input profile withholds (:data:`.schema.INPUT_PROFILES`; v1: the engine) is not rendered at all --
neither its state nor its values -- though the stored record keeps it."""
validate_x(x, instruments, profile)
hidden = withheld(inputs)
lines: list[str] = []
for name in instruments_for(profile):
if name == "language":
if name == "language" or name in hidden:
continue
state = str(instruments[name]["state"])
if state != "ran":
@@ -129,8 +139,9 @@ def _case(
family: str,
profile: str,
language: str,
inputs: str = DEFAULT_INPUTS,
) -> str:
signals = render_signals(x, instruments, profile)
signals = render_signals(x, instruments, profile, inputs)
return f"Family: {family}\nLanguage: {language}\nCode:\n{code.rstrip()}\nSignals:\n{signals}\nRoles:\n{render_roles(roles)}"
@@ -181,6 +192,7 @@ class ProgramSpec:
lam: float = 0.5
n1: int = 40
priors: str = "off"
inputs: str = DEFAULT_INPUTS # the input profile: which instruments the classifier never sees (v1: the engine)
max_output_tokens: int = MAX_OUTPUT_TOKENS
@@ -228,7 +240,7 @@ def render_prefix(spec: ProgramSpec, demos: Sequence[Demo], examples: Mapping[st
"cited_lines": list(demo.cited_lines)} # fmt: skip
parts.append(
f"Demonstration {number}:\n"
+ _case(code, example.x, example.instruments, example.roles, example.family, spec.profile, example.language)
+ _case(code, example.x, example.instruments, example.roles, example.family, spec.profile, example.language, spec.inputs)
+ f"\nAnswer: {json.dumps(answer, sort_keys=True)}"
)
return "\n\n".join(parts)
@@ -249,10 +261,12 @@ def render(
code = excerpt_text(example.excerpt_sha) or ""
blocks.append(
f"Example {number} (label: {label_word(example.label)}):\n"
+ _case(code, example.x, example.instruments, example.roles, example.family, spec.profile, example.language)
+ _case(code, example.x, example.instruments, example.roles, example.family, spec.profile, example.language, spec.inputs)
)
t = candidate.target
blocks.append("Candidate:\n" + _case(candidate.code, t.x, t.instruments, candidate.roles, t.family, spec.profile, candidate.language))
blocks.append(
"Candidate:\n" + _case(candidate.code, t.x, t.instruments, candidate.roles, t.family, spec.profile, candidate.language, spec.inputs)
)
messages: list[dict[str, object]] = [
{"role": "system", "content": render_prefix(spec, demos, examples, excerpt_text)},
{"role": "user", "content": "\n\n".join(blocks)},
@@ -454,7 +468,7 @@ class Program:
def prepare(self, candidate: Candidate, fold: Fold, *, seed: int = 0) -> tuple[Prompt, Retrieval]:
target = candidate.target
got = retrieve(target, self.memory, fold, vectors=self.vectors, n1=self.spec.n1, k=self.spec.k_ret, lam=self.spec.lam, seed=seed,
priors=self.spec.priors) # fmt: skip
priors=self.spec.priors, inputs=self.spec.inputs) # fmt: skip
usable = [Demo.from_dict(d) for d in demonstrations_for([d.as_dict() for d in self.spec.demos], self.index, target, fold)]
prompt = render(self.spec, candidate, got.examples, usable, self.index, self.excerpt_text)
assert_boundary([*prompt.example_ids, *prompt.demo_ids], self.index, target, fold, vectors=self.vectors)
+10 -5
View File
@@ -8,7 +8,8 @@ profile, computed per instrument block so that missing is explicit, never impute
both is ``0``, in one is ``1``;
- different states (``ran`` vs ``none``/``failed``/``not_applicable``, or ``failed`` vs ``none``): ``1``; the
same non-``ran`` state: ``0``;
- ``D`` is the uniform mean over the profile's blocks; prior features are dropped with ``priors = "off"``.
- ``D`` is the uniform mean over the profile's blocks; prior features are dropped with ``priors = "off"``, and the blocks
of instruments the input profile withholds (``inputs``, default ``v1``: the engine) are left out entirely.
Ties break by ``sha256(example id + seed)``; the first ``n1 = 40`` go to stage 2, a cosine re-rank of the code
embeddings, ``r = lam (1 - D) + (1 - lam) cos``. Without the target's vector the whole retrieval is
@@ -34,7 +35,7 @@ from .embeddings import cosine
from .examples import Example
from .folds import Fold
from .labels import repo_name
from .schema import UNBOUNDED_FLOATS, FeatureSpec, features_for, instruments_for
from .schema import DEFAULT_INPUTS, UNBOUNDED_FLOATS, FeatureSpec, features_for, instruments_for, withheld
N1 = 40
K_RET = 6
@@ -91,9 +92,12 @@ def gower(
profile: str,
*,
priors: str = "off",
inputs: str = DEFAULT_INPUTS,
) -> float:
"""The per-instrument Gower distance of two records of ``profile`` (module docstring), in [0, 1]."""
total, blocks = 0.0, _blocks(profile, priors)
"""The per-instrument Gower distance of two records of ``profile`` (module docstring), in [0, 1]. The blocks of
instruments the input profile withholds (v1: the engine) are not compared, so neighbours cannot carry them."""
hidden = withheld(inputs)
total, blocks = 0.0, [(name, specs) for name, specs in _blocks(profile, priors) if name not in hidden]
for name, specs in blocks:
sa, sb = a_instruments.get(name, {}).get("state", "none"), b_instruments.get(name, {}).get("state", "none")
if sa != sb:
@@ -192,11 +196,12 @@ def retrieve(
lam: float = LAM,
seed: int = 0,
priors: str = "off",
inputs: str = DEFAULT_INPUTS,
) -> Retrieval:
"""The ``k`` balanced examples for ``target`` from the eligible part of ``pool`` (module docstring)."""
candidates = [e for e in pool if e.profile == target.profile and eligible(e, target, fold)]
scored = sorted(
((gower(target.x, target.instruments, e.x, e.instruments, target.profile, priors=priors), e) for e in candidates),
((gower(target.x, target.instruments, e.x, e.instruments, target.profile, priors=priors, inputs=inputs), e) for e in candidates),
key=lambda pair: (pair[0], _tie(pair[1].id, seed)),
)[:n1]
mode = "signals+embeddings" if target.vector is not None and vectors else "signals_only"
+19 -2
View File
@@ -94,6 +94,15 @@ EXCLUDED_FIELDS = frozenset(
) # fmt: skip
# What the classifier may see of a record (design, Risks: label-tracking signals, 2026-10-01). A record is stored whole;
# an input profile names the instruments whose state and values are withheld from the prompt, the demonstrations, the
# instruction proposer and the retrieval distance. On the pairs the engine's "read the file, asked no question" state
# tracks the label (a fixed side loses its sink), so v1 withholds the engine entirely; it returns only through a
# pre-registered A/B on negatives where it cannot track a fix.
INPUT_PROFILES: Mapping[str, tuple[str, ...]] = {"v1": ("engine",), "full": ()}
DEFAULT_INPUTS = "v1"
class FeatureRecordError(ValueError):
"""A record names a feature outside the allow-list, a value outside its type, or a label/identity field."""
@@ -172,6 +181,13 @@ def instruments_for(profile: str) -> tuple[str, ...]:
return tuple(name for name in INSTRUMENTS if name in used)
def withheld(inputs: str) -> frozenset[str]:
"""The instruments an input profile withholds from the classifier (:data:`INPUT_PROFILES`)."""
if inputs not in INPUT_PROFILES:
raise FeatureRecordError(f"unknown input profile {inputs!r} (one of {', '.join(INPUT_PROFILES)})")
return frozenset(INPUT_PROFILES[inputs])
def feature_set_digest() -> str:
"""sha256 of the canonical feature list: any change to a name, type, cap, vocabulary or profile changes it."""
canonical = json.dumps([asdict(spec) for spec in FEATURES], sort_keys=True, separators=(",", ":"))
@@ -251,8 +267,9 @@ def names(specs: Iterable[FeatureSpec] = FEATURES) -> tuple[str, ...]:
__all__ = [
"BY_NAME", "EXCLUDED_FIELDS", "FEATURES", "FINALS", "INSTRUMENTS", "LANGUAGES", "MECHANISMS", "NOVELTY", "OPERATIONS",
"BY_NAME", "DEFAULT_INPUTS", "EXCLUDED_FIELDS", "FEATURES", "FINALS", "INPUT_PROFILES", "INSTRUMENTS", "LANGUAGES", "MECHANISMS",
"NOVELTY", "OPERATIONS",
"ORIGINS", "PROFILES", "RULE_TAG_MECHANISM", "RUNGS", "SCHEMA_VERSION", "SOURCE_KINDS", "STATES", "UNITS",
"FeatureRecordError", "FeatureSpec", "feature_set_digest", "features_for", "instruments_for", "names",
"validate_record", "validate_x",
"validate_record", "validate_x", "withheld",
] # fmt: skip
+28 -8
View File
@@ -14,8 +14,11 @@ root: the excerpts and candidates are inputs, never committed):
Units carry no label. A pair side is a files-only Workspace holding the excerpt (and the documents that register
it) under ``repo/`` and the candidates under ``inputs/`` -- outside the hunt's root, so the tools cannot read them;
its name is opaque (``h-<sha>``): the pair's name says its CWE and its side. ``agree`` gets no declared sites, so
no ``site_match`` is computed. ``units.json`` maps each unit name to its source, reference, side, repository, pin
and candidates for the host-side feature build; it names functions and stays local.
no ``site_match`` is computed. A side's candidates are only the functions that side declares (the excerpt builder's
matcher, :func:`declares`): a labelled function absent from a side -- a fixed side whose fix removed or renamed it --
is never asked there and is listed under ``absent_on_side`` instead. ``units.json`` maps each unit name to its
source, reference, side, repository, pin and candidates for the host-side feature build; it names functions and stays
local.
Every Run entry has a budget; the sum over each Run is held under ``ceiling`` (USD) by construction: verify gets
:data:`USD_PER_HUNT` per hunt of an excerpt or a repository (scaled down when the hunts would exceed the ceiling),
@@ -44,7 +47,7 @@ from .tasks.repo_facts import DECLARATION, GLOBAL, _declared_name
from .tasks.roles import CHUNK_LINES
from .tasks.verify import OPERATIONS, units_of
__all__ = ["Unit", "declaration_line", "pair_units", "population_units", "render_harvest", "write_harvest"]
__all__ = ["Unit", "declaration_line", "declares", "pair_units", "population_units", "render_harvest", "write_harvest"]
Candidate = tuple[str, str, int]
REPO = "repo"
@@ -79,14 +82,18 @@ class Unit:
files: tuple[tuple[str, str], ...] = field(default=())
verify: tuple[Candidate, ...] = field(default=()) # the candidates of ``family`` (all of them when empty)
sizes: tuple[tuple[str, int, int], ...] = field(default=()) # (path, characters, lines) of a pin's candidate files
absent_on_side: tuple[str, ...] = field(default=()) # labelled ``path::function`` names this side does not declare
@property
def paths(self) -> list[str]:
return sorted({c[0] for c in self.candidates})
def index_row(self) -> dict[str, Any]:
return {"source": self.source, "ref": self.ref, "side": self.side, "repo": self.repo, "pin": self.pin,
"family": self.family, "candidates": [list(c) for c in self.candidates]} # fmt: skip
row: dict[str, Any] = {"source": self.source, "ref": self.ref, "side": self.side, "repo": self.repo, "pin": self.pin,
"family": self.family, "candidates": [list(c) for c in self.candidates]} # fmt: skip
if self.absent_on_side:
row["absent_on_side"] = list(self.absent_on_side)
return row
def _opaque(*parts: str) -> str:
@@ -106,6 +113,14 @@ def declaration_line(lines: Sequence[str] | None, path: str, function: str) -> i
return next((i + 1 for i, line in enumerate(lines) if _declared_name(pattern, line) == function), 1)
def declares(lines: Sequence[str] | None, path: str, function: str) -> bool:
"""Whether a side's file declares ``function``, by the excerpt builder's matcher
(:func:`..learn.excerpt.function_bounds`): a candidate is asked only where an excerpt of it exists."""
from ..learn.excerpt import function_bounds
return bool(lines) and function_bounds(lines or (), detect_language(Path(path)) or "", function) is not None
def _read_labels(labels: Path) -> list[dict[str, Any]]:
return [json.loads(line) for line in labels.read_text(encoding="utf-8").splitlines() if line.strip()]
@@ -135,13 +150,18 @@ def pair_units(labels: Path, catalog: Path) -> list[Unit]:
lines = {path: text.splitlines() for path, text in texts.items()}
def located(names: Iterable[str], lines: Mapping[str, list[str]] = lines) -> tuple[Candidate, ...]:
return tuple(sorted({(p, fn, declaration_line(lines.get(p), p, fn)) for c in names for p, _, fn in [c.partition("::")]}))
"""The candidates this side declares: a name taken from the other side is never asked here."""
split = [(p, fn) for c in names for p, _, fn in [c.partition("::")]]
return tuple(sorted({(p, fn, declaration_line(lines.get(p), p, fn)) for p, fn in split if declares(lines.get(p), p, fn)}))
labelled = set().union(*families.values())
absent = tuple(sorted(c for c in labelled if not declares(lines.get(c.partition("::")[0]), *c.partition("::")[::2])))
asked = located(families[verify[0]]) if verify else () # none declared here: no verify unit on this side
role = "vulnerable" if side == "vuln" else "fixed"
out.append(Unit(
_opaque("pairs", name, side), "pairs", name, role, case.repo or f"pairs/{case.slice}/{name}",
blob_sha1(texts[case.relpath]), verify[0] if verify else "", located(set().union(*families.values())),
tuple(sorted(texts.items())), located(families[verify[0]]) if verify else (),
blob_sha1(texts[case.relpath]), verify[0] if asked else "", located(labelled),
tuple(sorted(texts.items())), asked, absent_on_side=absent,
)) # fmt: skip
return out
+18
View File
@@ -325,6 +325,24 @@ class MemoryStore(ABC):
ordered = [merged[i] for i in sorted(merged)]
self._put(key, "".join(_line(r) + "\n" for r in ordered).encode("utf-8"), _labels(ordered))
def drop_rows(self, repo: str, pin: str, ids: Iterable[str]) -> int:
"""Remove the rows ``ids`` from one repository + pin object (a rebuild that no longer produces them); returns
how many were removed. The object is deleted when it ends up empty."""
key = f"repos/{repo_dir(repo)}/{pin}.jsonl"
found = self._get(key)
unwanted = set(ids)
if found is None or not unwanted:
return 0
rows = _parse_lines(found[0], key)
kept = [r for r in rows if str(r["id"]) not in unwanted]
if len(kept) == len(rows):
return 0
if kept:
self._put(key, "".join(_line(r) + "\n" for r in kept).encode("utf-8"), _labels(kept))
else:
self._delete(key)
return len(rows) - len(kept)
def ingest_rows(self, run: str, task: str, rows: Sequence[Mapping[str, Any]], facts: bytes | None = None) -> IngestResult:
"""One (run, task)'s rows and facts; a repeat of the same rows is skipped by the index."""
checked = [validate_row(dict(r), f"{run}/{task} row {i + 1}") for i, r in enumerate(rows)]