2026-09-06 15:16:29 +02:00
|
|
|
|
# Brief: the detector optimizer
|
|
|
|
|
|
|
|
|
|
|
|
## Problem
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
`learning-harness` built an honest measurement layer with a closed family taxonomy, a class-aware pair
|
2026-09-06 15:16:29 +02:00
|
|
|
|
scorer, per-family detector configurations, a split enforced by construction, verifier-gated reporting,
|
|
|
|
|
|
and a journal with attribution. What it did **not** build is a search. `run_learning_round` is a
|
|
|
|
|
|
hand-rolled hill-climber: one proposal, one minibatch, one holdout check, a threshold rule, accept or
|
|
|
|
|
|
revert. Every defect four review rounds found in it was a defect of that improvised loop rather than of
|
|
|
|
|
|
the measurement underneath it.
|
|
|
|
|
|
|
|
|
|
|
|
The maintainer's judgement, and the reason for this spec: the remaining work is not a sequence of
|
|
|
|
|
|
patches to that loop. It is adopting an interface and an algorithm that already exist, are published,
|
|
|
|
|
|
and are better than what we would arrive at by fixing ours one finding at a time.
|
|
|
|
|
|
|
|
|
|
|
|
## What the current loop gets wrong, measured
|
|
|
|
|
|
|
|
|
|
|
|
Round zero on vibe-py, `deepseek-v4-flash`, K = 5, $0.69, 23/30 pair-correct:
|
|
|
|
|
|
|
|
|
|
|
|
| family | scorable | recall | silence | Youden | noise floor |
|
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
|
|
|
| injection | 20 | 0.75 | 0.90 | +0.65 | **11** |
|
|
|
|
|
|
| access_control | 3 | 1.00 | 1.00 | +1.00 | 3 |
|
|
|
|
|
|
| config_secrets | 3 | 1.00 | 1.00 | +1.00 | 1 |
|
|
|
|
|
|
| deserialization | 2 | 1.00 | 1.00 | +1.00 | 0 |
|
|
|
|
|
|
| output_encoding | 1 | 1.00 | 1.00 | +1.00 | 1 |
|
|
|
|
|
|
| path | 1 | 1.00 | 1.00 | +1.00 | 1 |
|
|
|
|
|
|
|
|
|
|
|
|
Four defects follow from the loop's shape, not from its bugs:
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
1. The acceptance rule cannot discriminate. `decide` compares raw deltas: any regression on train or
|
2026-09-06 15:16:29 +02:00
|
|
|
|
holdout rejects, a tie rejects. The measured floor says 11 of 20 injection pairs disagree with
|
|
|
|
|
|
*themselves* across five runs, so a delta over a minibatch of 8 is mostly noise. The spec built
|
|
|
|
|
|
`sign_test` and `reliable_change` for exactly this and the rule calls neither.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
2. The holdout is an input to the decision. `decide` reads `holdout_delta`. Req 5.4 keeps the
|
2026-09-06 15:16:29 +02:00
|
|
|
|
*proposer* blind to the holdout, and then the *selector* reads it every round. Repeated rounds
|
2026-10-12 18:16:41 +02:00
|
|
|
|
selecting on holdout performance is search-time overfitting of the test set, the same leak class
|
2026-09-06 15:16:29 +02:00
|
|
|
|
this project has already measured once, one level up.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
3. Noise is fought with repetition instead of diversity. K = 5 spends five rollouts per pair per
|
2026-09-06 15:16:29 +02:00
|
|
|
|
side estimating variance. The published alternative spends that budget on more candidates and lets a
|
|
|
|
|
|
frontier keep whatever wins somewhere.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
4. The proposer is told a score, not what went wrong. `FailureFacts` carries outcomes, run strings
|
2026-09-06 15:16:29 +02:00
|
|
|
|
and counts. The single largest lever in the published work is textual feedback from the *evaluation
|
2026-10-12 18:16:41 +02:00
|
|
|
|
trace*, and our evaluator produces exactly the right material (which finding landed on which line,
|
2026-09-06 15:16:29 +02:00
|
|
|
|
whether it fell inside the labeled span, whether the fixed side leaked, which family was claimed)
|
|
|
|
|
|
and then throws all of it away.
|
|
|
|
|
|
|
|
|
|
|
|
## What others built
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
**GEPA, Reflective Prompt Evolution (Agrawal et al., arXiv:2507.19457, ICLR 2026 oral).** Algorithm 1,
|
2026-09-06 15:16:29 +02:00
|
|
|
|
read from the paper rather than from a summary:
|
|
|
|
|
|
|
|
|
|
|
|
- Split `D_train` into `D_feedback` and `D_pareto`. Initialise the candidate pool `P = [Φ]` and score
|
|
|
|
|
|
the seed on every instance of `D_pareto`, giving a score **matrix** `S[candidate][instance]`.
|
|
|
|
|
|
- Each iteration: select a candidate from the Pareto frontier; select a module round-robin; run it on a
|
|
|
|
|
|
minibatch drawn from `D_feedback`; collect scores, traces and **textual feedback** via a feedback
|
|
|
|
|
|
function `μ_f`; have a reflection LM rewrite that module's prompt; re-run the mutated system on the
|
|
|
|
|
|
*same* minibatch.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- Accept if and only if the minibatch average improved. Only then is the candidate scored on all of
|
2026-09-06 15:16:29 +02:00
|
|
|
|
`D_pareto` and added to the pool with its parent recorded.
|
|
|
|
|
|
- Return the candidate with the best average on `D_pareto`.
|
|
|
|
|
|
|
|
|
|
|
|
Algorithm 2, the selection: for each instance take the best score any candidate achieved; keep the
|
|
|
|
|
|
candidates that achieve it; drop strictly dominated ones; sample a survivor with probability
|
|
|
|
|
|
proportional to the number of instances it leads on.
|
|
|
|
|
|
|
|
|
|
|
|
Three things about that are directly relevant to us. The acceptance gate is **cheap and noisy on
|
2026-10-12 18:16:41 +02:00
|
|
|
|
purpose** (a single minibatch comparison) because the frontier, not the gate, is what protects
|
2026-09-06 15:16:29 +02:00
|
|
|
|
against a bad accept: anything that wins on one instance is retained. Selection is **stochastic and
|
|
|
|
|
|
diversity-preserving**, which is what stops the search collapsing onto one lineage. And the expensive
|
|
|
|
|
|
full evaluation is spent only on candidates that already passed the cheap gate.
|
|
|
|
|
|
|
|
|
|
|
|
Reported: 93% on MATH with a DSPy `ChainOfThought` program against 67% unoptimised, +10% over MIPROv2
|
|
|
|
|
|
and +20% over GRPO with **35× fewer rollouts**; on IFBench, optimal prompts after 678 rollouts against
|
|
|
|
|
|
GRPO's 24,000. Sample efficiency is the claim that matters for us, because our rollouts cost money.
|
|
|
|
|
|
|
|
|
|
|
|
**DSPy's optimizer interface.** `MIPROv2` proposes instruction and demonstration candidates and searches
|
|
|
|
|
|
them with Bayesian optimisation (Optuna TPE), scoring on minibatches and running a full validation pass
|
2026-10-12 18:16:41 +02:00
|
|
|
|
on the incumbent every `minibatch_full_eval_steps`, an explicit two-fidelity design for a noisy metric.
|
2026-09-06 15:16:29 +02:00
|
|
|
|
`dspy.GEPA` takes `metric`, `reflection_lm`, a budget (`auto` / `max_full_evals` / `max_metric_calls`),
|
|
|
|
|
|
`reflection_minibatch_size`, `candidate_selection_strategy` (`pareto` | `current_best`) and
|
|
|
|
|
|
`track_stats`, and its metric returns `Prediction(score, feedback)` rather than a float.
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
**SIMBA, Stochastic Introspective Mini-Batch Ascent (`dspy.SIMBA`).** Two of its ideas fit our corpus
|
2026-09-06 15:24:37 +02:00
|
|
|
|
better than GEPA's do. It runs `num_candidates` (6 by default) program variants per example, buckets the
|
2026-10-12 18:16:41 +02:00
|
|
|
|
trajectories, and ranks examples by **max-to-min score gap**, deliberately spending the budget on the
|
2026-09-06 15:24:37 +02:00
|
|
|
|
instances where variants disagree most. On our corpus that is not a nuisance, it is the training signal:
|
|
|
|
|
|
the 11 injection pairs that disagree with themselves are precisely the pairs where a rule change can flip
|
|
|
|
|
|
an outcome, and uniform minibatch sampling would spend most of its budget on the 9 that never move. It
|
2026-10-12 18:16:41 +02:00
|
|
|
|
also alternates two strategies rather than round-robining blindly, `append_a_demo` (a successful
|
2026-09-06 15:24:37 +02:00
|
|
|
|
trajectory becomes a demonstration) and `append_a_rule` (the model introspects on the bucket and writes a
|
2026-10-12 18:16:41 +02:00
|
|
|
|
rule), which is exactly the split our levers already have between `counterexamples`/`hard_negatives` and
|
2026-09-06 15:24:37 +02:00
|
|
|
|
`checklist`/`prompt`. Candidates are sampled by softmax over historical scores at temperature 0.2, a
|
|
|
|
|
|
cheaper third option beside GEPA's `pareto` and `current_best`.
|
|
|
|
|
|
|
|
|
|
|
|
**The reflection meta-prompt** (GEPA Appendix C) is short and tells us what the feedback function must
|
|
|
|
|
|
produce. It shows the reflector the current instruction, then "inputs, outputs and feedback" for the
|
|
|
|
|
|
minibatch, and asks it to infer the task, extract "all niche and domain specific factual information ...
|
|
|
|
|
|
as a lot of it may not be available to the assistant in the future", and name any generalizable strategy.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
That is a prompt and a specification for `μ_f`: feedback has to carry the domain facts a detector
|
|
|
|
|
|
cannot re-derive (which sink, which span, which side leaked) rather than a verdict.
|
2026-09-06 15:24:37 +02:00
|
|
|
|
|
|
|
|
|
|
**GEPA's merge** is gated strictly: two candidates are crossed only when they share a common ancestor,
|
|
|
|
|
|
optimised *disjoint* sets of prompts, are both Pareto-optimal, and both beat the ancestor's aggregate.
|
|
|
|
|
|
Worth having once we have per-lever candidates, and worth not having before then.
|
|
|
|
|
|
|
2026-09-06 15:16:29 +02:00
|
|
|
|
**The standalone `gepa` library** (MIT) exposes `gepa.optimize(seed_candidate, trainset, valset,
|
2026-10-12 18:16:41 +02:00
|
|
|
|
task_lm, reflection_lm, max_metric_calls)` over a `GEPAAdapter` protocol of three methods
|
|
|
|
|
|
(`evaluate`, `make_reflective_dataset`, `propose_new_texts`), with a seed candidate that is just a dict
|
2026-09-06 15:16:29 +02:00
|
|
|
|
of component name to text. That adapter shape is the interface we want whether or not we take the
|
|
|
|
|
|
dependency: it is the smallest surface that separates *what a detector is* from *how it is searched*.
|
|
|
|
|
|
|
|
|
|
|
|
## What this spec proposes
|
|
|
|
|
|
|
|
|
|
|
|
Restate the detector as an optimisable program and put a published optimiser behind it.
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- A detector is a candidate: a dict of named text components (`prompt`, `checklist`,
|
2026-09-06 15:16:29 +02:00
|
|
|
|
`hard_negatives`, `counterexamples`) plus its frozen non-text settings (tools, budgets). That is
|
|
|
|
|
|
already what `FamilyConfig` holds; it becomes the seed candidate.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- The metric returns a score and a reason. `score_pair_family` gains a feedback string built from
|
2026-09-06 15:16:29 +02:00
|
|
|
|
what the evaluation already knows. This is the highest-value change in the spec and it is independent
|
|
|
|
|
|
of which optimiser sits on top.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- `D_pareto` is carved out of the train split. The holdout leaves the acceptance rule entirely and
|
2026-09-06 15:16:29 +02:00
|
|
|
|
is scored once, for reporting, when the search is over. This is a requirements change to Req 9.5 and
|
|
|
|
|
|
the most important honesty consequence of the redesign.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- An adapter protocol (`evaluate`, `make_reflective_dataset`, `propose_new_texts`) with two backends:
|
2026-09-06 15:16:29 +02:00
|
|
|
|
our own implementation of Algorithms 1 and 2 by default, so the core install keeps
|
|
|
|
|
|
`dependencies = []`, and `gepa` behind an optional extra so its improvements arrive for free.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- The Pareto pool replaces the accept/revert tree. `Archive` already records per-pair winners; it
|
2026-09-06 15:16:29 +02:00
|
|
|
|
becomes the frontier, and the journal's parent records become the ancestry GEPA needs.
|
|
|
|
|
|
|
2026-09-06 15:29:36 +02:00
|
|
|
|
## Where the noise actually is, and why the architecture may be wrong
|
|
|
|
|
|
|
|
|
|
|
|
The maintainer's objection, and I think it is correct: the harness treats the detector as a black box
|
2026-10-12 18:16:41 +02:00
|
|
|
|
emitting a noisy scalar, and everything follows from that (K runs, majority vote, a measured floor, a
|
|
|
|
|
|
regression budget, a sign test). That is the stance you take when you cannot see inside the thing you are
|
2026-09-06 15:29:36 +02:00
|
|
|
|
measuring. We can see inside it.
|
|
|
|
|
|
|
|
|
|
|
|
The variance is not in the judgement. `temperature: 0` is sent and thinking is disabled, and 11 of 20
|
|
|
|
|
|
injection pairs still disagree with themselves. A traced run shows why: over four steps the model chose
|
|
|
|
|
|
different files to read and different grep patterns, and reached its conclusion from different context
|
|
|
|
|
|
each time. The freedom is in the **trajectory**, and we built statistics to average over it instead of
|
|
|
|
|
|
removing it.
|
|
|
|
|
|
|
|
|
|
|
|
This is exactly what DSPy exists to argue about, and three of its constructs attack the source rather
|
|
|
|
|
|
than the symptom.
|
|
|
|
|
|
|
|
|
|
|
|
**Constraints as predicates, enforced at generation time** (`dspy.Assert`/`Suggest`, arXiv:2312.13382).
|
|
|
|
|
|
A constraint is a boolean function of the output; on failure the past output *and the error* are injected
|
2026-10-12 18:16:41 +02:00
|
|
|
|
into a retry. We already have the predicates (is the reported line inside a parsed function, does the
|
|
|
|
|
|
named sink occur at that line, is the claimed family in the taxonomy, did the run cite a tool result),
|
2026-09-06 15:29:36 +02:00
|
|
|
|
and we apply every one of them **after the fact, at scoring time, to decide whether to count a finding**.
|
|
|
|
|
|
Applying them at generation time to decide whether to *accept* one is free, needs no dependency, and is
|
|
|
|
|
|
the single highest-value change available. The paper reports near-total intrinsic constraint satisfaction
|
|
|
|
|
|
and 5–15% downstream gains.
|
|
|
|
|
|
|
|
|
|
|
|
**Typed decomposition instead of one free-running loop.** Our detector is a single ReAct loop that decides
|
|
|
|
|
|
which files to read, what to grep, when to stop and what to report; every one of those is a variance
|
2026-10-12 18:16:41 +02:00
|
|
|
|
source. DSPy's thesis is that each step gets a signature (a typed input/output contract), and a little
|
2026-09-06 15:29:36 +02:00
|
|
|
|
control flow composes them. Ours would be roughly: locate candidate sink sites; trace whether an untrusted
|
|
|
|
|
|
value reaches each; check for a guard between source and sink; judge. **Several of those steps need no
|
2026-10-12 18:16:41 +02:00
|
|
|
|
model at all**. The CST, the entry-point mapper, `flows` and `obligations` already answer them
|
2026-09-06 15:29:36 +02:00
|
|
|
|
deterministically, and we are currently paying a model to re-derive facts we hold.
|
|
|
|
|
|
|
|
|
|
|
|
**Best-of-N against a checkable reward, instead of a majority vote** (`dspy.Refine`, `BestOfN`). Both
|
|
|
|
|
|
spend K samples. A majority vote assumes the modal answer is right and discards the disagreement; Refine
|
|
|
|
|
|
ranks the N attempts by an explicit reward and feeds a hint from the failures into the next attempt. We
|
|
|
|
|
|
have checkable rewards for free. The same budget, used as evidence rather than as averaging.
|
|
|
|
|
|
|
|
|
|
|
|
**Why this matters more for our models, not less.** GEPA and SIMBA are demonstrated with a strong
|
|
|
|
|
|
reflection model over a weaker task model, and their gains come from evolving instructions. A cheap model
|
|
|
|
|
|
fails on an under-specified task and does well on a narrow typed one. So the cheaper the detector model,
|
2026-10-12 18:16:41 +02:00
|
|
|
|
the more of the return sits in decomposition and constraints and the less in prompt evolution, which is
|
2026-09-06 15:29:36 +02:00
|
|
|
|
the opposite of the order this spec originally proposed.
|
|
|
|
|
|
|
2026-09-06 15:49:54 +02:00
|
|
|
|
### The experiment, run (2026-09-06, `benchmarks/measurements/2026-09-06-trajectory-divergence.json`)
|
|
|
|
|
|
|
|
|
|
|
|
Twelve injection pairs, both sides, K = 5, temperature 0, thinking disabled, recording the tool-call
|
|
|
|
|
|
sequence of every run. $0.23.
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
**The intended statistic could not be computed. Across 120 runs there was not one repeated trajectory**. There were
|
2026-09-06 15:49:54 +02:00
|
|
|
|
4.67 distinct trajectories per vulnerable side and 4.92 per fixed side, out of 5, and 78 distinct tool-*name*
|
|
|
|
|
|
sequences ignoring arguments entirely. The detector is not sampling one measurement five times. It is running
|
|
|
|
|
|
five different investigations and voting on the results.
|
|
|
|
|
|
|
|
|
|
|
|
Asked as a gradient instead, the relationship is monotone and clean:
|
|
|
|
|
|
|
|
|
|
|
|
| trajectory similarity (Jaccard over tool calls) | run pairs | disagreed on the outcome |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| high (≥ 0.75) | 2 | 0 (0.0%) |
|
|
|
|
|
|
| mid (0.4–0.75) | 24 | 1 (4.2%) |
|
|
|
|
|
|
| low (< 0.4) | 94 | 29 (30.9%) |
|
|
|
|
|
|
|
|
|
|
|
|
Three more facts from the same run:
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- The variance is a search problem, and the search is `grep_repo`. 555 grep calls against 181 `read_file`,
|
2026-09-06 15:49:54 +02:00
|
|
|
|
71 `entry_points`, 43 `find_refs`, 13 `read_definition`. The model spends its budget guessing patterns to
|
2026-10-12 18:16:41 +02:00
|
|
|
|
locate a sink, while the CST, the entry-point mapper, `flows` and `obligations` already know where the
|
2026-09-06 15:49:54 +02:00
|
|
|
|
sinks are.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- All of the instability is on the vulnerable side. Every one of the twelve pairs produced exactly one
|
2026-09-06 15:49:54 +02:00
|
|
|
|
distinct finding set on the fixed side. The detector is reliably silent on the fix and unreliably vocal on
|
|
|
|
|
|
the bug, so the noise is entirely in detection and none of it in false positives.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- Findings converge far more than trajectories do. 78 distinct trajectories, but one to three distinct
|
2026-09-06 15:49:54 +02:00
|
|
|
|
finding sets per pair. Many different investigations reach the same answer; six of twelve pairs are stable
|
|
|
|
|
|
despite never once repeating a trajectory.
|
|
|
|
|
|
|
|
|
|
|
|
**What this does to the noise architecture.** The floor is not measuring the detector's uncertainty about a
|
|
|
|
|
|
question. It is measuring how often an unconstrained search happens to reach the sink. A majority vote over
|
2026-10-12 18:16:41 +02:00
|
|
|
|
five runs is therefore an unweighted ensemble of five arbitrary search
|
|
|
|
|
|
procedures rather than variance reduction, and a regression budget derived from it is a budget for the search's luck. K runs, the floor,
|
2026-09-06 15:49:54 +02:00
|
|
|
|
the budget and the sign test are all statistical treatment for a structural problem, and the 0% and 4.2%
|
|
|
|
|
|
rows of that table say what happens when the structure is fixed instead.
|
2026-09-06 15:29:36 +02:00
|
|
|
|
|
2026-09-06 15:50:44 +02:00
|
|
|
|
## Reflection on the approved specification
|
|
|
|
|
|
|
|
|
|
|
|
Read against that measurement, `learning-harness` divides cleanly into a part that stands and a part that
|
|
|
|
|
|
was answering the wrong question.
|
|
|
|
|
|
|
|
|
|
|
|
**Stands, and is worth more now than before.** The closed taxonomy; class-aware pair scoring; the split
|
|
|
|
|
|
enforced by construction; the unscorable vocabulary and its denominators; verifier-gated reporting; the
|
|
|
|
|
|
corpus repairs; the publication machinery; the per-pair archive. None of it depends on the noise framing,
|
|
|
|
|
|
and all of it is what made this experiment possible to run and cost $0.23. A measurement layer that can
|
|
|
|
|
|
answer "is our own instability structural?" in twenty minutes is the thing that was missing before.
|
|
|
|
|
|
|
|
|
|
|
|
**Was answering the wrong question.**
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- Req 3.5, K ≥ 3 runs per pair. The premise is variance reduction over a repeated measurement. There is
|
2026-09-06 15:50:44 +02:00
|
|
|
|
no repeated measurement: 120 runs, 120 trajectories. K runs is an *ensemble of distinct search
|
|
|
|
|
|
procedures*, and a majority vote over it is an unweighted ensemble, not an estimate. It should either be
|
2026-10-12 18:16:41 +02:00
|
|
|
|
renamed and published as an ensemble, which changes what every family number means, or dropped once the
|
2026-09-06 15:50:44 +02:00
|
|
|
|
search is constrained.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- Req 8.2, the noise floor as the regression budget. The floor measures how often an unconstrained
|
2026-09-06 15:50:44 +02:00
|
|
|
|
search happens to reach the sink. Deriving a regression budget from it means the worse the search wanders,
|
|
|
|
|
|
the more collateral damage a round is licensed to do. That is backwards, and it is why injection's budget
|
|
|
|
|
|
is 11 of 20.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- Req 9.5, the acceptance rule. Already flagged for reading the holdout every round. Now also: it
|
2026-09-06 15:50:44 +02:00
|
|
|
|
compares deltas between two ensemble votes, and calls the difference an improvement.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- The sign test and `reliable_change`. Added this session in good faith; they test whether an ensemble's
|
2026-09-06 15:50:44 +02:00
|
|
|
|
vote moved, which is not the question either.
|
|
|
|
|
|
|
|
|
|
|
|
**What the data adds that neither spec anticipated.**
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- The instability is entirely one-sided. All twelve pairs produced exactly one finding set on the fixed
|
2026-09-06 15:50:44 +02:00
|
|
|
|
side. Specificity is solid and recall is the noisy half, so the leak ceiling and the false-positive
|
|
|
|
|
|
machinery are sound and it is the recall numbers that carry the caveat.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- `grep_repo` is the variance engine. It accounts for 555 calls against 181 `read_file` and 71 `entry_points`. The
|
2026-09-06 15:50:44 +02:00
|
|
|
|
single highest-leverage change in either spec is not a prompt, a lever or an optimiser: **stop asking the
|
|
|
|
|
|
model to locate the sink.** The CST, the entry-point mapper, `flows` and `obligations` already know. Give
|
2026-10-12 18:16:41 +02:00
|
|
|
|
the detector the candidate sites and ask it the question only a model can answer. Does untrusted input
|
2026-09-06 15:50:44 +02:00
|
|
|
|
reach this one, and is there a guard.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
- The loop is partly self-correcting, so this should be a large win rather than a marginal one: 78
|
2026-09-06 15:50:44 +02:00
|
|
|
|
distinct trajectories collapse to one to three finding sets per pair, and half the pairs are already
|
|
|
|
|
|
stable despite never repeating an investigation.
|
|
|
|
|
|
|
|
|
|
|
|
**Limits of the experiment, stated so the conclusion is not over-read.** Twelve pairs, one family, one
|
|
|
|
|
|
slice, one model. The high-similarity bucket holds only two run pairs, so its 0% is decoration; the
|
|
|
|
|
|
defensible number is the mid bucket's 4.2% over 24 run pairs against 30.9% over 94. And the direction of
|
2026-10-12 18:16:41 +02:00
|
|
|
|
causation is inferred, not established. Similar trajectories may be a symptom of easy pairs rather than a
|
2026-09-06 15:50:44 +02:00
|
|
|
|
cause of agreement. The way to settle that is the constrained detector itself: fix the investigation, and
|
|
|
|
|
|
if the floor does not collapse, this reading was wrong.
|
|
|
|
|
|
|
2026-09-06 15:24:37 +02:00
|
|
|
|
## The constraint that outranks the algorithm
|
|
|
|
|
|
|
|
|
|
|
|
Both algorithms assume a dataset. GEPA splits `D_train` into `D_feedback` and `D_pareto`; SIMBA's default
|
2026-10-12 18:16:41 +02:00
|
|
|
|
batch size is 32. **Our injection family has 12 train teachers** (fewer than one SIMBA batch), and after a
|
2026-09-06 15:24:37 +02:00
|
|
|
|
GEPA split roughly six instances on each side. Every other family has one to three.
|
|
|
|
|
|
|
|
|
|
|
|
So the honest statement of what this spec can and cannot buy: GEPA and SIMBA are sample-efficient in
|
|
|
|
|
|
*rollouts*, not in *instances*. They will not shortcut a corpus of twelve. The optimiser is worth building
|
|
|
|
|
|
because the current loop is the wrong shape and will stay wrong at any corpus size, but the binding
|
|
|
|
|
|
constraint on results is the train split, and it will remain so after this spec ships. Two things move it,
|
|
|
|
|
|
neither of which is an algorithm: reviewing agent-vfc rows above tier `title` (29 rows, currently unable to
|
|
|
|
|
|
teach anything), and harvesting more pairs per family.
|
|
|
|
|
|
|
2026-09-06 15:16:29 +02:00
|
|
|
|
## Scope
|
|
|
|
|
|
|
|
|
|
|
|
In: the search over a family's text components, the feedback function, the candidate pool and its
|
|
|
|
|
|
selection, the budget accounting, and the artifacts that make a run reproducible.
|
|
|
|
|
|
|
|
|
|
|
|
Out, and deliberately: the taxonomy, the classifier, the pair scorer's *outcome* rules, the verifier
|
|
|
|
|
|
boundary, the write roots, redaction, the three gates. Those are the measurement, and a search whose
|
2026-10-12 18:16:41 +02:00
|
|
|
|
objective is not trustworthy is worse than no search. Nothing here relaxes them. In particular, the
|
2026-09-06 15:16:29 +02:00
|
|
|
|
proposer stays blind to the holdout, verifier code and gold labels stay outside every write root, and
|
|
|
|
|
|
no pattern or policy is written by a model.
|
|
|
|
|
|
|
|
|
|
|
|
## Decisions for the maintainer
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
0. Is the optimiser even the next thing to build? Proposed no, and this reverses the order above.
|
2026-09-06 15:29:36 +02:00
|
|
|
|
Constraints at generation time, typed decomposition and best-of-N attack the variance at its source; an
|
|
|
|
|
|
optimiser only tunes text around a program whose freedom is the problem. Proposed sequence: measure where
|
2026-10-12 18:16:41 +02:00
|
|
|
|
the variance is, constrain generation, decompose the loop, then optimise, and at a trainset of twelve the
|
2026-09-06 15:29:36 +02:00
|
|
|
|
optimiser that fits is `BootstrapFewShot` (collect traces that scored well, install them as demonstrations
|
|
|
|
|
|
in a lever we already have) rather than an evolutionary search with nothing to search over.
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
0b. Adopt DSPy itself? Proposed no. Our detector is not a DSPy program. It is an agentic tool loop over
|
2026-09-06 15:24:37 +02:00
|
|
|
|
a repository with curated tools, redaction on every prompt, path clamping, a "no findings unless a tool
|
|
|
|
|
|
was called" rule and a final-answer turn, several of which are security properties rather than
|
|
|
|
|
|
conveniences. Handing that loop to `dspy.ReAct` and the calls to litellm would put them behind someone
|
|
|
|
|
|
else's abstraction, and DSPy could only ever be an optional extra under `dependencies = []`, which means
|
2026-10-12 18:16:41 +02:00
|
|
|
|
two code paths for the thing that *is* the product. What DSPy would buy (signatures, adapters,
|
|
|
|
|
|
multi-module composition), we barely need at one module per family. The optimisers are available without
|
2026-09-06 15:24:37 +02:00
|
|
|
|
the framework: `gepa` ships standalone under MIT, and SIMBA's two ideas are a hundred lines of logic.
|
|
|
|
|
|
Proposed instead: take the interface shape and the algorithms, not the framework.
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
1. Reimplement Algorithms 1 and 2, take `gepa` as an extra, or both? Both is proposed: ours by
|
2026-09-06 15:16:29 +02:00
|
|
|
|
default (zero-dependency, and the security properties are ours to own), `gepa` behind an extra for
|
|
|
|
|
|
comparison. The cost is one adapter and one conformance test that both backends must pass.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
2. Drop the holdout from the acceptance rule? Proposed yes, and it amends approved Req 9.5. The
|
2026-09-06 15:16:29 +02:00
|
|
|
|
holdout becomes what its name says.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
3. Sample instances by disagreement (SIMBA) or uniformly (GEPA)? Proposed SIMBA's ranking. Our measured
|
2026-09-06 15:24:37 +02:00
|
|
|
|
floor makes it the better fit, and it turns the noise floor from an obstacle into an instance sampler.
|
|
|
|
|
|
|
2026-10-12 18:16:41 +02:00
|
|
|
|
4. K = 1 inside the search, K ≥ 3 only for published numbers? Proposed yes. That is roughly a 5×
|
2026-09-06 15:16:29 +02:00
|
|
|
|
budget reclaim spent on more candidates, which is where the sample efficiency comes from.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
5. Does the reflection model differ from the detector model? The published work uses a stronger
|
2026-09-06 15:16:29 +02:00
|
|
|
|
reflector than the task model. We have `deepseek-v4-pro` configured as the judge; proposed to reuse it.
|
2026-10-12 18:16:41 +02:00
|
|
|
|
6. Is `access_control` still the family we care most about? It has one train teacher. Until agent-vfc
|
2026-09-06 15:16:29 +02:00
|
|
|
|
rows are reviewed above tier `title`, the optimiser cannot work on it, whatever its algorithm.
|
|
|
|
|
|
|
|
|
|
|
|
## Open questions
|
|
|
|
|
|
|
|
|
|
|
|
- Does `gepa` run against an OpenAI-shaped base URL that is not OpenAI (our DeepSeek endpoint), and what
|
|
|
|
|
|
does it pull in? Both must be checked before the extra is offered, under the no-undeclared-dependency rule.
|
|
|
|
|
|
- Our "module" granularity: is a module a family, or a lever within a family? Round-robin over levers is
|
2026-10-12 18:16:41 +02:00
|
|
|
|
the closer analogue, but the levers are not independent. A checklist edit and a prompt edit interact.
|
2026-09-06 15:16:29 +02:00
|
|
|
|
- The feedback function's failure mode is a reflection model that writes plausible prose about a trace it
|
|
|
|
|
|
misread. What check makes a feedback string answerable rather than persuasive?
|
|
|
|
|
|
- GEPA's acceptance is a single noisy comparison. Our floor says a single comparison is close to a coin
|
|
|
|
|
|
flip on this corpus. Does the frontier really absorb that at n ≈ 20 instances, or is our corpus simply
|
|
|
|
|
|
too small for the algorithm's assumptions?
|
|
|
|
|
|
|
|
|
|
|
|
Sources: [GEPA (arXiv:2507.19457)](https://arxiv.org/abs/2507.19457) ·
|
|
|
|
|
|
[DSPy GEPA overview](https://dspy.ai/api/optimizers/GEPA/overview/) ·
|
|
|
|
|
|
[DSPy MIPROv2](https://dspy.ai/api/optimizers/MIPROv2/) ·
|
2026-09-06 15:24:37 +02:00
|
|
|
|
[gepa-ai/gepa](https://github.com/gepa-ai/gepa) ·
|
|
|
|
|
|
[DSPy SIMBA](https://dspy.ai/api/optimizers/SIMBA/) ·
|
2026-09-06 15:29:36 +02:00
|
|
|
|
[dspy/teleprompt/simba.py](https://github.com/stanfordnlp/dspy/blob/main/dspy/teleprompt/simba.py) ·
|
|
|
|
|
|
[DSPy Assertions (arXiv:2312.13382)](https://arxiv.org/pdf/2312.13382) ·
|
|
|
|
|
|
[dspy.Refine](https://dspy.ai/api/modules/Refine/) ·
|
|
|
|
|
|
[DSPy (arXiv:2310.03714)](https://arxiv.org/pdf/2310.03714)
|