# blockworx KDL document format

> **Retired — the format is gone. See [`json-format.md`](json-format.md).**
>
> blockworx documents are JSON (`single-author-playbook.md`, D13). The KDL
> encoder went with the consolidation; the read-only parser and its walker
> (`src/schema/kdl.rs`, `src/schema/decode.rs`) were deleted at the
> choreographer playbook's step 7·11, closing D14's quarantine. Nothing in
> the tree reads KDL any more: a `.kdl` file named on the command line or
> handed to Import is refused with a message saying so.
>
> This page is kept as the record of what the format was — the way
> `src/log/`'s removal was ledgered rather than erased — so a document
> still lying in a drawer can be read by hand and re-typed as JSON. It
> describes no code that exists.

blockworx used to store a diagram as KDL, filling the schema in
`src/schema/model.rs`. It was parsed by the since-deleted
`src/schema/decode.rs`; `model.rs` is what survives, and is the authority if
anything here is unclear.

The schema stores each route's full path as its **corner waypoints** (every bend
is a `wp`), plus connectivity and labels. Route ids, the solved edge list, and
crossings are not stored — but on load the geometry is *reconstructed* from the
stored corners (straight legs drawn directly; the auto-router is invoked only for
a leg that is non-colinear or crosses a block), not re-autorouted from scratch.
A clean document therefore loads with little or no routing, and hand-adjusted
geometry survives save/load unchanged. (The auto-router still runs in full only
when drawing a *new* route.)

## Document shape

The top level of the file is the document. It has exactly:

```kdl
version 2          // format version — written always, optional when hand-writing
name "CT Scanner"  // display name — optional

top "b13"          // id of the root (sheet) block — required, appears once

block "b1" x=… y=… w=… h=… { … }   // one flat `block` node per block
block "b2" x=… y=… w=… h=… { … }

asset "9f3a2c81d4e7b026.svg" { svg r#"…"# }   // image content, after the blocks
```

Blocks are a **flat list**. Hierarchy is expressed by each block's `children`
node (a list of child ids), forming a tree whose root is the `top` block. A
block's `x`/`y` are **absolute** grid coordinates, not relative to its parent —
children may sit anywhere, even outside the parent's rect.

## `version`

The document format version. The editor always writes it; a file without one
reads as version 1, since the node became explicit without the format otherwise
changing, so hand-written documents may leave it out.

| version | change |
|---------|--------|
| 1       | the original format; asset ids were `i<N>`, assigned by position |
| 2       | asset ids are content-derived (`<hash>.<ext>`) |

Version 1 documents still read as they are — the asset-id reader is permissive —
and are rewritten at the current version on the next save.

A version *newer* than the reading build is refused outright, pointing at the
version number, rather than being parsed on a guess — the node exists precisely
because a newer writer may use syntax an older reader would otherwise misread as
something else. That check runs before the rest of the document, so the error
names the version rather than whichever unfamiliar token came first.

## `name`

The document's display name. Optional: when absent, the editor falls back to
the name of the file holding the document, so a hand-written `.kdl` needs no
ceremony to be named sensibly. It is part of the *document*, not derived from
the file path, so renaming is an ordinary undoable edit that travels with the
file.

Block, area, and pin coordinates and sizes are integer **grid units**
(`x`,`y` are `i32`; `w`,`h` are `u32`). `image`/`icon` are the exception — they
are free-positioned, so their `x y w h` are floats. Floats also appear for label
offsets and positions.

## Ids

