Skip to content

feat(rust): reasonix-render crate skeleton with serde SceneFrame#953

Merged
esengine merged 1 commit into
mainfrom
rust-stage1-render-skeleton
May 15, 2026
Merged

feat(rust): reasonix-render crate skeleton with serde SceneFrame#953
esengine merged 1 commit into
mainfrom
rust-stage1-render-skeleton

Conversation

@esengine
Copy link
Copy Markdown
Owner

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

  • 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 serde-derived scene types mirroring
      `src/cli/ui/scene/types.ts` (`SceneFrame`, `SceneNode`, `TextRun`,
      `TextStyle`, `BoxLayout`, `Color`, etc.).
    • Binary reads JSONL from stdin, decodes one `SceneFrame` per line,
      debug-prints it. That's the minimum that proves the boundary.
    • 6 round-trip tests: simple text frame, named color, hex color,
      nested box with layout, full-frame serde symmetry, rejection of
      unknown node kinds.
  • `target/` ignored at the workspace root. `Cargo.lock` committed
    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

  • No ratatui. Rendering lands once the trace path feeds real app
    frames; the decoded debug print is enough to prove the boundary.
  • No CI step. Rust tests run locally for now. A `cargo test` step
    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.
  • No JS-side changes. `schemaVersion: 1` is the load-bearing
    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

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
@esengine esengine merged commit d700c46 into main May 15, 2026
4 checks passed
@esengine esengine deleted the rust-stage1-render-skeleton branch May 15, 2026 14:51
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant