audit: cycle 0110 tidy — verb-dissolution cycle 1 closed drift-clean
Architect review (scope e7c7bde..b7aaa0b) found three items; resolutions: - fix: docs/authoring-guide.md + glossary aligned with the shipped vocabulary (std::sweep selection group optional/all-or-nothing, selection-free = terminal-only, worked example verified against the live binary). - fix: the #203 wrapped/raw axis-name convention is now single-sourced in campaign_run.rs (wrapped_to_raw_axis + raw_matches_wrapped, a cross-documented inverse pair; dispatch_sweep and both bind sites call through it; inverse-property unit test). Ratify note: this unifies two previously different algorithms onto the documented first-segment semantics — the old loose ends_with suffix-match also accepted sub-segment shorthands (e.g. axis "length" against "sma_signal.fast.length") that no stored document or test used; such shorthands now refuse loudly (unbound/unknown axis) instead of matching, the refuse-don't-guess reading of #203. - carry: run_blueprint_sweep/blueprint_sweep_family remain generically real-capable though the dispatch no longer routes real data to them; the residual capability is shared DataSource machinery, its removal falls out with the built-in/synthetic branch retirement (#159), and the synthetic-only status is documented at the fn. What holds: C24 canonical form (flatten group leaves every stored content id unchanged, golden pin untouched), C18 lineage (topology_hash IS content_id_of, single store write resolves the strategy ref; selection-free arm still persists the full family), C25 closed- vocabulary discipline (all-or-nothing group, Blockly-clean), C3 (ms/ns conversion through the ingest seam's own named fn). Ledger: #109 open-thread status lifted to cycles 0107-0110 reality (executor shipped, amendment package landed, verb dissolution running as milestone #210 with sweep dissolved). Regression gates: cargo test --workspace 1056/0; clippy -D warnings clean; cargo doc clean. Cycle spec+plan removed at close per convention. refs #210
This commit is contained in:
@@ -123,11 +123,34 @@ struct CampaignRunLine<'a> {
|
||||
campaign_run: &'a CampaignRunRecord,
|
||||
}
|
||||
|
||||
/// Suffix-join each raw campaign axis onto exactly one wrapped param (wrapped
|
||||
/// == raw, or wrapped ends with ".{raw}" — the established suffix-match
|
||||
/// pattern), then require every wrapped slot to be covered. Pure and
|
||||
/// independent of the env/data seam so the three fault arms are unit-testable
|
||||
/// without a project, a store, or a loaded blueprint.
|
||||
/// Strip the wrapper's exactly-one leading node segment from a WRAPPED param
|
||||
/// path, yielding the RAW campaign-axis name the doc speaks (#203): the bind
|
||||
/// convention prefixes a strategy's own param paths with one root-composite
|
||||
/// segment before they enter the runnable (wrapped) param space, so the raw
|
||||
/// axis is everything after that segment's dot. Returns `name` unchanged if
|
||||
/// there is no dot (defensive; every wrapped param space produced by
|
||||
/// `blueprint_axis_probe` has one). This is the single definition of "the
|
||||
/// wrapper segment" — [`raw_matches_wrapped`] is built on it so a wrap-depth
|
||||
/// change cannot desync the two directions of the #203 convention.
|
||||
pub(crate) fn wrapped_to_raw_axis(name: &str) -> &str {
|
||||
name.split_once('.').map(|(_, rest)| rest).unwrap_or(name)
|
||||
}
|
||||
|
||||
/// Does a RAW campaign-axis name (the doc's own namespace) address this ONE
|
||||
/// wrapped param path? True for an exact match (an unwrapped param, if one
|
||||
/// ever exists) or when stripping the wrapper's one node segment
|
||||
/// ([`wrapped_to_raw_axis`]) yields exactly `raw`. Inverse of
|
||||
/// `wrapped_to_raw_axis`, co-located and built on it (#203): the two are the
|
||||
/// only two places the wrapper-segment convention is expressed.
|
||||
pub(crate) fn raw_matches_wrapped(raw: &str, wrapped: &str) -> bool {
|
||||
wrapped == raw || wrapped_to_raw_axis(wrapped) == raw
|
||||
}
|
||||
|
||||
/// Suffix-join each raw campaign axis onto exactly one wrapped param
|
||||
/// ([`raw_matches_wrapped`] — wrapped == raw, or stripping the wrapper's one
|
||||
/// node segment yields raw), then require every wrapped slot to be covered.
|
||||
/// Pure and independent of the env/data seam so the three fault arms are
|
||||
/// unit-testable without a project, a store, or a loaded blueprint.
|
||||
fn bind_axes(
|
||||
space: &[ParamSpec],
|
||||
strategy_id: &str,
|
||||
@@ -138,7 +161,7 @@ fn bind_axes(
|
||||
let hits: Vec<usize> = space
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.name == *raw || p.name.ends_with(&format!(".{raw}")))
|
||||
.filter(|(_, p)| raw_matches_wrapped(raw, &p.name))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
match hits.as_slice() {
|
||||
@@ -166,8 +189,7 @@ fn bind_axes(
|
||||
// Speak the RAW campaign-axis namespace (the doc's own): the
|
||||
// wrapped space prefixes the signal's params with one node
|
||||
// segment, so the raw path is everything after it (#203).
|
||||
let raw =
|
||||
spec.name.split_once('.').map(|(_, rest)| rest).unwrap_or(&spec.name);
|
||||
let raw = wrapped_to_raw_axis(&spec.name);
|
||||
return Err(MemberFault::Bind(format!(
|
||||
"open param \"{raw}\" of strategy {strategy_id} is bound by no \
|
||||
campaign axis (wrapped path: {})",
|
||||
@@ -803,6 +825,18 @@ mod tests {
|
||||
assert!(!is_content_id(&format!("{}g", "a".repeat(63))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #203: `wrapped_to_raw_axis` and `raw_matches_wrapped` are the inverse
|
||||
/// pair of one wrapper-segment convention — stripping the wrapper off a
|
||||
/// wrapped name must satisfy the match predicate against that SAME
|
||||
/// wrapped name, and a raw name from a DIFFERENT wrapped param must not.
|
||||
fn raw_matches_wrapped_and_wrapped_to_raw_axis_are_inverses() {
|
||||
let wrapped = "sma_signal.fast.length";
|
||||
assert_eq!(wrapped_to_raw_axis(wrapped), "fast.length");
|
||||
assert!(raw_matches_wrapped(wrapped_to_raw_axis(wrapped), wrapped));
|
||||
assert!(!raw_matches_wrapped("fast.length", "sma_signal.slow.length"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// bind_axes resolves a raw axis name against the ONE wrapped param whose
|
||||
/// path ends with ".{raw}" (or equals it), producing a co-indexed point.
|
||||
|
||||
@@ -4529,11 +4529,7 @@ fn dispatch_sweep(a: SweepCmd, env: &project::Env) {
|
||||
// `param_space`) refuses every axis as unknown.
|
||||
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
|
||||
.iter()
|
||||
.map(|(n, v)| {
|
||||
let raw =
|
||||
n.split_once('.').map(|(_, rest)| rest.to_string()).unwrap_or_else(|| n.clone());
|
||||
(raw, v.clone())
|
||||
})
|
||||
.map(|(n, v)| (campaign_run::wrapped_to_raw_axis(n).to_string(), v.clone()))
|
||||
.collect();
|
||||
verb_sugar::run_sweep_sugar(
|
||||
&raw_axes, &name, &symbol, from_ms, to_ms, &canonical, env,
|
||||
|
||||
+26
-4
@@ -122,18 +122,40 @@ block's typed slots headlessly:
|
||||
|
||||
```
|
||||
$ aura process introspect --vocabulary
|
||||
std::sweep evaluate the campaign's axes-space; reduce members to R metrics; select a winner
|
||||
std::sweep evaluate the campaign's axes-space; reduce members to R metrics; optionally select a winner (the selection group metric+select is all-or-nothing; omitted = selection-free, terminal-stage-only)
|
||||
std::gate filter survivors by a conjunction of typed metric predicates
|
||||
std::walk_forward rolling in-sample optimize + out-of-sample test
|
||||
std::monte_carlo R-bootstrap over realised R (terminal annotator): ...
|
||||
std::generalize cross-instrument worst-case floor (terminal annotator): ...
|
||||
$ aura process introspect --block std::sweep
|
||||
std::sweep — evaluate the campaign's axes-space; reduce members to R metrics; select a winner
|
||||
metric required, metric name (see metric_vocabulary)
|
||||
select required, select rule: argmax | plateau:mean | plateau:worst
|
||||
std::sweep — evaluate the campaign's axes-space; reduce members to R metrics; optionally select a winner (the selection group metric+select is all-or-nothing; omitted = selection-free, terminal-stage-only)
|
||||
metric optional, metric name (see metric_vocabulary)
|
||||
select optional, select rule: argmax | plateau:mean | plateau:worst
|
||||
deflate optional, bool
|
||||
```
|
||||
|
||||
`metric` and `select` form one **selection group**: all-or-nothing (a
|
||||
document naming one without the other is refused, "the selection group is
|
||||
all-or-nothing"), `deflate` composes only when the group is present. Omit
|
||||
the group entirely for a **selection-free sweep** — the family itself is
|
||||
the result, no winner is chosen. A selection-free sweep is only legal as
|
||||
the pipeline's *last* stage (the executor's preflight refuses one followed
|
||||
by any other block, since a downstream stage would have no nominee to
|
||||
consume):
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "explore-only-sweep",
|
||||
"pipeline": [ { "block": "std::sweep" } ]
|
||||
}
|
||||
```
|
||||
|
||||
The executor records this stage's family (every member run) but no
|
||||
`StageSelection` and no nominee — recording a winner here would fabricate a
|
||||
selection intent the document never expressed.
|
||||
|
||||
### Worked example: full v2 pipeline (sweep → gate → walk-forward → Monte-Carlo → generalize)
|
||||
|
||||
```json
|
||||
|
||||
+15
-5
@@ -2317,11 +2317,21 @@ the project layout and docs-by-role are open (#192 context).
|
||||
two-tier validation, the op-script-precedent introspection contract
|
||||
(`aura process|campaign introspect --vocabulary/--block/--unwired/--content-id`), and
|
||||
content-addressed stores (C18 realization note) — authorable and checkable headless, no
|
||||
compile, no run. Still open here: the executor question (deliberately unscoped until the
|
||||
intent-persistence diagnosis is re-tested, #189), the "once it carries" dissolution of the
|
||||
hard-wired verbs into the campaign path (user decision 2026-07-03 on #188), and the
|
||||
remaining #188 amendment package (role-model contract entry; C20 "plain Rust loops"
|
||||
clause; C22 wording; invariant-10 clarification).
|
||||
compile, no run. **Status update (cycles 0107–0110).** The executor question resolved and
|
||||
shipped (cycles 0107/0108, #198/#200: `aura-campaign` executes the v2 pipeline shape over
|
||||
the `MemberRunner` seam); the #188 amendment package landed 2026-07-03 (C25 role-model
|
||||
entry, C20/C22 refinements, invariant-10 clarification). The "once it carries" dissolution
|
||||
of the hard-wired verbs (user decision 2026-07-03 on #188) is **running as the milestone
|
||||
"Verb dissolution" (#210)**: the fork triage of 2026-07-04 decided its design (dissolve
|
||||
the real-data blueprint branches only, built-in/synthetic branches stay verb-wired until
|
||||
#159; generated documents auto-registered; full behaviour parity modulo the recorded
|
||||
additive instrument stamp), and cycle 0110 dissolved the first verb — `aura sweep
|
||||
<bp.json> --real …` now runs as sugar over a generated, content-addressed campaign
|
||||
document through the one executor, enabled by the `std::sweep` selection group becoming
|
||||
optional (all-or-nothing, selection-free = terminal-stage-only; wire form and every
|
||||
stored content id unchanged). Ad-hoc sweep intent no longer evaporates into shell
|
||||
history — the #188 diagnosis's cure applied to the verbs themselves. Remaining there:
|
||||
generalize, walkforward, and mc's R-bootstrap path (#210 decision 7 order).
|
||||
- **Inferential honesty of the World — family-selection without false-discovery
|
||||
control (tracked: milestone "Inferential validation (defend against false
|
||||
discovery at sweep scale)", #144 / #145 / #146; adjacent #139).** The World's
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ A downstream consumer node, never part of the strategy: the signal-quality side
|
||||
|
||||
### campaign document
|
||||
**Avoid:** experiment doc, campaign file
|
||||
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|run`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization.
|
||||
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|run`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. `std::sweep`'s own selection group (`metric`+`select`) is optional, all-or-nothing, and permitted only as the pipeline's terminal stage when omitted (a selection-free sweep, #210).
|
||||
|
||||
### campaign run
|
||||
**Avoid:** campaign execution record, realized campaign
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,290 +0,0 @@
|
||||
# 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