77ad977623f3bfb254d631f66d18a67ca74c0216
70 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77ad977623 |
fieldtest: cycle-0011 — 3 examples, 5 findings
Per-cycle fieldtest of the aura-ingest ingestion boundary (issue #7), from the public interface only. 3 examples (transpose/normalize core; close_stream -> deterministic harness run with epoch-ns end-to-end; load_m1_window over real AAPL.US 2006-08 bars + None paths), all green. 0 bugs, 0 friction. Two spec_gaps named for follow-up (neither a code defect; both filed to the tracker): - load_m1_window returns Some(empty M1Columns) — not None — for an in-coverage but bar-empty window. The rustdoc's "None if no data in [from_ms,to_ms]" reads bar-level but the realized boundary is file-level (it inherits data-server's file-skip None). A consumer matching None == "no bars" is wrong for the in-coverage-empty case. -> tighten the load_m1_window rustdoc (one line). - AAPL.US (the carrier/integration-test symbol) has only ~21 M1 bars/month, so the SMA-cross never warms and the end-to-end run yields all-zero (degenerate but valid) metrics. The boundary is correct (C2 warm-up); the milestone's shipped demo data is too thin to show a non-degenerate trace. -> weigh against C22's "newcomer sees a populated trace" before the Walking-skeleton close (document expected density or name a denser demo symbol/window). |
||
|
|
63ea7eb3b1 |
audit: cycle 0011 tidy — record the external-dependency firewall invariant
Architect drift review (f68258b..HEAD) found no code drift — the firewall is
real at the dependency-graph level (aura-core/std/engine/cli carry no external
dep; the chrono/regex/zip tree is confined to aura-ingest), and C3/C7/C1 are
faithfully realized. Four ledger-level items, all resolved here:
- [ledger-drift] The zero-external-dependency commitment — the load-bearing
rationale for making aura-ingest a crate rather than a feature-gate — lived
only in spec 0011. Recorded in the ledger: C16 now states the engine workspace
is zero-external-dependency by commitment, names aura-ingest as the ingestion
edge / external-dependency firewall, and forbids an external dep in any engine
crate other than aura-ingest.
- [ledger-drift] C16's reuse taxonomy did not place aura-ingest. C16 now lists
the non-node engine crates (aura-engine, aura-cli, aura-ingest).
- [ledger-debt] C12's eager-materialization-now / Arc<[T]>-sharing-later choice
was only in the spec. C12 now carries a "Status (cycle 0011)" note recording
the deliberate gap.
- [debt] aura-engine/Cargo.toml carried a stale comment ("data-server enters
when the ingestion task starts") that contradicted the firewall it should
protect. Replaced with the dependency-pure / firewall-in-aura-ingest note.
The External components data-server bullet is updated to reality (pulled in by
aura-ingest; workspace now resolves from a populated cargo cache, not fully
offline).
Regression gate: no-op (commands.regression empty); architect is the sole gate.
Cycle 0011 is drift-clean. (Drift-clean, not a milestone close — the
Walking-skeleton milestone close additionally needs its end-to-end milestone
fieldtest, a separate deliberate act.)
|
||
|
|
a24729e97f |
feat(aura-ingest): data-server M1 ingestion at the one merge boundary
aura's first real data source (closes #7, Walking-skeleton milestone). A new aura-ingest workspace crate transposes data-server's AoS M1Parsed records into SoA base columns (C7), normalizes Unix-ms to canonical epoch-ns at the one ingestion boundary (C3), and feeds the existing k-way merge a real close-price stream so a backtest runs over real bars, deterministically (C1). Surface: - unix_ms_to_epoch_ns(time_ms) -> Timestamp (= ms * 1_000_000), the single C3 unit normalization. - M1Columns: the OHLCV bar as a bundle of base columns (open/high/low/close/ spread: f64, volume: i64, ts: epoch-ns) — SoA, C7. - transpose_m1(&[M1Parsed]) -> M1Columns: pure AoS->SoA (C1). - M1Columns::close_stream() -> Vec<(Timestamp, Scalar)>: the price input the SMA-cross sample strategy consumes. - load_m1_window(server, symbol, from_ms, to_ms): drains data-server's chronological chunks, transposes once at the boundary. Design (per spec 0011): - Eager materialization, not a lazy/shared Source abstraction: C12's cross-sim Arc<[T]> sharing has no consumer until the multi-sim orchestration cycle; deferred, the transpose logic carries over. - aura-ingest is the external-dependency firewall. data-server transitively pulls chrono + regex + zip (+ their trees) into Cargo.lock; isolating it in this one crate keeps aura-core/std/engine zero-external-dependency and the frozen deploy artifact (C6 replays recorded streams, never re-ingests) clean. cargo test --workspace now needs a populated cargo cache (one Gitea fetch, done). - Scope: boundary + tests only, no aura run CLI arg surface (M1 first; tick and the CLI wiring are follow-ups). Tests: 6 hermetic unit tests on hand-built M1Parsed (normalization, field-wise transpose, purity, close_stream order, empty edges) + one gated integration test (tests/real_bars.rs) that runs the cycle-0007 sample harness over real AAPL.US 2006-08 close bars and asserts finite metrics + two-run bit-identical JSON (C1); it skips cleanly where /mnt/tickdata is absent. On this machine it ran the real path. Workspace gates green: test (86), clippy -D warnings, doc -D warnings. Minor: transient unused-import warnings in the new lib.rs across the additive build steps resolved once load_m1_window consumed the imports; final -D warnings gate clean. |
||
|
|
44aa90be00 |
plan: 0011 data-server M1 ingestion boundary
Placeholder-free 6-task plan for spec 0011 (refs #7). T1 scaffolds the aura-ingest crate + workspace member + unix_ms_to_epoch_ns (proves the data-server git dep resolves); T2 M1Columns + transpose_m1; T3 close_stream; T4 load_m1_window (data-server adapter, compile-gated against the real API); T5 the gated real-bars integration test (cycle-0007 sample harness over real AAPL.US close bars, finite + deterministic, skips where data absent); T6 full-workspace gates. Mirrors report.rs build_two_sink_harness with the shipped aura_std::Recorder. External data-server signatures verified against its source in the spec commit. |
||
|
|
df0574a75d |
spec: 0011 data-server M1 ingestion at the one merge boundary
First real data source (refs #7, Walking-skeleton milestone). A new `aura-ingest` crate pulls data-server as a cargo git dependency, transposes its AoS M1Parsed records into SoA base columns (C7), normalizes Unix-ms to canonical epoch-ns at the one ingestion boundary (C3), and feeds the existing k-way merge a real close-price stream so a backtest runs over real bars (C1). Decisions captured: - Eager materialization (not a lazy/shared Source abstraction): C12's cross-sim Arc<[T]> sharing has no consumer until the multi-sim orchestration cycle, so building it now would be speculative infra; deferred, transpose logic carries over. - aura-ingest is the external-dependency firewall: data-server transitively pulls chrono+regex+zip; isolating it in its own crate keeps core/std/engine zero-external-dep. cargo test --workspace now needs a populated cargo cache. - Scope: boundary + tests only, no aura run CLI arg surface (would force the unsettled #16 arg-parse decision); M1 first, tick deferred. grounding-check returned BLOCK on the single load-bearing external assumption (the data-server public API) — structurally unratifiable by an aura-side test because data-server is brand-new this cycle; all 9 aura-internal assumptions were ratified by named green tests. Overridden after verifying data-server's API against its actual source (lib.rs DataServer::new/has_symbol/ stream_m1_windowed/next_chunk/DEFAULT_DATA_PATH; records.rs M1Parsed fields, types, Copy), per the agent's documented override path. User-approved. |
||
|
|
f68258b044 |
fieldtest: cycle-0010 — 3 examples, 6 findings
Public-interface-only field test of the walking-skeleton closing seam (#8): the `aura run` CLI and the reusable aura-std::Recorder sink. A standalone consumer crate (path-deps on the three engine crates, as a C16 project would) drives the real binary as a subprocess and wires Recorder via the raw Harness::bootstrap API. Examples (all built from HEAD and ran green): - c0010_1: drive `aura run` as a downstream CLI/jq consumer — single-line JSON report, byte-identical across two runs (C1), bad-args → exit 2 + exact usage on stderr with empty stdout. - c0010_2: wire Recorder onto Sma(3) via raw bootstrap, drain the mpsc::Receiver — rows match the rustdoc warm-up model exactly. - c0010_3: Recorder over [I64, Bool, Timestamp] — the four-kind claim (never exercised by the CLI's f64-only path) holds, verified from rustdoc alone. Findings: 0 bugs, 4 working, 1 spec_gap, 1 friction. - [spec_gap] `aura run <trailing-arg>` is accepted (exit 0), not rejected — the arg parse keys only on the first token being `run` and ignores the rest. The cycle_scope's "any other or missing subcommand -> exit 2" does not unambiguously cover `run <junk>`; the binary chose the lenient reading. Tracked for a ratify-or-tighten decision. - [friction] verifying the documented {manifest, metrics} JSON nesting needs a hand-rolled string walker — the zero-dep consumer has only to_json() and no typed read-path. Low-priority tidy. Both non-bug findings filed to the forward queue; neither blocks the cycle. |
||
|
|
1433b13495 |
audit: cycle 0010 tidy (clean)
Architect drift review over 7a8d209..559903a (aura run CLI, #8): drift-clean, carry-on. Regression gate is a no-op (commands.regression empty); architect is the sole gate and found nothing actionable. What holds against the ledger: - C22 closing seam genuinely delivered: `aura run` bootstraps a CLOSED harness WITH two sinks and emits a populated, non-trivial trace (one exposure sign flip, a real drawdown) — a newcomer sees a real trace, as C22 promises. - C16 respected: Recorder ships in aura-std as a domain-free universal block (parameterised over ScalarKind/Firing/Sender, no instrument/strategy concepts); sample_harness lives in aura-cli, not aura-engine, so the engine stays domain-free while wiring stays plain Rust (C17/C20 — raw Harness::bootstrap, no DSL). - C7 purity: mpsc::Sender out-of-graph handle, no Rc/RefCell. C8 sink contract: output: vec![], eval returns None. C14 structured face: real binary → canonical JSON on stdout, exit-code contract integration-tested. C1 determinism pinned. Acknowledged, spec-deferred debt (tracked, not silent drift) — filed to the forward queue: - Three near-identical Recorder definitions now coexist: the shipped aura-std block (the strict superset — four kinds) plus the two #[cfg(test)] engine fixtures (f64-only) in report.rs / harness.rs. The spec explicitly defers de-dup; flagged for a later tidy. - manifest.commit is option_env!("AURA_COMMIT") → "unknown" — the C18 identity field is a placeholder until build.rs git capture (named non-goal). The one implementer deviation (a scoped #[allow(clippy::type_complexity)] on sample_harness) is consistent with the identical allow on aura-engine's build_two_sink_harness fixture — not drift. |
||
|
|
559903a14e |
feat(aura-cli): aura run — end-to-end sample-harness CLI
The Walking-skeleton milestone's closing seam: a real `aura run` binary that
bootstraps a built-in sample harness, runs it deterministically, and prints the
cycle-0009 metrics+manifest report as canonical JSON to stdout — the headline
C14 "run a sim, emit structured metrics" move, end-to-end, from a binary for the
first time.
Two deliverables:
- aura-std::Recorder — a reusable recording sink node (the glossary *sink* role,
C8/C22): a pure consumer (output: vec![]) over kinds.len() input columns,
holding an mpsc::Sender<(Timestamp, Vec<Scalar>)> as its out-of-graph
destination. mpsc keeps the engine's purity invariant (C7): no Rc/RefCell.
Supports all four base scalar kinds; returns None until every column is warm.
Promotes the shape the #[cfg(test)] fixtures already proved into a shipped,
reusable block (the fixtures stay as historical snapshots; de-dup is a later
tidy).
- aura-cli `run` subcommand — synthetic_prices / sample_harness / run_sample /
main. The sample harness (synthetic source → SMA(2)/SMA(4) → Sub → Exposure →
SimBroker → two Recorder sinks) is authored in plain Rust over the raw
Harness::bootstrap(nodes, sources, edges) API (C17/C20) — the open
experiment-builder DSL thread is deliberately not committed this cycle. main
hand-parses one subcommand: `run` prints run_sample().to_json() (exit 0),
anything else prints a one-line usage to stderr (exit 2). aura-cli gains
aura-std + aura-core path deps; the workspace stays zero-(external-)dependency.
The built-in synthetic stream (7 ticks, rises then reverses) yields a non-trivial
demo trace: equity [0,0,0,0,-0.08,-0.17,-0.13] → total_pips -0.13, max_drawdown
0.17, exposure_sign_flips 1. The run_sample unit test pins the integer flip count
exactly and the two f64 metrics within 1e-9 (the real run's float dust is ~7e-15,
confirming the tolerance); determinism is pinned exactly (two runs → identical
JSON). tests/cli_run.rs drives the built binary for the observable exit/stdout
contract.
manifest.commit is filled from option_env!("AURA_COMMIT") (defaults to
"unknown"); real HEAD capture via build.rs is deferred. Non-goals untouched:
experiment-builder DSL, aura new, Aura.toml schema, the data-server source (#7).
One deviation from the plan's verbatim code: a scoped
#[allow(clippy::type_complexity)] on sample_harness (its (Harness, Receiver,
Receiver) return tuple trips the lint under -D warnings) — consistent with the
identical allow on the build_two_sink_harness fixture in aura-engine. Verified
myself: cargo test --workspace (61 green, 0 red), clippy --all-targets
-D warnings (clean), cargo doc -D warnings (clean), and both smoke runs.
closes #8
|
||
|
|
9159a550a3 | chore: ignore fieldtest crate build artifacts (fieldtests/*/target/) | ||
|
|
93195aa04b |
plan: 0010 aura run CLI
Placeholder-free, verbatim-code plan for cycle 0010 (#8). Two deliverables in dependency order, four tasks: 1. aura-std::Recorder — a four-kind recording sink (pure consumer, output: vec![], holds an mpsc::Sender), mirroring the existing #[cfg(test)] fixture; wired into lib.rs alphabetically; two unit tests (f64 capture after warm-up, None-until-all-columns-warm). 2. aura-cli run subcommand — synthetic_prices / sample_harness / run_sample / main over the raw Harness::bootstrap API (no builder DSL), + aura-std/aura-core path deps; a unit test pinning determinism and the hand-computed metrics. 3. tests/cli_run.rs — integration test driving the built binary (run -> exit 0 + single-line JSON; no args -> exit 2 + usage stderr). 4. Workspace gates (test / clippy -D warnings / doc -D warnings). The chosen synthetic stream (7 ticks, rises then reverses) is traced tick-by-tick in the plan: equity [0,0,0,0,-0.08,-0.17,-0.13] -> total_pips -0.13, max_drawdown 0.17, exposure_sign_flips 1. The integer flip count is pinned exactly; the two f64 metrics within 1e-9 (dust ~1e-15); determinism pinned exactly. refs #8 |
||
|
|
4a78170dfb |
spec: 0010 aura run CLI
Cycle 0010, the Walking-skeleton milestone's closing seam: an `aura run`
subcommand that bootstraps a built-in sample harness (synthetic source →
SMA-cross → Exposure → SimBroker → recording sinks), runs it
deterministically, and prints the cycle-0009 metrics+manifest report as
canonical JSON to stdout.
The crux the cycle resolves: no recording sink node ships today (recording
lives only as #[cfg(test)] fixtures), and Harness::run returns () with a sink
as the only data-out path. So this cycle first ships a reusable
aura-std::Recorder (a pure consumer holding an mpsc::Sender, purity-preserving
per C7), then wires the CLI on top.
Load-bearing decisions, user-approved at the brainstorm gate:
- Recorder ships in aura-std (universal block, C16), not engine or CLI-local.
- Sample harness is authored as a plain Rust constructor over the raw
Harness::bootstrap API (C17/C20) — the open experiment-builder DSL thread is
deliberately NOT committed this cycle.
- Zero-dependency CLI: hand-parsed args (no clap); manifest commit from
option_env!("AURA_COMMIT") defaulting to "unknown" (build.rs git capture
deferred).
Grounding-check PASS (all load-bearing codebase assumptions ratified by green
tests). Non-goals: experiment-builder DSL, aura new, Aura.toml schema, the
data-server source (#7), build.rs git capture.
refs #8
|
||
|
|
7a8d2097e7 |
docs(aura-engine): document to_json JSON schema + integer-token rendering
Resolves the two doc-level findings from the cycle-0009 fieldtest
(docs/specs/fieldtest-0009-run-metrics.md), both the same doc pass on
RunReport::to_json's rustdoc:
- spec_gap: the JSON key names and {manifest,metrics} nesting were not on the
public surface — a consumer parsing the JSON (the C18 registry, the deferred
aura run printer) could not author against it from rustdoc alone. Now stated:
keys mirror the struct field names in fixed order, window is a 2-element
[from,to] array, params is an insertion-order object, with a worked sample
object. Ratifies field-name-mirroring as the contract for the C14 face.
- friction: a whole-valued f64 renders without a fractional part (12.0 -> 12),
so one f64 field may appear as an integer or decimal token across runs. Now
documented, with the consumer guidance to parse as a number, never key off
token shape.
Doc-only change; no behaviour change. The sample is a `text` fence (not a
doctest). Verified: RUSTDOCFLAGS="-D warnings" cargo doc -p aura-engine clean;
clippy -p aura-engine --all-targets -D warnings clean; doctests 0.
refs #6
|
||
|
|
a982b96ecc |
fieldtest: cycle-0009 — 4 examples, 6 findings
First fieldtest of the run-metrics + manifest report surface. A standalone
downstream-consumer crate (fieldtests/cycle-0009-run-metrics/) path-depends on
the engine crates and exercises the post-0009 surface from the public interface
only (rustdoc + ledger + glossary + project layout, never crates/*/src).
Primary axis empirically met: the north-star "a run emits metrics + manifest"
move is reachable from rustdoc alone — drain two recording sinks -> f64_field ->
summarize -> RunManifest -> to_json, metrics matching the hand model on the first
run, deterministic across reruns.
Findings: 0 bugs, 1 friction, 1 spec_gap, 4 working.
- working x4: north-star reachable from rustdoc; SimBroker firing/slot docs (a
resolved 0007 gap) now carry the example; summarize metric definitions exact
on six degenerate inputs (incl. negative-curve drawdown + flat-as-sign-0);
f64_field panics precise and well-located.
- spec_gap: to_json's JSON key names + {manifest,metrics} nesting are not on
the public surface — a consumer parsing the JSON (C18 registry, the deferred
aura run printer) cannot author against it from rustdoc alone.
- friction: to_json renders whole-valued f64 without a decimal point (3.0 ->
"3"), so one f64 field appears as integer or decimal token within one schema.
Both doc-level findings are the same doc pass and matter mainly for the deferred
aura run (#8) and the C18 registry that will parse this JSON. Spec feeds the next
plan as reference.
refs #6
|
||
|
|
4dc1526196 |
audit: cycle 0009 tidy (clean)
Architect drift review over 521a16e..80d7bfb (run-metrics-and-manifest):
no drift, no debt. The shipped report surface matches the ledger:
- Pure-additive as claimed: lib.rs gains only `mod report;` + one re-export
line; harness.rs `run` signature is untouched (`-> ()`). No engine / Harness
/ node-contract change — the cycle-0006 sink mechanism (C8/C22) is consumed
read-only, not altered.
- C1/C12: summarize is a total, pure, value-only reduction; the e2e test
asserts bit-identical metrics + JSON across two runs.
- C18 sequencing: ships manifest + metrics "from day one", explicitly defers
the registry/index to its later milestone. C14 structured face delivered as
hand-rolled JSON; the zero-dependency stance is preserved (all four crates
path-only deps).
- params-as-honest-precursor wording matches the 0008 typed-param-space
deferral (node.rs); f64_field panic-as-wiring-bug matches the engine's
existing "checked at wiring" convention.
Forward note (not drift, no contract speaks to it, spec documents it as an
accepted assumption): to_json renders f64 via `{}` and is valid JSON only under
the finite-value invariant — a NaN/Inf would emit bare NaN/inf tokens. The
invariant holds today (SimBroker integrates finite returns, Exposure clamps),
so C14 "machine-readable" is not breached. Worth a debug-assert or named
constant only if a future broker can produce non-finite equity — flag it then.
Regression gate: profile commands.regression is empty — no-op; architect is the
sole gate and is green. carry-on.
|
||
|
|
80d7bfbbf0 |
feat(aura-engine): run metrics + manifest report surface
Cycle 0009 (Walking skeleton milestone). A run's two C18 "from day one"
artefacts — a reproducible manifest and summary metrics — land as a new,
pure-additive `report` module in aura-engine:
- summarize(equity, exposure) -> RunMetrics: a post-run pure reduction over a
run's recorded pip-equity + exposure streams. total_pips (last cumulative
value), max_drawdown (worst running-peak-to-trough), exposure_sign_flips (a
turnover proxy: adjacent normalized-sign changes, flat distinct from
long/short via a three-way sign0). Total and pure: empty -> zeros, identical
inputs -> identical metrics (C1/C12).
- RunManifest: the reproducible descriptor (commit, params as name->value
pairs, data-window, seed, broker label). Caller-supplied — the engine cannot
introspect a git commit or seed. params is the honest precursor to the
deferred typed param-space (node.rs; 0008 audit).
- RunReport { manifest, metrics } + to_json(): canonical, hand-rolled JSON for
the structured C14 face.
- f64_field(rows, field): bridges a recording sink's Vec<Scalar> rows to
summarize; panics on a kind/width mismatch (a wiring bug), like the engine's
other "checked at wiring" violations.
Why post-run, not a node: a node emits one record per eval with no terminal
eval (C8), so an end-of-run reduction has no home as a node; the World drains
its cycle-0006 recording sinks after Harness::run and folds them here. Matches
C18's "store manifests + metrics, re-derive results on demand".
Why hand-rolled JSON, not serde: the workspace is deliberately zero-dependency
and the schema is tiny/closed/flat; serde is reconsidered when the run-registry
milestone grows the schema. (User-approved at spec review.)
Scope: the surface + a determinism end-to-end test reusing the cycle-0007
two-sink harness; the `aura run` subcommand that prints the report is #8. No
engine/Harness/node-contract change; workspace stays zero-dependency.
Verified: cargo test --workspace green (aura-engine 40, incl. 10 new report
tests); clippy --all-targets -D warnings clean; RUSTDOCFLAGS=-D warnings cargo
doc clean.
closes #6
|
||
|
|
cb66708d26 |
plan: 0009 run metrics + manifest
Six-task plan for the cycle-0009 report surface (issue #6): module scaffold + RunMetrics/RunManifest/RunReport types (Task 1), the pure summarize reduction (Task 2, RED-first), the f64_field recorded-stream adapter (Task 3, RED-first), RunReport::to_json canonical zero-dep JSON (Task 4, RED-first), an end-to-end determinism test reusing the cycle-0007 two-sink harness (Task 5), and the full workspace gates (Task 6). Pure-additive in a new aura-engine report module; the re-export line grows per task so the crate compiles at every boundary. refs #6 |
||
|
|
bf72924db7 |
spec: 0009 run metrics + manifest
Cycle 0009 (Walking skeleton milestone, issue #6): a post-run pure-function reduction over the recorded pip-equity + exposure streams into summary metrics (total pips, peak-to-trough drawdown, exposure sign-flip count) plus a caller-supplied RunManifest (commit, params, data-window, seed, broker), paired as RunReport with canonical hand-rolled zero-dep JSON (C14). Lives in a new aura-engine report module; no engine/Harness/node-contract change. The reduction is post-run, not a node (C8 caps a node at one record per eval with no end-of-run eval); matches C18's manifest+metrics-per-run-from-day-one. The `aura run` subcommand that prints the report is deferred to #8. Grounding-check PASS (all load-bearing assumptions ratified by green tests / authoritative sources). Spec-validation parse pass is a documented no-op (no parser configured for the profile). refs #6 |
||
|
|
521a16e56f |
docs(aura-std): document Add/LinComb firing policy + multi-row-per-ts shape
Resolves the two spec_gaps from the cycle-0008 fieldtest
(docs/specs/fieldtest-0008-sum-combinators.md), the same class the 0007 fieldtest
raised for SimBroker (and which
|
||
|
|
90e298d3b4 |
fieldtest: cycle-0008 — 5 examples, 6 findings
First fieldtest of the sum combinators. A standalone downstream-consumer crate
(fieldtests/cycle-0008-sum-combinators/) path-depends on the engine crates and
exercises the post-0008 surface from the public interface only (rustdoc + ledger
+ glossary + project-layout, never crates/*/src).
Primary axis empirically met: the north-star two-signal combine move now uses the
shipped Add (Add::new() dropped in exactly where the 0007 fixture hand-authored
an Add2) — the 0007 boilerplate is retired, end to end through SimBroker to a
deterministic recorded pip curve.
Findings: 0 bugs, 1 friction, 2 spec_gaps, 3 working.
- working ×3: Add2 retired; LinComb weighted/variadic sums exact (<1e-12);
warm-up barrier + empty-weights panic as documented.
- spec_gap: Add/LinComb per-input firing policy (Firing::Any / mode-A as-of
join) absent from the public surface — same class as the 0007 SimBroker gap.
- spec_gap: a fired combinator emits one row per *cycle*, so a heterogeneous
same-timestamp multi-source trace can carry >1 row per timestamp (correct per
C4/C5, undocumented at this surface).
- friction: nested consumer crate still needs the empty [workspace] table
(carried from 0006/0007; #9 closed the docs half; folds into aura new).
Spec feeds the next plan as reference.
|
||
|
|
45920faa8f |
audit: cycle 0008 tidy — correct LinComb param-claim wording
Architect drift review (fa2835e..HEAD) at cycle close. Regression gate is a no-op (commands.regression empty); architect is the sole gate. What holds (no drift): purely additive C16 extension, one node per file, no engine/manifest change; C2/C8 warm-up discipline correct (None until every leg present, no implicit cold-leg 0.0); C7 zero per-cycle alloc. Shipping the named convenience nodes Add (and pre-existing Sub) beside the general LinComb is NOT C9 adapter-zoo drift — they are distinct named producers, not coercion shims, mirroring the already-shipped Sub. Fixed [ledger-gap/high]: lincomb.rs rustdoc and the feat commit body called the weights "the node's tunable parameters (C8/C12)", but aura_core/src/node.rs states tunable params (C12/C19) are deliberately not part of the schema yet (spec 0002 "Out of scope"). The weights are construction parameters that configure the node and fix its arity (C11) — the natural home for combination tuning params once the schema-level param surface lands, but not yet surfaced to any optimizer. Rustdoc reworded to say exactly that. No ledger change: the C8 text vs node.rs tension pre-exists this cycle and is already documented in node.rs; only the new rustdoc over-claimed. Carry-on [drift/high]: the cycle-0007 fieldtest still imports Sub-only and keeps its hand-authored Add2 (acceptance said the fieldtest *can* drop it). The 0007 fieldtest crate is a historical snapshot of what that run did; rewriting it retroactively would falsify why Add2 existed. The structural drop-in feasibility (the acceptance content) is confirmed by the architect (add.rs == sub.rs modulo operator; LinComb([1,1]) == Add; Add2 byte-identical to Add). End-to-end demonstration belongs to a cycle-0008 fieldtest (fresh example on the shipped node), dispatched next — not a retroactive edit of the 0007 record. Gates: cargo test -p aura-std 14 passed / 0 failed; clippy --all-targets -D warnings clean; RUSTDOCFLAGS="-D warnings" cargo doc -p aura-std clean. |
||
|
|
84130c2cab |
feat(aura-std): Add + LinComb sum combinators
aura-std shipped Sma, Sub, Exposure, SimBroker but no sum, so the north-star
"combine one signal with another" research move (C10) could not be expressed
from shipped blocks — the cycle-0007 fieldtest had to hand-author a project-local
Add2. This adds the missing combinator(s):
- Add — two-input f64 sum (a + b), the parameterless companion to Sub. Mirrors
sub.rs modulo the operator.
- LinComb { weights } — N-input weighted sum (Σ wᵢ·xᵢ). The weights are the
node's tunable parameters (C8/C12) and fix its arity (weights.len() inputs);
this is the form the north-star "combine A and B *with weights*" reaches for.
LinComb([1,1]) is Add; LinComb([1,-1]) is Sub.
Both withhold output (None) until *all* inputs are present — consistent with Sub,
and causally clean: a cold input leg is never silently folded in as 0.0.
LinComb::new panics on empty weights (build-time param error, like Sma::new).
Both ship because each is independently reached-for: Add for readability symmetry
with Sub, LinComb for the weighted/tunable combination — mirroring the project's
already-shipped choice to keep Sub as a named node beside a general form.
Hand-driven unit tests in the established aura-std style (5 new): Add sum, the
Add == LinComb([1,1]) identity, the N>2 warm-up, and the empty-weights panic.
Verified: cargo test -p aura-std (14 passed), clippy --all-targets -D warnings
clean, RUSTDOCFLAGS="-D warnings" cargo doc -p aura-std --no-deps clean.
closes #11
|
||
|
|
23e4cdae14 |
plan: 0008 sum combinators (Add + LinComb)
Bite-sized RED-first plan for the two new aura-std nodes. Task 1 ships Add (mirrors sub.rs modulo operator), Task 2 ships LinComb (Vec<f64> weights param, variadic schema, empty-weights panic), Task 3 the crate-wide test/clippy/doc gates. No engine change. refs #11 |
||
|
|
288e75e7fd |
spec: 0008 sum combinators (Add + LinComb)
Ships the missing sum combinator(s) in aura-std so the north-star combine move
(C10) is expressible from shipped blocks. Two nodes: Add (parameterless two-input
f64 sum, companion to Sub) and LinComb { weights } (N-input weighted sum, weights
as tunable params + arity, C8/C12). Both withhold output until all inputs are
present (no implicit cold-leg 0.0).
Resolves the cycle-0007 fieldtest [friction]: the consumer had to hand-author a
project-local Add2 to combine two signals.
refs #11
|
||
|
|
fa2835ebcc |
docs: capture the empty [workspace] table gotcha for nested project crates
A project crate that path-depends on the engine and lives under the engine repo (common during authoring) is rejected by cargo unless its Cargo.toml carries an empty [workspace] table. Surfaced repeatedly by the cycle-0006 and cycle-0007 fieldtests as a non-obvious onboarding one-liner. Document it on the consumer-facing surface (project-layout, the "A project repo" section) so a nested-project author can predict and fix it. The aura new scaffolder will emit the line once it exists (folds into the CLI work); the docs capture closes the actionable half now. closes #9 |
||
|
|
e9f4352640 |
docs(aura-std): document SimBroker input slots + firing/warm-up shape
Resolves the two spec_gaps from the cycle-0007 fieldtest
(docs/specs/fieldtest-0007-signal-quality.md): the consumer-facing SimBroker
struct rustdoc stated only the integration formula, leaving two things a
downstream author cannot infer from the public surface —
- input slot order (slot 0 = exposure, slot 1 = price). Both slots are f64, so
a swapped wiring is NOT caught at bootstrap (no kind mismatch) and silently
yields a wrong-but-plausible equity curve. The order is now documented as
load-bearing on the struct doc, with that hazard called out.
- firing / warm-up emission shape: both inputs are Firing::Any, so the broker
fires on every price-fresh cycle and reads a cold exposure leg as flat 0.0 —
emitting equity rows (leading 0.0s) from the first price tick, one per price
cycle, not one per exposure value. A consumer needs this to predict the
recorded curve's length and leading values.
Doc-only change; no behaviour change. Verified: RUSTDOCFLAGS="-D warnings" cargo
doc -p aura-std clean (intra-doc links to crate::Exposure and aura_core::Firing
resolve); clippy -p aura-std --all-targets -D warnings clean.
refs #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f3cd2e1320 |
fieldtest: cycle-0007 — 4 examples, 7 findings
First fieldtest of the signal-quality loop. A standalone downstream-consumer crate
(fieldtests/cycle-0007-signal-quality/) path-depends on the engine crates and
exercises the post-0007 surface from the public interface only (rustdoc + specs +
ledger + glossary, never crates/*/src):
1 single-signal quality backtest, end to end (SMA-cross -> Exposure -> SimBroker
-> Recorder pip curve)
2 Exposure clamp + sizing (hard ±1 saturation, sign preserved)
3 sim-optimal integration: short-on-falling pays positive pips; pip_size=2 curve
exactly halves pip_size=1
4 north-star combine-two-signals (two MA-cross spreads summed into one exposure)
Findings: 3 working (carry-on), 2 spec_gap, 2 friction.
- spec_gap: SimBroker firing policy + cold-exposure->0.0 warm-up emission shape
not on the public surface (only in the feat commit body).
- spec_gap: SimBroker input slot order (0=exposure, 1=price) only in C10 prose /
commit body; a swapped wiring is not caught at bootstrap (both f64).
- friction: the north-star "combine two signals" move needs a sum/LinComb
combinator aura-std does not ship (hand-authored Add2 in the fixture).
- friction: standalone consumer crate still needs the empty [workspace] table
(carried from cycle-0006; tracked as #9).
Spec at docs/specs/fieldtest-0007-signal-quality.md feeds the next plan.
refs #4 #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e7542864c6 |
audit: cycle 0007 tidy — add C10 Realization (cycle 0007) note
Architect drift review (f000373..HEAD) found one item: the C10 reframe was
amended in framing but carried no "Realization (cycle 0007)" note recording what
actually shipped — the spec's own ship gate required it (C8/C22 carry their
realization notes; C10 did not). Added: the signal-quality half realized as
Exposure { scale } + SimBroker { pip_size } on the unchanged, domain-free engine,
with the position-management half explicitly confirmed deferred.
Everything else holds: the reframe is propagated consistently across CLAUDE.md
invariant #7, the seven touched glossary entries + the new exposure-stream entry,
and project-layout.md (no stale "position table is the strategy output"
survivor); the engine stays domain-free (the only RefCell hit in aura-engine/src
is a #[cfg(test)] doc comment); C1 determinism and C2 causality are each pinned by
a behaviour test asserting observable output, not internals.
No regression scripts configured (commands.regression empty) — that gate is a
no-op; architect is the gate. Cycle 0007 is drift-clean.
refs #4 #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3b49074156 |
feat(aura-std): signal-quality loop — Exposure node + sim-optimal broker
Realizes the C10 reframe (cycle 0007): the strategy DAG's output is an
intent/exposure stream, and a sim-optimal broker integrates it into a synthetic
pip-equity curve that measures SIGNAL QUALITY — the project's first trading
result, and its primary research loop.
Two new aura-std nodes (plain structs; the engine stays domain-free):
- Exposure { scale }: the decision/sizing node — clamp(signal/scale, -1, +1),
one f64 exposure per fired cycle, None until warmed up. Sizing/risk live in
`scale`.
- SimBroker { pip_size }: class (a) of C10 — consumes exposure (slot 0) + price
(slot 1), integrates the return earned by the exposure HELD INTO each cycle
(prev_exposure, decided at t-1) so it is causal / look-ahead-free (C2), and
emits cumulative pips. pip_size is held reference metadata (C7/C15), never
streamed.
End-to-end proof in the aura-engine harness test module (it dev-depends on
aura-std): a SMA(2)-SMA(4) cross -> Exposure -> SimBroker -> Recorder sink wires
a full signal-quality backtest; the recorded equity matches a hand-computed pip
curve ([0,0,0,0, 0.035]) and is bit-identical across two runs (C1). The engine
itself is unchanged — pure node authoring on the frozen substrate.
Verified: cargo test --workspace green (20 core + 30 engine + 9 std; 8 new);
clippy --all-targets -D warnings clean; engine/core surface-purity grep (no
dyn Any/Rc/RefCell) clean.
Note: the plan inlined `Window::first()`, which aura-core's Window does not
expose; the shipped code uses the semantically identical `Window::get(0)`
(newest value, or 0.0 when the exposure leg is cold).
closes #4
closes #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
386e1a9c3d |
plan: 0007 signal-quality loop (exposure + sim-optimal broker)
Bite-sized, placeholder-free plan for spec 0007: Task 1 ships the Exposure node (clamp signal/scale to [-1,+1]) in aura-std; Task 2 the SimBroker node (causal lagged exposure*return -> cumulative pips); Task 3 two aura-engine end-to-end tests (pip-equity recording with a hand-computed curve + determinism); Task 4 the full workspace gates (test/clippy/purity grep). Each node's lib.rs module registration is folded into its own task so every per-task compile gate is satisfiable. refs #4 #5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe77b13e73 |
spec: 0007 signal-quality loop (exposure stream + sim-optimal broker)
Cycle 0007 bundles Gitea #4 (exposure node) and #5 (sim-optimal broker) — the two halves of the signal-quality loop, the realization of the C10 reframe. Ships two aura-std nodes: Exposure { scale } (clamp(signal/scale, -1, +1)) and SimBroker { pip_size } (causal prev_exposure * dprice / pip_size integration, no look-ahead). Engine unchanged — pure node authoring on the frozen substrate. grounding-check: PASS (all substrate assumptions ratified by green tests). refs #4 #5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0d7deb3a91 |
design(C10): reframe strategy output to intent/exposure stream; position table becomes a derived layer
The DAG expresses exactly one state at time t and a node emits at most one record per eval (C8), so a *sequence* of position events — where one decision instant (a stop-and-reverse) needs a close AND an open at the same event_ts — cannot be the DAG's per-cycle output without violating C8. The state the DAG can express faithfully is the desired exposure (one value per cycle); the buy/sell/close events are its first difference, a derived consequence. C10 is reframed accordingly: the strategy's primary, backtestable output is an intent/exposure stream (one signed, bounded f64 in [-1,+1] per cycle). Signal quality is measured by the sim-optimal broker integrating exposure*return into a synthetic pip-equity curve. The broker-independent position-event table survives as a decoupled, derivable, downstream position-management layer (computed table, not a per-eval output) feeding realistic broker nodes for viability/deploy — no longer the DAG output nor the signal-quality measure. Touches the ledger contract (INDEX.md C10 + provenance/milestone/C20 ancillary), the always-loaded summary (CLAUDE.md invariant #7), the glossary (broker, equity stream, position table, realistic broker, signal, sim-optimal broker, strategy reframed + new exposure-stream entry), and the north-star layout doc. Sealed specs/plans (0001-0006) left as historical record. refs #4 #5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f000373507 |
docs(C8): state output:vec![] as the sink declaration + eval return-width contract
Closes the spec_gap from the cycle-0006 fieldtest
(docs/specs/fieldtest-0006-substrate.md): the public surface never stated that
an empty `output` is THE pure-consumer (sink) declaration, nor defined what a
`Some` return paired with an empty output means.
The behaviour was already principled and defended in the engine — this only
documents it (no code change):
- `output: vec![]` is the pure-consumer declaration; there is no separate
Sink type/trait/marker.
- `eval` must return a row whose width equals `schema.output.len()`, so a pure
consumer returns `None` or a zero-width `Some(&[])`; the run loop
debug-asserts the width (harness.rs).
- field-wise wiring resolves `Edge::from_field` against the producer's output
at bootstrap, so no edge can bind a field of a zero-output node (it fails
with `BadIndex`) — a sink is structurally unwireable as an in-graph
producer; its only output is the out-of-graph side effect.
Ledger C8 gains an "Encoding & return contract" clause; aura-core node.rs
rustdoc (module, NodeSchema, Node trait) updated from the pre-0006 "1..K,
producer or transformer" wording to the three-role (producer/consumer/both)
contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0bf15cb069 |
fieldtest: cycle-0006 sink recording — 5 examples, 6 findings
First fieldtest of the project. A standalone downstream-consumer crate
(fieldtests/cycle-0006-substrate/) path-depends on the engine crates and
exercises the post-0006 substrate from the public interface only (rustdoc +
specs + ledger, never crates/ source):
1 custom recording node via the Node contract + Ctx::now()
2 fan-out/fan-in DAG, 3-arg bootstrap, run() -> ()
3 two interior streams recorded from one run (the cycle headline)
4 byte-identical recorded output across two runs (C1 determinism)
5 mis-wired recording edge rejected (KindMismatch) at bootstrap
Findings: 3 working (carry-on), 2 friction, 1 spec_gap.
- friction: a nested standalone consumer crate fights the workspace
resolver (needs an empty [workspace] table — Cargo's own hint).
- friction: recorder boilerplate is hand-rewritten per author (C9
deliberately ships no library sink).
- spec_gap: the public surface never states output: vec![] is THE sink
declaration, nor defines a Some return paired with empty output.
Spec at docs/specs/fieldtest-0006-substrate.md feeds the 0007 plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
fb2385d9de | chore: gitignore docs/postmortems/ (local run telemetry) | ||
|
|
e5d80959e0 |
test: M×N×K stress matrix — node fan-out, deep chains, wide layers
The milestone's green end-to-end gate (#3): prove the closed bootstrap + run substrate carries arbitrary node-level fan-out / fan-in / depth / width DAGs deterministically (C1) and computes every recorded stream correctly. Closes the two coverage holes the engine tests left open after cycle 0006 — node fan-out (one PRODUCING node read by several consumers; prior coverage was source fan-out only) and deep-chain / wide-layer topologies. Six tests appended to harness.rs's test module, composing the existing Sma / Sub / Recorder / BarrierSum fixtures (no new fixtures, no aura-std surface — C9): - node_fan_out_identical_taps_record_identical_streams - node_fan_out_divergent_consumers_each_compute_their_own - node_fan_out_under_mixed_firing_each_consumer_records_per_policy - deep_transform_chain_propagates_end_to_end - wide_parallel_layer_multi_sink_records_each_stream - milestone_end_to_end_mixed_dag_records_every_stream_deterministically Each asserts exact (Timestamp, Vec<Scalar>) recorded values (hand-computed from the SMA/Sub + firing/warm-up semantics) AND determinism via a second fresh-harness drain proving bit-identical streams. 28 engine tests green (22 + 6), clippy -D warnings clean. No engine misbehaviour surfaced. closes #3 |
||
|
|
3a02fbf383 |
audit: cycle 0006 tidy (clean) — glossary record-reality (sink is a role)
Architect drift review over 57310b8..HEAD: clean — no drift, no debt. The C1/C7 boundary is correctly drawn (recorded values escape only via a node-held mpsc::Sender with no out-edge; nothing recorded re-enters the graph; the engine surface shrank). C2 holds (Ctx::now() is the present cycle's ts). The C8/C22 cycle-0006 realization notes accurately describe what shipped — no overclaim. Test fixtures stayed in #[cfg(test)] (C9). Regression set is empty (profile) — architect is the sole gate; green. Record-reality: the glossary 'sink' entry defined a sink as 'a pure consumer node with no output', which the shipped TapForward node (records AND forwards an output) now contradicts. Updated to reality: a sink is a recording *role*, not a type — a node may be a pure consumer or record and produce in the same eval (C8 'both'). No invention; the entry now matches the substrate this cycle landed. |
||
|
|
1100a60c76 |
feat: recording is a node role, not a type (multi-sink substrate)
Replace the engine's single observe: usize recording affordance with recording-by-node, so one run records many streams. A recording node reads its typed input windows + ctx.now() in eval and pushes the record to a destination it holds as a field (a channel, a chart handle) — an out-of-graph side effect. There is no Sink type, trait, or engine flag: a pure-consumer node returns None, and a node may record AND return a forwarded output in the same eval (the C8 "both" case). In-graph routing stays engine-owned data (the edge table); the escape out of the graph is the node's own side effect, and that boundary is the determinism / graph-as-data boundary. Engine surface shrinks: Ctx gains now: Timestamp + now() (C2-causal, the present cycle's timestamp); Harness loses the observe field, its observe >= n bootstrap check, and the per-cycle observed-row collection; bootstrap drops its 4th param; run returns (). Recorded streams are now sparse and timestamped (a record per fired cycle) instead of the dense Vec<Option<row>>. The Ctx::new signature change touched 9 call sites across three crates (not the 3 the spec estimated) — aura-std's node tests and the engine run loop were threaded too. The engine test suite migrated to a test-local Recorder fixture whose read-back is an mpsc channel, never Rc/RefCell, keeping aura-engine/src purity-clean (C7). Eight new proof tests cover the multi-sink headline, producer-and-sink, mixed-kind recording, all-fields tap, both recorder firing modes, determinism, and recorder-edge kind rejection. C8/C22 gain cycle-0006 realization notes. Gates: workspace test 45 green (core 20, std 3, engine 22), clippy -D warnings clean, purity grep clean (only a comment names Rc/RefCell). closes #2 |
||
|
|
7b8e8010dd |
plan: 0006 sink recording
Six tasks: (1) aura-core Ctx::now(); (2) thread the Ctx::new signature change through aura-std's five test call sites; (3) aura-engine production shrink (remove observe, run -> (), thread the run-loop Ctx); (4) migrate the engine test suite to a test-local Recorder fixture + drained channel (the finish-threading task — the test module is one compile unit, so it restores the full cargo test); (5) eight new node-recording proof tests; (6) ledger C8/C22 realization notes + the ship gate (workspace test, clippy, purity grep). Recon found the spec undercounts Ctx::new call sites (9, not 3): the aura-std and aura-engine run-loop sites also break on the signature change and are threaded in tasks 2-3. refs #2 |
||
|
|
22ee99abcd |
spec: 0006 sink recording
A sink is a role, not a type: recording is an out-of-graph side effect a node performs in eval, pushing to a destination it holds as a field. No Sink type, no trait, no engine sink-awareness. The engine stays the router for in-graph routing (edge table = graph-as-data); the node-side push is the only out-of-graph escape, and that boundary is the determinism boundary. A node may be producer and sink at once (C8 both). Engine surface shrinks: Ctx gains now(); the observe index (field, bounds check, per-cycle collection) is removed; bootstrap drops its 4th param; run returns (). Recording streams are sparse and timestamped (only fired cycles), replacing the dense Vec<Option<row>>. Realization notes land on C8 and C22; no new contract. refs #2 |
||
|
|
57310b83c7 |
audit: cycle 0005 tidy — glossary record-reality (node-output-record)
Architect drift review over 9ff90b5..5228542 (node-output-record): drift_found,
all low-severity, NO contract violation. The load-bearing invariants hold:
- C7 forbids intact — the reused `scratch` buffer (cleared+extended, warm after
cycle 1) and node-owned `[Scalar; K]` output buffers give zero per-cycle heap
alloc on the inter-node forward path; no fifth scalar type, no dyn-Any, no
heterogeneous buffer (the TwoField mixed-kind producer is rejected at bootstrap,
never streamed). Purity grep clean. The sharpened C7 guarantee text matches the
shipped per-field kind check + the Edge::from_field scatter.
- C8 revised text faithful — one output port (out_len), record of 1..K columns
(Vec<FieldSpec>), field-wise binding only (N edges, no whole-record bind), scalar
= degenerate K=1. No internal contradiction with C7/C9/C10.
- C6 untouched, verified against the run loop — one eval per node per cycle, every
out-edge stamps SlotState { fresh_at: cycle_id, last_ts: ts } with the same ts,
so the K fields of one record are co-fresh by construction. fires() unchanged.
- C3/C4/C5/C9 unperturbed (diff touches only node/edge plumbing); C2 lookback
enforcement unperturbed (column.rs/ctx.rs/any.rs not in the diff).
- C10 coherence confirmed — a position event is a 5-field record
(event_ts/action/position_id/instrument_id/volume), exactly the K-field
base-column record C8/C7 now sanction; the decision/sizing node is an ordinary
K=5 producer, the broker an N-edge field-wise consumer. No contract gap blocks
the next consumer; C10 correctly out of scope this cycle.
Resolved [low] (the one confirmed drift) — glossary record-reality. The ledger
C7/C8 moved this cycle but the glossary lagged. Fixed three entries to the
post-0005 reality (this is audit's autonomous glossary write authority,
record-reality only): `node` ("at most one output" -> "at most one output port; a
producer's output is a record of 1..K base-scalar columns, scalar = degenerate
K=1"), `composite` (now connects the multi-column stream to the node-output model
— the record a producer's eval returns, bound field-wise), `edge` (forwards "one
field (from_field) of a producer's eval output record", per-field kind check).
The architect named `node` + `composite`; `edge` is an orchestrator-observed
addition in the same record-reality pass (post-from_field it read "forwards one
node's eval output", now imprecise for K>1).
Regression gate: profile regression list empty — architect is the sole gate, now
clean (36 tests: aura-core 19 + aura-std 3 + aura-engine 14; clippy -D warnings
clean; purity grep clean). Cycle 0005 is drift-clean.
Carry-on forward note (next-cycle owner, not a fix now):
- [low] cycle 0004's depth>1 join-lookback guard test remains merely OWED, not a
fresh hole — the substrate enforces it via Window bounds (column.rs/ctx.rs
untouched this cycle) and 0005 added no depth>1 join. Co-fresh borrowed-row
routing does not interact with per-input lookback. A confirmation test is still
owed when a depth>1 join lands.
NOT a milestone close: the walking-skeleton milestone still needs its end-to-end
fieldtest (ingest -> signal -> backtest -> position table -> broker -> pip-equity).
This cycle delivers the record-output substrate that the trading half (C10) now
builds on.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5228542bfd |
feat: node output is a record (composite stream), not a single scalar
Cycle 0005 (BLOCKER #1): a node's output generalizes from one scalar to a record of K >= 1 named base-scalar columns, with a scalar being the degenerate K = 1 case. A node keeps exactly one output port; its payload is now an ordered bundle of base columns (a composite stream, C7). This unblocks every multi-column producer the engine needs next -- OHLCV bars and, later, the C10 position-event table -- none of which could exist while a node emitted at most one column. Revises C8, sharpens C7 in the design ledger. Contract (aura-core): - `NodeSchema.output: ScalarKind` -> `Vec<FieldSpec>` (FieldSpec = { name: &'static str, kind }; the field position is what an Edge binds, the name is metadata for later sinks/playground, C18). - `Node::eval -> Option<Scalar>` -> `-> Option<&[Scalar]>`: a node fills a buffer it owns (sized once at construction) and returns a borrowed K-field row; `None` still means filter/not-warmed. This is the C7-faithful representation chosen with the user: zero per-cycle heap allocation on the forward path (rejected: Option<Vec<Scalar>> allocates per fire; an inline fixed [Scalar; N] bakes a width cap; engine-owned output columns invert the eval model). Scalar output is the degenerate 1-field record -- no separate scalar path. Field-wise binding (aura-engine): - `Edge` gains `from_field: usize` -- which producer column this edge forwards. Consuming a whole record is N edges; there is no "bind whole record" mechanism. - bootstrap kind-checks per field (`from.output.get(from_field)` -> BadIndex if out of range; `field.kind != slot.kind` -> KindMismatch). - the run loop copies a producer's returned row into one reused scratch buffer (resolving the borrow; no per-cycle alloc once warm) and scatters scratch[from_field] into each out-edge slot. `NodeBox.out_len` (set at bootstrap) backs a debug_assert on the returned row width. `run` returns `Vec<Option<Vec<Scalar>>>` (the observed node's full row per cycle -- a materialization-surface alloc, not the inter-node hot path). - the K fields of one record are co-fresh by construction (one eval, one timestamp), so C6 is untouched. aura-std: Sma/Sub migrate to the 1-field degenerate case (hold a [Scalar; 1] buffer; behaviour unchanged). Sub drops #[derive(Default)] (Scalar has no Default) for a manual Default. Proof (aura-engine tests, +5): an Ohlcv 5-field bundler fed by five timestamp-aligned barrier sources emits one complete bar per timestamp (ohlcv_bundles_five_field_record); a downstream Sub binds high(1)-low(2) field-wise, observed end-to-end + a determinism re-run (edge_binds_single_field_high_minus_low); a second consumer binds close(3)-open(0) on the same record (distinct_edges_read_distinct_fields); must-fail: bootstrap_rejects_from_field_out_of_range (BadIndex), bootstrap_rejects_per_field_kind_mismatch (a TwoField [f64,i64] producer, the i64 field into an f64 slot). All prior 0003/0004 tests adapted to from_field + the Vec return, expected vectors unchanged (scalar = 1-field record). Gates re-run by the orchestrator (not just the agent): cargo build/test --workspace --all-targets (36: aura-core 19 + aura-std 3 + aura-engine 14, 0 failed) / clippy --workspace --all-targets -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean. Ledger C8/C7 edits verified (Option<&[Scalar]> present, "one series per node" gone, cycle-0005 realization note added). The glossary composite/node record-reality pass is the audit-time follow-up. closes #1 refs walking-skeleton Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f95fe0f4e4 |
plan: 0005 node output record
Four tasks, ordered by compile-gate discipline (each leaves a coherent gate satisfiable): 1. aura-core contract (FieldSpec, NodeSchema.output -> Vec<FieldSpec>, Node::eval -> Option<&[Scalar]>) + aura-std Sma/Sub degenerate migration, threaded in one task. The signature change breaks aura-engine, so the gate is SCOPED to `-p aura-core -p aura-std` (engine is knowingly broken until task 3). 2. aura-engine production: Edge.from_field, NodeBox.out_len, per-field bootstrap kind/range check, reused-scratch field-indexed run forward, run -> Vec<Option<Vec<Scalar>>>. Breaks the in-module tests, so the gate is a PARTIAL `cargo build -p aura-engine --lib` + clippy --lib only. 3. aura-engine tests: migrate the 3 firing fixtures to the slice return; add the Ohlcv 5-field bundler + TwoField mixed-kind fixture; the field-binding proof (high-low, close-open), K>1 output, determinism, and must-fail (from_field OOB -> BadIndex, per-field kind mismatch -> KindMismatch); adapt all prior 0003/0004 tests to from_field + the Vec return. Full workspace gate + purity grep. 4. design ledger: C8 revision (one output port carrying a record) + C7 sharpening (composite-bundle is the node-output model). Docs-only; glossary record-reality is audit-time, not this plan. Orchestrator decisions baked in: the spec's illustrative `schemas_out_len[nidx]` is committed as a `NodeBox.out_len` field (set at bootstrap) read into a run-loop local; the firing fixtures hold a `[Scalar; 1]` buffer constructed at each call site; Sub drops `#[derive(Default)]` (Scalar has no Default) for a manual Default that keeps clippy's new_without_default quiet; the engine lib.rs doc is left untouched (no single-scalar phrase there -- only harness.rs doc + Edge doc move). plan-recon confirmed every spec-named path exists and the compile-driven site set (6 output: literals, 6 fn eval, 6 Edge literals, 11 .run callers); from_field / FieldSpec / Ohlcv are net-new. No content-pin/golden-file twin-miss risk. refs #1 refs walking-skeleton Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e9b4d51c30 |
spec: 0005 node output record
Cycle 0005 (BLOCKER, milestone walking-skeleton): generalize a node's output from a single scalar to a record of base-scalar columns (K >= 1), with a scalar being the degenerate K = 1 case. A node keeps exactly one output port; that port's payload becomes an ordered list of named, typed base columns (a composite stream, C7). This unblocks every multi-column producer the engine needs next -- OHLCV bars and, later, the C10 position-event table -- none of which can exist while a node emits at most one column. This revises C8 and sharpens C7. The load-bearing design call, made with the user: the eval output representation under C7's "no per-event heap allocation". eval becomes `-> Option<&[Scalar]>` -- the node fills a buffer it owns (sized once at construction) and returns a borrowed K-field row; the engine copies it into one reused scratch row (resolving the borrow, zero per-cycle alloc once warm) and scatters each field to its out-edges. `None` still means filter/not-warmed. Rejected alternatives, on substance: returning `Option<Vec<Scalar>>` allocates per fire (violates C7's forbids); an inline fixed `[Scalar; N]` record bakes a magic width cap; engine-owned output columns invert the eval model (write-into-out + a fired flag) and split "fired" from "produced" for an output-lookback benefit we do not need (history is bound as an input, not read from a node's own past). The borrowed row is the minimal faithful generalization of today's `Some(Scalar)` push route, K-wide. Binding stays field-wise only: `Edge` gains `from_field`; consuming a whole record is N edges; no "bind whole record" mechanism. The K fields of one record are co-fresh by construction (one eval, one timestamp), so C6 is untouched. The record is structural bundling over the four base types (C7 intact) -- no fifth scalar type, no dyn Any, no heterogeneous buffer. Scope is the substrate only: the contract change, the Sma/Sub degenerate migration, the engine from_field wiring, and a neutral OHLCV proof (a 5-field bundler fed by five timestamp-aligned barrier sources + a downstream Sub binding high - low field-wise, plus must-fail cases: from_field out of range -> BadIndex, per-field kind mismatch -> KindMismatch). The C10 position table and broker nodes are out of scope -- later cycles that consume this capability. Self-review clean; grounding-check PASS (14 load-bearing "before"-state assumptions ratified by named green tests; C6/C7/C8 match the ledger as written; parse-every-block a documented no-op -- profile declares no spec_validation). refs #1 refs walking-skeleton Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9ff90b5d80 |
audit: cycle 0004 tidy — diamond-barrier test + ledger/glossary record-reality
Architect drift review over 02fe891..f37b2a3 (firing-policies-and-merge):
drift_found, all medium/low, no contract violation. Invariants hold: C1 (the
merge is a deterministic linear scan, strict < on (timestamp, source-index) gives
C4 tie-break by declaration order; only per-call allocs), C3 (one merge at
ingestion), C5/C6 (both rails proven with worked vectors; sample-and-hold falls
out of the push model), C7 (type-erased edges, no dyn-Any/Rc/RefCell — purity grep
clean). Resolved the 0003 forward-note: cycle_id is now a real read field (the
freshness epoch, fresh_at == cycle_id), not write-only.
Resolved [med] (test gap): the barrier's timestamp token was proven only for the
multi-source same-timestamp case; C6's within-source diamond rejoin (one source,
two unequal-latency paths rejoining at a barrier) was claimed by the spec but
untested. Added `within_source_diamond_rejoin_barrier_fires`
(source -> {SMA(2), SMA(4)} -> BarrierSum): once SMA(4) warms at cycle 4, both
paths emit each cycle at the same timestamp and the barrier fires
(15+13, 17+15, 19+17). The code was already correct — every push in a cycle
stamps last_ts = that cycle's timestamp (source pushes and produced-edge forwards
alike) — so this closes the coverage gap, not a bug.
Resolved [med] (glossary record-reality): the run-count entry claimed it "drives
freshness-gated recompute", now false — the engine's firing gate reads the
per-wiring-slot cycle epoch (fresh_at), not Column::run_count. Sharpened the entry
to record reality: run-count is the node-visible per-column push counter;
fresh_at is the engine's per-slot freshness epoch. Not redundant (different
levels); both are maintained consistently on every push.
Resolved [low] (ledger silent on the barrier token): added a C6 "Realization
(cycle 0004)" note recording the load-bearing decision — barrier token is the
timestamp not cycle_id (forced by C4's same-timestamp-tie rule), firing tagged
per input with Firing::{Any, Barrier(u8)}, a mode-A input being its own trivial
group (so "per input group" and the per-input tag coincide).
Carry-on forward notes (next-cycle owners, not fixes now):
- [low] cycle_id is materialized internally (the freshness epoch) but not yet
surfaced in run output (run returns Vec<Option<Scalar>>). Its first observable
consumer is a sink / clock node (C18) or a timestamped trace — materialize the
surfacing then.
- [low] all join fixtures declare lookback 1; a test guarding C8's "a node never
reads beyond its declared lookback" for a depth>1 join is still owed (the
substrate already enforces it via Window bounds, so this is confirmation, not a
hole).
Regression gate: profile regression list empty — architect is the sole gate, now
clean (31 tests: aura-core 19 + aura-std 3 + aura-engine 9; clippy -D warnings
clean; purity grep clean). Cycle 0004 is drift-clean. NOT a milestone close: the
walking-skeleton milestone still needs its end-to-end fieldtest (ingest -> signal
-> backtest -> position table -> broker -> pip-equity), of which this cycle
delivers the firing/merge layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f37b2a35d3 |
feat(engine): firing policies A/B + the k-way ingestion merge
Cycle 0004: the reactive-semantics layer. The engine now merges multiple
timestamped sources into one chronological cycle stream (C3) and gates each
node's re-evaluation by a per-input firing policy (C5/C6) — both rails shipping
together, as the user required ("must sit right from the start").
- `Firing` (aura-core) — a per-input enum on `InputSpec`, where C8 places firing:
`Any` (mode A, fire-on-any-fresh + hold / as-of join) and `Barrier(u8)` (mode B,
all-fresh barrier / synchronizing join). Default-free; Sma/Sub declare
`Firing::Any`, so under a single source ticking every cycle their behavior is
unchanged (backward compatible — the 0003 fan-out/join DAG still emits
[None,None,None,2,2,2]).
- `SourceSpec` + the k-way merge (aura-engine) — `run` takes one
`(Timestamp, Scalar)` stream per source and merges them by (timestamp, source
index) (C4 ties by declaration order). One record = one cycle. This is the
permanent ingestion core; the Source trait + data-server IO (C3/C11) are a
later orthogonal cycle (the user scoped this cycle to the reactive semantics).
- Two read clocks, both load-bearing (resolves the 0003 audit's "0004 owns
cycle_id" with no write-only field): each push stamps the input slot with
`fresh_at` (the cycle_id — freshness epoch, C5: fresh iff fresh_at == cycle_id)
and `last_ts` (the data timestamp — barrier token, C6: a barrier group is
complete iff all members carry last_ts == T). The `fires()` predicate ORs the
groups; the ">=1 member fresh this cycle" clause is the once-per-timestamp
guard. Sample-and-hold falls out of the push model: a non-firing node pushes
nothing, so consumers keep seeing the held value via window[0].
Key design call, studied against RustAst (src/ast/rtl/streams/): the barrier
token is the data *timestamp*, not cycle_id. RustAst's
last_cycle_per_input.all(== cycle_id) synchronizes one source's fan-out but
cannot synchronize independent sources — under C4 four same-timestamp sources
are four different cycle_ids, so the cycle_id barrier never fires for C6's own
OHLC example. Substituting timestamp is the minimal faithful generalization
(fires both the diamond rejoin and the multi-source bar). Mode A and the k-way
merge are both new (RustAst had neither); RustAst's total_count push counter is
aura's already-built Column::run_count.
Both rails proven on identical sources, firing-only difference:
- mode A (AsOfSum): [None,None,120,130,140,240] — holds the slow input;
- mode B (BarrierSum): [None,None,120,None,None,240] — waits for timestamp
coincidence;
- mixed A+B (MixedSum): the OR-combine fires both when the barrier pair completes
(holding the as-of input) and when the as-of input ticks (holding the pair).
Plus determinism (C1, bit-identical re-run of each rail) and the three bootstrap
rejections re-expressed on SourceSpec.
Gates green (re-run by the orchestrator, not just the agent): cargo build/test
(30: aura-core 19 + aura-std 3 + aura-engine 8) / clippy --workspace
--all-targets -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell)
clean — the harness doc phrases RustAst's model without the literal tokens to
avoid self-match.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
10a40127b6 |
plan: cycle 0004 firing policies and merge
Three tasks, ordered by compile-gate discipline (each leaves a coherent build
gate satisfiable):
1. aura-core Firing enum + InputSpec.firing, threaded through aura-std Sma/Sub
in the same task (the field addition breaks both callers) -> full workspace
gate green.
2. aura-engine production rewrite: SourceSpec, SlotState (fresh_at + last_ts, the
two read clocks), NodeBox/Harness fields, the k-way merge run loop, and the
fires() firing predicate. The bootstrap/run signature change breaks the
in-module tests, so this task ends on a PARTIAL gate (cargo build -p
aura-engine --lib, which excludes #[cfg(test)]).
3. aura-engine tests: rewrite the 5 cycle-0003 tests onto the multi-source API
and add the firing rails -- mode A as-of hold, mode B barrier wait, mixed A+B
OR-combine, each with worked expected vectors -> full workspace gate.
Orchestrator decisions baked in: run length-mismatch is a caller bug -> assert_eq!
(not a typed error, not a silent no-op); historical specs/plans (0002/0003) pin
pre-cycle disk state and are left frozen; the test module gets an explicit
`use aura_core::{InputSpec, NodeSchema}` (harness.rs production never names those
types, so `use super::*` does not bring them, and putting them in the top-level
use would be an unused-import under -D warnings); the harness doc keeps RustAst's
model phrased without the literal Rc/RefCell tokens so the purity grep does not
self-match.
Grounding-check on the parent spec PASS; plan-recon confirmed every spec-named
path exists and the "before" shapes match disk.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4cbb7795ef |
spec: cycle 0004 firing policies and merge
Fourth walking-skeleton cycle: the reactive-semantics layer — both firing
policies (C6), the k-way ingestion merge (C3), the monotonic cycle_id clock
(C4), and freshness-gated recompute / sample-and-hold (C5). This is the first
point where the engine departs from RustAst rather than sharpening it.
Both rails ship together (user directive: "must sit right from the start"):
- A = fire-on-any-fresh + hold (as-of join);
- B = all-fresh barrier (synchronizing join), accumulating across cycles.
Per-input-group, mixable on one node. Encoded as Firing::{Any, Barrier(u8)} on
InputSpec (where C8 places firing); default Any keeps Sma/Sub unchanged.
Key design resolution (studied against RustAst src/ast/rtl/streams/): the
barrier token is the data *timestamp*, not cycle_id. RustAst's
last_cycle_per_input.all(== cycle_id) synchronizes one source's fan-out but
cannot synchronize independent sources — under C4 four same-timestamp sources
are four different cycle_ids, so the cycle_id barrier never fires for C6's own
OHLC example. Substituting timestamp is the minimal faithful generalization
(fires both the diamond rejoin and the multi-source bar). Mode A and the k-way
merge are both new (RustAst had neither).
Two read clocks, both load-bearing (resolves the 0003 audit's "0004 owns
cycle_id" with no write-only field): cycle_id is the freshness epoch (C5 reads
it via per-slot fresh_at == cycle_id); timestamp is the barrier token (C6 reads
it via last_ts == T). Sample-and-hold falls out of the existing push model — a
non-firing node pushes nothing, so consumers see the held value via window[0].
Scope (user-chosen): reactive semantics only — in-memory (timestamp, value)
sources; the merge is the permanent ingestion core, the Source trait (C11) +
data-server IO are a later orthogonal cycle. bootstrap/run/InputSpec signatures
change (the 0003 tests re-express on the multi-source API).
Headline test pair (identical sources, firing-only difference): A emits
[None,None,120,130,140,240] (holds); B emits [None,None,120,None,None,240]
(waits for timestamp coincidence). Plus determinism (C1), mixed A+B, the 0003
fan-out/join compat baseline, and the bootstrap rejections.
Grounding-check PASS: 10 load-bearing substrate assumptions ratified by green
tests; the spec's "before" shapes match disk exactly.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
02fe89176f |
audit: cycle 0003 tidy — rename Sim -> Harness (glossary alignment)
Architect drift review over 71c8f6e..dd5c3fa (deterministic-sim-loop). Invariants hold: C1 (single-threaded, bit-identical re-run, alloc-free per-cycle ring path), C7 (AnyColumn type-erased edges, Box<dyn Node> is the node object not a dyn-Any payload, kinds checked once at bootstrap), C8/C9 (DAG wired, Kahn rejects cycles, None forwards nothing = sample-and-hold seed). Column::run_count already exists, so the C5 freshness primitive lives at the substrate — the deferral is honest, not a hole. Resolved [high]: the runtime type was named `Sim`, colliding with the glossary's load-bearing distinction — `sim` is the disjoint *executable unit / parallelism* framing, whereas the thing built here (a closed root graph of source + nodes + observe under a clock) is by C20 / the glossary a **harness**. Renamed `Sim` -> `Harness`, `sim.rs` -> `harness.rs`, and the module/exports/doc to match, before any other code takes a dependency on the wrong term. Gates re-run green (26 tests, clippy -D warnings, purity grep). Resolved [med] (glossary gap): added an `edge` entry anchoring the new wiring vocabulary (Edge / source target) to the glossary — record-reality per the boss glossary-write authority. Carry-on with forward notes (next-cycle owners, not fixes now): - [med] C4 monotonic cycle_id has no realization in the runtime type yet (the cycle clock is the record iteration; the explicit counter's first reader is freshness). Cycle 0004 owns materializing it together with C5. - [low] Sub declares lookback 1; once depth>1 join nodes exist, add a test guarding C8's "a node never reads beyond its declared lookback". Regression gate: profile regression list empty — architect is the sole gate, now clean. Next direction: freshness-gated recompute + a second source (C5/C3). |
||
|
|
dd5c3fad96 |
feat(engine): the deterministic single-source sim loop
Cycle 0003: aura-engine's first real content — a Sim that runs a wired DAG of
nodes deterministically, cycle by cycle. First point in the project where
authored nodes execute against data, not just under a hand-fed Ctx.
- `Sim` (aura-engine) — a bootstrapped, frozen root graph: a flat Vec<NodeBox>
(each node owns its input columns, the cycle-0002 shape) + an index edge
table, topologically ordered (Kahn). `bootstrap` sizes every input column from
its node's schema, kind-checks each edge and source target, and rejects
directed cycles — C7's "type check paid once at wiring" generalized to the
whole topology (`BootstrapError::{KindMismatch, BadIndex, Cycle}`).
- `run` — the deterministic loop: per record, forward the source value into its
target slots, evaluate nodes in topo order, capture the observed node's output
at eval time, and forward each `Some` output into its consumers' input columns
(a `None` forwards nothing — the structural seed of sample-and-hold). The loop
destructures `&mut self` into disjoint field borrows and allocates nothing on
the per-cycle path. This is the flat, monomorphized sharpening of RustAst's
reference-counted, interior-mutable observer push graph (C1/C7).
- `Edge` / `Target` (aura-engine) — producer->consumer and source->consumer wiring.
- `Sub` (aura-std) — a 2-input f64-difference node, so the loop is proven on a
real fan-out + join DAG (source -> {SMA(2), SMA(4)} -> Sub), with a determinism
assertion (C1: a second identical run is bit-identical).
Engine library depends only on aura-core; a test-only dev-dependency on aura-std
lets the integration test wire real Sma/Sub nodes. Sim carries a hand-written,
node-opaque `Debug` impl (Box<dyn Node> is not Debug; the impl prints
nodes.len() + the index/topology fields, needed by the bootstrap-rejection tests'
`unwrap_err`).
Deliberately deferred (recorded as decisions): freshness-gated recompute /
sample-and-hold (C5 -> cycle 0004, with the second source that makes it testable);
the explicit monotonic cycle_id counter (its first reader is freshness, 0004);
the Source trait + data-server ingestion + k-way merge (C3/C11); the builder API
(C19); the real sink + run registry (C18/C22).
Gates green: cargo build/test (26: 18 aura-core + 3 aura-std + 5 aura-engine)/
clippy -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean.
refs walking-skeleton
|
||
|
|
0419e54987 |
plan: cycle 0003 deterministic single-source sim loop
Three tasks, each leaving the workspace green: 1. Sub 2-input difference node in aura-std (+ test). 2. Sim in aura-engine: Edge/Target/BootstrapError + bootstrap (schema-sizing, edge kind-check, Kahn topo-sort, cycle/bad-index/kind-mismatch rejection) + the forwarding run loop (disjoint &mut-self destructure, no per-cycle alloc), with 5 tests (chain, fan-out/join + determinism, cycle, kind-mismatch, bad-index). aura-engine gains a test-only dev-dependency on aura-std. 3. Workspace gate: build / test (26) / clippy -D warnings / purity grep. Orchestrator decisions baked in: observe captured at eval-time (no output column); Sim stores only fields run reads (no write-only cycle_id/source_kind that would trip `field never read`); the sim.rs doc avoids the literal Rc/RefCell tokens so the purity grep does not self-match. |