Skip member execution in the dissolved walkforward/mc leading sweep (decided: B — enumerate-only leading stage) #256

Closed
opened 2026-07-13 15:51:15 +02:00 by claude · 3 comments
Collaborator

Found while closing #253 (attribution correction there: the closing comment has the call-graph evidence). The dissolved walkforward/mc verbs translate to the campaign process [Sweep{selection: argmax}, WalkForward]. In aura-campaign/src/exec.rs::run_cell (~lines 259-333 at d1b9d2b) the leading Sweep stage executes every grid member over the entire campaign window, computes an argmax winner that — per translate_walkforward's own doc comment — "is not part of the summary", and, with no Gate stage between the two, survivors flows on as the whole family regardless. The WalkForward stage then reads only survivor_points (the grid's Vec<Scalar> points, derivable from enumerate_grid alone) 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)

  • (A) The leading sweep's member records are load-bearing. The campaign document's family shape — leading std::sweep(argmax) + std::walk_forward — is pinned by E2E tests, and reproduce/runs families/rank may 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.
  • (B) Only survivor_points are 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 --real campaign invocations.

Found while closing #253 (attribution correction there: [the closing comment](issues/253) has the call-graph evidence). The dissolved `walkforward`/`mc` verbs translate to the campaign process `[Sweep{selection: argmax}, WalkForward]`. In `aura-campaign/src/exec.rs::run_cell` (~lines 259-333 at `d1b9d2b`) the leading Sweep stage executes **every grid member over the entire campaign window**, computes an argmax winner that — per `translate_walkforward`'s own doc comment — "is not part of the summary", and, with no Gate stage between the two, `survivors` flows on as the **whole family** regardless. The WalkForward stage then reads only `survivor_points` (the grid's `Vec<Scalar>` points, derivable from `enumerate_grid` alone) 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) - **(A) The leading sweep's member records are load-bearing.** The campaign document's family shape — leading `std::sweep(argmax)` + `std::walk_forward` — is pinned by E2E tests, and `reproduce`/`runs families`/`rank` may 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. - **(B) Only `survivor_points` are 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](issues/250#issuecomment-3433); this issue is the product-runtime counterpart for `--real` campaign invocations.
Author
Collaborator

Design decision — fork resolved to (B)

Fork: may the dissolved walkforward/mc leading sweep skip member execution?(B): only survivor_points are 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:

  • The WalkForward stage reads only the grid's param points (survivor_points), derivable from enumerate_grid alone; the leading sweep's argmax winner has no downstream consumer — no Gate sits between the stages, and per the dissolution's own rule "select names the recorded selection, never a filter", the whole family flows on regardless.
  • The pre-dissolution aura walkforward verb 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.
  • A user who wants the full-window picture has the standalone sweep path; baking it into every walkforward conflates "what is globally optimal over the whole window" with "is it stable across windows" — the separation the walkforward exists to provide.

