docs(cycle): sweep-dissolution spec + plan (cycle-live working artifacts)
Cycle 1 of the verb-dissolution milestone (refs #210): the real-data blueprint sweep re-cut as sugar over a generated, auto-registered campaign document; optional selection group in the sweep stage vocabulary; terminal-only preflight rule. Spec auto-signed on a grounding-check PASS (decision log on #210); ephemeral per convention, removed at cycle close.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,290 @@
|
|||||||
|
# Sweep dissolution — the real-data blueprint sweep as campaign sugar — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-07-04
|
||||||
|
**Status:** Draft — awaiting sign-off
|
||||||
|
**Authors:** orchestrator + Claude
|
||||||
|
|
||||||
|
Cycle 1 of the milestone "Verb dissolution — the campaign path as canonical
|
||||||
|
orchestration" (anchor issue #210; design basis: the #210 coverage map + the
|
||||||
|
fork-triage comment of 2026-07-04, all forks decided). This cycle re-cuts the
|
||||||
|
**real-data blueprint branch** of `aura sweep` as thin sugar over a generated,
|
||||||
|
auto-registered campaign document executed by the one campaign executor. The
|
||||||
|
built-in and synthetic branches stay verb-wired (#210 decision 2); the other
|
||||||
|
three verbs follow in later cycles riding on this cycle's translator skeleton
|
||||||
|
(#210 decision 7).
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
`aura sweep <blueprint.json> --real SYM --from A --to B --axis k=v,… [--name N]`
|
||||||
|
stops executing through its inline `run_blueprint_sweep` path and instead:
|
||||||
|
|
||||||
|
1. registers the blueprint (already shipped behaviour, kept),
|
||||||
|
2. generates a selection-free single-stage **process document** and a
|
||||||
|
**campaign document** expressing exactly the invocation's intent,
|
||||||
|
3. auto-registers both into the content-addressed stores (#210 decision 6),
|
||||||
|
4. runs them through `aura-campaign`'s executor via the existing
|
||||||
|
`CliMemberRunner`,
|
||||||
|
5. prints the same member lines as today's verb output, modulo one additive
|
||||||
|
field: the campaign substrate stamps `"instrument":"<SYM>"` into each
|
||||||
|
member's manifest (`campaign_run.rs` stamps `manifest.instrument`; the
|
||||||
|
inline path leaves it `None` and serde omits it). The stamp is kept —
|
||||||
|
it is provenance the substrate deliberately records; stripping it in
|
||||||
|
sugar mode would fork the substrate's behaviour for presentation
|
||||||
|
cosmetics. No existing test pins the field's absence on the dissolved
|
||||||
|
branch (#210 decision 5, refined by the grounding-check finding of
|
||||||
|
2026-07-04).
|
||||||
|
|
||||||
|
The invocation's intent — instrument, window, axes, strategy — thereby becomes
|
||||||
|
durable, diffable, reproducible data instead of evaporating into shell history
|
||||||
|
(#188 diagnosis). The inline real-data execution arm is deleted in the same
|
||||||
|
cycle; "thin sugar" replaces the path, it never adds a parallel one.
|
||||||
|
|
||||||
|
Prerequisite vocabulary change (#210 decision 1): the `std::sweep` stage's
|
||||||
|
selection becomes optional — permitted only when the sweep is the terminal
|
||||||
|
stage of its pipeline.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Three layers change; the seams between them are all shipped:
|
||||||
|
|
||||||
|
- **`aura-research`** — the `std::sweep` block's selection triple
|
||||||
|
(`metric`, `select`, `deflate`) becomes an optional *group*: either all
|
||||||
|
absent (a selection-free sweep) or `metric`+`select` present (`deflate`
|
||||||
|
defaulting as today). A half-populated triple is unrepresentable — the
|
||||||
|
hand-rolled schema-strict deserializer refuses `metric` without `select`
|
||||||
|
and vice versa. Introspection (schema tables, `--node` describe) and the
|
||||||
|
canonical form follow: the absent group serializes as absent fields, so
|
||||||
|
every existing document's canonical bytes and content id are unchanged.
|
||||||
|
- **`aura-campaign`** — preflight gains one structural rule: a selection-free
|
||||||
|
sweep is permitted iff it is the pipeline's terminal stage (every downstream
|
||||||
|
stage consumes the family's selection state or the nominee; refusing
|
||||||
|
non-terminal selection-free sweeps keeps that contract intact). The executor
|
||||||
|
skips `select_sweep_winner` for a selection-free sweep: no `StageSelection`
|
||||||
|
is recorded (recording one would fabricate intent — the audit-lie all triage
|
||||||
|
lenses rejected), no nominee is produced, the family is appended exactly as
|
||||||
|
today.
|
||||||
|
- **`aura-cli`** — a new translator module (the shared skeleton later verb
|
||||||
|
cycles extend) maps the verb's argv to the two generated documents,
|
||||||
|
auto-registers them, invokes the campaign run path, and presents the result
|
||||||
|
through the verb's own stdout contract (member lines only; the campaign
|
||||||
|
path's `selection_report` and final `campaign_run` stdout lines are
|
||||||
|
suppressed in sugar mode — the run record itself is still appended to
|
||||||
|
`campaign_runs.jsonl`). The `--real` arm of `run_blueprint_sweep`'s dispatch
|
||||||
|
is replaced by this path; the synthetic arm and `--list-axes` stay untouched.
|
||||||
|
|
||||||
|
## Concrete code shapes
|
||||||
|
|
||||||
|
### The user-facing invocation (unchanged surface, new substrate)
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ aura sweep blueprints/signal.json --real GER40 \
|
||||||
|
--from 1725148800000 --to 1727740800000 \
|
||||||
|
--axis sma_fast.LENGTH=8,12,16 --name probe
|
||||||
|
{"family_id":"…","report":{…}}
|
||||||
|
{"family_id":"…","report":{…}}
|
||||||
|
{"family_id":"…","report":{…}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stdout: one member line per grid point, the same `family_member_line` shape
|
||||||
|
as today plus the additive `"instrument":"GER40"` manifest stamp (see Goal,
|
||||||
|
point 5). New observable effects, all additive:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ls runs/processes runs/campaigns # both docs now exist, content-addressed
|
||||||
|
runs/processes/<process-id>.json
|
||||||
|
runs/campaigns/<campaign-id>.json
|
||||||
|
$ aura campaign runs # the realized run is recorded
|
||||||
|
{"campaign_run":{"campaign":"<campaign-id>","name":"probe",…}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The generated documents (exact intent, closed vocabulary)
|
||||||
|
|
||||||
|
Process document (constant for every selection-free sweep — repeated ad-hoc
|
||||||
|
sweeps dedupe onto one stored doc by content id):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"kind": "process",
|
||||||
|
"name": "sweep",
|
||||||
|
"pipeline": [ { "block": "std::sweep" } ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Campaign document (one per distinct invocation shape; `--name` becomes the
|
||||||
|
campaign name, defaulting as the verb defaults today):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"kind": "campaign",
|
||||||
|
"name": "probe",
|
||||||
|
"data": { "instruments": ["GER40"],
|
||||||
|
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ] },
|
||||||
|
"strategies": [ { "ref": { "content_id": "<blueprint-content-id>" },
|
||||||
|
"axes": { "sma_fast.LENGTH": { "kind": "I64", "values": [8, 12, 16] } } } ],
|
||||||
|
"process": { "ref": { "content_id": "<process-id>" } },
|
||||||
|
"seed": 0,
|
||||||
|
"presentation": { "persist_taps": [], "emit": ["family_table"] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`seed: 0`: no stage of a selection-free single-sweep pipeline consumes the
|
||||||
|
seed (no deflation, no bootstrap); a fixed zero keeps the generated bytes
|
||||||
|
deterministic so identical invocations produce identical content ids.
|
||||||
|
|
||||||
|
### Vocabulary change, before → after (`aura-research`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// before
|
||||||
|
StageBlock::Sweep { metric: String, select: SelectRule, deflate: bool }
|
||||||
|
|
||||||
|
// after — the selection triple is one optional group
|
||||||
|
StageBlock::Sweep { selection: Option<SweepSelection> }
|
||||||
|
pub struct SweepSelection { pub metric: String, pub select: SelectRule, pub deflate: bool }
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire form is unchanged for existing documents: `{"block":"std::sweep",
|
||||||
|
"metric":"…","select":"…"}` parses into `Some(SweepSelection{…})`;
|
||||||
|
`{"block":"std::sweep"}` parses into `None`; `metric` without `select` (or
|
||||||
|
vice versa) is a schema error naming the incomplete group. Serialization
|
||||||
|
omits the absent group, so canonical bytes of every existing document are
|
||||||
|
byte-identical and no stored content id moves.
|
||||||
|
|
||||||
|
### Preflight rule, before → after (`aura-campaign`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// before: sweep always selects; nothing to check.
|
||||||
|
// after: a selection-free sweep must be terminal.
|
||||||
|
if sweep.selection.is_none() && !is_terminal_stage {
|
||||||
|
return Err(ExecFault::SelectionFreeSweepNotTerminal { … });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no separate preflight fault type — `preflight` returns `ExecFault`
|
||||||
|
(`aura-campaign/src/lib.rs`), whose prose lives in `aura-cli`'s exhaustive
|
||||||
|
`exec_fault_prose` match; the new variant extends both (compiler-enforced).
|
||||||
|
|
||||||
|
Executor arm: `selection: None` → append the family, emit `family_table`
|
||||||
|
lines per the presentation, record the stage realization **without** a
|
||||||
|
`StageSelection`, produce no nominee.
|
||||||
|
|
||||||
|
### Verb dispatch, before → after (`aura-cli`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// before (dispatch_sweep, blueprint branch, --real present):
|
||||||
|
run_blueprint_sweep(doc, axes, name, persist, DataSource::Real{…}) // inline exec
|
||||||
|
|
||||||
|
// after:
|
||||||
|
let gen = translate_sweep(&argv)?; // argv → (ProcessDoc, CampaignDoc)
|
||||||
|
let ids = register_generated(®, &gen)?; // auto-register both, content-addressed
|
||||||
|
run_generated_campaign(®, env, ids, RunPresentation::MemberLinesOnly)?;
|
||||||
|
// The presentation mode is a parameter threaded through the existing
|
||||||
|
// campaign-run entry (`campaign_run.rs`): `aura campaign run` keeps the
|
||||||
|
// full mode (member lines + selection_report + final campaign_run line);
|
||||||
|
// sugar mode suppresses everything but the family_table member lines.
|
||||||
|
// The record append is untouched in both modes.
|
||||||
|
```
|
||||||
|
|
||||||
|
The synthetic arm (`--real` absent) keeps calling the existing inline path
|
||||||
|
unchanged; `--list-axes` keeps its probe path unchanged.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
1. **`aura-research`**: `SweepSelection` extraction; hand-rolled deserializer
|
||||||
|
update (group-or-nothing rule + refusal prose); canonical-form
|
||||||
|
serialization keeps omit-absent; introspection contract updated (the
|
||||||
|
sweep block's schema table marks the selection group optional-terminal);
|
||||||
|
intrinsic validation unchanged otherwise.
|
||||||
|
2. **`aura-campaign`**: preflight terminal-only rule + fault variant with
|
||||||
|
house-style prose; executor selection-free arm (no `StageSelection`, no
|
||||||
|
nominee); `PER_MEMBER_METRICS`/`RANKABLE_METRICS` untouched.
|
||||||
|
3. **`aura-cli`**: `verb_sugar` translator module — `translate_sweep`
|
||||||
|
(argv → documents; CSV axis parsing reuses the shipped `--axis` parser and
|
||||||
|
the axis-kind derivation from the blueprint's open params), generated-doc
|
||||||
|
registration, sugar-mode presentation (suppress `selection_report` +
|
||||||
|
`campaign_run` stdout lines; member lines pass through), dispatch rewire
|
||||||
|
of the `--real` blueprint arm, deletion of the now-dead inline real-data
|
||||||
|
execution arm of `run_blueprint_sweep`.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
argv ──translate_sweep──▶ ProcessDoc + CampaignDoc (canonical JSON)
|
||||||
|
──register_generated──▶ runs/processes/<id>.json, runs/campaigns/<id>.json
|
||||||
|
──campaign run path──▶ preflight ▶ executor ▶ CliMemberRunner (real archive)
|
||||||
|
├─▶ families.jsonl (FamilyKind::Sweep, as today)
|
||||||
|
├─▶ campaign_runs.jsonl (the realized-intent record)
|
||||||
|
└─▶ stdout: family_table member lines ONLY (sugar mode)
|
||||||
|
```
|
||||||
|
|
||||||
|
Family naming follows the campaign convention (`{campaign8}-…-w0-s0`); the
|
||||||
|
user's `--name` handle lives on the campaign document and its run record. The
|
||||||
|
member stdout lines never carried the family name; their only delta against
|
||||||
|
the inline path is the additive `instrument` manifest stamp (Goal, point 5).
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
- Verb-surface refusals keep the shipped exit-code contract: usage errors
|
||||||
|
exit 2 (clap, unchanged), runtime refusals exit 1 with `aura: …` prose.
|
||||||
|
- Translator refusals (unknown axis name, malformed CSV, missing geometry)
|
||||||
|
reuse the existing refusal prose paths — the pinned rejection tests keep
|
||||||
|
their bytes.
|
||||||
|
- Preflight faults surface through the existing campaign fault prose;
|
||||||
|
the new `SelectionFreeSweepNotTerminal` prose names the rule and the fix
|
||||||
|
("a sweep without a selection must be the last stage").
|
||||||
|
- A generated document that fails validation is a bug, not a user error —
|
||||||
|
the translator's output is preflighted before any member runs, so a fault
|
||||||
|
aborts with exit 1 before touching the archive.
|
||||||
|
|
||||||
|
## Testing strategy
|
||||||
|
|
||||||
|
1. **Characterization first (the reproduction evidence) — SHIPPED.** The pin
|
||||||
|
test `sweep_real_blueprint_member_lines_pin_the_inline_contract`
|
||||||
|
(`crates/aura-cli/tests/cli_run.rs`, gated, green on this host) pins the
|
||||||
|
inline path's observable behaviour: member count (exactly 4 stdout lines)
|
||||||
|
and odometer order, the swept bindings in `manifest.params`, exactly one
|
||||||
|
`FamilyKind::Sweep` family with 4 members — and, as of today, the
|
||||||
|
*absence* of the `instrument` manifest key on the inline path. The re-cut
|
||||||
|
task flips **exactly that one assertion** (absence → equals the `--real`
|
||||||
|
symbol, the sanctioned additive delta); every other assertion survives
|
||||||
|
unchanged, including the exactly-4-lines pin, which forces sugar mode to
|
||||||
|
suppress the campaign path's final `campaign_run` stdout line. The sibling
|
||||||
|
pin (the campaign runner's stamp) lives in
|
||||||
|
`campaign_run_real_e2e_sweep_gate_walkforward` and is untouched.
|
||||||
|
2. **Translator unit tests (ungated, pure).** argv → exact canonical JSON
|
||||||
|
bytes of both generated documents (including `seed: 0`, axis kinds, the
|
||||||
|
`content_id` refs); identical invocations → identical content ids; `--name`
|
||||||
|
flows to the campaign name.
|
||||||
|
3. **Vocabulary tests (ungated, `aura-research`).** Selection-free sweep
|
||||||
|
parses/serializes/round-trips; half-populated group refused with named
|
||||||
|
prose; existing fixture documents' canonical bytes and content ids
|
||||||
|
unchanged (the no-drift pin).
|
||||||
|
4. **Preflight tests (`aura-campaign`).** Selection-free terminal sweep
|
||||||
|
accepted; selection-free sweep followed by any stage refused; executor
|
||||||
|
records no `StageSelection` and produces no nominee for the
|
||||||
|
selection-free arm.
|
||||||
|
5. **Existing suite green unchanged.** All current sweep tests (synthetic,
|
||||||
|
built-in, `--list-axes`, rejection prose) stay untouched and green — they
|
||||||
|
pin the branches this cycle does not dissolve.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- The `--real` blueprint form of `aura sweep` executes through the campaign
|
||||||
|
executor; its inline real-data execution arm is deleted (no parallel path).
|
||||||
|
- Both generated documents are auto-registered content-addressed on every
|
||||||
|
sugar run; a `campaign_run` record is appended; repeated identical
|
||||||
|
invocations dedupe onto the same document ids.
|
||||||
|
- Member-line stdout of the dissolved form is identical to the pre-change
|
||||||
|
verb modulo the additive `instrument` manifest stamp: the shipped
|
||||||
|
characterization pin survives the re-cut with exactly one sanctioned
|
||||||
|
assertion flip (instrument-absence → instrument-equals-symbol); all its
|
||||||
|
other assertions — including exactly 4 stdout lines — are untouched.
|
||||||
|
- A selection-free `std::sweep` is a valid terminal stage of the document
|
||||||
|
vocabulary — parseable, introspectable, content-addressable — and refused
|
||||||
|
with named prose anywhere else in a pipeline; no existing document's
|
||||||
|
content id moves.
|
||||||
|
- Synthetic/built-in sweep branches, `--list-axes`, and all their pinned
|
||||||
|
tests are untouched.
|
||||||
|
- `cargo test --workspace` green; `cargo clippy --workspace --all-targets
|
||||||
|
-- -D warnings` clean.
|
||||||
Reference in New Issue
Block a user