- Block id: `b<N>` — the letter `b` then a non-negative integer (`b0`, `b13`).
- Pin id: `p<N>` — the letter `p` then a non-negative integer (`p1`, `p4`).
- Asset id: a file name — a content hash and an extension
  (`9f3a2c81d4e7b026.png`). See [`asset`](#asset).
- Ids are unique within their kind. Only **blocks, pins and assets** carry ids;
  routes, texts, areas and image placements are id-less (ids are minted on
  load).

## `block`

```kdl
block "b1" x=29 y=29 w=18 h=16 role=3 locked=true {
    title "block_1"           // the block's name (a label)
    type "Add"                // the block's type (a label)
    pin  "p1" "in_0" loc="w0" dir="input" type="bit"
    route "p1" "b2:p1" name="net"
    text "a note" x=86 y=13
    area x=0 y=0 w=10 h=4 { title "group" }
    image "9f3a2c81d4e7b026.svg" x=0 y=0 w=8 h=8   // a background image
    icon  "9f3a2c81d4e7b026.svg" x=2 y=2 w=6 h=6   // this block's foreground icon
    children "b2" "b3"        // ordered child ids (render z-order)
}
```

`x y w h` are required props. `role` (accent index `0..=255`) and `locked` are
optional and omitted at their defaults. Every child node (`title`, `type`,
`pin`, `route`, `text`, `area`, `image`, `icon`, `children`) is optional. A
block has at most one `icon`.

## Labels (`title`, `type`)

A block's name (`title`) and type (`type`) are both **label** nodes:

```kdl
title "block_1" side="top" offset=-45.0 hidden=true
```

- arg 0: the text (omitted when empty).
- `side`: `top` | `center` | `bottom` — omitted when it equals the label's
  default placement (reconstructed on load).
- `offset`: float, omitted when `0`.
- `hidden`: omitted when `false`.

## `pin` (ports)

A `pin` node declares a block's connection point. On the block that *owns* it as
a boundary it renders as a **port**; referenced from the parent it acts as a pin.

```kdl
pin "p1" "clk" loc="w1" dir="input" type="bit" tag="1"
pin "p5" "AXI" loc="e8"        // geometry omitted → auto-placed on load
```

- arg 0: pin id (`p<N>`), required.
- arg 1: name (optional; omitted when empty).
- `loc`: **required.** Boundary slot as `"{w|e}{offset}"` — `w`=west (left),
  `e`=east (right), then a slot offset (`w0`, `w1`, `e2`…). Two pins on the same
  side need distinct offsets.
- `dir`: `input` | `output` | `in-out` — omitted means `in-out`.
- `type`: the pin's secondary "type" label (free text, e.g. `"bit"`). Distinct
  from `dir`.
- `tag`: short designator label; `tag-hidden=true` hides it.
- `x` `y` `w`: the port's on-canvas rect. **Omit all three** to let the loader
  auto-place the port from its `loc` (height is always the fixed port height and
  is never stored). Only emit them to pin an explicit position. If you omit
  geometry you never have to compute port coordinates — the common case for
  generated documents.
- `fliplr=true`: flip the port stub from its natural inward facing.
- `pin-accent` / `port-accent` / `port-pin-accent`: accent indices (`0..=255`).

## `route`

Routes connect two anchors and live **inside the block that owns the wire**
(typically the sheet/parent that contains both endpoints).

```kdl
route "p4" "b8:p2" name="net_1" role=2 {
    wp 12 35              // waypoint (grid units); `locked=true` optional
    wp 2 41 locked=true
    label 76.6           // wire-label position along the route (float)
}
route "b11:p3" "b12:p1"  // no body → a childless node
```

- arg 0 / arg 1: the `from` / `to` anchors (positional, no `from=`/`to=`).
- An **anchor** is either:
  - `"p<N>"` — a **port on the block that owns this route** (the parent/sheet).
  - `"b<K>:p<M>"` — pin `p<M>` on child block `b<K>`.
- `name`, `role` optional. Waypoints and labels are omitted when empty; wire
  geometry beyond stored waypoints is recomputed.

A pin may appear in multiple routes (fan-out / multiple drivers is allowed — the
model does not enforce electrical validity).

## `text`, `area`, `image`, `icon`

```kdl
text "a note" x=10 y=5 role=1                 // free-floating text; x/y omitted → 0
area  x=0 y=0 w=12 h=6 role=2 { title "…" }   // a titled box, sits above blocks
image "9f3a2c81d4e7b026.svg" x=0 y=0 w=8 h=6   // places that asset as a background image
icon  "9f3a2c81d4e7b026.svg" x=2 y=2 w=6 h=6   // places it as the block's icon
```

`area` was spelled `comment` before the rename; the importer still accepts the
old keyword, and writes `area`.

An `image` (a free background annotation) and an `icon` (a block's foreground
badge, at most one per block) share the same node shape: the id of the [`asset`]
they draw, then the `x y w h` box it is stretched to fill. Unlike other nodes,
`image`/`icon` `x y w h` are **floats** — they size and position freely (no grid
quantization); an `icon` is kept within its block's rect. Naming an asset no
`asset` node defines is an error.

[`asset`]: #asset

## `asset`

```kdl
asset "9f3a2c81d4e7b026.svg" {
    svg r#"<svg viewBox="0 0 10 10">…</svg>"#  // KDL v1 raw string; the fence
}                                              // widens as needed
asset "4b71e0c39a5d2f88.png" {
    png "iVBORw0KGgo…"                         // base64-encoded PNG bytes
}
```

An image, stored once under an id its placements reference, with exactly one of
an `svg` or `png` child. Assets are **top-level** nodes emitted after every
block: an image placed twenty times is written once, so a diff shows the diagram
rather than repeated payloads.

The id is **content-derived**: the first 16 hex characters of the blake3 hash of
the image's bytes, then `.svg` or `.png`.

Two consequences worth relying on. Equal images get equal ids in every document,
so an image is stored once no matter how it arrived. And no edit ever *renames*
an asset — under the old positional `i<N>` ids, deleting the first image
renumbered every later reference and rewrote the whole file.

The reader is more permissive than the writer: any filename-safe token is
accepted (no `/`, no `..`), because requiring a hand-author to compute a hash
before inlining an SVG would be hostile, and because version-1 documents name
their assets `i1`. The editor rewrites them as hashes on the next save.

An asset nothing references is dropped on save — the list is content shared by
placements, not a standing library. Inline image data (an `svg`/`png` child on
an `image`/`icon` node, as written before this format) is rejected with an
error pointing at the offending node.

## Minimal example

```kdl
top "b0"

block "b0" x=0 y=0 w=20 h=20 {
    title "sheet"
    pin "p1" "in" loc="w0" dir="input" type="bit"
    pin "p2" "out" loc="e0" dir="output" type="bit"
    route "p1" "b1:p1"
    route "b1:p2" "p2"
    children "b1"
}

block "b1" x=6 y=6 w=8 h=8 {
    title "block_0_0"
    type "Add"
    pin "p1" "a" loc="w0" dir="input" type="bit"
    pin "p2" "s" loc="e0" dir="output" type="bit"
}
```

## Reading a legacy document

There is one door left, and it only goes in: the Import dialog, a clipboard
paste, and the courtesy file open all parse a `.kdl` document through
`schema::{kdl, decode}` and fold it through `schema::lower`. Nothing writes
KDL — an imported document leaves again as JSON (`json-format.md`), which
also renumbers ids and renames assets to their content hashes.

To convert a file you already have, open it and save the session as a
container, or import it and export the result:

```sh
cargo run -p blockworx -- input.kdl
```

A parse or fold failure logs to stderr and the editor opens an empty session
instead of the file; success means the document parsed, every route anchor
resolved, and the child tree folded.