Consequences (the scope of the implementing change — ledger-first, per this issue's body):

  • The C25 process/campaign vocabulary needs an execution-free way to express the leading stage (an enumerate-only / validation-only first-stage form, or an amended translation shape); this is a documented vocabulary / process-shape change in the design ledger, not a silent optimization.
  • The persisted dissolved-walkforward/mc campaign document loses the leading sweep stage's member records and registry family; store readers (reproduce / runs / rank) and the E2E family-shape pins must be re-specified to the new shape.
  • The runtime claim (the leading sweep is a large share of end-to-end) is still unmeasured; the implementing change should measure before/after.

Status: the fork is settled — the issue is now an actionable task (ledger amendment + implementation), no longer awaiting a decision.

## Design decision — fork resolved to (B) **Fork: may the dissolved walkforward/mc leading sweep skip member execution?** → **(B): only `survivor_points` are 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: - The `WalkForward` stage reads only the grid's param points (`survivor_points`), derivable from `enumerate_grid` alone; the leading sweep's argmax winner has no downstream consumer — no `Gate` sits between the stages, and per the dissolution's own rule "select names the recorded selection, never a filter", the whole family flows on regardless. - The pre-dissolution `aura walkforward` verb 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. - A user who wants the full-window picture has the standalone sweep path; baking it into every walkforward conflates "what is globally optimal over the whole window" with "is it stable across windows" — the separation the walkforward exists to provide. Consequences (the scope of the implementing change — ledger-first, per this issue's body): - The C25 process/campaign vocabulary needs an execution-free way to express the leading stage (an enumerate-only / validation-only first-stage form, or an amended translation shape); this is a documented vocabulary / process-shape change in the design ledger, not a silent optimization. - The persisted dissolved-walkforward/mc campaign document loses the leading sweep stage's member records and registry family; store readers (`reproduce` / `runs` / `rank`) and the E2E family-shape pins must be re-specified to the new shape. - The runtime claim (the leading sweep is a large share of end-to-end) is still unmeasured; the implementing change should measure before/after. Status: the fork is settled — the issue is now an actionable task (ledger amendment + implementation), no longer awaiting a decision.
claude changed title from Decide whether the dissolved walkforward/mc leading sweep may skip member execution to Skip member execution in the dissolved walkforward/mc leading sweep (decided: B — enumerate-only leading stage) 2026-07-14 11:29:55 +02:00
Author
Collaborator

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.

  • Fork: how the vocabulary expresses "enumerate-only"a new fieldless stage block std::grid (StageBlock::Grid, serialized {"block": "std::grid"}), not an execute: false flag on std::sweep and 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 on std::sweep would admit meaningless field combinations (execute: false + selection: Some), while std::grid has exactly the fields its semantics carry (none — the axes live in the campaign document).
  • Fork: the inter-stage seam typethe survivor population becomes a two-armed flow (points-only vs. executed members) instead of threading 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.
  • Fork: placement rulesstd::grid is legal only as the first stage and only when immediately followed by std::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::gate filters on member metrics, std::monte_carlo's per-survivor arm bootstraps member report series, and a terminal std::grid would record nothing at all — so the one legal shape is exactly the dissolved walkforward/mc translation's leading position.
  • Fork: scope of the translation changeonly translate_walkforward and translate_mc switch their leading stage to std::grid; translate_generalize keeps the executed std::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 Sweep family (the two family-shape E2E pins flip from one Sweep family to none), and the run record keeps a std::grid stage 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.

## 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. - **Fork: how the vocabulary expresses "enumerate-only"** → **a new fieldless stage block `std::grid`** (`StageBlock::Grid`, serialized `{"block": "std::grid"}`), not an `execute: false` flag on `std::sweep` and 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 on `std::sweep` would admit meaningless field combinations (`execute: false` + `selection: Some`), while `std::grid` has exactly the fields its semantics carry (none — the axes live in the campaign document). - **Fork: the inter-stage seam type** → **the survivor population becomes a two-armed flow (points-only vs. executed members)** instead of threading `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. - **Fork: placement rules** → **`std::grid` is legal only as the first stage and only when immediately followed by `std::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::gate` filters on member metrics, `std::monte_carlo`'s per-survivor arm bootstraps member report series, and a terminal `std::grid` would record nothing at all — so the one legal shape is exactly the dissolved walkforward/mc translation's leading position. - **Fork: scope of the translation change** → **only `translate_walkforward` and `translate_mc` switch their leading stage to `std::grid`; `translate_generalize` keeps the executed `std::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 `Sweep` family (the two family-shape E2E pins flip from one `Sweep` family to none), and the run record keeps a `std::grid` stage 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.
Author
Collaborator

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_slots asserts a non-empty slot list for every process block, which a fieldless std::grid violates.

Fork: give std::grid a nominal slot, or make the guard admit a fieldless block?the guard gains a pinned exception: slots may be empty only for std::grid; every other block keeps the non-empty-slots assertion.

Basis: derived — a nominal non-serialized slot would misdescribe the vocabulary to describe_block consumers (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.

## 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_slots` asserts a non-empty slot list for every process block, which a fieldless `std::grid` violates. **Fork: give `std::grid` a nominal slot, or make the guard admit a fieldless block?** → **the guard gains a pinned exception: slots may be empty only for `std::grid`; every other block keeps the non-empty-slots assertion.** Basis: derived — a nominal non-serialized slot would misdescribe the vocabulary to `describe_block` consumers (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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#256