Skip member execution in the dissolved walkforward/mc leading sweep (decided: B — enumerate-only leading stage) #256
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while closing #253 (attribution correction there: the closing comment has the call-graph evidence). The dissolved
walkforward/mcverbs translate to the campaign process[Sweep{selection: argmax}, WalkForward]. Inaura-campaign/src/exec.rs::run_cell(~lines 259-333 atd1b9d2b) the leading Sweep stage executes every grid member over the entire campaign window, computes an argmax winner that — pertranslate_walkforward's own doc comment — "is not part of the summary", and, with no Gate stage between the two,survivorsflows on as the whole family regardless. The WalkForward stage then reads onlysurvivor_points(the grid'sVec<Scalar>points, derivable fromenumerate_gridalone) and re-sweeps the grid per IS window from scratch.Claim (unverified, plausible from structure): for a year-2025 2×2 real walkforward (~6.1 s debug end-to-end), the leading full-window sweep is a large share of the runtime — it is the same grid over a window ≥ any single IS window.
The design fork this raises (ledger territory, not a mechanical fix)
std::sweep(argmax)+std::walk_forward— is pinned by E2E tests, andreproduce/runs families/rankmay read the sweep stage's persisted member records as the family's record set (C25 process-doc semantics). Then the computation is the product's contract, not waste, and the fix would be a documented decision to change the process shape (e.g. a validation-only leading stage for walkforward/mc translations), with reproduce/rank semantics re-specified.survivor_pointsare consumed and the member records of the leading sweep are incidental. Then executing members for a selection that is discarded is pure waste, and the translation can enumerate the grid without running it — behaviour-preserving for the walkforward summary, but the persisted campaign document loses the sweep stage's run records, which is observable to store readers.Either way the resolution changes what a persisted dissolved-walkforward campaign document contains, so it belongs in the design ledger (the C25 process/campaign vocabulary) before any implementation.
Context: the measured cost frame for the suite is in the #250 breakdown; this issue is the product-runtime counterpart for
--realcampaign invocations.Design decision — fork resolved to (B)
Fork: may the dissolved walkforward/mc leading sweep skip member execution? → (B): only
survivor_pointsare consumed; the leading sweep's member records are incidental, and the translation may enumerate the grid without executing it.Basis: owner decision, 2026-07-14, choosing option (B) of this issue's body after reviewing the fork.
Ground recorded with the decision:
WalkForwardstage reads only the grid's param points (survivor_points), derivable fromenumerate_gridalone; the leading sweep's argmax winner has no downstream consumer — noGatesits between the stages, and per the dissolution's own rule "select names the recorded selection, never a filter", the whole family flows on regardless.aura walkforwardverb never computed a full-campaign-window sweep (it swept the grid per IS window only, per the issue-210 coverage map). The full-window member records are therefore a by-product of the translation shape, not behaviour any consumer predates; the only things pinning them today are the E2E family-shape tests added with the dissolution itself.Consequences (the scope of the implementing change — ledger-first, per this issue's body):
reproduce/runs/rank) and the E2E family-shape pins must be re-specified to the new shape.Status: the fork is settled — the issue is now an actionable task (ledger amendment + implementation), no longer awaiting a decision.
Decide whether the dissolved walkforward/mc leading sweep may skip member executionto Skip member execution in the dissolved walkforward/mc leading sweep (decided: B — enumerate-only leading stage)Implementation-fork decisions (derived) — realizing fork B
Fork B (enumerate-only leading stage) leaves four implementation forks open; they resolve as follows. Basis for each: derived, with the groundable rationale inline.
std::grid(StageBlock::Grid, serialized{"block": "std::grid"}), not anexecute: falseflag onstd::sweepand not executor detection of the sweep→walk_forward adjacency. Rationale: the decision on this issue requires a documented vocabulary form, which rules out silent executor detection; and a separate block is the typed form — a flag onstd::sweepwould admit meaningless field combinations (execute: false+selection: Some), whilestd::gridhas exactly the fields its semantics carry (none — the axes live in the campaign document).Option<RunReport>through the existing tuple. Rationale: a points-only population and an executed-member population are semantically different states with different legal consumers (walk_forward needs points; gate / monte_carlo per-survivor / generalize-nomination need reports), and an enum seam makes that distinction type-visible while the pipeline preflight guarantees report-consuming stages never see the points-only arm.std::gridis legal only as the first stage and only when immediately followed bystd::walk_forward, enforced where the existing pipeline-shape rules live (the campaign preflight beside "the first stage must be std::sweep" and the terminal-selection-free-sweep rule). Rationale: every other neighbor is a report consumer —std::gatefilters on member metrics,std::monte_carlo's per-survivor arm bootstraps member report series, and a terminalstd::gridwould record nothing at all — so the one legal shape is exactly the dissolved walkforward/mc translation's leading position.translate_walkforwardandtranslate_mcswitch their leading stage tostd::grid;translate_generalizekeeps the executedstd::sweep(argmax)leading stage. Rationale: the generalize stage consumes the per-cell nominee, which for its pipeline is the leading sweep's argmax winner report — an executed member — so its leading sweep is load-bearing, unlike the walkforward/mc case where only the parameter points cross the seam.Consequences confirmed against the tree before speccing: nothing in the walkforward/mc summary path reads the leading sweep's family or winner (the summary is rebuilt purely from the walk_forward family's own reports), the walk_forward stage re-sweeps survivor points per IS window itself, and the survivor points it receives today are exactly the enumerated grid in odometer order — so the visible walkforward/mc grades are byte-identical after the change, and the existing exact-grade pin tests ratify the preservation. The observable delta is confined to what fork B names: the persisted campaign document and registry lose the leading stage's member records and
Sweepfamily (the two family-shape E2E pins flip from oneSweepfamily to none), and the run record keeps astd::gridstage realization line without a family id.One behavioural edge, accepted: a member fault in the leading sweep (e.g. a data gap) today aborts the cell at stage 0 before walk_forward runs; with an enumerate-only leading stage the same fault surfaces in the walk_forward stage instead (same fault, later stage ordinal). Per-cell fault semantics are being generalized separately on issue 272.
Follow-up derived decision — the fieldless block vs. the vocabulary slot guard
The grounding check on the spec surfaced a counter-ratifying test:
vocabularies_enumerate_every_block_with_typed_slotsasserts a non-empty slot list for every process block, which a fieldlessstd::gridviolates.Fork: give
std::grida nominal slot, or make the guard admit a fieldless block? → the guard gains a pinned exception: slots may be empty only forstd::grid; every other block keeps the non-empty-slots assertion.Basis: derived — a nominal non-serialized slot would misdescribe the vocabulary to
describe_blockconsumers (the stage genuinely has no slots), while a blanket relaxation would silently drop the documented-slots guard for all blocks; the pinned single exception keeps the guard's protective scope and names the one legitimate fieldless block explicitly.