Skip to content

AGENTS.md

What this is

Hecatomb (heca) is an AG Grid debugger. One crate builds two artifacts:

  • a wasm32 cdylib (src/lib.rs → src/wasm/) — the debugger UI/logic injected into a host page; scans for AG Grid tables and renders into every .hecatomb mount point.
  • a native heca bin (src/main.rs) — packages the wasm into a JS bundle and serves a local dev page.

Distribution model: the build output target/hecatomb.js is a single self-contained file (base64 wasm + inlined CSS, no external assets) that a user drops into any page via one <script> tag to get a full debugger — including man-in-the-middle injection / data overwrite and analysis. Every feature must preserve this single-file, zero-runtime-dep property; this is why the wasm is base64-inlined and t.css is inlined into the view.

Build & run (read this first)

.cargo/config.toml sets the default target to wasm32-unknown-unknown, so plain cargo build/check/run target wasm.

  • The heca bin MUST be run with --target x86_64-unknown-linux-gnu, e.g. cargo run --bin heca --target x86_64-unknown-linux-gnu. Omitting the target builds the bin for wasm and fails.
  • Run the bin via cargo from the repo root: paths resolve from CARGO_MANIFEST_DIR (falls back to cwd), so running the raw binary elsewhere breaks target/ lookup.

Use the justfile:

  • just build — compiles the wasm lib (--release --target wasm32-unknown-unknown --lib), then runs the bin to base64-inline target/wasm32-unknown-unknown/release/hecatomb.wasm into template.js, writing target/hecatomb.js + target/index.html.
  • just serve — builds, then serves http://127.0.0.1:10000. Override port: ... --bin heca --target x86_64-unknown-linux-gnu -- serve <port> (or -- serve --port N).

target/ is gitignored and regenerated by just build. No tests, lint, or CI exist.

Architecture / FFI (non-obvious)

  • No wasm-bindgen / web-sys / js-sys. The wasm is loaded raw via WebAssembly.instantiate in src/template.js. Do not introduce bindgen tooling.
  • All JS/DOM access crosses a hand-rolled FFI:
    • Rust imports host fns via unsafe extern "C" (console_log, mount_view, ag_grid_count, ag_grid_label, ag_grid_schema, ag_grid_inject, download_text, config_text) and exports run/rescan/generate/save_config/apply_config via #[unsafe(no_mangle)] pub extern "C" (edition 2024 syntax).
    • Strings pass as ptr + len into wasm linear memory; JS reads/writes wasm.instance.exports.memory.buffer. No serde/JSON. Structured data (schemas, generated rows) is encoded by hand with ASCII Unit/Record separators — see src/wasm/wire.rs and the matching constants in template.js. Variable-length reads use the "needed length" retry pattern (wire::pull): the JS importer returns the full byte length and Rust grows its buffer and calls again.
    • To add a JS capability: add an unsafe extern "C" import in Rust and the matching env.<name> fn in src/template.js.
  • DOM scanning and grid-API reads live in JS (template.js: scanGrids/describe via .ag-root-wrapper; real row data via agGrid.getGridApi(...) + getDisplayedRowAtIndex/node.data); src/wasm/grid.rs pulls results across FFI. Synthetic-data inference + generation lives in Rust (src/wasm/model.rs), config save/load text format in src/wasm/config.rs, PRNG in src/wasm/rng.rs.
  • src/template.js and src/template.html are include_str!'d into main.rs; {{}} in template.js is replaced with the base64 wasm. Editing templates requires re-running just build.
  • wasm modules are gated behind #[cfg(target_arch = "wasm32")] (lib.rs, external.rs); the native bin build compiles no wasm code.
  • UI markup is built with horrorshow in src/wasm/view.rs; styles come from src/t.css (Tachyons), include_str!'d and inlined so the panel is self-contained. The UI is a fixed bottom-right flyout: a small square launcher toggles a panel (open state lives in JS — panelOpen/hecatomb-open — and is re-applied after every mount_view, since each render replaces innerHTML). Flyout chrome + the colour theme are a scoped FLYOUT_CSS block in view.rs (positioning/animation Tachyons can't express, plus a documented warm "Daily Stoic" parchment/ink/amber palette). Theme colours hang off the functional element classes (.hecatomb-rescan, .heca-generate, .heca-card, ...) so the markup carries only layout utilities; the palette table is the doc comment above FLYOUT_CSS.

Conventions / constraints

  • Minimal dependencies: target 0–2 crates, prefer zero. Currently exactly two: base64 (bin packaging) + horrorshow (wasm-side HTML). A new dep is a deliberate decision, not a default.
  • Put performance-critical work (e.g. scanning and generating row data) in Rust/wasm, not JS.

Features

Reads a grid's row data via the grid API, generates similar synthetic data, and saves/reloads a per-table config file — all reachable from the per-grid controls in the panel (Generate / Save config / Load config). Both client-side and server-side row models are supported: reads sample loaded rows through getDisplayedRowAtIndex/node.data; injection sets rowData for client-side and swaps the serverSideDatasource (and datasource for infinite) for server-side. Column types are inferred in Rust (model.rs: bool/int/float/date/enum/free-text) and rows are generated with a seeded PRNG (rng.rs). Config files are a readable tab-separated format (config.rs), saved via download_text and reloaded via a hidden file input → config_text → apply_config.

src/template.html is only a local test fixture (copied to target/index.html by the build, served by just serve) and is not part of the distributed artifact; it holds one client-side (#example-grid) and one server-side (#server-grid) ag-grid-enterprise example grid.