plan: 0033 unify-runreport-json-serde
Two-task RED→GREEN plan for spec 0033. Task 1 (RED, test-only): flip the engine canonical-form golden + the CLI sweep golden to the serde array-of-pairs/.0-float shape, add the to_json_equals_serde_disk_shape pin test — all fail against the current hand-rolled to_json. Task 2 (GREEN, atomic): promote serde_json dev-dep -> normal dep, swap to_json's body to serde_json::to_string, delete the now-unused json_str helper, refresh the method + module-header doc comments — one build+clippy gate flips every Task-1 assertion green. graph_model.rs:30 + INDEX.md:729 stale cross-refs deferred to cycle-close audit. refs #54
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# Unify RunReport JSON (serde) — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0033-unify-runreport-json-serde.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Render `RunReport` stdout JSON through serde (the same encoder the run
|
||||
registry uses on disk), retiring the hand-rolled `to_json` writer, so a record's
|
||||
stdout bytes equal its `runs.jsonl` bytes — pinned by a test.
|
||||
|
||||
**Architecture:** `RunReport` already derives serde; the registry already
|
||||
serializes via `serde_json::to_string`. This cycle swaps `RunReport::to_json`'s
|
||||
body to delegate to `serde_json::to_string`, deletes the now-unused hand-rolled
|
||||
`json_str` helper, promotes `serde_json` from a dev-dep to a normal dep of
|
||||
`aura-engine`, and flips the two RunReport JSON goldens (engine canonical-form +
|
||||
CLI sweep) to the serde shape. RED-first: Task 1 moves the goldens + adds the pin
|
||||
test (RED against the current hand-rolled writer); Task 2 swaps the body + dep
|
||||
(GREEN).
|
||||
|
||||
**Tech Stack:** `aura-engine` (`report.rs`, `Cargo.toml`), `aura-cli` integration
|
||||
goldens (`tests/cli_run.rs`), serde / serde_json.
|
||||
|
||||
**Parse-the-bytes gate (self-review #9):** the profile declares no
|
||||
`spec_validation` → the gate is a no-op. All inlined bodies are Rust (caught by
|
||||
the `implement` compile gate) or TOML; the JSON goldens live inside Rust string
|
||||
literals. No surface-language snippet needs a parser.
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-engine/src/report.rs:6-8` — module-header doc: drop the
|
||||
now-false "`to_json` is a hand-rolled writer" claim.
|
||||
- Modify: `crates/aura-engine/src/report.rs:59-103` — `RunReport::to_json` doc
|
||||
comment + body swapped to serde.
|
||||
- Modify: `crates/aura-engine/src/report.rs:159-175` — delete the `json_str`
|
||||
free function (unused after the swap).
|
||||
- Test: `crates/aura-engine/src/report.rs:389-412` — canonical-form golden
|
||||
updated to the serde shape.
|
||||
- Test: `crates/aura-engine/src/report.rs` (tests module, after line 412) — new
|
||||
`to_json_equals_serde_disk_shape` pin test.
|
||||
- Modify: `crates/aura-engine/Cargo.toml:8-17` — `serde_json` dev-dep → normal
|
||||
dep.
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs:213-216` — sweep golden `params`
|
||||
substring updated to the serde array-of-pairs shape.
|
||||
|
||||
**Out of scope (deferred to cycle-close audit, per the spec's INDEX.md:729
|
||||
deferral precedent):** `crates/aura-engine/src/graph_model.rs:30` stale
|
||||
cross-reference to `RunReport::json_str` (a different file, #58/#28 territory —
|
||||
not widened into here); `docs/design/INDEX.md:729` ledger cross-ref.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Move the goldens + add the pin test (RED)
|
||||
|
||||
All edits here are test/golden code only. `serde_json` is already a dev-dep, so
|
||||
the new pin test compiles without the Task-2 dep promotion. Against the current
|
||||
hand-rolled `to_json` (params-object / whole-int floats), all three assertions
|
||||
FAIL — the intended RED state proving the serde shape genuinely differs.
|
||||
|
||||
**Files:**
|
||||
- Test: `crates/aura-engine/src/report.rs:389-412` (canonical-form golden)
|
||||
- Test: `crates/aura-engine/src/report.rs` (new pin test after line 412)
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs:213-216` (sweep golden)
|
||||
|
||||
- [ ] **Step 1: Update the engine canonical-form golden to the serde shape**
|
||||
|
||||
In `crates/aura-engine/src/report.rs`, in `to_json_renders_the_canonical_form`,
|
||||
replace the expected raw-string literal (currently line 410) — only the
|
||||
`r#"..."#` argument changes, the `RunReport { ... }` fixture above it is
|
||||
untouched:
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
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}}"#,
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
r#"{"manifest":{"commit":"abc123","params":[["sma_fast",2.0],["sma_slow",4.0],["exposure_scale",1.0]],"window":[1,6],"seed":0,"broker":"sim-optimal(pip_size=1.0)"},"metrics":{"total_pips":12.0,"max_drawdown":1.0,"exposure_sign_flips":1}}"#,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the `to_json_equals_serde_disk_shape` pin test**
|
||||
|
||||
In `crates/aura-engine/src/report.rs`, insert this test immediately after
|
||||
`to_json_renders_the_canonical_form` (after its closing `}` at line 412, before
|
||||
`runreport_serde_round_trips`). `serde_json` is called fully-qualified (the
|
||||
existing `runreport_serde_round_trips` does the same — no new `use` needed):
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn to_json_equals_serde_disk_shape() {
|
||||
// the same RunReport value the canonical-form test builds.
|
||||
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 },
|
||||
};
|
||||
// stdout (to_json) and disk (serde_json::to_string) are now the same bytes.
|
||||
assert_eq!(report.to_json(), serde_json::to_string(&report).unwrap());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the CLI sweep golden to the serde shape**
|
||||
|
||||
In `crates/aura-cli/tests/cli_run.rs`, in
|
||||
`sweep_prints_four_json_lines_and_exits_zero`, replace the `params` assert
|
||||
(lines 213-216):
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
assert!(
|
||||
first.contains("\"params\":{\"sma_cross.fast.length\":2,\"sma_cross.slow.length\":4,\"exposure.scale\":0.5}"),
|
||||
"got: {stdout}"
|
||||
);
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
assert!(
|
||||
first.contains("\"params\":[[\"sma_cross.fast.length\",2.0],[\"sma_cross.slow.length\",4.0],[\"exposure.scale\",0.5]]"),
|
||||
"got: {stdout}"
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify the engine canonical-form golden FAILS (RED)**
|
||||
|
||||
Run: `cargo test -p aura-engine to_json_renders_the_canonical_form`
|
||||
Expected: FAIL — `assert_eq!` left/right mismatch (current hand-rolled output has
|
||||
`"params":{...}` and whole-int floats; the new expected has `"params":[[...]]`
|
||||
and `.0` floats).
|
||||
|
||||
- [ ] **Step 5: Verify the pin test FAILS (RED)**
|
||||
|
||||
Run: `cargo test -p aura-engine to_json_equals_serde_disk_shape`
|
||||
Expected: FAIL — `report.to_json()` (hand-rolled, object/int) `!=`
|
||||
`serde_json::to_string(&report)` (array-of-pairs / `.0`).
|
||||
|
||||
- [ ] **Step 6: Verify the CLI sweep golden FAILS (RED)**
|
||||
|
||||
Run: `cargo test -p aura-cli sweep_prints_four_json_lines_and_exits_zero`
|
||||
Expected: FAIL — the spawned binary still renders the params-object form, so
|
||||
`first.contains("\"params\":[[...]]")` is false (panic with `got: {stdout}`
|
||||
showing the object form).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Swap the body + promote the dep (GREEN)
|
||||
|
||||
Promote `serde_json` to a normal dep, swap `to_json`'s body to serde, delete the
|
||||
now-unused `json_str`, and refresh the two doc comments. These MUST land together:
|
||||
the body swap references `serde_json` in non-test code (needs the normal dep), and
|
||||
deleting `json_str` while the hand-rolled body still calls it would not compile —
|
||||
so the dep move, the body swap, and the `json_str` deletion are one atomic task
|
||||
with a single build+clippy gate. This flips every Task-1 assertion to GREEN.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/Cargo.toml:8-17`
|
||||
- Modify: `crates/aura-engine/src/report.rs:6-8` (module header)
|
||||
- Modify: `crates/aura-engine/src/report.rs:59-103` (`to_json` doc + body)
|
||||
- Modify: `crates/aura-engine/src/report.rs:159-175` (delete `json_str`)
|
||||
|
||||
- [ ] **Step 1: Promote `serde_json` to a normal dependency**
|
||||
|
||||
In `crates/aura-engine/Cargo.toml`, move `serde_json` from `[dev-dependencies]`
|
||||
to `[dependencies]`. The two blocks (lines 8-17) become:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
aura-core = { path = "../aura-core" }
|
||||
# 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 }
|
||||
# serde_json renders RunReport JSON for both the registry (disk) and stdout
|
||||
# (RunReport::to_json) — one shape, no hand-rolled writer (amended C16).
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
aura-std = { path = "../aura-std" }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Swap `to_json`'s doc comment + body to serde**
|
||||
|
||||
In `crates/aura-engine/src/report.rs`, replace the whole `to_json` doc comment +
|
||||
method (lines 59-103 — from the `/// Render canonical…` doc block through the
|
||||
method's closing `}`) with:
|
||||
|
||||
```rust
|
||||
/// Render the canonical, machine-readable JSON (C14) via serde — the same
|
||||
/// encoder the run registry uses on disk, so a record's stdout shape and its
|
||||
/// `runs.jsonl` shape are byte-identical. `params` is an array of
|
||||
/// `[name, value]` pairs; finite `f64` fields carry a fractional part
|
||||
/// (`2.0`). Consumers must parse values as numbers, never key off the token
|
||||
/// shape.
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).expect("a finite RunReport always serializes")
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Delete the now-unused `json_str` helper**
|
||||
|
||||
In `crates/aura-engine/src/report.rs`, delete the entire `json_str` free function
|
||||
(lines 159-175, the `/// Minimal JSON string rendering…` doc through the fn's
|
||||
closing `}`). Its only callers were inside the `to_json` body just replaced, so
|
||||
nothing references it now (leaving it would trip `clippy -D warnings` as
|
||||
`dead_code`). The independent `json_str` in `graph_model.rs` is untouched.
|
||||
|
||||
- [ ] **Step 4: Refresh the module-header doc**
|
||||
|
||||
In `crates/aura-engine/src/report.rs`, replace the module-header lines 6-8:
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
//! 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.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
//! and folds them here. Output is canonical JSON (C14): the schema is tiny,
|
||||
//! closed, and flat. `to_json` renders via serde (the report types derive it,
|
||||
//! cycle 0029) — the same encoder the run registry uses, so a record's stdout
|
||||
//! and on-disk shapes coincide.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Build the workspace (compile gate)**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: success — `aura-engine` compiles with `serde_json` as a normal dep and
|
||||
the serde-backed `to_json`; no `unused`/`dead_code` from the removed `json_str`.
|
||||
|
||||
- [ ] **Step 6: Verify the engine canonical-form golden PASSES (GREEN)**
|
||||
|
||||
Run: `cargo test -p aura-engine to_json_renders_the_canonical_form`
|
||||
Expected: PASS — `to_json()` now emits the serde shape matching the updated
|
||||
expected string.
|
||||
|
||||
- [ ] **Step 7: Verify the pin test PASSES (GREEN)**
|
||||
|
||||
Run: `cargo test -p aura-engine to_json_equals_serde_disk_shape`
|
||||
Expected: PASS — `to_json()` and `serde_json::to_string(&report)` are now the
|
||||
same bytes.
|
||||
|
||||
- [ ] **Step 8: Verify the CLI sweep golden + structural golden PASS (GREEN)**
|
||||
|
||||
Run: `cargo test -p aura-cli sweep_prints_four_json_lines_and_exits_zero`
|
||||
Expected: PASS — the binary now renders params as the array-of-pairs serde form.
|
||||
|
||||
Run: `cargo test -p aura-cli run_prints_json_and_exits_zero`
|
||||
Expected: PASS — the structural asserts (commit prefix, `window:[1,7]`,
|
||||
`total_pips:` key, `exposure_sign_flips:1}}`) are shape-agnostic and stay green.
|
||||
|
||||
- [ ] **Step 9: Full workspace gate**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — all tests green, including the determinism asserts
|
||||
(`report_is_deterministic_end_to_end`, `run_sample_is_deterministic_and_non_trivial`,
|
||||
`run_macd_compiles_from_nested_composite_and_is_deterministic`,
|
||||
`sweep_report_is_deterministic`) which compare two serde outputs and stay green.
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: clean — no `dead_code` (json_str gone), no warnings.
|
||||
Reference in New Issue
Block a user