closes#272
A member fault (no-data, bind, run, or a caught panic) is now a recorded
per-cell outcome instead of aborting the whole campaign and discarding every
already-computed cell. The incident that motivated this (a 22-instrument
campaign lost ~36 healthy cells ~6.7 min in because Copper had an archive gap)
now completes: the healthy cells persist, the gap cell is recorded as failed,
and the run exits 3.
Direction (owner decision 2026-07-14): run to completion and report
compromised results; no coverage preflight, no window synthesis.
Containment granularity:
- The CELL for a sweep-stage member fault (a grid hole structurally
compromises winner selection, so the whole cell fails).
- The FOLD for a walk_forward member fault (independent time windows): the
surviving folds pool into the family, failed folds are recorded as
StageRealization.window_faults, and the summary names the ratio.
- aura-registry: additive CellFault / CellFaultKind (closed:
no_data|bind|run|panic|window) / WindowFault / CellCoverage, plus
fault/coverage fields on CellRealization and window_faults on
StageRealization — all serde-default-skipped, so pre-#272 campaign_runs
lines parse and round-trip byte-identical.
- aura-campaign: run_cell returns a fault-annotated CellRealization instead of
Err (execute's accumulate-then-append-once tail is unchanged and now
persists every healthy cell + the one run record); a `contain` split keeps
ExecFault::Registry and doc-shape preflight faults global while Member/Window
become per-cell/per-fold. Member panics are caught with
catch_unwind(AssertUnwindSafe) at all three member-run sites (sweep IS/OOS)
and recorded as MemberFault::Panic — a member panic no longer aborts the
process. The wf stage partitions Registry faults (global) from Member/Window
(per-fold) and filters faulted-fold placeholders (the
"faulted-member-placeholder" broker sentinel) out of the persisted family.
- aura-cli: exec_fault_prose gains the Panic arm; CliMemberRunner::window_coverage
derives effective bounds + interior gap months from the #264 archive
primitives; present_campaign prints per-cell failure notes + a completion
summary and threads the failed-cell count; a run with >=1 failed cell exits 3
("completed with failed cells") uniformly across `aura campaign run` and the
dissolved sweep/walkforward/mc/generalize verbs (exit_on_campaign_result).
Usage stays 2, refused-before-running stays 1, clean stays 0.
Tests: the global-abort pins flip to containment (execute + the two wf fault
tests → fold-containment + all-folds-fail-the-cell); new panic-containment
tests on both the sweep path (PanicRunner) and the wf path (this commit adds
the wf mirror the loop left uncovered); a new gapped-archive e2e (one covered
cell + one gap cell → exit 3); the ~14 CLI exit-1 pins move to the exit-3
register; a pre-#272-line byte-identical round-trip guard.
Suite: cargo test --workspace green (1309 tests, 0 failed); clippy clean.
Decision log: #272 comments (fork rationale, the fold Registry/Member split,
the placeholder sentinel, uniform exit-3).
Follow-up (minor, not blocking): the plan under-scoped Task 1 to aura-registry
though the additive fields also touch aura-campaign's exec.rs literals — the
loop absorbed it mechanically; a future plan for a cross-crate additive-field
change should scope every crate's construction sites in the first task.
closes#256
Fork B (owner decision 2026-07-14): the dissolved walkforward/mc
translations' leading sweep executed the full grid over the whole campaign
window and persisted a Sweep family, yet only the enumerated parameter
points ever crossed the stage seam (the wf stage re-sweeps them per IS
window itself). The leading stage is now the fieldless vocabulary block
std::grid: it enumerates, executes nothing, persists nothing.
- aura-research: StageBlock::Grid ({"block":"std::grid"}), schema-strict
parse arm (empty slot list: every key but "block" is refused by the
generic unknown-slot check), PROCESS_BLOCKS entry, intrinsic-tier no-op
arm; the vocabulary test's non-empty-slots guard carries a pinned
std::grid-only exception (a nominal slot would misdescribe the
vocabulary to describe_block consumers).
- aura-campaign: the inter-stage seam is a typed two-armed StageFlow
(points-only vs executed members); gate / mc-per-survivor fence the
points-only arm with defensive PipelineShape faults; preflight admits
std::grid only as the first stage and only immediately before
std::walk_forward (every other neighbor consumes executed reports).
- aura-cli: translate_walkforward / translate_mc lead with
StageBlock::Grid; the two family-shape E2E pins flip to zero Sweep
families; translate_generalize keeps its executed sweep(argmax) — its
generalize stage consumes the argmax winner report as the cell nominee.
- docs: dated #256 amendment in the ledger's verb-dissolution narrative;
the authoring-guide vocabulary transcript gains the std::grid line.
Behaviour preservation: the exact-grade real-data pins
(walkforward_real_e2e_pins_the_exact_current_grade,
mc_r_bootstrap_real_e2e_pins_the_exact_current_grade) pass unmodified —
survivor points reach the wf stage in the same odometer order as before.
Measured (the #256 acceptance measurement; debug build, real GER40 2025,
2x2 grid, `aura walkforward --real`, 3 runs each):
before (a55e4cf): 6.27 / 6.18 / 6.18 s
after: 4.52 / 4.49 / 4.45 s (~ -27% wall clock)
Suite: cargo test --workspace green (0 failed); clippy clean.
Decision log: #256 comments (fork rationale incl. the StageFlow seam and
the slot-guard exception).
The second risk-regime variant vol_tf{period_minutes, length, k}
computes the vol estimator over completed time buckets: an H1 signal
gets an H1-matched stop in the research matrix instead of a hand-scaled
minute stop (the issue's k-inflation workaround retires). Additive
externally-tagged serde (stored Vol documents keep their content ids);
the executor arm wires the VolTfStop primitive via the builder+bind
chain; validate checks period_minutes/length/k positivity per regime.
The member-manifest round-trip holds for both variants (C1): vol_tf
members stamp stop_period_minutes/stop_length/stop_k and the reproduce
path re-derives the VolTf stop whenever stop_period_minutes is present
— never a silent default-Vol fallback. The Regimes slot label and the
unit notes name the new variant (period_minutes IS the stop's
timescale); glossary, authoring guide, and design ledger record the
second variant. Default regime and sugar paths byte-untouched.
Coverage: node units (rollover-only emission, bucket-delta math, the
constant-|delta| correspondence with the per-cycle regime), executor
fold, serde/validate units, the manifest round-trip unit, and a
hostless two-regime e2e (distinct ordinals; vol_tf members stamp their
timescale, vol members do not).
closes#262
Each cost component's knob (cost_per_trade / slip_vol_mult /
carry_per_cycle) now accepts one number for every cell — today's form,
byte-identical on the wire so stored documents keep their content ids
(CostValue's custom serde, the Axis precedent) — or an instrument-keyed
map resolved per cell at member construction. A mixed-scale matrix
(GER40 ~2e4, EURUSD ~1.1) keeps consistent cost units in ONE document:
the N-copies workaround retires and generalize-under-constant-costs
becomes expressible again.
Validation is strict both ways at the intrinsic tier: a map missing a
campaign instrument, or naming one the campaign does not list, refuses
at validate with prose naming the component and instruments (no silent
default — a partial map would reproduce exactly the silent unit
inconsistency the issue reports). The instrument threads through
cost_knob/cost_nodes_for/run_blueprint_member from the cell (run side
and persist re-run side receive the same value — the C1 drift-alarm
lockstep); the manifest stamps the resolved per-cell value under the
unchanged cost[k].<knob> key, and the sugar/reproduce paths pass an
inert instrument (their specs are scalar by construction). Fork
decisions and the discarded first attempt: issue #260 comments.
New coverage: serde round-trips (map form; scalar stays bare), validate
cross-check units, both prose directions pinned, and two hostless e2es
over the synthetic SYMA+SYMB archive — per-cell resolution visible in
the member manifests, and the validate refusal.
closes#260
Cycle-close audit over the /boss batch 84e1075..bdafbde (#258#259#264#266#247#265#269). Architect verdict: substance clean — C1
byte-identity (conduit serde-skipped, pinned floats verbatim, uncosted
bit-identical), C10 one-home-for-cost (all four conduit consumers read
the already-netted series), C18 (conduit off the wire, --version and
manifest single-sourced) all hold. Two medium debt items, both fixed
here rather than carried:
- render_bind_error's catch-all Debug-framed the remaining BindError
variants, making #269's no-identifier-on-stderr claim argv-gated
rather than structural. The match is now exhaustive: DuplicateBinding,
EmptyAxis, EmptyRange render as axis-usage prose; a Compile fault — a
blueprint defect, not a usage error — renders as a prose frame with
the compile detail explicitly labelled internal (per-variant
CompileError prose belongs to the graph-build surface, not this
boundary). Unit tests pin the arms.
- The #265 knob-unit notes covered constant and vol_slippage but not
carry.carry_per_cycle, which carries the identical price-unit-vs-R
trap; the note set and its pins now cover all three cost components.
No regression scripts are configured (the architect is the sole gate);
full workspace suite green, clippy clean. Spec and plan working files
discarded per the audit lifecycle.
refs #265 refs #269
The four silently-misreadable cost/risk knobs (cost_per_trade,
slip_vol_mult, vol.length, vol.k) must carry one-line unit/semantics
annotations in campaign introspect --unwired — blocking the price-vs-R
and length-as-timescale misreadings that produce no error today.
refs #265
The OOS bootstrap conduit RMetrics.trade_rs becomes net_trade_rs and
carries the COST-NETTED per-trade R (r − cost_in_r, trade order). Every
conduit consumer is thereby net-when-costed: the monte-carlo pooled-OOS
and per-survivor bootstraps, the walk-forward oos_r pooling, and the
deflation null-max — which previously compared a net observed statistic
against a gross-resampled null whenever a costed campaign selected on
net_expectancy_r. An uncosted run is bit-identical (empty cost stream
⇒ cost 0.0 per trade), so every existing golden pin stays green.
Design: one conduit, no knob — an explicit net: knob would let a costed
campaign silently produce a gross headline again (the exact misreading
of the issue's evidence). Per-member RMetrics scalar fields stay gross;
net_expectancy_r keeps its meaning. Wire shape unchanged (serde(skip)).
The net series is materialized as a separate expression in summarize_r;
the pinned-float expressions (net_sum, the SQN pair, the lockstep
r_metrics_from_rs copies) keep their tokens verbatim. Fork decisions
and rationales: issue #259 comments.
New coverage, both hostless over the synthetic SYMA archive: a costed
campaign twin shifts the pooled-OOS bootstrap (sweep→gate→wf→mc) and
every per-survivor bootstrap (sweep→mc) below its gross sibling, with
the trade population unchanged; a unit test pins the conduit as the
cost-netted series with the empty-stream degeneracy. Existing conduit
tests renamed with the field.
Verified: full workspace suite green (71 result blocks), clippy clean,
zero bare trade_rs tokens remain, ledger conduit prose updated in place.
closes#259
Every hand-rolled test-sandbox helper keyed its directory on
std::process::id() under env::temp_dir(), so the pre-create wipe never
matched a prior run's path: one directory stranded per helper site per
test-binary invocation, >50k dirs / 15.5 GiB on the 16 GiB tmpfs by
2026-07-13.
The remedy extends commit 84e1075's knob-lab pattern to every site:
fixed, tag-keyed, PID-FREE names under the build-tree tmp anchor, each
site keeping its pre-create remove_dir_all — which now genuinely
reclaims the previous run. Integration/bench targets use
env!("CARGO_TARGET_TMPDIR"); src-internal unit-test helpers (cargo sets
that var only for integration/bench targets) derive the same anchor
from env!("CARGO_MANIFEST_DIR") + ../../target/tmp. Residue is bounded
to one directory per site, on disk instead of the tmpfs, per checkout.
One deliberate exception: project_new.rs's tmp() stays under
env::temp_dir() because two of its tests (new_outside_a_work_tree_*)
need a base genuinely detached from any git work tree, and target/
lives inside the checkout's work tree. Its name carries a per-checkout
hash discriminator instead, so concurrently running checkouts (which
share /tmp) cannot race on the fixed name while each run still
reclaims its predecessor.
tempfile-RAII was considered and rejected (issue #258 decision log):
it cannot cover the process-lifetime OnceLock scaffolds (statics never
drop), adds a dependency, and still strands kill-leftovers on the
tmpfs.
Verified: full workspace suite green (incl. the new RED regression
test temp_cwd_sandbox_does_not_leak_under_env_temp_dir), clippy clean,
sandboxes observed under target/tmp with stable names across runs.
Stale pre-fix PID-keyed dirs in /tmp are not swept by this change and
age out only by manual cleanup.
closes#258
Every E2E test that mutated the shared demo-project fixture store now
mints its own tempdir project whose 2-line Aura.toml points at the
once-built fixture crate by absolute [nodes] pointer; the runs/ store
follows the project root, so no two tests contend and libtest's
default thread parallelism applies. project_load keeps driving the
fixture directly (the suite's only proof of the inline-crate routing
arm); its single store-mutating test is that binary's only one, so no
lock is needed there either. This also closes the documented
cross-process race window (#223): there is no shared mutable store
left to race on.
Measured on the data-full host: cli_run 211s -> 57.2s (140 tests),
research_docs 21.1s -> 2.1s (73 tests), project_load green, clippy
clean.
refs #250
The references to the relocated _open blueprints follow the plan's
two-way split: tests that only need a sweepable blueprint sweep the
closed twins via bound-override axes (#246) with their axis strings
byte-unchanged (the wrap prefix is the blueprint's internal name,
identical across twins); the 12 tests that genuinely exercise
open-param semantics (run/mc closed-guard refusals, subset-axes
MissingKnob, --list-axes open/bound line mix, gang dissolution and
campaign paths) read the fixtures under tests/fixtures/. The
research_docs/project_load store seeds sweep the closed twin (campaign
axis references resolve via the #246 reopen path); the INDEX.md history
note records the relocation.
Two deviations from the plan, both verified against the tree: doc
comments referencing the bare filename (no examples/ prefix) escaped
the global path swap and were reworded to stay truthful; the seed
variable open_bp was renamed closed_bp (the plan kept it, but the
rename touches only the binding and its uses and removes a
misdescription).
Verification: cargo test -p aura-cli --test cli_run (140 passed, 0
failed, real data exercised); --test research_docs; --test
project_load; the four byte-pinned exact grades unchanged; grep-clean
for examples/r_*_open.json across crates+docs; full workspace suite
green.
built_project/project_lock/ScratchPath/ScratchGuard move from their three
per-binary copies (cli_run.rs, research_docs.rs, project_load.rs's
built_fixture near-copy) into tests/common/mod.rs; project_load's one
shared-store-mutating test now takes the lock it previously lacked. The
module doc states the boundary plainly: the Mutex serializes threads
within one process only — process-parallel runners still race on the
shared fixture store (documented, dormant under cargo test's sequential
binaries; see the issue thread for the observed process-parallel repro).
closes#223
The closing proof: a GER40 campaign with a constant+vol_slippage cost
block runs to exit 0; every member's net_expectancy_r < expectancy_r
(costs bite), the same doc without the cost block reproduces today's
exact gross numbers (golden parity), net_r_equity.json persists on
request, and the costed family reproduces bit-identically. One plan
literal corrected in place: the member-line filter exact-matches the
line shape so the campaign_run summary record (which nests a family_id)
no longer false-matches.
Verified: e2e green on the real archive, full workspace suite green,
clippy -D warnings clean.
closes#234
TapChannel gains Net; tap_channel routes the full closed vocabulary
(producibility moved to the caller); the trace path drains the sixth
channel and persists the cost-adjusted net curve. A cost-less document
requesting net_r_equity keeps a skip notice, reworded to name the
remedy (add a cost block) — not a refusal. Two deliberate pin moves:
the absence pin re-pins to routed, the skip-notice e2e re-pins to the
reworded form. Post-review: positive write-path e2e added (a costed
GER40 campaign lands net_r_equity.json with the curve differing from
r_equity by the accrued costs).
Verified: routing pin + positive e2e green (real archive), full
workspace suite green, clippy -D warnings clean.
refs #234
cost_nodes_for (beside stop_rule_for_regime) is the one CostSpec ->
bound-builder binding — every component fully bound, so the wrapped
param space stays cost-invariant. run_blueprint_member joins the cost
rows into summarize_r (member metrics genuinely net) and stamps the
cost components into the manifest params; CliMemberRunner threads the
doc's cost. The re-run sides re-derive the model from the manifest:
reproduce via cost_specs_from_params (the #233 stop pattern — a costed
family reproduces bit-identically) and persist_campaign_traces binds
the same model so the C1 drift alarm compares like with like.
Hand-computed pin: a constant cost model yields
net_expectancy_r == expectancy_r - cost_per_trade/|entry-stop| on a
synthetic member.
Verified: full workspace suite green, clippy -D warnings clean; in-loop
spec + quality gates passed per task.
refs #234
DataSection gains an additive bindings block (role -> column; serde
default + skip-if-empty, so binding-less documents keep their content
ids — pinned). Validation is two-tier along the existing line: the
intrinsic tier checks binding VALUES against the column vocabulary
(DocFault::UnknownBindingColumn, alias-annotated display register); the
resolver tier checks binding KEYS against the loaded strategies'
input_roles() (RefFault::BindingRoleUnknown — a key must name a role of
at least one campaign strategy). A cross-surface pin keeps the doc-tier
vocabulary and the CLI binding module from drifting.
CliMemberRunner threads the campaign overrides into resolve_binding on
BOTH halves (column opening and wrap), keeping the C1 drift alarm
comparing like-with-like; the verb sugar passes no bindings (name
defaults rule).
Proof: an archive-gated campaign e2e rebinds price -> open and yields
different realized metrics from the close-bound run; refusal prose
exact-pinned at both tiers.
Verified: full workspace suite green, clippy -D warnings clean;
independent quality review, all findings repaired.
refs #231
The campaign walk_forward stage picked its per-window in-sample winner by
optimize_deflated (argmax) unconditionally, dropping the stage's `select`
field, and both the preflight and the `walkforward` verb's `--real` campaign
sub-branch refused plateau:* with a forward-pointer here (the #210 decision-4
deferral; the synthetic non-`--real` sub-branch has accepted plateau since
#173). Honour plateau selection when no gate precedes the wf stage.
- Preflight: the plateau-in-wf refusal is narrowed to gate-preceded stages.
With no preceding gate the survivors are the full sweep grid, so the
parameter lattice (grid.axis_lens) is exact and optimize_plateau is sound; a
gate filters survivors below the grid, breaking the lattice, so plateau after
any gate stays refused. The refusal is static — document validity must be
data-independent (a gate's filtering depends on the data) — and keeps the
existing "a gated survivor subset has no parameter lattice" message, which now
describes exactly the only case it fires.
- Executor: run_walk_forward_stage threads the per-cell grid.axis_lens and picks
each window's in-sample winner via select_sweep_winner — the same dispatch the
sweep stage uses. (Argmax, deflate=true) is byte-identical to the old
optimize_deflated call, so the argmax grade and content-id pins are unchanged;
the plateau arms read axis_lens and ignore deflate.
- CLI: the walkforward verb threads --select through translate_walkforward /
run_walkforward_sugar (a walkforward-local param, not the shared four-verb
SugarInvocation), mapping the parsed Selection to the research SelectRule via
select_rule_of; the refusal and its select_is_plateau helper are deleted.
Scope is walkforward-only — the mc verb's wf-stage select is untouched.
The plateau:worst grade anchor (stitched_total_pips = -9683776.67) is distinct
from the argmax anchor (-10398606.67), proving the plateau path genuinely
changes winner selection rather than falling back to argmax. A gate-free plateau
process validates clean while a gate-preceded one is refused (a new
campaign-validate e2e). Argmax defaults stay byte-identical.
closes#215
closes#216
The remaining introspection e2e pins: the bare-{} envelope probe (the
issue's exact reproduction) lists the optional risk slot; a bound
(non-empty) risk list closes it (no open slots); an EXPLICIT empty
'risk': [] still counts as open (pins the non-emptiness branch, which an
is_none()-based probe would get wrong); the --block std::risk assert is
sharpened to pin the optional rendering and the block doc line (the old
bare contains("risk") matched std::risk trivially).
Docs: the authoring guide's bare-{} transcript gains the risk line, the
§3 worked example gains a two-regime risk section (heading updated), a
prose paragraph explains the structural risk axis (stop defines R;
absent = the same default vol regime the verbs bind), and both validate
transcripts show the new matrix counts. The glossary gains the
'risk regime' entry (canonical term; avoid: stop regime, risk section).
Verified: cargo test --workspace 62 result groups / 0 failures; clippy
-D warnings clean; cargo doc 0 warnings; the guide's transcript strings
grep-match the shipped hint/summary bytes (1/2/1).
The structural risk-regime axis worked end to end but was invisible on the
public surface (refs #216): the --unwired probe swept only required slots
(risk is #[serde(default)]), no vocabulary block described RiskRegime, and
the validate summary counted axes/windows but no regimes or cells.
- open_slots_campaign gains its one optional-slot probe: a risk-less (or
empty/non-array) document lists
'open slot: risk (optional, list of stop regimes { vol: { length, k } };
absent = one default regime)'; a bound list closes the slot. Never a
required flip — content-id parity of risk-less documents is load-bearing.
- CAMPAIGN_SECTIONS gains std::risk (SlotKind::Regimes, required: false) so
--vocabulary lists it and --block std::risk describes the regime shape.
- The intrinsic validate summary now reports the realized matrix, mirroring
the exec cross-product exactly (strategies x instruments x windows x
regimes; empty risk = one default cell, marked '(default)'). Points stay
a grid property, deliberately not a cell factor.
Sanctioned migrations (recorded on #216): the two unit pins encoding the old
invisibility — open_slots_report_partial_documents_by_path now pins exactly
one open slot on the complete risk-less fixture (doubling as the probe's
unit pin; fixture bytes unchanged), and the presentation-hint test migrates
from full-vector equality to locating its entry (its purpose is the hint
text, not the vector length).
Verified: cargo test -p aura-research (29/0), -p aura-cli --test
research_docs (60/0), clippy -D warnings clean.
Two RED tests pinning the lossy record boundary: CellRealization carries
no regime, so persist_campaign_traces' drift-alarm re-run hardcodes the
default vol-stop and false-fails C1 for any campaign with a non-default
risk regime + persist taps (facet A), and campaign_cell_key has no regime
discriminator so two regimes' traces would collide in one dir (facet B,
masked by A's refusal today).
refs #219, refs #212
Cut 1b of the hard-wired demo retirement — the removal half. With the proven
example shipped in cut 1, delete the r-sma builder and its whole transitive
closure so no production function constructs the r-sma topology any more; it
exists only as data (crates/aura-cli/examples/r_sma{,_open}.json).
Deleted: sma_signal (the builder), r_sma_graph, run_r_sma (aura run --harness
r-sma), r_sma_sweep_family (aura sweep --strategy r-sma), the synthetic inline
r-sma walkforward/mc machinery (r_sma_space/r_sma_sweep_over/run_oos_r/
run_mc_r_bootstrap/mc_r_bootstrap_report + the walkforward_family RSma arm), both
fixture emitters, the round-trip + cut-1 byte-fidelity tests, the R_SMA_FAST/
SLOW/BIAS_SCALE consts, and the Strategy::RSma / HarnessKind::RSma / McArgs::RealR
variants. The three dissolved-verb synth sites (generalize/walkforward/mc
--strategy r-sma --real) now embed the shipped open example via include_str!
instead of building it — byte-safe, guarded by the unchanged dissolved-verb
real-archive goldens. The two now-redundant fixtures move to the examples
(deleted, ~40 readers repointed). The --cost-* flags on `aura run` retire with
the harness (their sole consumer was run_r_sma; cost-model coverage survives
engine/std/composites-level).
The retired demo surfaces (--harness r-sma, sweep/walkforward/mc --strategy r-sma
inline) have working data successors: aura run examples/r_sma.json, aura sweep
examples/r_sma_open.json --axis …, and the --strategy r-sma --real campaign path.
Deviation from the plan, noted: StopArm collapsed to StopRule in wrap_r's
signature (its only other variant, Open, died with r_sma_graph) — cleaner than
the planned vestigial single-variant enum; behaviour-preserving, touching
campaign_run.rs and the r-breakout/r-meanrev call sites.
Follow-up (not this cut): wrap_r's cost-node-building branch is now unreachable
(nothing passes Some(CostConfig) after run_r_sma's deletion) yet clippy-clean —
a latent cleanup for when the inline cost surface fully retires.
The stage1-r param-suffix remapping (r_sma_friendly_name + the FAST/SLOW/
STOP_LENGTH/STOP_K_SUFFIX consts) went dead and was removed, closing #167 by
deletion.
Verification (own, not the workflow's report): full `cargo test --workspace`
green; `cargo clippy --workspace --all-targets -- -D warnings` clean, no
dead-code residue; the cut-1 grade anchor (shipped_r_sma_example_reproduces_the_
builtin_grade) and the dissolved-verb real goldens (generalize/walkforward/mc
_dissolves_through_the_campaign_path) stay byte-identical. A first workflow
attempt was discarded after a subagent's `git checkout -- main.rs` recovery
silently reverted earlier tasks' edits (filed on the plugin tracker as
Brummel/Skills#23); this landing is the hardened re-run, verified by hand.
closes#167
refs #159
The runtime-binding slice — the regime's stop now actually reaches the graph and
the record:
- `StopArm { Open, Bound(StopRule) }` replaces `wrap_r`'s `stop_open: bool` so a
bound stop carries its params; all call sites threaded (the r-sma sweep path
stays Open, every bound site passes the resolved rule).
- `run_blueprint_member` takes a `stop: StopRule` and stamps `stop_length` /
`stop_k` into the member manifest (the ratified C18 extension — the stop is no
longer invisible on the campaign/blueprint path); its callers thread the
default, and `CliMemberRunner` maps `cell.regime` (None -> baked default,
Some(Vol) -> that vol stop).
- The characterization pin flips from asserting the stop's ABSENCE to requiring
the stamped default; a real two-regime e2e pins that each regime stamps its
own k.
Known deferred gap (concern, documented descope): `persist_campaign_traces`
re-runs the nominee to persist its traces and — since `CellRealization` carries
no regime — binds that re-run to the DEFAULT stop, so a non-default-regime
nominee's persisted trace would reflect the wrong stop. Only the persist path
(non-empty `persist_taps`) is affected; the core run/stamp/generalize path is
correct. Filed as a follow-up. Full workspace suite green.
refs #210
The first slice of the risk-regime structural axis (spec/plan abab1f1):
- aura-research: a serializable `RiskRegime` closed vocabulary (externally
tagged, sole variant `Vol { length, k }` — a content-addressable mirror of
the shipped two-variant StopRule, so a future Fixed regime is additive), and
an optional top-level `CampaignDoc.risk: Vec<RiskRegime>` that absent-
serializes (the `description` precedent) — content-id parity for every
risk-less document.
- validate_campaign refuses a non-positive regime (length < 1 or k <= 0, NaN
included) via a new `DocFault::BadRegime`; the CLI renders it as prose at the
research_docs seam (register/validate paths).
- The three CLI-seam E2E tests pin that a well-formed risk section passes
`campaign validate` and a bad one is refused with prose (exit 1, no Debug
leak) — the field is reachable at the seam a data author drives, not only in
library units.
Adding the field is compile-breaking at every `CampaignDoc { .. }` literal
(serde default does not cover Rust construction), so this slice also threads
`risk: vec![]` into the sites across aura-cli and aura-campaign — the
field-addition's compile-gate completion the plan should have scoped here. The
BadRegime prose reads `risk[i]: ...` (the implementer's terser wording over the
plan's illustrative string; the spec pinned no exact text). Full workspace
suite green.
refs #210
The std::sweep selection triple (metric/select/deflate) becomes ONE
optional group, Option<SweepSelection>, serde-flattened so the wire
form keeps the flat slots: every existing document parses to Some and
its canonical bytes — and content id — are unchanged (golden pin
byte-untouched). A half-populated group is refused by the schema-strict
parser (all-or-nothing); a bare {"block":"std::sweep"} is the
selection-free sweep.
Campaign side: preflight permits a selection-free sweep only as the
pipeline's terminal stage (new ExecFault::SelectionFreeSweepNotTerminal,
prose in aura-cli's exhaustive exec_fault_prose); the executor's
selection-free arm appends the family exactly as before but records no
StageSelection and produces no nominee (recording one would fabricate
intent — #210 fork decision 1).
Introspection follows: the sweep block's schema table marks the group
optional; CLI introspection pins updated. New tests: group-or-nothing
refusals, selection-free round-trip + distinct content id, terminal-only
preflight accept/refuse, selection-free execute realization.
refs #210
Two green pins the sweep-dissolution cycle rests on (refs #210):
- sweep_real_blueprint_member_lines_pin_the_inline_contract: the inline
real-data blueprint sweep prints one member line per grid point in
odometer axis order, carries the swept bindings in manifest.params,
persists exactly one Sweep family — and never stamps an instrument
key (RunManifest::instrument stays None on this path).
- campaign_run_real_e2e_sweep_gate_walkforward (extended): every
family_table member line emitted by the real campaign member-runner
path is stamped with the instrument under test.
Together they pin the one behavioural delta between the two paths (the
additive instrument stamp) that the dissolution's parity contract is
phrased against.
The --unwired help line names the whole-envelope starting move (start
from a bare {}), closing the 0106-F1 discoverability gap the narrowed
issue kept open; the wrong-kind refusal names the exact key a blind
author must fix ('the "kind" key must be "process"') instead of
describing the mismatch abstractly. The third original item (block-ids
vs field names in --vocabulary) is sufficiently served by the
envelope probe per the issue's narrowing comment.
RED-first (tdd-author handoff): both pins observed failing on the old
help text / prose.
Gates: workspace 1043/0, clippy -D warnings clean.
closes#193
The deferral line dies; persist_taps is honored: after execute()
returns with a claimed trace_name, the CLI re-runs each cell's nominee
once in non-reduce mode (all four channels drained; windowed to the
nominee manifest's own ns bounds; bound via the shipped
point_from_params inverse against the wrapped probe space), asserts
METRICS equality against the recorded nominee (the C1 drift alarm —
hard refusal on divergence), and writes the requested-and-producible
taps through the existing TraceStore as
traces/{campaign8}-{run}/{strategy8}-{instrument}-w{n}/{tap}.json
(ensure_name_free once; window ordinal doc-derived by position match).
Loud lines: per-cell no-nominee skip, per-run net_r_equity
unproducible skip (no cost leg in the campaign runner), one summary.
The Task-1(b) seam test tightened to pin the deferral line's absence.
Gated e2e extended: persist_taps [equity, r_equity] over GER40 —
trace_name in the record, both tap files on disk, aura chart reads
the cell back — ran its FULL path on this host (local archive
present), not the skip branch.
Gates: workspace 1041/0, clippy -D warnings clean.
closes#201
Task 1: the tap namespace is a closed vocabulary — tap_vocabulary()
(the wrap convention's four sink names; escalation = new entry or an
authored blueprint sink, never an open node-path namespace) with
DocFault::UnknownTap in validate_campaign (index-addressed, prose
enumerating the vocabulary via the single source), SlotKind::TapKinds
on the persist_taps slot, open-slot hint naming the taps. The 0107
deferral seam test split into its two 0109 pins (unknown tap refuses
at validate; a valid tap passes validate and hits the member-data
seam) — the deferral eprintln itself survives until task 3.
Task 2: CampaignRunRecord.trace_name lands serde-default sparse; the
claim contract holds across the seam — execute stamps the sentinel iff
persist_taps is non-empty, append_campaign_run composes
Some("{campaign8}-{run}") onto the stored line (safe prefix take at
the registry seam), execute mirrors the same derivation onto the
returned copy; None stays None; the hand-seeded byte-identity dump pin
survives (absent -> skipped).
Gates: workspace 1037/0, clippy -D warnings clean.
refs #201
The recorded bootstrap/generalization evidence is no longer write-only:
'aura campaign runs' lists every stored record (campaign id, run
counter, cell count, first cell's stage signature); 'aura campaign
runs <id> [run]' dumps the BARE stored line(s) — the store form,
byte-identical, never the {"campaign_run": ...} run-emit wrapper (the
F10 parsing trap). A lookup, not an action: empty/unmatched notes and
exits 0 (the runs-family convention); outside a project the campaign
family's project refusal.
RED-first (tdd-author handoff): hermetic seed of one hand-written
store line into the fixture project, list + byte-identical dump pinned,
observed failing on clap's unknown-subcommand exit 2.
Gates: workspace 1027/0, clippy -D warnings clean.
closes#206
--metrics now tags the four generalizable names (expectancy_r,
net_expectancy_r, sqn, sqn_normalized) via check_r_metric as the
predicate — the registry's own R-expectancy rule, no fourth roster
site (#190). The GeneralizeNonRMetric refusal names the real rule
with the roster pointer instead of mislabeling R-denominated-but-
unranked metrics (max_r_drawdown is a drawdown in R, not a 'pip
metric').
RED-first (tdd-author handoff): the extended --metrics pins and the
flipped prose pins (unit + seam) observed failing on the old output.
Gates: workspace 1026/0, clippy -D warnings clean.
closes#207
valid == runnable for every pure document/campaign property: inside a
project, once intrinsic + referential pass, validate fetches the
referenced process from the store and runs the data-free
aura_campaign::preflight (promoted to pub) — an executable shape gains
'campaign document valid (executable): pipeline shape and static
guards pass'; a shape/param/metric/instrument fault refuses under
'campaign is not executable:' with the exec_fault_prose detail, exit 1,
before any member runs. Outside a project the skip line is unchanged
(the third tier needs the store like the second).
Collateral the RED author predicted: the referential e2e's OK fixture
(PROCESS_DOC: deflate+plateau sweep, overfit_probability gate) was
intrinsically-valid-but-never-executable — that test now registers an
executable twin (argmax/no-deflate, per-member gate metric); the base
PROCESS_DOC fixture stays untouched for all other consumers.
RED-first (tdd-author handoff): the third-tier pin observed failing on
the two-line validate output.
Gates: workspace 1026/0, clippy -D warnings clean.
closes#205
The retired v1-boundary test is replaced by v2 pins: mc-before-wf
refuses with the executor shape prose (while process validate accepts
the same doc — the tier boundary observable across two commands, added
by the task-2 e2e phase and kept), single-instrument generalize
refuses data-free on the built fixture project, and the two
addressing-mode tests' stale rationale comments now name the
member-data seam they actually hit. PROCESS_BLOCKS doc strings drop
'in v1' (introspection text only — no content id moves). The gated
real-data e2e extends its process with the monte_carlo suffix and
asserts the pooled_oos bootstrap in the final campaign_run line — ran
its full assert path on this host.
Task-5 duplicates the plan had double-drafted (task 2's e2e phase
pre-landed three of its tests) were correctly NOT re-added.
Gates: workspace 1025/0 (fully green again), clippy -D warnings clean.
refs #200
Task 1: RBootstrap and Generalization gain serde derives;
lineage.rs gains StageBootstrap (per_survivor | pooled_oos) and
CampaignGeneralization; StageRealization.bootstrap and
CampaignRunRecord.generalizations land serde-default sparse (a
pre-0108 campaign_runs.jsonl line still parses — pinned), threaded
None/empty through every literal site. No behaviour change.
Task 2: ExecFault::UnsupportedStage is gone; the preflight now admits
the v2 shape sweep [gate]* [wf]? [mc]? [generalize]? via a monotone
rank walk (precise per-pair PipelineShape prose) and statically
refuses: single-instrument generalize (GeneralizeNeedsInstruments),
non-R generalize metrics via check_r_metric (GeneralizeNonRMetric —
no fourth roster site), zero mc params (ZeroBootstrapParam). CLI
prose swapped accordingly. The loop's E2E phase added a tier-boundary
pin: process validate accepts [sweep, mc, walk_forward] while campaign
run refuses it data-free.
KNOWN RED (documented in the plan, flips in task 5):
crates/aura-cli/tests/research_docs.rs::campaign_run_v1_boundary_refuses_mc_process
still pins the retired 'not executable in v1' prose. Interim gates:
registry 57/0, campaign 33/0, cli --bin 111/0, workspace build clean.
refs #200
The 17-name metric vocabulary lists with applicability tags derived
from aura-campaign's existing pub rosters (rankable | gate |
annotation) — no fourth roster site (#190); the mode rides the shared
introspect struct and answers for both doc families. The three
metric refusals (intrinsic unknown-metric, executor unrankable and
gate-not-per-member) now point at the mode, so the
exists-but-not-rankable split (profit_factor is emitted per member yet
not selectable) is no longer trial-and-error.
RED-first (tdd-author handoff): six same-seam pins (roster lines +
tags, one-mode guard incl. the extended usage line, both-families
parity, three refusal pointers) observed failing, then the mode +
hints landed.
The worked-examples half of #197 stays open (docwriter territory
post-stability, per the issue itself).
Gates: workspace 1001/0, clippy -D warnings clean.
refs #197
A campaign draft can now validate-so-far AND report its open ref slots
(0106 fieldtest F4): open_slots_campaign treats an absent ref, {} and
null uniformly as the open slot (strategies[i].ref with the two-arm
hint; process.ref with the content-id-only hint). Draft affordance of
the probe only — the strict parse tier keeps refusing placeholders (a
placeholder is not a valid document form; canonical bytes stay
untouched).
RED-first (tdd-author handoff): library pin + CLI --unwired e2e
observed failing (probe reported 'no open slots' over placeholder
refs), then the two ref predicates routed through ref_slot_is_open.
Gates: workspace 995/0, clippy -D warnings clean.
closes#195
The copyable token is now the valid ref form everywhere: all three
register lines print 'registered <family> <id> (<path>)' and both doc
introspect --content-id modes print the bare 64-hex id (converging on
graph introspect's existing form). CLI FILE-or-id targets (campaign
run, graph introspect --params) strip a leading 'content:' before
resolving — a CLI arg is ephemeral input, leniency costs nothing. Doc
ref FIELDS stay bare-only (two accepted spellings would make
semantically identical docs hash apart — canonical-form byte
stability outranks input leniency); instead the referential not-found
prose appends 'refs use the bare 64-hex id — drop the content: prefix'
when the offending ref carries it.
RED-first (tdd-author handoff): eight same-seam assertions across the
three surfaces observed failing on the prefixed forms, then the five
display sites, two target ladders, and the conditional hint landed.
Gates: workspace 993/0, clippy -D warnings clean.
closes#194
validate == runnable (fieldtest 0107 F7): validate_campaign_refs now
checks the reverse direction — every open param of the resolved
strategy must be bound by some campaign axis — emitting the new
RefFault::ParamNotCovered { strategy, param } with the RAW
param_space() path; ref_fault_prose phrases it ('open param "X" is
bound by no campaign axis'). The executor's every-open-knob-required
rule is thereby mirrored at validate time, and campaign run's
referential gate catches an uncovered knob before any member binds.
The e2e fixture that legitimately under-covered (fast only) gains the
slow.length axis.
Second half, RED-first: the member-bind refusal no longer leaks the
wrapped bind-time path — it names the raw campaign-axis namespace with
the wrapped path as a parenthetical ('open param "threshold" ...
(wrapped path: sma.threshold)').
RED evidence: E0599 on the pinned ParamNotCovered test (tdd-author
handoff), then the flipped bind_axes prose pin observed failing on the
wrapped form.
Gates: workspace 988/0, clippy -D warnings clean.
closes#203
Closes the F5 authoring gap: a campaign's strategy ref + axes are now
authorable from the public surface without running a sweep first.
'aura graph register <blueprint.json>' puts the canonical form into the
content-addressed store (id == topology hash) and prints the stored
path via the pub blueprint_path accessor; 'aura graph introspect
--params <FILE|64-hex-ID>' lists the RAW composite param_space — the
exact namespace campaign axes are validated against (the wrapped
--list-axes namespace stays the sweep-verb view); --content-id gains an
optional FILE that shape-discriminates a blueprint envelope from an
op-list document (stdin behaviour byte-identical without FILE).
is_content_id promoted pub(crate) and shared with graph_construct so
the two FILE-or-id surfaces cannot drift on the id shape.
RED evidence was produced retroactively by the loop (stash-probe: the
5 new tests fail for the expected reasons without the src changes) —
ordering deviation noted, outcome unaffected.
Gates: graph_ 46/0, workspace 987/0, clippy -D warnings clean.
closes#196
refs #198
campaign_run.rs: target resolution (file is register-then-run sugar;
bare 64-hex is the canonical id — #198 decision 1), project gate before
any store write, zero-fault referential gate, process fetch + v1
preflight, then aura_campaign::execute with the CLI MemberRunner over
the shipped loaded-blueprint convention: per-member blueprint reload,
reduce-mode wrap, unique suffix-join of raw campaign axis names onto
the wrapped param_space, windowed real data via the shipped ms->ns
source seam (absent archive/zero-bar windows -> NoData). Emission:
family_table / selection_report lines gated on the doc's emit list
(names debug_asserted against emit_vocabulary), the campaign_run
record line always, persist_taps deferred LOUDLY on stderr before
execution (F7 lesson), zero-survivor cells noted on stderr with exit 0.
exec_fault_prose/member_fault_prose keep the Debug-free house seam.
Seam tests: 8 campaign_run_* e2e tests incl. outside-project, bogus
target, unknown id, v1-boundary (mc process registers fine, run
refuses), persist_taps ordering, and the gated real-data
sweep->gate->walkforward e2e — which ran its full assert path on this
host (local GER40 2024-09 archive) rather than the data-less skip.
Also: exec.rs campaign-id guard tightened to lowercase hex, matching
is_content_id and the store's self-keyed form (review nit).
Gates: workspace 975/0, clippy -D warnings clean.
refs #198
folds/in_sample_bars/out_of_sample_bars mapped to nothing
WindowRoller::new accepts; the block now carries in_sample_ms/
out_of_sample_ms/step_ms/mode (WfMode: rolling|anchored) — the
roller's three lengths in the campaign window's own epoch-ms unit
plus both shipped RollModes (#198 decision 2). New intrinsic fault
ZeroWalkForwardLength (path-addressed) refuses zero lengths at the
doc tier, earlier than the roller's NonPositiveLength. Golden
canonical pin, PROCESS_FIXTURE, introspection tables, and the
aura-cli prose/fixture twins moved together; fieldtest corpus
untouched (historical record — stored walk_forward docs get new
content ids by design).
Gates: aura-research 14/14, aura-cli green, workspace build clean.
refs #198
Architect drift review (holds: C14 headless authoring, C16/C17 no-DSL with
aura-research a true leaf, content-id move behaviour-preserving per the
untouched id goldens). Resolution per item:
- fix (docs): C18 gains the cycle-0106 realization note (processes/ +
campaigns/ stores, document canonical form, the content-id primitive's
library home, the referential tier, no-executor status); the #109
analysis-meta-level open thread records the artifact half shipped v1 and
what stays open (executor question, verb dissolution 'once it carries',
the remaining #188 amendment package). Glossary: new campaign document +
process document entries; content id generalized to all three artifact
classes; experiment marked superseded by the campaign document (#188
re-cut of the builder-API thread).
- fix (code): the --unwired guide advertised an identity_id process ref
that validate_campaign refuses — the process.ref open-slot hint now says
content-id-only (strategy refs keep both); lib + seam pins updated.
- filed forward: #190 (metric-vocabulary single-sourcing, the 0105 roster
drift class), #191 (identity-ref store scan is O(store), performance
only).
- carry-on: regression gates green by hand — cargo test --workspace 916/0,
clippy -D warnings clean, cargo doc --no-deps 0 warnings; no baseline to
ratify (project has no metric baseline scripts).
Cycle 0106 spec and plan retired per convention (git rm); durable record
is the ledger notes above plus #188/#189.
refs #189
The campaign half of research_docs.rs: validate (intrinsic always;
referential tier when a project env is discovered, with the explicit
skip line outside one), introspect (--vocabulary/--block/--unwired/
--content-id, same exactly-one-of usage guard), register via the
registry's campaign_path accessor, and RefFault prose (Display-free,
unit-pinned). Five binary seam tests (skip line, empty-axis prose +
exit 1, section vocabulary, spec-example open slots, content-addressed
registration) reuse the file's temp_cwd helper — the plan's
tempfile::tempdir() snippet deviated from the crate's zero-extra-dep
test convention and was adapted, disclosed in the loop's report.
The referential CLI branch inside a real loadable project stays
fieldtest territory by the plan's own scope note (project::load needs a
built dylib); its logic is pinned by the aura-registry tests.
Gates: cargo test --workspace 916/0 (885 pre-cycle + 31 new), clippy -D
warnings clean, cargo doc --no-deps 0 warnings. The worked spec example
runs end-to-end headless: author both documents as text, validate with
per-tier report lines, introspect vocabularies and open slots, obtain
stable content ids, register into the store — no Rust compile, no run
executed.
closes#189
New research_docs.rs module: role-addressed process verbs with house-style
fault prose (Display-free enums phrased at the seam), the introspect
exactly-one-of usage guard (exit 2), and content-addressed registration.
Seven binary seam tests pin intrinsic-ok/refusal prose, vocabulary/block/
unwired/content-id introspection, exit codes, and the stored file's
location under the runs root.
Quality-gate finding folded in (and back into the plan): register no
longer re-derives the store path from runs_root — aura-registry now
exposes process_path/campaign_path, the same single mapping its put/get
pair routes through, so the printed path cannot drift from the store
layout.
Verification: research_docs seam tests 7/0, registry 49/0, clippy clean.
refs #189