From a1d4c679d23a767e8b6636d6d00b25391e0aae44 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Thu, 11 Jun 2026 15:50:41 +0200 Subject: [PATCH] Add a simple UI for visualizing the plans --- .editorconfig | 5 ++++- AGENTS.md | 3 +++ tools/plan-viewer/README.md | 37 +++++++++++++++++++++++++++++++++++ tools/plan-viewer/index.html | Bin 0 -> 26085 bytes 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tools/plan-viewer/README.md create mode 100644 tools/plan-viewer/index.html diff --git a/.editorconfig b/.editorconfig index 9dd1efb..500711b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -[*.{rs,hs,py}] +[*.{rs,hs,py,js}] max_line_length = 100 [*.md] @@ -20,3 +20,6 @@ indent_size = 2 [*.{yaml,yml,json}] indent_size = 2 + +[*.{css,html}] +indent_size = 2 diff --git a/AGENTS.md b/AGENTS.md index ea29a6c..e3efb88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,9 @@ The shape today is: - `tools/exporter/`: Haskell tool that consumes hand-authored `.scenario.json` files in `tools/exporter/examples/` and emits the runner-IR JSON consumed by `crates/plan-runner`. See [`tools/exporter/README.md`](tools/exporter/README.md). +- `tools/plan-viewer/`: static HTML viewer for `plan-runner` fixtures. + It evaluates a fixture in the browser and renders the plan DAG, per-node relations, input facts, and oracle comparison. + See [`tools/plan-viewer/README.md`](tools/plan-viewer/README.md). - `external/`: git submodules. `external/geolog` provides the Haskell query planner used by the exporter; `external/geomerge` is the Rust CRDT crate consumed by `storage::adapters::geomerge`. diff --git a/tools/plan-viewer/README.md b/tools/plan-viewer/README.md new file mode 100644 index 0000000..b1e2d51 --- /dev/null +++ b/tools/plan-viewer/README.md @@ -0,0 +1,37 @@ +# Plan Viewer + +A static HTML viewer for `plan-runner` fixtures. +It renders the plan DAG, the input facts, and the relation computed at every plan node, so a fixture can be inspected without reading raw JSON. + +## Usage + +Open [`index.html`](index.html) in a browser, then drop a fixture from `crates/plan-runner/fixtures/` onto the page or pick one with the file input. + +When the repository is served over HTTP, a fixture can also be loaded through a query parameter: + +```sh +python3 -m http.server 8000 +# then open: +# http://localhost:8000/tools/plan-viewer/index.html?fixture=../../crates/plan-runner/fixtures/two_atom_join.json +``` + +## What It Shows + +- **Plan DAG**: one box per plan node, laid out left to right by join depth, with `L` and `R` labels on join inputs and the root node marked. + Each box shows the node's output columns and row count. +- **Selected Node**: a plain-language description of the operator, its output columns, and the full relation computed at that node. + Wildcard columns (names starting with an underscore) are dimmed. +- **Result Bindings**: the root relation projected to the fixture's `expected_bindings` columns, with a per-row check against the oracle and a list of any expected rows the plan did not produce. +- **Input Facts**: one table per relation in the fixture's schema. + +The header badge reports whether the evaluated bindings match the fixture's `expected_bindings` as a multiset, mirroring `plan_runner::verify`. + +## Scope + +The viewer re-implements the scan, semijoin, and natural join semantics of `crates/query-ops` and the execution order of `crates/plan-runner` in JavaScript, for display only. +The Rust crates and their tests remain the correctness oracle; if the two ever disagree, the Rust behavior wins and the viewer has a bug. + +Unsupported cases: + +- plan node actions other than `scan` and `join` +- fixtures without a `query` and `schema` block diff --git a/tools/plan-viewer/index.html b/tools/plan-viewer/index.html new file mode 100644 index 0000000000000000000000000000000000000000..de59322ef03709ee0895d61bc61a8c5eac7e918a GIT binary patch literal 26085 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 literal 0 HcmV?d00001