Hypothesis portfolio (Stages 1, 2, 2b, 2c and 2d)
Decision-model calls are cheap, so the optimizer should not pick one winner and commit. It should keep many
ideas (rubric, example and question changes), keep evidence about each for a long time, and screen many
candidates cheaply. Stage 1 is the library layer (ledger and screen). Stage 2, below, wires it into the live loop as an opt-in (mode dev_screen). Stage 2b adds forward shadow evaluation (mode shadow, the default when screening is on).
The ledger (hypothesis_ledger.py)
HypothesisLedger(db) works over the flywheel SQLite database. Ideas stay in control_ideas; the ledger only
references their ids.
idea_evaluations(idea_id, item_id, correct, label, choice, confidence, context_version, evaluated_at), keyed by(idea_id, item_id, context_version). Recording the same key again replaces the row.idea_verdicts(idea_id, at, verdict, reason, evidence)is append-only. Verdicts arescreened_out,advanced,promoted,retired,revived, each with a plain-language reason.
Queries: correctness_vector, items_evaluated, paired_comparison(a, b) (gained, lost, net and an exact
two-sided sign-test p-value over shared items), running_score(idea, since=None) (Beta(1,1) posterior mean and
95% interval, so an idea can be tracked on newly arriving labels), revive_candidates() (screened-out ideas
whose score on evaluations made after that verdict looks promising), stubborn_items(min_ideas=3) (items every
evaluated idea gets wrong, a hint that the label or the item is the problem), and
summary_for_optimizer(limit=...), a deterministic, length-capped list of dicts (idea id, short text, items
tested, accuracy, item ids it fixed and broke versus the incumbent, last verdict and reason).
The screen (idea_screen.py)
await screen(candidates, incumbent, items, scorer, ...) is engine-agnostic. scorer(idea, item) is injected
and returns something with choice, confidence and correct; the screen never calls a model itself.
- Items are shuffled deterministically by
seed. A reserve (reserve_fraction, default 20%) is split off. - Each stage scores the surviving candidates, and the incumbent once (shared), on the next block of screening items, then keeps the top fraction by cumulative accuracy. Default stages: 50 items keep half, 200 items keep half, then all remaining screening items.
- Confirmation scores the finalists and the incumbent on the reserve. Each finalist gets a paired exact sign
test against the incumbent, adjusted across finalists with Holm's step-down method (valid under any
dependence, never weaker than Bonferroni). A finalist clears the gate only if its net gain is positive and
its adjusted p-value is below
alpha. - Every evaluation and verdict is written to the ledger when one is supplied.
concurrencybounds in-flight scorer calls. If a stage or the confirmation would push calls pastmax_calls, the screen stops before it and returns a partial result withtruncated=True; a truncated screen never clears the gate.
ScreenResult carries ranked finalists, which cleared, per-stage tables, the paired statistics and the total
scorer calls.
Why the reserve exists
Scoring many ideas on one dev set and keeping the best finds noise: the maximum of N equally good ideas is above average by chance, and a sign test on the same items that picked it is not valid. Items used to screen or advance an idea are never used to confirm it. The reserve is touched once, by the finalists only, so the test sees fresh items and the family-wise correction only has to cover the finalists. With all-equal candidates the gate passes rarely (the tests simulate 200 screens and require at most 10%).
Stage 2: the live loop (opt-in, default off)
IdeaScreeningConfig (idea_screening.py) is passed as DecisionFlywheel(..., idea_screening=...) and saved in
runtime_state like the selection policy, so a resumed run keeps it. Defaults: enabled=False,
max_candidates=8, proposals_per_round=3, finalists=2, reserve_fraction=0.2, alpha=0.05,
max_screen_calls=2000, stages=((50,0.5),(200,0.5),(None,None)). Study script flags: --idea-screening,
--screening-proposals, --screening-finalists, --screening-max-candidates, --screening-reserve,
--screening-alpha, --screening-max-calls; the config is written to protocol.json. With any primary
objective other than accuracy, one idea-screening-unsupported-objective event is logged and the legacy
one-trial-per-control path runs. With it off, nothing changes.
Where it applies: the rubric and example_ids controls in ControlScheduler (improve_controls), and the
rubric stage of optimize_stage (what the prequential study runs). In dev_screen mode the stage screens only
once a non-provisional rubric exists and development has the per-class minimum. In shadow mode it screens whenever
screening is enabled and the active rubric is nonempty, provisional or not and with or without development coverage,
because shadow evaluation never selects on the development partition (ideas are judged on forward stories; diagnosis
reads only recorded predictions and revealed training stories). From a blank start the first nonempty rubric still
comes from the legacy 'initialized provisionally' path (shadow needs an incumbent to compare against); from the next
rubric round on, screening replaces the legacy 'working rubric refined with a decaying recency preference' branch,
which never also runs in a round that screens.
Validation status after a shadow promotion: the promoted context is recorded as evaluated, including when the
incumbent was provisional. The justification is the forward paired evidence (the promoted event has
promotion_path: shadow-evaluation and shadow_evidence), not development coverage, so the context-validation floor
does not apply to it. Because it is no longer provisional, the legacy recency-allowance refinement stops for it.
Questions (tasks) stay on the legacy path.
Each round:
- One optimizer call asks for
proposals_per_roundDISTINCT proposals along different axes (reply shape{"proposals":[...]}, 1 to N objects, validated strictly; duplicates and extra keys are rejected). An optimizer withoutpropose_manygives one proposal. Each distinct, changing proposal is saved as a control idea and emitshypothesis-discovered. - Candidates are the new ideas, then ideas never yet screened, then
revive_candidates(), then ideas not screened for three cycles, capped atmax_candidates. DevelopmentScorer(idea, row)applies the idea's configuration throughwheel._answers, the same decision path, answer cache, byte ceiling and request ceiling the flywheel uses, and reads the decision answer (no fitted head). It refuses any item that is not a development row. Cache hits cost no request.screen()runs successive halving over the screening items (development minus reserve).- Confirmation (
confirm()): only the topfinalistsgo to the reserve, so Holm stays small. A finalist carried over as positive-but-not-significant takes a slot, and at least one slot goes to a fresh finalist. The paired exact sign test uses every reserve item both the finalist and the incumbent have been scored on, across rounds (read from the ledger; only missing pairs are scored). Cleared means net gain above zero and the Holm-adjusted p belowalpha. Positive but not significant is kept asadvancedand re-confirmed next round; no gain isscreened_out; nothing is deleted. - The best cleared finalist gets the usual fitted trial (
improve), and the selection policy still has to accept it. InControlSchedulerthe trial joins the cycle's best-eligible promotion (promote_trial); in the stage pathimprovepromotes directly. Only then is the verdictpromoted, with gained, lost and p. The previous incumbent stays in the ledger.
The reserve rule: an item is in the reserve when a hash of the task fingerprint and its id falls under
reserve_fraction. It does not depend on labels, order or when the item arrives, so later items join the
reserve without ever having been screened, and reserve items never enter a screen. Because looks accumulate,
a candidate is tested several times as the reserve grows. The test in idea_screening_test.py simulates this:
with no true difference, 6 looks of 20 new items gave a false-promotion rate of 3.5% (4.8% at 10 looks); a
true advantage of about 20 gained and 5 lost per 100 items cleared 64% of the time by round 3 and 97% by
round 6. This is repeated testing without an alpha-spending correction, so keep alpha at 0.05 or lower.
Protected items are never scored: the scorer accepts development rows only, and the protected set is validated disjoint from development. Reserve outcomes are hidden from the optimizer.
Budget: max_screen_calls bounds scorer calls per round, further capped by the wheel's remaining request
ceiling minus the room the fitted trial needs. If a stage or the confirmation would exceed it, the round is
truncated and promotes nothing (control-trial-deferred, reason session request budget). Exhausting the
ceiling mid-screen also truncates.
Optimizer context: prior_control_ideas (newest 20, compact), hypothesis_ledger from
summary_for_optimizer (10 ideas, 5 fixed and 5 broke item ids each) and stubborn_items (20 ids). The
digest excludes reserve items. These are development item ids, so the run records
evaluation_context_exposed=true whenever screening is on.
Determinism: screening items are shuffled with the cycle number as the seed and the reserve is hash-defined, so the same inputs and recorded responses repeat a round exactly.
Stage 2b: forward shadow evaluation (opt-in, mode shadow)
Why dev-partition screening was too small
A real validation run showed Stage 2 promoting nothing. Each round screened on 16 to 29 stories and confirmed on a reserve of 3 to 12, because it only used the engine's small development partition. With 18 ideas and 893 evaluations no finalist could clear a paired test. Decision-model calls cost a fraction of a cent, so labels, not calls, are the scarce thing. Forward shadow evaluation gets evidence from stories that arrive anyway.
How it works
Every new story is scored by the incumbent (as always) and by a few candidate ideas, the shadow set, before its label is revealed. Those stories were never seen by any idea's author (the optimizer), so the evidence is clean. It grows by one paired result per idea per story with no extra labels.
- Rubric stages (and the scheduler's
rubricandexample_idscontrols) still ask the optimizer forproposals_per_rounddistinct proposals and save them as control ideas. In shadow mode nothing is scored on the development partition; the new ideas take free shadow slots, newest first.tasksideas are not shadowed. - In
run_cycle_replay, after the incumbent predicts and the feedback policy chooses, each shadow idea answers throughwheel._answers(the same request ceiling, answer cache and durable record as every decision, sowheel.requestsstays truthful and cache hits are free). Then the label is revealed, and correctness for each idea and for the incumbent is written to the ledger withcontext_version='shadow'. The incumbent's side of the pair is its decision-model answer (not the fitted head), the same thing the idea's answer is. - Per story with any shadow scoring, one
portfolio-scoredevent records the incumbent's choice and confidence and each idea's, with no label, so disagreement can be analysed later. Nothing routes on it yet (Stage 3). - After each such story, and at each rubric trigger,
reviewcompares every shadow idea with the incumbent on the shared forward stories.
Forward-only rule
The replay ticks a story sequence number once per story. An idea records the sequence number at which it was
created (created_seq) and joins the shadow set at or after that; it is scored only on stories with a larger
sequence number. The shadow_scored table logs every (idea, story, sequence) so this can be audited, and the tests
assert it from the model's call log.
The protected-partition rule (how it is read)
The replay has three roles: training, development and scoreboard. Scoreboard stories are the permanent
evaluation firewall: their labels are never revealed to the runtime. Stories the feedback policy does not select
are not revealed either. The shadow set is scored only on stories that (a) are selected for review, so their label
will be revealed, and (b) are not scoreboard. Anything else is never scored by a shadow idea, never enters the
ledger and is never evidence for promotion. Training and development stories are both allowed: for a shadow idea
they are forward stories it was not written from, and nothing else about them is used. Because only reviewed
stories count, the evidence is about the stories the policy selects, which can differ from the whole stream
under selective feedback.
Promotion and alpha spending
Per idea and per incumbent, the idea gets at most shadow_looks looks, at the predetermined shared-story counts
shadow_min_items * k. A look clears when the exact two-sided sign test p-value is below alpha / shadow_looks
and gained is greater than lost. A fixed number of looks at predetermined counts tested at alpha / looks is a
Bonferroni bound: however the looks are correlated, an idea that is truly no better clears with probability at
most alpha. Testing every look at alpha instead would exceed it (in the simulation, 6.0% against 0.6%). It is
conservative, so power is lower than an unadjusted test. With k ideas in the shadow set at once, the chance that
some equal idea clears is up to k * alpha; each idea is held to alpha. A verdict advanced is recorded when an
idea first reaches shadow_min_items shared stories with a positive net.
An idea that clears is promoted by a distinct, logged path, not the fitted-trial path: idea-promoted-by-shadow
(evidence: gained, lost, p, n, looks, threshold), then the idea's change becomes the active decision context
(classifier-activated, then a promoted event with promotion_path='shadow-evaluation'), verdict promoted,
and the idea record gains an attempt. No head is fitted in the promotion; the usual classifier retraining refits
it on its own schedule, so predictions use the new decision answer until then.
Eviction
An idea is evicted (verdict screened_out, reason in plain words, slot freed for the next waiting idea) once it has
shadow_evict_after shared forward stories and gained is not greater than lost. Ideas that have not reached the
count keep their slot, which is the exploration allowance. An idea that uses all its looks without clearing is
also evicted. Nothing is ever deleted: ledger rows and verdicts stay.
The incumbent changes
The incumbent's id is its configuration fingerprint. Its ledger rows are written under that id, so a paired comparison only uses stories scored while that incumbent was active. After a promotion the old rows stay in the ledger but never pair with the new incumbent; every remaining idea restarts a comparison and its looks against the new incumbent on the stories that follow (an idea is applied to the current incumbent, so it means "this change on top of what is active now").
Optimizer context
In shadow mode the digest given to the optimizer is each shadow idea's forward record (stories, fixed and broke
counts with a few story ids, running accuracy, last verdict, shadow status) plus forward stories every voter
got wrong, with the same caps as Stage 2. There is no hidden reserve. It contains only stories that have arrived
and whose labels were revealed, never a story awaiting its label. Because development stories can appear in it,
the run still records evaluation_context_exposed=true.
Options and defaults
IdeaScreeningConfig gains mode (shadow or dev_screen; default shadow), shadow_ideas (default 4 since Stage 2c, was 3),
shadow_min_items=40, shadow_evict_after=30, shadow_looks=10, max_shadow_calls=3000. All are validated,
saved in runtime_state and kept on resume. A Stage 2 configuration saved without a mode resumes as dev_screen.
Study flags: --screening-mode, --shadow-ideas, --shadow-min-items, --shadow-evict-after, --shadow-looks,
--max-shadow-calls, recorded in protocol.json with the rest of the config. Screening is still off unless
--idea-screening is given.
Budget safety
Shadow calls count against max_shadow_calls and the wheel's request ceiling. Before each shadow call the
evaluator checks that the remaining requests exceed what normal work still needs (one per remaining story, plus
the training and development size for a fitted trial). Otherwise it stops scoring, logs shadow-scoring-stopped
once per reason, and resumes by itself if the reason goes away. A failed shadow call is logged
(shadow-scoring-failed) and skipped; it never blocks the decision.
What the simulations say
Using the module's own look rule on simulated paired outcomes (3000 runs, 10 looks of 40 stories): equal ideas
were promoted 0.6% of the time. A true +10 point idea (incumbent 75% right, idea 85%) was promoted 81% of the time
within 400 forward stories, median about 160, and 98% if early eviction is switched off; eviction at 30 stories
costs power, so raise shadow_evict_after if ideas are expensive to produce. A smaller +5 point idea was found
about a third of the time within 400 stories.
Not built
Routing on shadow disagreement (Stage 3) and cross-run idea memory (Stage 4).
Stage 2c: proposal operators (opt-in with idea screening, shadow mode)
Why
In the last real run 18 proposed rubrics were scored on forward stories and 16 were evicted with fixed about equal to broke; several changed Jev's answers on none of their 30 stories. The optimizer returned timid wording edits of one rubric, so shadow evaluation had nothing to find, while a held-out test showed the true labeling rubric (different rules: topic-specific rules for sports and animals, "outcome must be established in the text") beats the learned one by 6.5 points. The problem was proposal diversity and targeting, not evaluation. Stage 2c changes how ideas are asked for. Evaluation, alpha spending and promotion are unchanged.
The operators
Each idea comes from one operator; the optimizer is told which proposals serve which operator.
mutate: today's multi-proposal request, careful revisions of the incumbent.bold: a rubric that differs structurally from the incumbent and prior ideas: different decision rules, topic- or category-specific rules, explicit boundary cases and counter-rules, guidance for both labels.target: the optimizer sees a bounded sample of stubborn stories and is asked to fix those failure patterns without breaking what works.combine: the optimizer sees two complementary rubrics with their fixed/broke records and merges the winning clauses of each into one rubric.
IdeaScreeningConfig.proposal_mix is a validated mapping such as {"mutate":0,"bold":2,"target":2,"combine":2}.
Unset, it means the default: bold=2, target=2, combine=2 in shadow mode (and nothing in dev_screen mode, where a
mix is rejected and proposals_per_round keeps its meaning). In shadow mode the mix total (6 by default) replaces
proposals_per_round for the rubric control; example_ids still uses proposals_per_round. A mix must have
mutate or bold above 0 so a round can always run. shadow_ideas now defaults to 4; a larger queue is fine because
eviction frees slots (and the alpha-spending threshold divides by the number of ideas shadowed at once).
Optimizer calls per round
One call per round returns every operator's proposals (propose_operators; the reply is {"proposals": [{"operator", "rationale", "rubric"}, ...]}). The reply is parsed strictly: a missing, unknown or unrequested operator, more
proposals from an operator than were asked for, or an empty list is rejected. Fewer is allowed. The only extra
calls are novelty retries: if the novelty gate rejects bold proposals, up to novelty_retries (default 1) more calls
re-ask for just the rejected number. A round with no rejection is exactly one call. Optimizers without
propose_operators (custom ones) get the plain multi-proposal request sized to the mix total, with a
proposal-operator-skipped event (operator all).
Caps on what is sent
At most target_sample stories (default 8, at most 20), each excerpt 400 characters and each title 160; two parent
rubrics of at most 4000 characters, with at most 8 story ids each; plus the existing digest caps (20 prior ideas, 10
ledger rows, 5 ids per row, 20 stubborn ids). The operator block is therefore a few kilobytes on top of the usual
briefing.
Novelty gate (bold)
Distance is 1 - Jaccard over lowercased word 3-shingles (a text with fewer than 3 words uses single words).
0 is identical and 1 shares nothing. A proposal's novelty is its smallest distance to the incumbent rubric and every
prior rubric idea (and the earlier proposals of the same reply). A bold proposal below min_novelty (default 0.35)
is rejected (proposal-novelty-rejected), re-asked within the retry bound, and dropped with a
proposal-operator-skipped event if it still fails. Other operators store the number but are not gated. The measure
sees surface overlap, not meaning: it catches copies and light edits, and a pure paraphrase passes.
The target sample
Candidates are stories every tried idea got wrong (HypothesisLedger.stubborn_items, minimum 3 ideas, across
forward and development evidence) and then the incumbent's newest errors. Only training-role stories can be shown:
their text, label and human explanation were already revealed to the optimizer. Development and protected stories,
and any story whose text equals one, are never shown, and neither is a story the runtime has no revealed text for.
Candidates are grouped by error direction (true label, wrong answer) and taken in turn across groups, stubborn
first, so the sample is balanced. Each story carries title, excerpt, true label, the wrong answer, and the
explanation. With no such story yet, the operator is skipped with an event.
The complementary pair (combine)
For ideas X and Y on the stories both were scored on (forward evidence): fixes_Y_not_X is Y right and X wrong,
fixes_X_not_Y the reverse. A pair qualifies when both are at least 2 and they share at least 10 stories. The score
is the smaller of the two; the best score wins. Ties prefer more members with positive net versus the incumbent,
then more shared stories, then the lower ids. The incumbent may be one member. Pairs already combined are not
chosen again. With fewer than two ideas that have 10 evaluated stories, or no qualifying pair, combine is skipped:
a proposal-operator-skipped event with the reason, and nothing is redistributed to the other operators. Both
parents are recorded.
Lineage
Each idea in control_ideas may carry operator, parents (idea ids; the incumbent appears as its
incumbent-... id) and novelty. Older ideas lack them, and readers treat that as normal. Creating an idea emits the
usual hypothesis-discovered plus proposal-operator {idea_id, operator, parents, novelty}.
summary_for_optimizer and the shadow digest show operator, short parents and outcome (the last verdict, or
untested), and the digest adds operator_outcomes once any idea has an operator.
HypothesisLedger.operator_outcomes() returns per operator: proposed, advanced (ever advanced or promoted),
promoted, and mean forward net gain (the latest recorded net per idea, None when none). lineage(idea_id) returns
the idea and its ancestors, nearest first.
Options and flags
proposal_mix, min_novelty=0.35, target_sample=8, novelty_retries=1, all validated and saved with the rest of the
config. Study flags: --proposal-mix bold=2,target=2,combine=2, --min-novelty, --target-sample; recorded in
protocol.json through as_json. Unset flags leave the defaults. --shadow-ideas now defaults to 4.
Not built
Clause-level splitting of rubrics and cross-run idea memory.
Stage 2d: root-cause diagnosis and element screening (opt-in, shadow mode)
Why
Until now the optimizer got raw feedback records and a one-line hint to "diagnose disagreements". It never saw a confusion matrix or any aggregation, and never did root-cause analysis. On the editorial task the incumbent's errors are almost all missed publishes (it says publish for about 20% of stories while editors publish 40%); a confusion matrix shows that at a glance, the optimizer never saw it, and it made timid wording edits. A Jev-only pilot (400 stories, about 2 cents) asked nine yes/no element questions in the same request as the main decision: Jev answered the sub-questions accurately (element versus the labeler's recorded factor, AUC 0.83 to 0.999), individual elements carried signal (AUC versus the label 0.77 and 0.79 for the two best), and conditional on the main decision's probability the elements it under-weights stood out. That conditional analysis is root-cause evidence. The incremental accuracy from elements was modest (5-fold CV 80.0 to 82.8%) because the main decision's ranking is already strong, so element tests are used chiefly as diagnosis and as candidate head features, not as a promise of big gains.
DiagnosisConfig (nested in IdeaScreeningConfig.diagnosis, default disabled, shadow mode only) runs this pipeline at
each rubric round, BEFORE the Stage 2c proposal call. Disabled, every earlier behavior is unchanged and the saved
configuration has no diagnosis key (so existing stage keys stay valid).
The pipeline (diagnosis.py, element_stats.py)
- Error summary. The incumbent's confusion matrix on revealed stories, built from
predictionevents recorded before the story's firsthuman-feedbackevent (never a prediction made with the label known), using the main-decision answer. Scope: any revealed non-protected story (training or development), as counts only. Predictions made under the current classifier version are used when there are at least 20; otherwise every recorded prediction is used andscopesaysall-versions. The summary lists each error cell with its count, its rate within the true label and overall, a direction (missed publish,wrongful publish; generic wording for more than two labels), and the actual versus answered share of each label. It is a compact text block for prompts and a structured dict, stored in thediagnosis-summaryevent. - Samples. For each error cell with at least
min_cellerrors (default 5; at most 3 cells, largest first; the count of visible training errors must also reachmin_cell), errors are ordered confident-first (the probability put on the wrong answer) and dealt intoindependent + 1DISJOINT samples (default A, B and C): consecutive groups of three go to A, B and C in a seeded random order, so each spans the whole confidence range. Each holds up tosampleserrors (default 8). Per true label a contrast set of correctly answered stories (contrast, default 4 per sample) is dealt the same way; contrast stories are the correct answers closest to the boundary (lowest confidence), because those are the ones a patch can break. Samples are disjoint across cells, labels and each other, and deterministic: the seed is a hash of the task fingerprint and the round number. Each story shows about 400 characters of text, true label, the model's probabilities and the human explanation already revealed. Only training-role, revealed stories whose id and normalized text differ from every development and protected story can appear (the same exclusions as the Stage 2c target sample); development, protected and unrevealed stories never appear. - RCA optimizer calls. Per cell, one independent diagnosis call per diagnosis sample (A and B by default), each
returning
{"hypotheses": [...]}whose evidence ids must be errors shown in that sample. Then ONE reconcile-and-critique call, given both diagnoses, the stories shown and the summary, returns{"causes": [...], "generic_elements": [...]}. Each cause has exactlyid, name, cell, description, evidence_story_ids, proposed_rule, patch, must_keep_story_ids, confirmed, candidate_element. Validation is strict: unknown ids, evidence that is not an error of the cause's own cell, must-keep ids that are not contrast stories, duplicate cause ids or names, over-length text, a missing patch, a bad element or missing/unknown fields are rejected. A rejected reply is re-asked once with the error text (diagnosis-reply-rejectedevents); a second failure drops that diagnosis (or, for the reconcile call, the round's causes).confirmedis a claim that the code verifies: it stays true only if the cause's evidence overlaps the evidence cited by EVERY independent diagnosis of the cell (confirmed_claimedkeeps the claim).patchis a short clause appended to the incumbent rubric for the trial. Cause ids are stored asr<round>.<id>. - Cause trial. For each cause the decision model scores incumbent rubric + patch on the held-aside sample (C) of
its cell and on C's contrast stories, through
wheel._answers(so the request ceilings, the answer cache and the durable records apply).fixedis the held-aside errors now answered with their true label;brokeis held-aside contrast stories that stop being right;fix_rate = fixed / n_errors,break_rate = broke / n_contrast. A cause passes iffixed >= max(2, 0.25 * n_errors)and3 * broke <= fixed. This is a FILTER that tells the proposal step which causes look real; it is not a gate and promotes nothing. Forward shadow evaluation stays the arbiter. A cell with fewer than two held-aside mismatches isuntestable; a trial cut off by a cap isincomplete; neither passes. - Element screening. Candidates are each cause's
candidate_elementplus up toelement_candidates(default 8) generic elements the reconcile call may add; each is a yes/no{name, question}(name: lowercase letters, digits, underscores; the task is calledel_<name>). An element already evaluated with the same question, or already deployed, is not re-screened. All candidates are scored in ONE Jev request per story: the same classify-with-tasks request shape with a temporaryClassifierConfigthat adds the element tasks (the active config is never changed), over up toelement_sample(default 200) revealed training stories, most recent first, alternating between labels. Results go in the ledger tableelement_evaluations(one row per element and round, with the full metrics as JSON).
Element metrics (exact definitions)
positive is the selection policy's positive class, else the task's first label. main is the main decision's
probability of the positive class in the same response; the element score is its P(yes).
- AUC versus the label. Mann-Whitney AUC of
P(yes)for separating positive-class stories from the rest, ties counted half. AUC on errors. Within each true label, the AUC ofP(yes)for separating the incumbent's errors from its correct answers (the incumbent's answer in the same response);Nonewhen a side is empty. - Incremental value. Repeated (
5) stratified 5-fold cross-validation. In each fold two ridge-penalised logistic models (L2 0.01 on slopes) are fitted on the training folds: baselogistic(logit(main))and augmentedlogistic(logit(main) + element)(mainclipped to [0.001, 0.999]; the element enters as its probability). Per story and per repeat, log-loss gain = base loss minus augmented loss on the held-out fold (positive is better), averaged over the repeats; the accuracy gain compares out-of-fold decisions at 0.5.mean_*_gainaverages over stories. - Bootstrap lower bound. 500 resamples of the per-story gains (seeded); the bound is the 5th percentile of the resampled means, i.e. the lower end of a central 90% interval. It reflects which stories were drawn, not the variation of the model fit, so it is a screening bound and not a test.
- Keep rule. An element is kept when its mean log-loss gain is positive and its bootstrap lower bound is above zero.
Candidates are considered by descending gain; one whose correlation (Pearson, on
P(yes)) with an already kept element is at least 0.95 is not kept as well (redundant). The correlation with every kept element is stored. - Screening needs at least 40 scored stories with at least 8 of each class, otherwise it is skipped
(
element-screening-skipped).
Kept elements are registered as ideas with operator: "element" of the tasks control, through the existing feature-bank
path (feature-discovered), so the existing tasks control and ML head path can use them. Nothing is promoted, no head is
trained and nothing is shadowed here; the optimizer digest lists kept and failed elements with their gains.
Proposals use the diagnosis (ask_round)
The proposal prompt gains diagnosis: the confusion summary text; the causes with their description, patch, whether
they were confirmed, and the trial result (passing_cause_ids lists those that passed; unconfirmed causes are
flagged); the kept elements with their gains; and the elements that failed, so they are not retried blindly. Every
proposal may carry addresses, a list of cause ids from this round. An unknown id drops that proposal (a
proposal-operator-skipped event), it does not crash the round. When no complementary pair of ideas exists yet but at
least two causes passed, combine is asked to merge those causes' patches (cause_patches) instead of being skipped.
addresses is stored in the idea payload, in lineage() and in the proposal-operator event.
HypothesisLedger.cause_outcomes() returns, per stored cause: its name, cell, confirmed, trial_passed, and the ideas
that addressed it with operator, latest verdict and latest recorded forward fixed/broke (credit assignment); the
optimizer digest carries it, so later rounds see which causes' fixes worked.
Calls per round and caps
Optimizer calls: independent x cells + 1 reconcile, then the Stage 2c proposal call. Typical (binary task, one or two
qualifying cells): 3 to 5 diagnosis calls. Worst case with defaults: 3 cells x 2 + 1 = 7, doubled by corrective retries to
14. Before starting, if the transport states a ceiling (calls_remaining()) that cannot cover the planned calls plus the
proposal call, or while running if it would leave no room for the proposal call, diagnosis stops and emits
diagnosis-skipped with the reason (the summary is still given to the proposals). It never exceeds the ceiling.
Decision-model (Jev) calls: the cause trial scores at most samples + 2 x contrast = 16 stories per cause (defaults;
6 causes at most = 96), then element screening scores up to element_sample stories (200), one request each however
many elements. max_jev_calls (default 400) bounds the round; cache hits are free. Every call goes through
wheel._answers, so the wheel's request ceilings and the run ledger count them. The same headroom Stage 2 screening keeps
for normal decisions (len(training) + 2 x len(development)) is kept. The diagnosis-summary event ends with the
accounting: optimizer calls used and Jev calls used.
Options and flags
DiagnosisConfig: enabled=False, samples=8, contrast=4, min_cell=5, max_causes=6, independent=2 (2 to 4),
element_candidates=8, element_sample=200, max_jev_calls=400; validated, saved and resumed with the rest of the
screening config. Study flags: --diagnosis, --diagnosis-samples, --diagnosis-independent, --element-candidates,
--element-sample, --diagnosis-max-jev-calls, recorded in protocol.json through idea_screening.diagnosis.
--diagnosis needs --idea-screening in shadow mode.
Not built
Cross-run memory of causes, clause-level splitting and routing on disagreement. Shadowing tasks ideas (kept elements are
only registered). Head training and promotion are not rewired.
Still not built
- Routing on the disagreement of the shadow ideas to send items to review (Stage 3). Stage 2b only records
portfolio-scored. - Cross-run idea memory, so screened-out ideas are scored on later labels and revived (Stage 4). Until then
revive_candidates()is wired but only fires if something else scores a screened-out idea.