diff --git a/Cargo.lock b/Cargo.lock index 76ac85f..02f901f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,10 @@ dependencies = [ [[package]] name = "aura-core" version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] [[package]] name = "aura-engine" @@ -65,6 +69,8 @@ version = "0.1.0" dependencies = [ "aura-core", "aura-std", + "serde", + "serde_json", ] [[package]] @@ -418,6 +424,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jobserver" version = "0.1.34" @@ -597,6 +609,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -617,6 +639,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha1" version = "0.10.6" @@ -898,6 +933,12 @@ dependencies = [ "zstd", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 587ee9f..8d6934e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,7 @@ edition = "2024" version = "0.1.0" license = "proprietary" publish = false + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/crates/aura-core/Cargo.toml b/crates/aura-core/Cargo.toml index 268bb91..8689637 100644 --- a/crates/aura-core/Cargo.toml +++ b/crates/aura-core/Cargo.toml @@ -6,3 +6,7 @@ license.workspace = true publish.workspace = true [dependencies] +serde = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/crates/aura-core/src/scalar.rs b/crates/aura-core/src/scalar.rs index c184dd0..21412a2 100644 --- a/crates/aura-core/src/scalar.rs +++ b/crates/aura-core/src/scalar.rs @@ -4,7 +4,7 @@ /// Canonical engine time: epoch-nanoseconds UTC. Newtype over `i64` (C7). /// `Default` (epoch 0) lets `Column` pre-size its ring. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)] pub struct Timestamp(pub i64); /// The kind tag of a scalar / column, used for the edge-time type check (C7). @@ -108,4 +108,15 @@ mod tests { assert_eq!(a, b); assert_eq!(s, a); } + + #[test] + fn timestamp_serde_round_trips() { + let ts = Timestamp(1_700_000_000_000_000_000); + let json = serde_json::to_string(&ts).expect("serialize Timestamp"); + // a newtype struct serializes transparently as its inner i64 — this is + // what lets RunManifest's `window` render as a [from, to] integer array + assert_eq!(json, "1700000000000000000"); + let back: Timestamp = serde_json::from_str(&json).expect("deserialize Timestamp"); + assert_eq!(back, ts); + } } diff --git a/crates/aura-engine/Cargo.toml b/crates/aura-engine/Cargo.toml index 68185d3..c10ed0b 100644 --- a/crates/aura-engine/Cargo.toml +++ b/crates/aura-engine/Cargo.toml @@ -7,9 +7,11 @@ publish.workspace = true [dependencies] aura-core = { path = "../aura-core" } -# No external dependencies: aura-engine is an engine crate and stays -# dependency-pure. The data-server ingestion edge lives in the aura-ingest crate -# (the external-dependency firewall — cycle 0011, INDEX.md C16), never here. +# serde is admitted under the amended C16 per-case dependency policy (INDEX.md): +# it gives the run-report types a typed (de)serialization path for the run +# registry (cycle 0029). serde's output is deterministic (C1-safe). +serde = { workspace = true } [dev-dependencies] aura-std = { path = "../aura-std" } +serde_json = { workspace = true } diff --git a/crates/aura-engine/src/report.rs b/crates/aura-engine/src/report.rs index 8b4c63b..5ee7325 100644 --- a/crates/aura-engine/src/report.rs +++ b/crates/aura-engine/src/report.rs @@ -3,15 +3,15 @@ //! **post-run pure reduction** over a run's recorded streams — a node cannot //! reduce end-of-run (C8 caps a node at one record per `eval`, with no terminal //! `eval`), so the World drains its recording sinks after [`Harness::run`](crate::Harness::run) -//! and folds them here. Output is canonical, hand-rolled JSON (C14): the schema -//! is tiny, closed, and flat, so the deliberately zero-dependency workspace -//! stays zero-dependency. +//! and folds them here. Output is canonical JSON (C14): the schema is tiny, +//! closed, and flat. `to_json` is a hand-rolled writer; the report types also +//! derive serde (cycle 0029) for the run registry's typed read-path. use aura_core::{Scalar, Timestamp}; /// Summary metrics reduced from a run's recorded streams — the `-> metrics` /// half of C12's atomic sim unit. Pure function of the recorded streams. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct RunMetrics { /// Final cumulative pip equity — the last value of the (cumulative) /// pip-equity curve. `0.0` if the curve is empty. @@ -30,7 +30,7 @@ pub struct RunMetrics { /// The reproducible run descriptor (C18). **Caller-supplied**: the engine /// cannot introspect a git commit, an RNG seed, or a broker label — the World /// that bootstraps and runs the harness fills these in. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct RunManifest { /// Node/engine identity: the git commit of the frozen artifact (C18 — /// commit = identity; the frozen bot *is* a commit). @@ -49,7 +49,7 @@ pub struct RunManifest { /// A run's full structured result: the descriptor plus the metrics it /// reproduces. The durable run record of C18 ("stores manifests + metrics, /// re-derives full results on demand"). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct RunReport { pub manifest: RunManifest, pub metrics: RunMetrics, @@ -57,8 +57,8 @@ pub struct RunReport { impl RunReport { /// Render canonical, machine-readable JSON (C14). Hand-rolled — the schema - /// is tiny, closed, and flat, so the deliberately zero-dependency workspace - /// stays so. + /// is tiny, closed, and flat. (The same types also derive serde for the run + /// registry's typed read-path, cycle 0029; this writer is kept for stdout.) /// /// # Shape /// @@ -410,4 +410,27 @@ mod tests { r#"{"manifest":{"commit":"abc123","params":{"sma_fast":2,"sma_slow":4,"exposure_scale":1},"window":[1,6],"seed":0,"broker":"sim-optimal(pip_size=1.0)"},"metrics":{"total_pips":12,"max_drawdown":1,"exposure_sign_flips":1}}"#, ); } + + #[test] + fn runreport_serde_round_trips() { + let report = RunReport { + manifest: RunManifest { + commit: "abc123".to_string(), + params: vec![ + ("sma_fast".to_string(), 2.0), + ("sma_slow".to_string(), 4.0), + ("exposure_scale".to_string(), 1.0), + ], + window: (Timestamp(1), Timestamp(6)), + seed: 0, + broker: "sim-optimal(pip_size=1.0)".to_string(), + }, + metrics: RunMetrics { total_pips: 12.0, max_drawdown: 1.0, exposure_sign_flips: 1 }, + }; + let json = serde_json::to_string(&report).expect("serialize RunReport"); + // window is a 2-element [from, to] array (Timestamp newtype is transparent) + assert!(json.contains("\"window\":[1,6]"), "window shape: {json}"); + let back: RunReport = serde_json::from_str(&json).expect("deserialize RunReport"); + assert_eq!(back, report); + } } diff --git a/crates/aura-ingest/Cargo.toml b/crates/aura-ingest/Cargo.toml index 39e7a2e..bed3f28 100644 --- a/crates/aura-ingest/Cargo.toml +++ b/crates/aura-ingest/Cargo.toml @@ -7,9 +7,9 @@ publish.workspace = true [dependencies] aura-core = { path = "../aura-core" } -# data-server: aura's first real data source AND its one external dependency -# (transitively chrono + regex + zip). Isolated in this crate — the firewall -# that keeps aura-core/std/engine zero-external-dep (spec §Architecture). +# data-server: aura's first real data source. It enters the workspace at the +# ingestion edge (transitively chrono + regex + zip). Dependencies are admitted +# by the amended C16 per-case policy (INDEX.md), not a blanket zero-dep wall. data-server = { git = "http://192.168.178.103:3000/Brummel/data-server.git", branch = "main" } [dev-dependencies] diff --git a/crates/aura-ingest/src/lib.rs b/crates/aura-ingest/src/lib.rs index f5d178e..d9a4204 100644 --- a/crates/aura-ingest/src/lib.rs +++ b/crates/aura-ingest/src/lib.rs @@ -7,9 +7,10 @@ //! as the engine source-stream shape (`Vec<(Timestamp, Scalar)>`) the SMA-cross //! sample strategy consumes. //! -//! This crate is the workspace's **external-dependency firewall**: it is the -//! only crate that links `data-server` (and its transitive `chrono`/`regex`/ -//! `zip`), so `aura-core`/`aura-std`/`aura-engine` stay zero-external-dependency. +//! This crate is the **data-source ingestion edge**: it links `data-server` +//! (and its transitive `chrono`/`regex`/`zip`) and normalizes it into aura's +//! columns here. Dependencies follow the amended C16 per-case policy (INDEX.md), +//! not a blanket zero-dependency wall. use aura_core::{Scalar, Timestamp}; use data_server::records::M1Parsed; diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index deb4395..79398b4 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -548,18 +548,23 @@ chosen strategy + broker freeze into a standalone binary. **A project is therefore always a Rust program built on the engine.** Beyond the node-reuse tiers, the engine workspace also carries non-node crates — `aura-engine` (the run loop), `aura-cli` (the `aura` binary), and **`aura-ingest`** (the -data-source ingestion edge, cycle 0011). `aura-ingest` is the -**external-dependency firewall**: it alone links external crates (via -`data-server`), keeping the other engine crates and the frozen deploy artifact -(C13) dependency-pure (see External components). The engine workspace is -**zero-external-dependency** by commitment — the hand-rolled JSON of C14/C18 and -the from-scratch `aura-core` substrate exist precisely to honour it; the one -sanctioned external tree enters at the ingestion edge and is firewalled there. +data-source ingestion edge, cycle 0011, where the `data-server` external tree +enters). **Dependency policy (amended 2026-06-10, cycle 0029).** Dependencies +are admitted by deliberate, **per-case review** — what a crate pulls in weighed +against what it buys — with **particular scrutiny for anything that enters the +frozen deploy artifact** (C13: this bot = this commit). Well-established +standard crates (`serde`, `rayon`, …) pass that review and are used wherever +they do the job, including in the bot. There is **no blanket zero-dependency +commitment** and **no blanket admission**; hand-rolling what a vetted standard +crate already does is the anti-pattern, not the dependency. This strikes the +original *"zero-external-dependency by commitment"* clause and the +*"`aura-ingest` is the sole external-dependency firewall"* framing; `aura-ingest` +remains the data-source ingestion edge, no longer a dependency wall. **Forbids.** Project-specific signals in the aura repo (it keeps at most example/fixture nodes under `examples/` for its own tests); a multi-project manager inside aura; a bespoke node registry/marketplace (cargo + Gitea *is* the -package mechanism); an external (crates.io / git) dependency in any engine crate -other than the ingestion-edge crate `aura-ingest`. +package mechanism). (Dependency admission is governed by the per-case policy +above, not a blanket ban.) **Why.** The engine/game split keeps the engine sharp and reusable while each project versions its own research with its own forward-queue. Promotion (local → shared → std) is the ordinary Rust reuse gradient, no new mechanism.