Architecture
Why the core never touches a browser API, and how that makes a full frame testable.
Three layers
crates/renderer/ kestrel-loom state, viewport, scales, panes, tools,
render loop, RenderCommand model — no browser API
crates/wasm/ kestrel-loom-wasm wasm-bindgen façade + Canvas 2D execution
js/ kestrel-loom.js grouped JavaScript wrapper — what you embed
Focus
Source: README.md (Layout), crates/renderer, crates/wasm, js/kestrel-loom.js
Show chart data
| Step | Kind | Leads to |
|---|---|---|
| Your page | input or output | kestrel-loom.js |
| kestrel-loom.js | step | kestrel-loom (WASM façade) |
| kestrel-chartkit | external | kestrel-loom (on_bar) |
| kestrel-loom | step | kestrel-loom-wasm (RenderCommands) |
| kestrel-loom-wasm | step | <canvas> |
| <canvas> | input or output |
Focus
Source: README.md (Layout), crates/renderer, crates/wasm, js/kestrel-loom.js
Show chart data
| Step | Kind | Leads to |
|---|---|---|
| Your page | input or output | kestrel-loom.js |
| kestrel-loom.js | step | kestrel-loom (WASM façade) |
| kestrel-chartkit | external | kestrel-loom (on_bar) |
| kestrel-loom | step | kestrel-loom-wasm (RenderCommands) |
| kestrel-loom-wasm | step | <canvas> |
| <canvas> | input or output |
A render pass in the core writes RenderCommands — lines, rectangles, text, candles,
indicator lines — into a renderer. The WASM crate executes them on a
CanvasRenderingContext2d; in tests, a recorder collects them instead.
Browser-free, enforced
CI fails if cargo tree -p kestrel-loom ever contains web-sys, js-sys or wasm-bindgen.
That rule is what keeps a whole frame testable without a browser.
Golden command streams
The core’s golden tests render deterministic scenes — fixed seed, fixed size, fixed time range —
and compare the resulting command stream with a committed fixture in
crates/renderer/tests/fixtures/:
full_frame_candlestick.txt
full_frame_with_bollinger_overlay.txt
full_frame_with_rsi_pane.txt
tools_trend_and_level.txt
The start of the frame with an RSI pane: the canvas is cleared, the grid drawn, then each candle follows as a wick line and a body rectangle, all with exact coordinates.
clear
line (0.00,288.00)-(736.00,288.00) w=1.00
line (0.00,246.86)-(736.00,246.86) w=1.00
line (0.00,205.71)-(736.00,205.71) w=1.00
line (0.00,164.57)-(736.00,164.57) w=1.00
line (0.00,123.43)-(736.00,123.43) w=1.00
line (0.00,82.29)-(736.00,82.29) w=1.00
line (0.00,41.14)-(736.00,41.14) w=1.00
line (0.00,0.00)-(736.00,0.00) w=1.00
line (173.85,0.00)-(173.85,288.00) w=1.00
line (334.53,0.00)-(334.53,288.00) w=1.00
line (495.20,0.00)-(495.20,288.00) w=1.00
line (655.88,0.00)-(655.88,288.00) w=1.00
line (36.98,62.83)-(36.98,95.49) w=1.00
rect (35.48,66.42) 3.00x15.87 fill=true stroke=false w=0.00
line (41.45,63.71)-(41.45,67.28) w=1.00
rect (39.95,64.70) 3.00x1.72 fill=true stroke=false w=0.00
line (45.91,52.18)-(45.91,90.56) w=1.00
rect (44.41,64.70) 3.00x12.62 fill=true stroke=false w=0.00
line (50.37,73.06)-(50.37,88.59) w=1.00
rect (48.87,77.32) 3.00x6.06 fill=true stroke=false w=0.00
line (54.84,79.14)-(54.84,97.72) w=1.00
rect (53.34,83.38) 3.00x8.57 fill=true stroke=false w=0.00
line (59.30,89.46)-(59.30,98.74) w=1.00A change in drawing behaviour fails the test even when the result still looks plausible in a browser. The fixtures are rewritten only on purpose:
UPDATE_GOLDEN=1 cargo test -p kestrel-loom --test golden_render_commands
The split with kestrel-chartkit
| kestrel-chartkit | indicator math, scoring, artifacts, the renderer-neutral scene model |
| kestrel-loom | viewport, scales, panes, tools, the render loop, canvas output |
kestrel-chartkit knows nothing about browsers; kestrel-loom knows nothing about how an RSI is computed. They meet at a data contract, and kestrel-loom pins kestrel-chartkit to a release tag.