Surfaced by the measurement-milestone fieldtest: a downstream author
reaching for session anchoring or declared taps had no public statement of
either — the semantics lived only in node source / the design ledger.
SessionFrankfurt / Session output semantics:
- glossary `session node`: corrected — it claimed three streams
(`bars_since_open`, `in_session`, `session_open_ts`), but the shipped node
emits ONE `i64` field `bars_since_open`. Now states the real contract: the
count of completed bar-periods since the local (tz-aware, DST-correct)
session open, close-instant indexed (09:15->1, 09:45->3; pre-open <=0), with
the Frankfurt 09:00 Europe/Berlin open + `period_minutes` knob + the
value-ignored `trigger`.
- authoring-guide: a "Session anchoring: the SessionFrankfurt preset"
subsection — schema (trigger / period_minutes / bars_since_open), the
close-instant multiples, the EqConst gating pattern, DST handling, and the
add-snippet. Verified against `aura graph introspect --node SessionFrankfurt`.
Declared-tap authoring (C27 / the #284 tap op):
- authoring-guide: the `tap` op-table row (seven ops -> eight) + a worked
"declaring a measurement tap" example. The example is verified: piping it
through `aura graph build` emits
`taps:[{"name":"spread","from":{"node":2,"field":0}}]`.
- design ledger op-list (INDEX.md): a `tap` bullet (seven verbs -> eight);
also corrected the `expose` "only verb that keeps `as`" claim — `tap` keeps
it too.
- README op-kind enumeration: adds `tap`.
Docs only; no code change. Both worked examples were run through the built
`aura` binary; the SessionFrankfurt schema was checked against introspect.
closes#285
Declared taps (C27, #282) were authorable only by hand-patching the raw
blueprint envelope's `taps` array, where `node` is a raw interior index —
an index miscount was the expected mistake and the root friction behind the
two `aura run` panics the measurement-milestone fieldtest found.
Add a `tap` op to the construction op-script vocabulary — the exact
output-side twin of `expose`. It names its source wire by the dotted
`"node.field"` identifier the op surface uses everywhere (`split_port` +
`resolve_output_field` + `node_index`), so
`{"op":"tap","from":"fast.value","as":"fast_ma"}` replaces patching
`{"node":14,"field":0}`.
Engine (construction.rs): `Op::Tap`, its `tap()` handler (a verbatim twin
of `expose` — same resolvers, same name-dedup, appending a `Tap` to a new
`taps`/`tap_names` namespace), and `OpError::DuplicateTap` (the twin of
`DuplicateOutput`, stricter than the raw envelope, which enforces no
tap-name uniqueness). `finish()` now threads `.with_taps(self.taps)` into
the built `Composite` — fixing a latent drop #282 left (it built
`.with_gangs` but never `.with_taps`, so op-built composites silently lost
their taps).
CLI (graph_construct.rs): `OpDoc::Tap` (externally tagged, `"as"`-renamed
like `expose`) + the `kind_label` / `From` / `format_op_error` arms (the
last compiler-enumerated by the exhaustive match).
No change to the `Tap` / `FlatTap` / `bind_tap` / `aura run` machinery — the
op-declared tap flows through the existing #282 compile + record path
unchanged.
Verification: 7 new tests green (4 engine: resolve+append, bad-port /
ghost-node refusal, duplicate-name refusal, finish-threads-into-FlatGraph;
1 CLI serde round-trip; 2 CLI E2E over the real `aura graph build` /
`aura run` subprocess — one pinning the exact serialized
`"taps":[{"name":"fast_ma","from":{"node":0,"field":0}}]`, one proving an
op-authored tap round-trips through build -> run -> persisted trace). Full
workspace suite green; clippy clean.
The RED-before-GREEN step was verified by a temporary stash of the
production edits (the `Tap` / `with_taps` machinery pre-existed from #282,
so a fresh RED-only test could not be authored) — the new tests fail
without the edits, pass with them.
Docs (the authoring-guide `tap` row, the ledger op-list `tap` line, and a
worked tap-authoring example) are deliberately out of this commit's scope —
they land in #285, the docs follow-up in this same cycle.
closes#284
Two user-authorable blueprint shapes crashed `aura run` with an internal
.expect instead of the CLI's clean aura: refusal register:
- a tap wire pointing out of range: run_signal_r's compile now handles
CompileError (TapWireOutOfRange) as an aura: refusal + exit 1, naming that
the blueprint does not compile to a runnable harness.
- an exposed output not named bias: a pre-check in dispatch_run (the single
loaded-blueprint call site, before the free-knob axis probe that welds the
hardcoded bias port and would itself panic) refuses with a message telling
the user aura run expects a strategy with a bias output and listing what the
blueprint exposes. The sweep path (run_blueprint_member) is untouched.
Surfaced by the measurement-milestone fieldtest as the sharp edge of raw-index
tap authoring (#284 tracks the name-addressed tap op that removes the miscount
class at the source).
closes#282
Two RED tests pinning that `aura run` refuses cleanly (exit 1 + an aura:
message) rather than panicking on a user-authorable blueprint: a tap wire
pointing out of range (run_signal_r's compile .expect swallows
TapWireOutOfRange into a panic, main.rs:1753) and an exposed output not named
bias (wrap_r's g.build().expect panics on UnknownOutPort from the hardcoded
bias weld, main.rs:1667). Surfaced by the measurement-milestone fieldtest.
refs #282
The measurement milestone overloaded 'tap': beside the closed campaign
persist_taps vocabulary (equity | exposure | r_equity | net_r_equity), the
word now also names a declared tap — a Composite.taps entry a hand-authored
blueprint declares on an interior output wire (C27), the output-side twin of
an input_role, bound run-mode-aware. Record-reality glossary update
reconciling the two senses; a worked authoring example is owed at docwriter
(#285).
Milestone acceptance corpus for 'Measurement as a first-class citizen',
authored as a downstream consumer (public interface only): a gated
rate/mux/counter over the four new cells, a SessionFrankfurt anchor study,
and the origin first-candle-momentum study on real GER40 data — each a
hand-authored blueprint plus one recorded run, zero throwaway Rust. The
three capabilities compose end-to-end and record via declared taps.
Findings routed separately: two aura-run panics-where-refusal-expected
(bad tap wire; non-bias output) to debug; a tap-authoring friction and two
doc gaps to follow-ups.
Architect drift review over 16ca4e1..HEAD: drift-clean but for one low
citation slip, now fixed here — snap_to_nearest_minute's doc cited the
record-then-replay boundary as C6 (firing policy); it is C11. What holds
(architect-confirmed): C27 taps match the contract (pure declaration,
compile-resolve+hoist through the shared lowering remap, caller-built
aura-core-only sink, unbound-is-inert, Tier-1 additive serde); the four #281
cells are C8/C6-clean (<=1 record/eval, warm-up None, When's quiet-None is the
Resample precedent, barrier stall correct); #280's snap respects C11 (node-local
reads only, firing/barrier timestamps untouched, stream verbatim); FlatGraph and
validate_wiring lockstep complete.
Regression (report-only bench): exit 0, all five surfaces fingerprint OK, deltas
within noise on a loaded box — no baseline update, nothing to ratify.
Scaling follow-up filed as #283: the single-run tap drain buffers O(cycles) rows
(the #138 profile) — fine for the bounded acceptance run, to be folded before a
full-history declared-tap study.
Records the tap/binding contract: taps are the output-side twin of input_roles
(C26), resolved and hoisted at compile, bound run-mode-aware via a caller-built
sink (engine stays aura-core-only), recorded on the single run and inert in a
sweep. Documents the deliberate DCE-deferral — build-time elision now,
chain-pruning when DCE (C23) lands — and the unbound-is-inert non-error
asymmetry vs a mandatory input role.
refs #282
The payoff: a hand-authored blueprint's declared taps become recorded output
from one `aura run`. run_signal_r binds each tap (hoisted into flat.taps after
wrap_r nests the signal) to a fresh Recorder before bootstrap, drains the
series after the run, and persists each as a ColumnarTrace via the existing
env.trace_store() — the same trace surface the campaign path feeds, so the
existing chart tooling reads them. Duplicate tap names are refused (the
caller-owned DuplicateBind dedup) before anything persists. A tap-free run
writes no trace store — byte-identical to today. Sweep/reduce leaves taps
unbound (run_blueprint_member untouched). Covered end-to-end: a persisted tap
series, the tap-free no-write guard, and the duplicate-name refusal.
refs #282
The run-mode-aware bind seam. bind_tap attaches a CALLER-BUILT Box<dyn Node>
sink to a declared tap — appending the sink node + an edge from the tap's
(node, field) to its input, before bootstrap so the Kahn sort orders it. The
engine constructs no aura-std type (it takes a Box<dyn Node>), keeping its
production dependency aura-core-only. bind_tap raises UndeclaredTap; duplicate
detection across binds is the caller's (the engine keeps no cross-call state),
mirroring how bind_sources' DuplicateFeed is a supply-level fault. TapBindError
is exported for the CLI. A bound tap records exactly the producer's per-cycle
output through the full public pipeline; an undeclared name is refused without
mutating the graph.
refs #282
The compile core. resolve_tap_wire resolves a blueprint Tap's interior
{node, field} through the lowering table exactly as OutField re-exports and
edges resolve — a leaf to its remapped index (output-arity checked), a nested
composite through its output remap. Taps are terminal (not re-exported through
the boundary): a flat_taps accumulator threads through the lowering recursion,
so an interior composite's taps hoist to the root FlatGraph.taps with remapped
wires — the case the CLI single-run wrapper relies on. validate_wiring gains a
tap leg (threaded at all three call sites incl. GraphSession::finish) and
CompileError::TapWireOutOfRange names a bad wire. Root-resolve, out-of-arity,
and nested-hoist are pinned green.
refs #282
Second slice: the resolved-tap carrier. FlatTap { name, node, field } is the
output-side twin of SourceSpec — its name survives compile, load-bearing for
by-name binding (#275). FlatGraph gains a taps field, threaded (empty) through
every construction site the compiler enumerates. Bootstrap ignores it (taps
materialize as real recorder nodes/edges before bootstrap in a later slice).
Purely additive: the inert-producer pin and full suite stay green.
refs #282
First slice of declared taps: a Tap { name, from: TapWire { node, field } }
is the output-side twin of input_roles — a pure data declaration naming an
interior output wire, no sink endpoint. Composite gains a taps field
(with_taps/taps(), additive) and the serde mirror CompositeData carries it
(Tier-1 additive: no format-version bump, absent-taps documents byte-stable).
Tap/TapWire are re-exported from the crate root beside Role/OutField so
downstream crates can author tap-bearing blueprints.
refs #282
Current-behaviour pin for the declared-taps design: a producer node
with a non-empty output and zero out-edges compiles, bootstraps, and
runs — its per-cycle output silently discarded — observably identical
to the same graph with the producer removed. This is the exact shape
of an unbound declared tap (a study wire left un-recorded in a sweep),
and it establishes that the engine already tolerates it: no DCE is
needed for correctness, because check_ports_connected gates only input
slots and the Kahn sort emits a no-out-edge node as a valid sink. The
build-time-elision soundness the tap mechanism will rely on.
refs #282
Two integration pins for the clock-enable design (#281), at the raw
FlatGraph boot seam. The gated-reducer equivalence is the crux claim:
SMA(When(x, gate), N) equals SMA(N) fed the gate-true subsequence
directly — When's quiet cycles do not advance the reducer's freshness,
so its state moves per forwarded sample, not per cycle. The barrier
stall pins that a quiet When member keeps its whole Barrier(0) group
from firing that cycle. An in-graph AboveGate fixture derives the Bool
gate co-fresh with the value leg.
refs #281
Second half of the #281 clock-enable set. CumSum is the generic
run-length accumulator with O(1) Neumaier-compensated state — the
branching Neumaier variant, not Sma's plain Kahan, so it stays exact
when a large running total absorbs a tiny sample (the standalone
accumulator has no windowed-magnitude guarantee; module doc explains
why the helper is not reused). When is the clock-enable driver: forwards
value iff gate, else a quiet cycle (eval->None), so every stateful
reducer composes gated unmodified — SMA(When(x,g),N) is the gated SMA.
Rosters CumSum and When (both count pins 31->33) and ships two engine
E2E fixtures: When forwarding/quiet-downstream through the real
GraphBuilder->compile->run seam, and a quiet When leg suppressing a
whole Resample Barrier(0) group (the deliberate stall semantics).
refs #281
First half of the #281 clock-enable set: Sign promotes the private
sign0() semantics to vocabulary (strict comparisons; NaN and -0.0
fall to 0.0), Select is the stateless 2:1 mux that consumes Bool
back into the F64 plane. Both zero-arg, Firing::Any, rostered
(count pins 29→31); an engine E2E drives both through the real
GraphBuilder→compile→run seam.
refs #281
Provider M1 stamps carry sub-second jitter around minute boundaries
(~55% of days stamp the 09:00 Berlin bar at 08:59:59.xxx), so any
consumer dividing or truncating the raw stamp mis-buckets boundary
bars: Resample and VolTfStop fold the bar into the previous bucket
instead of rolling over, Session demotes the first in-session bar to
at-open.
Fix per the decision logged on the issue: one shared
Timestamp::snap_to_nearest_minute in aura-core (round-half-up,
rem_euclid so negative epochs snap correctly), applied consumer-side
at the three membership computations. The recorded stream stays
byte-verbatim — no ingestion rewrite, record-then-replay untouched;
an ingestion-time rewrite was rejected because it would hide the raw
time axis from every consumer and could reorder the k-way merge
around boundaries.
Verified: the three formerly-RED tests pass individually; full
workspace suite green; clippy clean.
closes#280
Three RED tests pinning the same root cause at its three node sites:
Resample and VolTfStop bucket by raw integer division of the jittered
stamp (fold-back instead of rollover at bucket boundaries); Session
derives wall-clock minutes from the raw stamp (first in-session bar
demoted to at-open). A bar stamped sub-second before its nominal
minute must be bucketed by the nominal minute.
refs #280
Caught by the harness itself on its first post-commit run: the aura run
record line embeds manifest.commit — the binary's compile-time build sha
(crates/aura-cli/build.rs, the invariant-8 audit trail) — so hashing the raw
line flipped the fingerprint on every rebuild at a new commit and reported a
phantom correctness change (exit 1) at each cycle close. The rebase onto the
#191 main exposed it; nothing in #191 touched the record.
RED-first: run_line_fingerprint_is_invariant_across_build_commits pins the
symptom (two lines differing only in manifest.commit must hash identically);
the fix extracts run_line_fingerprint, which parses the line, blanks that
one volatile field, re-serializes (serde_json maps are sorted, so the bytes
are deterministic), and hashes. A missing manifest.commit is a hard error,
so a future record-schema move fails loudly instead of silently passing.
The cli_fixed_cost baseline is re-pinned under the new definition — ratify:
the fingerprint DEFINITION changed (provenance excluded, results still
covered, pinned by run_line_fingerprint_still_detects_result_changes); no
measured behaviour of aura moved.
refs #251
Architect drift review (cycle bfb8648..0389399): no domain-invariant breach.
What holds: aura-bench is bin-only dev tooling with zero production reverse
deps (invariant 8/C13); every committed surface runs on seeded synthetic
data in scratch temp projects (invariant 9); run_reps enforces cross-rep
fingerprint identity and the campaign fingerprint carries integer ordinals
across the child boundary, consistent with C1 and its cross-command ULP
carve-out; zip/clap per-case dependency notes in place (C16), and the ledger
correctly gains no entry — tooling lives in the project facts + README.
Three drift items, resolved in this commit:
- [medium] the winner_fingerprint doc claimed walk-forward regressions fail
the bench; the walk-forward stage in fact emits an empty realization
record, so coverage is only indirect (via the bootstrap trade count). Doc
now states the gap honestly; the engine-side fix is parked on #279.
- [low] the fingerprint's lenient JSON parse would silently erode on a
registry-schema rename: a new lockstep test builds a real
CampaignRunRecord from the aura-registry/aura-analysis/aura-engine types
(new dev-deps) and asserts the extracted fingerprint, so a rename breaks
this crate's tests instead.
- [low] LIBRARY_SURFACES was hand-synced with measure_surface: a new test
pins that every listed surface measures without a binary path.
Regression gates on the closing tree: cargo test --workspace 1379 green,
clippy -D warnings clean, doc build clean, aura-bench run exit 0 with five
'fingerprint OK' at <=2% drift. No baseline moved in this commit — the
bench baselines were pinned and verified in the feature commit.
refs #251
Tasks 7-8 of the bench-harness plan plus the orchestrator's sizing pass —
this completes the phase-1 harness (closes#251).
Driver: run [--surface|--quick|--reps|--out] / pin over five surfaces,
report-only comparison (drift NOTICE at >=10%, never a failure), fingerprint
mismatch exits 1 with a baseline-vs-measured block, infra errors exit 2,
debug builds refuse to measure. The Bench project-facts line and the crate
README (discipline: quiet box, warmup+median, deliberate re-pins) land with
it.
Workload sizing, measured then corrected: the release engine outran the
plan's guesses by ~100x, leaving sub-second child walls where spawn jitter
reaches the 10% threshold and flaps the report. Full-size workloads now land
in whole seconds — engine 10M bars (~0.7s), ingest 24 months x 20 fresh
drains (~0.4s), campaigns on a 24-month archive with a ~22-month window and
a 5x5 member grid (sweep ~1.4s, heavy ~5.5s), fixed-cost as in-rep medians
over spawn batches (20x help, 10x run). Quick mode keeps the small
E2E-fixture shape.
The campaign fingerprint folds all three realization layers — sweep winner,
gate survivor ordinals, bootstrap trade counts (pooled or per-survivor sum)
— so a wrong-result regression in any stage fails the bench, not just a
wrong sweep winner. Baselines pinned on this host (commit-stamped), verified
by a follow-up run: five fingerprints OK at <=2% drift under load.
Verification: cargo test --workspace 1379 green (bench tests all quick-mode;
the release-binary E2Es are #[ignore]d behind -- --ignored per the
suite-wallclock discipline), clippy -D warnings clean, doc build clean.
Tasks 5-6 of the bench-harness plan (refs #251). Host context probes
(hostname/nproc/loadavg/commit/date, all best-effort), the child runner
(release aura binary resolved from cargo's compiler-artifact JSON messages;
/usr/bin/time -v wraps the child for CPU% + peak RSS, wall measured
in-process), and the campaign surfaces:
- a scratch data-only project per repetition ([paths] only — no [nodes]; the
std-vocabulary fallback covers the SMA blueprints), synthetic archives for
4 instruments, blueprints seeded via tiny synthetic sweeps, process docs
registered through the real CLI;
- campaign_sweep (sweep-only process — narrow per-cell fan-out) and
campaign_heavy (sweep -> gate -> walk-forward -> 1000-resample bootstrap —
wide fan-out): one campaign shape misreads regressions in the other;
- fingerprint = winner ordinals parsed from the always-on final
{"campaign_run":...} stdout line.
The full campaign-sweep determinism E2E (real release binary, real scratch
runs) is #[ignore]d out of the default suite — it costs a release build plus
child campaign runs, which the suite-wallclock discipline forbids on every
cargo test; run it via `cargo test -p aura-bench -- --ignored`. Held
quality note: the heavy/quick process docs stay two verbatim consts (plan
prescription; a format! builder would trade the uniform &str contract for
cosmetic dedup).
Tasks 3-4 of the bench-harness plan (refs #251). The surface framework
(1 discarded warmup + N measured reps, median per metric, cross-rep
fingerprint identity enforced — a surface disagreeing with itself is
nondeterminism, never averaged away) and the two library surfaces:
- engine_throughput: 1M-bar seeded walk through an SMA-cross bias graph
(GraphBuilder -> compile -> bootstrap -> VecSource -> run), fingerprint =
the SeriesReducer finalize row.
- ingest_throughput: synthetic 48-byte M1 archive drained via open_columns
with a FRESH DataServer per rep, so the measured drain pays zip inflate +
parse instead of the in-memory FileCache; fingerprint = count + bit-XOR
of the close column.
Quick-mode determinism is pinned in-suite for both surfaces (C1 — the
harness's premise as currently-green tests); full-size numbers wait for the
release driver glue.
Tasks 1-2 of the bench-harness plan (refs #251): the new dev-only workspace
bin crate with the report-only comparison core (BaselineDoc serde, per-metric
relative drift, 10% NOTICE threshold, fingerprint-equality exit code) and the
deterministic synthetic inputs (seeded LCG price walk, the proven 48-byte
Delphi-record M1 zip archive writer, self-deleting scratch dirs).
The archive writer is deliberately a third copy of the test-fixture packer
(aura-cli tests / aura-ingest each carry one), byte-compatible by
construction and pinned here by a round-trip test through the real ingest
reader. The binary refuses debug builds (exit 2) — wall-clock numbers from a
debug profile measure the profile, not the code — pinned by an E2E spawn
test. Temporary #![allow(dead_code)] markers cover the not-yet-wired core
until the CLI glue lands; the surfaces and driver follow.
Cycle #191 (identity-ref index) closes drift-resolved. Architect review
held: results stay scan-identical via verify-on-hit (the same-identity-
twin choice was order-unspecified before and stays unspecified in kind);
zero caller/write-path/engine changes; the #276 append-lock discipline
correctly extended to the fourth JSONL path. No regression scripts are
configured; the architect review is the gate.
Drift items and resolution:
- C18 lacked a realization note for the new persistent sidecar store and
its index-first/verify-on-hit/repair-walk mechanism, and the "identity
refs by store scan" line was stale -> this commit adds the #191
realization note and dates the scan wording (fix, ledger-only).
- The repair pass grew the sidecar without bound while same-identity
twins coexisted (per-entry comparison against a mid-walk-stale
snapshot) -> fixed in-cycle via RED pin 35865bb + collect-then-diff-
append fix aa94f51; suite 1347/0, clippy clean.
Cycle spec and plan (git-ignored working files) removed per lifecycle.
refs #191
Drive the RED twin-store pin green: the repair walk now collects its
identity->content mappings into a local last-wins map and appends after
the walk only the entries whose final walk value differs from the
pre-walk snapshot (collect-then-diff-append, decision on #191). The old
per-entry comparison against the mid-walk-stale snapshot appended one
alternating line per scan-triggering lookup while same-identity twins
coexisted — the sidecar now converges to zero growth there too. Lookup
answers (first read_dir match) and verify-on-hit untouched.
Verification: workspace suite 1347 passed / 0 failed; clippy -D
warnings clean.
refs #191
Executable spec for the audit follow-up on the identity-index cycle:
once one full repair walk has run over a same-identity-twin store (two
blueprints sharing one identity id, distinct content ids), a further
scan-triggering lookup must leave blueprint_identity_index.jsonl's line
count unchanged while the twin identity still resolves. RED against the
current per-entry snapshot comparison (appends one alternating line per
scan-triggering lookup while twins coexist).
refs #191
Replace the per-call O(store) scan in find_blueprint_by_identity with a
persistent identity-id -> content-id sidecar index
(blueprint_identity_index.jsonl, sibling of runs.jsonl). Lookups consult
the index first and VERIFY every hit by loading that one blueprint under
the current roster and recomputing its identity id -- the index is a
cache, never an oracle, so results stay scan-identical under roster
drift, store surgery, or index corruption (up to the same-identity-twin
choice, unspecified in both paths and documented on the fn). Any miss or
failed verification runs the old scan as a full-store repair pass: no
early return, best-effort append of every absent-or-different mapping,
last line wins so later repairs heal stale lines. No write-path, engine,
or caller changes; a pre-index store backfills itself on the first miss;
a read-only store keeps scanning as before.
Decided against put-time index maintenance: it would need a roster-free
doc-level identity function whose equivalence to the loaded-composite
path no green test ratifies (the composite path canonicalizes
structurally on load); the repair path is the load-bearing mechanism
either way. Decision log: the issue's comments.
Verification: 7 new aura-registry tests (backfill, verified-hit-no-walk
via a poison-dir probe, one-pass legacy backfill, stale-line healing,
unloadable-under-roster preservation, garbage tolerance) plus a CLI e2e
(campaign validate resolving an identity ref cold then warm); workspace
suite 1346 passed / 0 failed; clippy -D warnings clean; doc build clean.
closes#191
The two fieldtest findings, resolved:
- friction: --parallel-instruments 0 now refuses in domain terms ("must be
a whole number of at least 1 — it bounds how many distinct instruments
are resident in parallel") via a custom clap value-parser, instead of
leaking Rust's NonZeroUsize wording; the zero-bound test pins the prose.
- spec_gap: the authoring guide's exit-code contract now states that a
gate-emptied cell (a std::gate leaving no survivors) is a successful
cell — exit 0 with an informational aura: note and a truncated
realization — and that exit 3 is reserved for cells that could not be
evaluated. Records observed behaviour; no code change.
Gates re-verified: workspace suite green, clippy -D warnings clean.
refs #277
Downstream-consumer fieldtest over the public surface at the close of the
maximize-throughput complex: a 4-instrument campaign authored, validated,
run and read back through the normal verbs; determinism probed across
--parallel-instruments 1/2/4/10 and a same-K repeat (byte-identical modulo
the run-counter suffix); duplicate-instrument refusal and faulted-cell
isolation (exit 3, healthy cells persist) confirmed under the parallel
loop. 5 working, 0 bugs, 1 friction (the zero-bound diagnostic leaks
clap's NonZeroUsize wording), 1 spec_gap (gate-emptied cell exit semantics
undocumented) — both resolved in the follow-up tidy.
refs #277
Resolutions for the architect's four drift items (all fix/document, none
ratified away):
- [medium] execute enforced family-name uniqueness only via the CLI's
validate tier: preflight now refuses duplicate campaign instruments
itself (defense in depth for direct callers), RED-first
(execute_refuses_duplicate_instruments).
- [medium] the parallel cell loop's C1 relationship lived only in the
git-ignored spec: C1 gains a realization note (docs/design/INDEX.md)
recording the chunked instrument-major schedule, the structural
residency bound, and the two scheduling-dependent fatal-path carve-outs
(fault attribution among completed cells; already-written family lines)
— both inert and outside the success-path bit-identity.
- [low] the fatal-path orphan-line honesty is part of that note.
- [low] the --parallel-instruments zero-reject acceptance criterion had no
protecting test: campaign_run_rejects_a_zero_parallel_instruments_bound
pins clap's NonZeroUsize usage error (exit 2).
Gates re-verified: workspace suite green, clippy -D warnings clean.
refs #277
The flip: within each K-instrument chunk (the structural residency bound
from the previous commit), all cells run concurrently on rayon's process-
global pool — the same pool the engine's member/window fan-out already
shares, so cells x members x windows feed one work-stealing scheduler and
the per-cell serial seams (ingest transpose, single-threaded bootstrap)
overlap with other cells' work. Results land in index-addressed slots and
the ordered outputs are rebuilt in document order: byte-identical outcomes
across worker counts and bounds (C1).
Fault routing, restructured for a loop that cannot mid-iterate abort:
member/window faults stay #272-contained inside run_cell; non-containable
faults (incl. registry writes — infrastructure, not cell pathology) latch
an atomic abort flag so unstarted cells never run, and after the join the
lowest document-order fault among completed cells propagates as the run's
error. Nothing partial is persisted at run level on the fatal path.
Property pins: determinism across thread counts and bounds (1-thread/K=1
== 8-thread/K=2), distinct live instruments never exceed K (refcounting
stub runner, 8-thread pool), a dead family store is run-fatal with no
partial campaign-run record, member faults stay contained under the
parallel loop. E2E fixtures (previous commit) pin the CLI prose and
document-order persistence across K.
Verified: workspace suite green, clippy -D warnings clean. rayon enters
aura-campaign under the amended C16 per-case policy (comment in
Cargo.toml, mirroring aura-engine).
closes#277
Two pieces of the parallel-cell-loop iteration (spec: parallel-cell-loop),
still sequential — behaviour-identical results, suite green unchanged:
- validate_campaign refuses a campaign document listing the same instrument
twice (DocFault::DuplicateInstrument + CLI prose). A duplicate enumerates
two identical cells and is the one input that could make two cells share a
registry family name; with distinct instruments the per-cell family name
(all four cell axes + stage ordinal) is unique by construction, so the
upcoming parallel appends can never race one name's run-index assignment.
- aura_campaign::execute flattens the 4-nested cell loop into a document-
order-indexed cell list, groups it by instrument ordinal, and walks
sequential chunks of K instrument groups; results land in index-addressed
slots and the ordered outputs (outcomes, realizations, nominee groups) are
rebuilt in document order. K arrives as a new NonZeroUsize parameter
(default DEFAULT_PARALLEL_INSTRUMENTS = 4), exposed as
--parallel-instruments on aura campaign run and threaded through the
run_campaign chain; the dissolved-verb sugar paths pass the default.
The chunk walk is the structural RAM bound for the parallel flip that
follows: no cell of an instrument outside the current chunk can be in
flight at all (chosen over a semaphore gate — blocking inside pool tasks
risks worker starvation; decision log on #277). Verified: workspace suite
green unchanged, clippy -D warnings clean, E2E fixtures pin the CLI prose
and the document-order persistence across K.
refs #277
Architect drift review for the #276 cycle (b048923..HEAD). What holds:
C1 is preserved (the append_write_lock sits at the results-persistence
edge, outside the lock-free sim loop); the mechanism is correct (the
guard spans the full read-counter-then-write critical section in both
locked functions, no re-entrancy via the load paths, poison entered by
into_inner); both fixed paths are pinned by property tests.
One [medium] drift item, resolved here on the fix path: Registry::append
(the flat runs store, runs.jsonl) was the third identical unsynchronized
OpenOptions::append + writeln! path, left standing unguarded beside its
two newly-guarded siblings — and it is public API with external
consumers (C18), so a concurrent caller would hit exactly the #276
corruption. The append_write_lock doc also enumerated the two guarded
stores as if exhaustive. Fixed RED-first: the sibling property test
failed 2/2 against the unguarded path (same "trailing characters"
merged-line corruption), then append takes the same lock and the doc now
names all three JSONL append paths. No other finding: put_blueprint /
put_doc (content-addressed idempotent writes) and TraceStore::write
(per-name whole-file writes) are race-tolerant by shape and correctly
outside the append scope.
Regression gate: no dedicated regression scripts configured; the
project-facts gates all green — cargo test --workspace no failures (the
three concurrency property tests among them), clippy --workspace
--all-targets -D warnings clean, cargo doc --workspace --no-deps clean.
Cycle #276 is drift-clean.
refs #276
Registry::append_family and append_campaign_run did unsynchronized
OpenOptions::append + writeln! to their shared JSONL stores; concurrent
writers through one shared &Registry interleaved one append's content
and newline writes with another's, merging records onto one physical
line and corrupting the store (the #276 RED test failed 3/3 with a
"trailing characters" parse error). The planned parallel campaign cell
loop (#277) makes exactly that caller shape reachable, so this is its
prerequisite.
Mechanism: one internal append_write_lock: Mutex<()> on Registry,
acquired by both append functions and held across the whole
read-the-run-counter-then-write critical section — serializing the
write also serializes the read-before-write counter derivation, so
concurrent appends under one name cannot double-assign a run index.
Mutual exclusion around a per-cell-per-stage microsecond write, not a
barrier: cells never rendezvous, and the sim event loop stays lock-free
(C1's "without locking" governs the sims; this lock sits at the
results-persistence edge outside the hot path). A poisoned lock is
entered anyway (PoisonError::into_inner): every store line is
independent, so one panicked writer must not wedge all later appends.
Scope, per the decision log on #276: in-process synchronization owned
by the Registry type itself; cross-process locking stays out of
contract (the corrected doc comments now state the actual guarantee).
The serial per-cell-buffer alternative stays open to #277 as an
ordering decision on top — this lock does not preclude it.
Adds the sibling test pinning append_campaign_run under the same
concurrent load. Verified: the #276 headline test green 3/3 (was red
3/3), sibling green, cargo test --workspace no failures, clippy
--workspace --all-targets -D warnings clean.
closes#276
Pin the #276 headline behaviour RED-first: N threads sharing one
Registry handle and concurrently appending families must leave
families.jsonl well-formed — every member of every successful append
survives as its own complete, parseable JSON line (none torn, merged
into a neighbour, or lost). Today's unsynchronized OpenOptions::append
+ writeln! interleaves concurrent writers' content and newline writes,
so the test fails deterministically (verified 3/3 runs: "trailing
characters" merged-line parse error).
The sharing topology — one shared &Registry across scoped threads —
models the planned parallel campaign cell loop's caller shape and
admits any of the issue's candidate fixes.
refs #276
Replace the per-call std::thread::scope fan-out in run_indexed with one
process-wide rayon pool. run_indexed's callers nest — sweep (grid
points), monte_carlo (seeds), and walk_forward (window bounds), where a
walk_forward window runs an inner sweep — and each std::thread::scope
spawned its own available_parallelism() OS-thread batch, so the two
levels multiplied to ~available_parallelism()^2 threads (measured
150-240 concurrent on a 24-core box during a 42-cell campaign, ~10x
oversubscription). rayon's pool is process-wide and persistent: a nested
par_iter work-steals within the same N workers instead of spawning, so
live workers never exceed the pool size.
Determinism (C1) is preserved bit-for-bit: run_indexed collects via
IndexedParallelIterator in enumeration order (not completion order), and
the float aggregation (summarize_r / McAggregate / stitch) runs after
the ordered collect, so IEEE-754 non-associativity introduces no
variance. The existing _with_threads(1) == _with_threads(8) == public
determinism tests stay green, now driving local rayon pools of 1 and N
workers via ThreadPool::install.
Structural inversion: the public sweep/monte_carlo/walk_forward become
the core (calling run_indexed on the ambient pool through a shared
assemble_* helper); the _with_threads variants collapse into thin,
test-only wrappers (#[cfg(test)]) that run the same assembly inside a
local pool. run_one stays Fn + Sync (no + Send): run_indexed borrows it
in the map closure and the wrappers borrow it into install — passing it
by value would move it into rayon's Map and force F: Send, a bound the
public callers do not carry (hence the load-bearing
#[allow(clippy::redundant_closure)] on run_indexed).
Adds a nested-oversubscription regression test (asserts nested
run_indexed never exceeds the pool size — the pre-rayon shape ran
POOL^2) and an ignored wall-time benchmark. rayon admitted under the
amended-C16 per-case dependency policy; it stays within C1 (parallelism
across sims, never within one).
This is the shared pool only — the first of three steps toward
saturating the cores. The diagnosis found the larger loss is the idle
valleys (~42% of the box idle: the sequential cell loop and the
single-threaded bootstrap), which the pool alone does not fill. Two
follow-ups are filed and recorded on #268: the Registry write path is
not concurrency-safe (append_family races on a shared families.jsonl),
the prerequisite for parallelizing the C1-disjoint cell loop with a RAM
budget bounding concurrently-active instruments (the FileCache retains a
symbol's parsed files while any reference is live).
Verified: cargo test -p aura-engine (276 passed, the C1 determinism
tests green), the new oversubscription test green, the benchmark runs
(no overflow), clippy -D warnings clean, cargo test --workspace no
failures.
closes#268
refs #275
Drift review (architect) over ad4249f..HEAD found the cycle drift-clean at the core
and confirmed what holds: C1 preserved (`run()`'s k-way merge / event loop has zero
diff — `run_bound` only reorders the supply via `bind_sources` then delegates to
`run`, byte-identity pinned); the C4 realization and C23 refinement ledger notes are
accurate and tightly scoped (`SourceSpec.role` is read in exactly one place, sourced
only from the lowering — no leak); and all three binding-carrying production run sites
moved to `run_bound` + `key_supply`, every remaining `.run(` being `#[cfg(test)]`. No
regression harness exists, so the architect is the sole drift gate.
One medium drift item fixed:
- FIX: the CLI's `.expect()` on `run_bound` covered Missing/Extra/Unnamed feed but not
`DuplicateFeed`. The blueprint path does not enforce root-role-name uniqueness (only
the construction op-script path does), so a duplicate-role blueprint would reach
`bind_sources`' `DuplicateFeed` and panic behind the `.expect()` instead of refusing.
Closed at the binding boundary: `resolve_binding` now refuses a duplicate input-role
name — a named `aura:`-register refusal on every CLI run path (single run, sweep, and
campaign all resolve the binding upfront), matching its existing price/close-ambiguity
refusal. The `.expect()` invariant is now genuinely true on all paths. Pinned by
`binding::duplicate_role_name_refuses`.
Two low items:
- `SourceBindError::Display` had no test (its renderer stays dead until a
decoupled-supply caller lands, #124). Added `source_bind_error_display_names_the_role`
pinning the message strings; reachability is deferred by design.
- The reverted fieldtest `SourceSpec` sweep leaves the corpus one more revival break
against the current engine API — carried, as the corpus is pre-existing bit-rot
unrelated to this cycle (documented in 9e30805 and on #275).
Suite: `cargo test --workspace` green (1321 passed, 0 failed); `cargo clippy
--workspace --all-targets -- -D warnings` clean. Spec + plan (git-ignored working
files) discarded.
closes#275
The CLI half of by-name source binding, plus the ledger record.
Every production run site that carries a ResolvedBinding — `run_signal_r`,
`run_blueprint_member` (sweep / reproduction), and the campaign re-run/trace path —
now keys its opened sources by role name via `key_supply(binding, sources)` and
drives the harness through `Harness::run_bound` instead of the positional
`run(sources)`. `key_supply` pairs each opened column with its declared role from
`binding.entries()` — the one place open-order and role-order meet, made explicit so
`bind_sources` verifies the wiring↔supply role match by name rather than by a
maintained canonical-order convention.
Because the current single-binding CLI derives both the `SourceSpec` roles (via
`wrap_r`) and the supply roles (via `key_supply`) from the same `binding.entries()`,
the bind cannot fail on this path — so the call site asserts the invariant with
`.expect`, matching the adjacent `close_handle.expect("ResolvedBinding guarantees a
close entry")` idiom. The named `SourceBindError` refusal path lives in
`bind_sources` for future decoupled-supply callers (independently-built multi-feed /
recorded sources, #124), where a mismatch becomes reachable.
Ledger: a C4 realization note (supply resolved by name into declaration order, so
supply order is no longer load-bearing; the tie-break guarantee is unchanged) and a
scoped C23 refinement (a `SourceSpec.role` is load-bearing for source binding; every
other flat-graph name stays a non-load-bearing raw-index symbol).
The fieldtest-corpus `SourceSpec` sweep (planned Task 5) was reverted: the fieldtest
packages already fail to build against the current engine API for reasons unrelated
to `SourceSpec` (bootstrap arity, removed `InputSpec`, drifted
`Recorder`/`SimBroker`/`RMetrics`, renamed `Scalar` helpers) — pre-existing bit-rot
from earlier cycles. A partial `SourceSpec` migration neither revives nor further
breaks them, so the scope decision to touch them (premised on their being
otherwise-buildable) was withdrawn; reviving the corpus is separate from #275.
Verification: `cargo test --workspace` green (0 failed; the real-data OHLC channel
e2e exercises the migrated `run_bound` path byte-identically); `cargo build
--workspace --all-targets` clean; `cargo clippy --workspace --all-targets -D
warnings` clean.
refs #275
The engine-side half of by-name source binding. `SourceSpec` carries a load-bearing
`role: Option<String>` — `Some(name)` for a spec lowered from a bound `Role` (carried
through the blueprint lowering, previously dropped at compile), `None` for a
hand-built raw-index spec. A pure `bind_sources(specs, keyed_supply)` resolves a
`Vec<(role, Box<dyn Source>)>` into the positional `Vec` `run` consumes, in
`SourceSpec` declaration order, erroring named (`SourceBindError`:
Missing/Extra/Duplicate/Unnamed feed) on a mis-bind. `Harness::run_bound` is the
ergonomic method the by-name production path calls.
The raw-index `run(Vec)` primitive and the k-way merge / per-cycle event loop are
untouched: a positional run stays byte-identical (C1, pinned by
`run_bound_out_of_order_matches_positional_run`), and every existing `run(Vec)` caller
keeps working (it ignores `role`). A `BTreeMap` index makes the Duplicate/Extra
diagnostics deterministic.
C4's tie-break guarantee ("source declaration order") is unchanged — `bind_sources`
emits in `SourceSpec` order, so the merge's source-index tie-break resolves on
declaration order regardless of the caller's supply order. The narrow C23 amendment
(a `SourceSpec.role` is load-bearing for source binding, the rest of the flat graph
stays raw-index) is deferred to the ledger note.
The four error-path RED tests assert via `.map(|_| ()).unwrap_err()`: the `Ok` arm
`Vec<Box<dyn Source>>` is not `Debug` (`Source` has no `Debug` supertrait), so a bare
`.unwrap_err()` would not compile — a plan-byte defect corrected here.
Also sweeps the 45 raw-index `SourceSpec { .. }` literals to `SourceSpec::raw(..)`
across the workspace (mechanical, compiler-enumerated) and pins that a bound root
role's name survives lowering.
Verification: `cargo test --workspace` green (0 failed; incl. 5 `bind_sources` cases,
the `run_bound` byte-identity test, the role-survives-lowering pin, and the four
`.sources`-equality twins under the now-stricter `role` equality);
`cargo build --workspace --all-targets` clean; `cargo clippy -p aura-engine
--all-targets` clean.
Drift review (architect) over a55e4cf..HEAD found the cycle substantially
clean — C25 closed vocabulary, C1 determinism/behaviour-preservation, the
additive-serde widening, exhaustive StageBlock matches, and the uniform exit-3
convention all hold. Two drift items resolved:
- RATIFY: the `SilencedPanic` member-boundary panic-hook silencer (added by the
#272 implementer, not named in the spec) is a legitimate mechanism — it keeps
"recorded, campaign continues" observably true on stderr by suppressing the
default crash backtrace around each contained `catch_unwind`. Its mutex
serialises only the ref-count/hook-swap (O(1)), never member computation, so
C1 disjoint-parallel determinism is preserved. Documented in the ledger's
#272 realization paragraph rather than left as undocumented global state.
- FIX: `cell_fault_kind_label` hand-wrote the snake_case strings that must match
`CellFaultKind`'s serde `rename_all` (two sources of truth an aggregate over
campaign_runs.jsonl could silently diverge from). Pinned with a test asserting
each label equals the serialized form; the efficient `&'static str` stays.
(The #272 commit's own concern-driven doc fixes and the wf panic-containment
test landed in d3b1a1a; this commit carries only the two audit-phase items.)
Suite: cargo test --workspace green (1311 tests, 0 failed); clippy clean.
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).
Cycle-close audit over the feature trio 75bfb93..d1b3a3d (#261#260#262). Architect verdict: clean — C1 (both stop variants and the
per-instrument cost round-trip through the manifest), C2 (rollover-only
emission), C18 (scalar cost wire form byte-identical; VolTf additive),
the four-scalar discipline (SessionFrankfurt bakes the timezone, no
string kind), and the match-arm lockstep across all four RiskRegime/
StopRule sites all hold; commit bodies match the diff. One low drift
item — the repeated 60·10^9 ns-per-minute literal in vol_tf_stop.rs —
fixed here as NS_PER_MINUTE rather than carried.
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 #262
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
k · sqrt(EMA(delta^2, length)) over completed period_minutes buckets —
the vol regime on a coarser clock, as a fused primitive in the Resample
mold: the in-progress bucket lives in node state, the finished bucket's
close is differenced against the previous one on rollover, and the stop
distance is emitted ONCE per completed bucket (C2: a partial bucket
never exists downstream; Firing::Any consumers hold the last value).
EMA follows Ema's exact convention (SMA seed over the first length
deltas, alpha = 2/(length+1)), pinned by a constant-|delta|
correspondence test against the per-cycle vol regime. Not rostered in
the std vocabulary (stop primitives are risk-executor internals).
First slice of #262; the regime variants, executor arm, and manifest
round-trip follow.
refs #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
A Frankfurt-baked zero-arg Session preset (open 09:00 Europe/Berlin as
literals) joins the std vocabulary as SessionFrankfurt, declaring
period_minutes: I64 (default 15) as its one scalar knob — the
single-knob roster pattern the cost nodes established. A data-only
blueprint can now reach a session/time-of-day stream (bars_since_open),
unblocking time-of-day conditioners for the confirm/refute research
model. Roster count pins bump 28 -> 29 in both conscious-act sites.
Fork decision on the issue: the preset variant over a param-vocabulary
extension — a timezone param would need a string kind, breaching the
four-scalar discipline; the preset keeps the vocabulary closed and is
the additive extension the roster-exclusion comment anticipates. The
4-arg Session::builder stays untouched; in_session/session_open_ts
stay deferred (#154).
closes#261