This project is a work in progress and is not ready for use. APIs, file format details, and CLI behavior are all subject to change without notice. Do not depend on this in any project yet.
A portable JSON-based file format for representing branching documents — trees of text nodes connected by edges. Think interactive fiction, game dialogue, decision trees, planning systems, or any tool working with non-linear text.
This repository contains the reference implementation: a validator, a viewer, published JSON Schemas, and a browser-based drag-and-drop viewer powered by WASM.
Status: Early development. See the roadmap below for what's done and what's still planned.
A .tree.json file describes a directed graph of text nodes. Each node has an id and content. Edges connect nodes, and edges marked isTrunk define the primary reading path through the document.
Here's a minimal example (examples/minimal.tree.json):
{
"formatVersion": "1.0",
"rootNodeId": "n1",
"nodes": [
{ "id": "n1", "content": "You stand at a crossroads in the forest." },
{ "id": "n2", "content": "You take the left path. A quiet village appears ahead." },
{ "id": "n3", "content": "You take the right path. The trees grow darker." }
],
"edges": [
{ "source": "n1", "target": "n2", "isTrunk": true },
{ "source": "n1", "target": "n3" }
]
}The trunk path goes n1 -> n2. Node n3 is a branch — an alternative path the reader could take.
- Rust 1.83+ (install via rustup)
- For WASM builds:
wasm32-unknown-unknowntarget andwasm-bindgen-cli
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cliClone the repo and build:
git clone <repo-url> tree-document-format
cd tree-document-format
cargo build --workspaceTry the CLI — validate an example file right away:
cargo run -p tree-doc-cli -- validate examples/story.tree.json
cargo run -p tree-doc-cli -- view examples/story.tree.jsonTry the browser viewer — build the WASM module and open the drag-and-drop HTML page:
cargo build -p tree-doc-wasm --target wasm32-unknown-unknown --release
wasm-bindgen --target web target/wasm32-unknown-unknown/release/tree_doc_wasm.wasm --out-dir web/pkg
cd web && python3 -m http.server 8080Then open http://localhost:8080 and drag any .tree.json file onto the page.
The CLI binary is called tree-doc and has three commands.
Runs JSON Schema validation followed by five graph integrity checks. Exits with code 0 if valid, 1 if errors found.
cargo run -p tree-doc-cli -- validate examples/minimal.tree.json✓ examples/minimal.tree.json is valid (3 nodes, 2 edges, tier 0)
Validating an invalid file shows precise diagnostics:
cargo run -p tree-doc-cli -- validate examples/invalid/trunk-cycle.tree.json✗ examples/invalid/trunk-cycle.tree.json has validation errors
error [trunk-cycle]: Trunk path contains a cycle: n1 -> n2 -> n3
at path: n1 -> n2 -> n3
warning [general-cycle]: Cycle detected among 3 nodes: n3, n2, n1
at path: n3 -> n2 -> n1
1 error, 1 warning
Renders the trunk (primary reading path) as a linear sequence, showing branch counts at fork points.
cargo run -p tree-doc-cli -- view examples/story.tree.jsonThe Enchanted Garden
────────────────────
7 nodes, 7 edges
[start] You discover a hidden gate in the garden wall. Ivy curls around iron bars.
├── [trunk] -> enter
└── +1 branch
· Climb the wall
[enter] You push the gate open and step inside. A fountain glistens at the center.
├── [trunk] -> fountain
└── +1 branch
· Wander the paths
[fountain] You approach the fountain. A coin glints at the bottom of the clear water.
├── [trunk] -> wish
[wish] You toss a coin into the fountain and make a wish. The water shimmers.
├── [trunk] -> ending
[ending] The garden seems to respond to your presence. You feel at peace.
└── (end of trunk)
Displays node count, edge count, trunk length, branch count, tier level, and validity.
cargo run -p tree-doc-cli -- info examples/story.tree.jsonexamples/story.tree.json
────────────────────────
Tier: 1
Nodes: 7
Edges: 7
Trunk length: 4
Branches: 3
Valid: yes
Try each example to see how the validator and viewer handle different documents:
# Valid documents
cargo run -p tree-doc-cli -- validate examples/minimal.tree.json
cargo run -p tree-doc-cli -- validate examples/story.tree.json
cargo run -p tree-doc-cli -- validate examples/empty-document.tree.json
cargo run -p tree-doc-cli -- validate examples/begin-to-end.tree.json
# Invalid documents — each triggers a different validation rule
cargo run -p tree-doc-cli -- validate examples/invalid/missing-fields.tree.json
cargo run -p tree-doc-cli -- validate examples/invalid/duplicate-ids.tree.json
cargo run -p tree-doc-cli -- validate examples/invalid/dangling-edge.tree.json
cargo run -p tree-doc-cli -- validate examples/invalid/trunk-cycle.tree.json
cargo run -p tree-doc-cli -- validate examples/invalid/general-cycle.tree.json
cargo run -p tree-doc-cli -- validate examples/invalid/orphan-node.tree.json
# View the trunk path of each valid document
cargo run -p tree-doc-cli -- view examples/minimal.tree.json
cargo run -p tree-doc-cli -- view examples/story.tree.json
cargo run -p tree-doc-cli -- view examples/empty-document.tree.json
cargo run -p tree-doc-cli -- view examples/begin-to-end.tree.json
# Summary info
cargo run -p tree-doc-cli -- info examples/minimal.tree.json
cargo run -p tree-doc-cli -- info examples/story.tree.jsonThe validator checks documents in two passes.
Pass 1 — Schema validation ensures the JSON structure matches the format (required fields, correct types). Uses an embedded JSON Schema (Draft 2020-12).
Pass 2 — Semantic validation checks graph integrity:
| Rule | Severity | What it checks |
|---|---|---|
duplicate-node-id |
Error | No two nodes share the same id |
dangling-edge |
Error | Every edge's source and target reference an existing node |
trunk-cycle |
Error | The trunk path (following isTrunk edges from root) does not loop |
general-cycle |
Warning | Strongly connected components in the full graph (cycles are valid for dialogue loops, but worth noting) |
orphan-node |
Advisory | Every node is reachable from the root via edges |
dangling-begin-end |
Error | If metadata.beginEndMapping is present, both beginNodeId and endNodeId must reference existing nodes |
Errors make the document invalid (exit code 1). Warnings and advisories are informational.
The Tree Document Format has three tiers of complexity:
- Tier 0 — Minimal:
formatVersion,rootNodeId,nodes,edges - Tier 1 — Adds
minReaderVersion,features, and document-levelmetadata(title, author, etc.) - Tier 2 — Multi-tree documents with
treesmap and cross-tree references (not yet implemented)
The validator auto-detects the tier and reports it in the output.
Tier 1 documents can optionally declare a begin-to-end mapping in their metadata. This indicates the document maps pathways between a specific starting state and a target ending state — useful for decision trees, process maps, and scenario planning.
{
"metadata": {
"beginEndMapping": {
"beginNodeId": "idea",
"endNodeId": "product_launched",
"includeDeadEnds": true
}
}
}| Field | Type | Required | Description |
|---|---|---|---|
beginNodeId |
string | Yes | ID of the starting node (should match rootNodeId) |
endNodeId |
string | Yes | ID of the target outcome node that successful paths converge on |
includeDeadEnds |
boolean | No | Whether the document includes dead-end branches that don't reach the end node |
The validator checks that both beginNodeId and endNodeId reference existing nodes (rule: dangling-begin-end).
Dead-end nodes — branches that terminate without reaching the end node — can be marked with "status": "dead_end" for easy identification. The status field on nodes is a free-form string; "dead_end" is a convention, not enforced by the validator.
See examples/begin-to-end.tree.json for a complete example.
The project includes a standalone HTML page (web/index.html) that runs entirely client-side — no backend needed. It loads a WASM build of the core library and provides a drag-and-drop interface for .tree.json files.
When you drop a file, the viewer shows three panels:
- Validation — pass/fail badge with full diagnostic listing (errors in red, warnings in yellow, advisories in blue)
- Document Info — tier, node count, edge count, trunk length, branch count
- Trunk View — the full trunk path rendered step-by-step with branch badges and labels
The UI uses a dark theme styled after GitHub's dark mode.
Step 1 — Build the WASM module:
cargo build -p tree-doc-wasm --target wasm32-unknown-unknown --releaseStep 2 — Generate JavaScript bindings:
wasm-bindgen --target web target/wasm32-unknown-unknown/release/tree_doc_wasm.wasm --out-dir web/pkgStep 3 — Serve the web/ directory (any static file server works):
cd web
python3 -m http.server 8080Step 4 — Open http://localhost:8080 in Chrome, Firefox, or Safari and drag a .tree.json file onto the page. Try it with any of the files in the examples/ directory.
If you haven't set up the WASM toolchain yet:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cliThe WASM build is published as @petaltank/tree-doc on GitHub Packages. This lets you validate, view, and inspect .tree.json documents from any JavaScript/TypeScript project.
1. Configure your project to use GitHub Packages for the @petaltank scope. Create or edit .npmrc in your project root:
@petaltank:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
You'll need a GitHub personal access token with read:packages scope. Set it as the GITHUB_TOKEN environment variable, or replace ${GITHUB_TOKEN} with the token directly.
2. Install the package:
npm install @petaltank/tree-doc3. Use it:
import { validate, view, info } from "@petaltank/tree-doc";
// Validate a document (WASM is loaded automatically by the bundler)
const result = validate(jsonString);
if (result.isValid) {
console.log(`Valid! ${result.stats.nodeCount} nodes, tier ${result.stats.tier}`);
} else {
result.errors.forEach(e => console.error(`${e.rule}: ${e.message}`));
}
// Get trunk view
const trunk = view(jsonString);
trunk.steps.forEach(step => {
console.log(`[${step.nodeId}] ${step.content}`);
});
// Quick summary
const summary = info(jsonString);
console.log(`${summary.nodeCount} nodes, ${summary.edgeCount} edges, tier ${summary.tier}`);All functions are fully typed — see the index.d.ts for ValidateResult, ViewResult, InfoResult, and related interfaces.
Webpack 5 — enable the asyncWebAssembly experiment:
// webpack.config.js
module.exports = {
experiments: { asyncWebAssembly: true },
};Vite — use vite-plugin-wasm:
// vite.config.js
import wasm from "vite-plugin-wasm";
export default { plugins: [wasm()] };Next.js — add to next.config.js:
module.exports = {
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};Run the full test suite:
cargo test --workspaceThis runs 46 tests covering:
- JSON parsing and serde roundtrips
- Schema validation (valid files pass, invalid files fail)
- All five semantic validation rules with dedicated test cases
- Trunk viewer traversal with various document shapes
- Edge cases: empty documents, self-loops, extra fields, missing trunk markers, multiple trunk edges
tree-document-format/
├── crates/
│ ├── tree-doc-core/ Core library (types, parsing, validation, viewer)
│ ├── tree-doc-cli/ CLI binary (validate, view, info commands)
│ └── tree-doc-wasm/ WASM bindings for browser use
├── npm/ Source files for the @petaltank/tree-doc package
├── schemas/
│ ├── tier0.schema.json JSON Schema (Draft 2020-12) for Tier 0
│ └── tier1.schema.json JSON Schema (Draft 2020-12) for Tier 1
├── scripts/ Build and publish scripts
├── examples/ Valid and invalid example documents
└── web/ Standalone HTML/CSS/JS browser viewer
The tree-doc-core crate can be used as a library in your own Rust project:
use tree_doc_core::{validate_document, parse, build_trunk_view};
// Validate a document
let json = std::fs::read_to_string("my-document.tree.json")?;
let result = validate_document(&json)?;
if result.is_valid {
println!("Valid! {} nodes, {} edges", result.stats.node_count, result.stats.edge_count);
} else {
for error in &result.errors {
eprintln!("{}", error);
}
}
// Build a trunk view
let doc = parse(&json)?;
let view = build_trunk_view(&doc)?;
for step in &view.steps {
println!("[{}] {}", step.node_id, step.content);
}- JSON Schemas (Tier 0 + Tier 1)
- Core library (types, parsing, schema + semantic validation, trunk viewer)
- CLI (
validate,view,info) - WASM browser viewer
- Example documents (valid + invalid)
- npm package (
@petaltank/tree-docon GitHub Packages) - Tier 2 multi-tree support
- Published crate on crates.io
- Stable 1.0 format spec
MIT