feat(rust): reasonix-render crate skeleton with serde SceneFrame#953
Merged
Conversation
First Rust code in the tree. Stage 1 spike per #868: prove the scene-graph contract round-trips JS → Rust through the JSONL trace written by REASONIX_SCENE_TRACE (#947 / #952). What's in this PR: - Cargo workspace at the repo root. `desktop/src-tauri` stays its own workspace via `exclude` — the Tauri shell does not roll into the renderer build. - `crates/reasonix-render` binary + library. Library exposes the serde-derived scene types; binary reads JSONL from stdin and debug-prints decoded frames. Six round-trip tests cover named colors, hex colors, nested boxes, full-frame serde symmetry, and rejection of unknown node kinds. - `target/` ignored at the workspace root. `Cargo.lock` committed because this is a binary crate. What's not in this PR: - ratatui. Rendering comes once the trace path actually feeds real app frames; printing the decoded debug form is enough to prove the boundary. - A `cargo test` step in CI. Will land alongside the next Rust PR so the toolchain install only pays its cost once we have more than one round-trip suite to gate on. - Any change to the JS side. The contract type (`schemaVersion: 1`) is the load-bearing piece both sides agree on; if a future field is added on either side, schemaVersion bumps and the boundary breaks loudly. Refs #868 #947
This was referenced May 15, 2026
esengine
added a commit
that referenced
this pull request
May 15, 2026
Locks in formatting and lint for the reasonix-render crate before the second Rust PR adds anything substantial. Without this gate, the Rust side drifts silently — JS contributors don't run cargo, and the crate's existing serde annotations are exactly the kind of code where one missed rename rots the boundary. What runs: - `cargo fmt --all -- --check` — formatting matches what rustfmt produces on stable. - `cargo clippy --all-targets -- -D warnings` — warnings are errors. No clippy allowlist; if we want to silence something, the suppression goes next to the offending code with a one-line reason. - `cargo test --manifest-path Cargo.toml` — the round-trip suite from #953. Tests run only at the workspace root, so the Tauri shell at `desktop/src-tauri` (its own excluded workspace) is untouched. Linux-only for now. macOS and Windows runners come when actual rendering work needs cross-platform terminal behavior gated; today all the Rust code is serde + std and would just burn CI minutes elsewhere. Also applies rustfmt to the existing crate — two cosmetic line wraps in `scene.rs` and one in `round_trip.rs` that `cargo fmt` wants. The behavior change is zero; this is just the diff rustfmt produces. Refs #868 #947 Co-authored-by: reasonix <reasonix@deepseek.com>
esengine
added a commit
that referenced
this pull request
May 15, 2026
…#958) Closes the last open Stage 0 item from #947 — a real session running with REASONIX_SCENE_TRACE=path/to/frames.jsonl now writes one frame per state change to the trace file. The Rust binary from #953 can read this trace and replay decoded SceneFrames end-to-end. The frame today is a one-line summary (card count, busy/idle, activity label), not a full lowering of the Ink tree. That's intentional: lowerInkToScene only handles Ink primitives, and the App tree is mostly stateful components. A full automatic lowering is blocked on either (a) finishing #565 so the render tree is decomposable, or (b) adding a function-component expansion helper — both bigger pieces of work. What this PR gets us anyway: a wired producer, the boundary proven on real data, and a stable shape for the Rust side to test against. When the lowering matures, only the body of useSceneTrace changes; the call site in App stays the same. Refs #868 #947 Co-authored-by: reasonix <reasonix@deepseek.com>
27 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First Rust code in the tree. Stage 1 spike per #868: prove the
scene-graph contract round-trips JS → Rust through the JSONL trace
written by `REASONIX_SCENE_TRACE` (#947 / #952).
What lands
own workspace via `exclude` — the Tauri shell does not roll into
the renderer build.
`src/cli/ui/scene/types.ts` (`SceneFrame`, `SceneNode`, `TextRun`,
`TextStyle`, `BoxLayout`, `Color`, etc.).
debug-prints it. That's the minimum that proves the boundary.
nested box with layout, full-frame serde symmetry, rejection of
unknown node kinds.
because this is a binary crate.
Smoke test
```
$ printf '...frame1...\n...frame2...\n' | cargo run -q --bin reasonix-render
frame 1: SceneFrame { schema_version: 1, cols: 80, rows: 24, root: Text { runs: [...], wrap: None } }
frame 2: SceneFrame { schema_version: 1, cols: 120, rows: 40, root: Box { layout: None, children: [] } }
```
What's not here
frames; the decoded debug print is enough to prove the boundary.
in `.github/workflows/ci.yml` lands with the next Rust PR so the
toolchain install cost is paid once we have more than one
round-trip suite to gate on.
agreement; future field additions on either side bump the version
and the boundary breaks loudly.
Why this much serde annotation
camelCase ↔ snake_case is mechanical, but I made each field's
`#[serde(rename = "...")]` explicit rather than using
`#[serde(rename_all = "camelCase")]` on the struct, so a future
field rename on either side is a deliberate edit on both sides
rather than a silent collision.
Refs #868 #947