DWSD.md

Flight Route DSL — Binding

A flight route is authored as the abstract design layer of a delivery: its path: is a pure design that references no board (see Path & bands). The bound-to: layer is the separate, optional concern that maps that abstract route onto one or more concrete boards — pinning a band (and, optionally, a stage’s item type) to a board position via the shared locator. This page covers the bound-to: section, the route→board derivation, and the Flow ⊆ Path invariant that keeps a binding honest. See the reference index for the route header and the full diagnostics family, and Path & bands for the bands and stages a binding refers back to.

When prose and code disagree, the code wins. Every key and diagnostic on this page was authored from src/route/yaml-parser.ts, the AST types (src/route/types.ts — BindEntry / BoardBinding / StageRefinement), and the validator rules (src/route/rules/r12-flow-subset-path.ts and src/route/rules/xref/r4-bound-to-board-ref.ts).

The bound-to: section

bound-to: is a list of band→board bindings. Each entry names one band (declared in bands:) and a list of boards it binds onto. The section keyword is exactly bound-to — workspace-consistent with the AI-agent bound-to: frontmatter from Phase 22.4.

Not keys — bind: / realizes: / flow:. The band→board binding section is the single bound-to: key. bind:, realizes:, and flow: are rejected, not aliased. flow: is the retired compat key: a root-level flow: raises one route.legacy-syntax error that names bound-to: as its replacement, and the canvas stays empty. bind: and realizes: were never keys — the tolerant parser silently ignores them (no field, no binding), so a route that writes one parses “clean” while binding nothing. Write bound-to:.

FieldValues / shapeAST fieldDefaultDiagnostic on misuse
bound-tolist of band→board bindingsboundTo? (BindEntry[])(none — absent = fully unbound)route.invalid-bound-to (entry skipped); route.flow-not-subset-path (error); route.unresolved-board-ref (warn)
bound-to[].bandstring — the band being bound (must be declared in bands:)BindEntry.band—route.flow-not-subset-path (error) when the band is not in bands:
bound-to[].boardslist of board bindingsBindEntry.boards (BoardBinding[])—route.invalid-bound-to (a malformed board entry is skipped)
bound-to[].boards[].boarda board locator / wikilink — e.g. "[[team-alpha]]"BoardBinding.board—route.unresolved-board-ref (warn) when the board is not in the workspace index
bound-to[].boards[].stagesoptional per-stage refinement — a list of { <stage name>: locator } pinning a stage to a board position. The key is the stage’s name: its token, or for a wikilink stage its link target (Feature: for [[Feature]] @ Train, never [[Feature]]:); an old lowercase spelling still works for one release with a route.stage-name-spelling warningBoardBinding.stages? (StageRefinement[])(none — a coarse binding omits it)route.flow-not-subset-path (error) when the pinned itemType is not in that band per path:

The band→board relation is N:M permissive: one band may bind several boards, and a coarse binding (a bare board: with no stages:) and a fine binding (a board: plus per-stage refinements) mix freely within one entry. A stage refinement maps a stage’s item type to a board position with the same locator grammar the rest of the workspace uses — lane:Frontend/col:Doing addresses a lane’s column; see The locator grammar for the full target syntax.

Route → board derivation — one route, several boards

The point of the split is reuse: the same abstract route (bands, stages, edges) is designed once and realized on several concrete boards. bound-to: is the mapping — for each band it names, it lists the board(s) that band lives on and (optionally) pins each stage’s item type to a concrete column/lane/split on that board. Because the relation is N:M, one route can drive many boards (a per-team board each realizing the same delivery route) and one board can host several bands.

An entirely unbound route is valid. When bound-to: is absent the boundTo field is omitted from the AST and no binding validation runs at all — the abstract route stands on its own as a design artifact. Binding is additive: you author the route first, then bind it when (and only when) you want to project it onto real boards.

The Flow ⊆ Path invariant — the one hard error

Every bound element must trace back to the path. Concretely, bound-to: may only name things the abstract route already declares:

  • The band in each bound-to: entry must be declared in bands:.
  • Each pinned stage (stages: refinement) must be a stage that actually lives in that band per path: — i.e. its name must match a (itemType, band) stage inferred from the path’s edge endpoints (exact name first; a stage that became a wikilink also accepts its old lowercase spelling for one release).

A violation of either is route.flow-not-subset-path (rule r12) — the one hard error in the binding family. It is AST-internal and pure: deterministic math over the route’s own AST, needing no workspace lookup. It is the structural guarantee that a binding can never bind a phantom band or a stage the path never visits.

  • A bound-to: band not declared in bands: → one error: “bound-to band '' is not declared in bands:”.
  • A stage refinement whose itemType is not in that band per path: → one error: “stage '' is not in band '' per path:”.

The rule fires only when the route is at least partially bound (bound-to: present); a fully unbound route and an unbound-but-declared band stay silent. Delivery-sink endpoints (()/⊙) are skipped when deriving the in-band stage set — the delivery terminator is not a stage. Even on error, the preview still renders — a diagnostic never blanks the board (T-23-03-AVAIL).

Stale board refs — a warning, never a blank

The Flow ⊆ Path rule is AST-internal. Resolving a board: locator against the workspace is a separate axis handled by the external rule xref/r4-bound-to-board-ref.ts. When a bound-to: board locator does not resolve to a board in the workspace index, it surfaces route.unresolved-board-ref — a warning, never an error. A single missing board referenced under several bands yields exactly one warning (deduped by locator).

This is deliberate graceful degradation: a stale or not-yet-created board ref is a normal state while authoring (you can bind to a board you have not written yet). It never blanks the preview — the route keeps rendering, and the author sees the last successfully-parsed board (note last-good render). The locator is treated as an opaque string — the parser performs no filesystem access; resolution happens in the host indexer.

Hiding the bindings

The bound-board badges on a band and the refinement pills on a stage form the Boards layer (key board-bindings; the row was called “Board bindings” until 18.10.2026), the first row of the Filter menu. It is visible by default. On the topology, units view and org chart the same row hides the board, route, topology and flight-route markers of a box. Unchecking it hides the badges and the pills (and the binding rows in an open drawer, which then shows the hidden-layer hint), so you can show a route first without its boards and then switch them on: “this is how the stages are bound to their boards”. Nothing on the route moves when the layer is hidden: a refined stage keeps its reserved lower area. SVG and PNG exports and the CLI’s --hide board-bindings prune the layer like every other one. The stage’s Flight Item Type row is never hidden, and the board markers of the topology and units views do not follow this layer. The layer key is board-bindings.

48.1 D-19 supersedes Phase 34 D-06 for route bindings: Phase 34 had exempted the route’s bound-board badges from filtering; they are filterable now. The board fl-badge and the child-count and unresolved badges stay unfiltered.

Worked example — a bound route

The binding.dwsd-flightroute corpus fixture — an abstract two-band route whose Delivery band is bound to a concrete board, with the story stage pinned to a lane’s column via the shared locator:

# binding — a `bound-to:` layer maps a route band to a concrete board via the shared locator; `for-domain:` names the domain responsible for the route.
title: Bound Route
for: feature
for-domain: "[[Delivery Ops]]"
bands:
  - name: Coordination
    fl: 2
  - name: Delivery
    fl: 1
path:
  - epic @ Coordination --> story @ Delivery
bound-to:
  - band: Delivery
    boards:
      - board: "[[team-alpha]]"
        stages:
          - story: "lane:Frontend/col:Doing"

The abstract path: copies an epic in Coordination into a story in Delivery. bound-to: then realizes the Delivery band on the [[team-alpha]] board, pinning the story stage to that board’s Frontend lane, Doing column. Flow ⊆ Path holds: Delivery is a declared band, and story is a stage that lives in Delivery per the path. Had the entry named a band not in bands:, or pinned a task stage that the path never places in Delivery, it would be a route.flow-not-subset-path error. Had [[team-alpha]] not existed in the workspace, it would be a route.unresolved-board-ref warning — and the route would still render.

for-domain names Delivery Ops as the domain responsible for the route; the preview shows it as a chip next to the title, navigable when the workspace has that work system or role.

See The locator grammar for the lane:Frontend/col:Doing board-locator target syntax, and Path & bands for the path elements a binding traces back to.

Like what you see? → Start — build your own org folder in ~2 min

binding.md
# Flight Route DSL — Binding
A flight route is authored as the **abstract design layer** of a delivery: its
`path:` is a pure design that references **no board** (see
[Path & bands](./path-and-bands.md)). The **`bound-to:`** layer is the separate,
optional concern that maps that abstract route onto **one or more concrete
boards** — pinning a band (and, optionally, a stage's item type) to a board
position via the shared locator. This page covers the `bound-to:` section, the
route→board derivation, and the **Flow ⊆ Path** invariant that keeps a binding
honest. See the [reference index](./index.md) for the route header and the full
diagnostics family, and [Path & bands](./path-and-bands.md) for the bands and
stages a binding refers back to.
> **When prose and code disagree, the code wins.** Every key and diagnostic on
> this page was authored from `src/route/yaml-parser.ts`, the AST types
> (`src/route/types.ts` — `BindEntry` / `BoardBinding` / `StageRefinement`), and
> the validator rules (`src/route/rules/r12-flow-subset-path.ts` and
> `src/route/rules/xref/r4-bound-to-board-ref.ts`).
## The `bound-to:` section
`bound-to:` is a list of **band→board bindings**. Each entry names one **`band`**
(declared in `bands:`) and a list of **`boards`** it binds onto. The section
keyword is exactly **`bound-to`** — workspace-consistent with the AI-agent
`bound-to:` frontmatter from Phase 22.4.
> **Not keys — `bind:` / `realizes:` / `flow:`.** The band→board binding section
> is the single **`bound-to:`** key. `bind:`, `realizes:`, and `flow:` are
> **rejected**, not aliased. `flow:` is the retired compat key: a root-level
> `flow:` raises one `route.legacy-syntax` **error** that names `bound-to:` as its
> replacement, and the canvas stays empty. `bind:` and `realizes:` were never keys —
> the tolerant parser silently ignores them (no field, no binding), so a route that
> writes one parses "clean" while binding nothing. Write `bound-to:`.
| Field | Values / shape | AST field | Default | Diagnostic on misuse |
|-------|----------------|-----------|---------|----------------------|
| `bound-to` | list of band→board bindings | `boundTo?` (`BindEntry[]`) | (none — absent = fully unbound) | `route.invalid-bound-to` (entry skipped); `route.flow-not-subset-path` (**error**); `route.unresolved-board-ref` (warn) |
| `bound-to[].band` | string — the band being bound (**must** be declared in `bands:`) | `BindEntry.band` | — | `route.flow-not-subset-path` (**error**) when the band is not in `bands:` |
| `bound-to[].boards` | list of board bindings | `BindEntry.boards` (`BoardBinding[]`) | — | `route.invalid-bound-to` (a malformed board entry is skipped) |
| `bound-to[].boards[].board` | a board **locator / wikilink** — e.g. `"[[team-alpha]]"` | `BoardBinding.board` | — | `route.unresolved-board-ref` (warn) when the board is not in the workspace index |
| `bound-to[].boards[].stages` | *optional* per-stage refinement — a list of `{ <stage name>: locator }` pinning a stage to a board position. The key is the stage's **name**: its token, or for a wikilink stage its link target (`Feature:` for `[[Feature]] @ Train`, never `[[Feature]]:`); an old lowercase spelling still works for one release with a `route.stage-name-spelling` warning | `BoardBinding.stages?` (`StageRefinement[]`) | (none — a **coarse** binding omits it) | `route.flow-not-subset-path` (**error**) when the pinned `itemType` is not in that band per `path:` |
The band→board relation is **N:M permissive**: one band may bind several boards,
and a **coarse** binding (a bare `board:` with no `stages:`) and a **fine**
binding (a `board:` plus per-stage refinements) mix freely within one entry. A
stage refinement maps a stage's item type to a board position with the **same
locator grammar** the rest of the workspace uses — `lane:Frontend/col:Doing`
addresses a lane's column; see [The locator grammar](../locator-grammar.md) for
the full target syntax.
## Route → board derivation — one route, several boards
The point of the split is **reuse**: the same abstract route (bands, stages,
edges) is designed once and **realized on several concrete boards**. `bound-to:`
is the mapping — for each band it names, it lists the board(s) that band lives on
and (optionally) pins each stage's item type to a concrete column/lane/split on
that board. Because the relation is N:M, **one route can drive many boards** (a
per-team board each realizing the same delivery route) and one board can host
several bands.
An **entirely unbound route is valid.** When `bound-to:` is absent the `boundTo`
field is omitted from the AST and no binding validation runs at all — the
abstract route stands on its own as a design artifact. Binding is **additive**:
you author the route first, then bind it when (and only when) you want to project
it onto real boards.
## The Flow ⊆ Path invariant — the one hard error
Every bound element must **trace back to the path**. Concretely, `bound-to:` may
only name things the abstract route already declares:
- The **band** in each `bound-to:` entry must be declared in `bands:`.
- Each pinned **stage** (`stages:` refinement) must be a stage that actually
lives in that band **per `path:`** — i.e. its name must match a `(itemType, band)`
stage inferred from the path's edge endpoints (exact name first; a stage that
became a wikilink also accepts its old lowercase spelling for one release).
A violation of either is **`route.flow-not-subset-path`** (rule **r12**) — the
**one hard `error`** in the binding family. It is **AST-internal and pure**:
deterministic math over the route's own AST, needing **no workspace lookup**. It
is the structural guarantee that a binding can never bind a phantom band or a
stage the path never visits.
- A `bound-to:` band **not declared in `bands:`** → one error:
*"bound-to band '<name>' is not declared in bands:"*.
- A stage refinement whose `itemType` **is not in that band per `path:`** → one
error: *"stage '<itemType>' is not in band '<band>' per path:"*.
The rule fires **only when the route is at least partially bound** (`bound-to:`
present); a fully unbound route and an unbound-but-declared band stay silent.
Delivery-sink endpoints (`()`/⊙) are skipped when deriving the in-band stage set —
the delivery terminator is not a stage. Even on error, **the preview still
renders** — a diagnostic never blanks the board (T-23-03-AVAIL).
## Stale board refs — a warning, never a blank
The Flow ⊆ Path rule is **AST-internal**. Resolving a `board:` locator against
the **workspace** is a separate axis handled by the external rule
**`xref/r4-bound-to-board-ref.ts`**. When a `bound-to:` board locator does not
resolve to a board in the workspace index, it surfaces
**`route.unresolved-board-ref`** — a **`warning`**, **never an error**. A single
missing board referenced under several bands yields exactly one warning (deduped
by locator).
This is deliberate **graceful degradation**: a stale or not-yet-created board ref
is a normal state while authoring (you can bind to a board you have not written
yet). It **never blanks the preview** — the route keeps rendering, and the author
sees the last successfully-parsed board (note last-good render). The locator is
treated as an **opaque string** — the parser performs no filesystem access;
resolution happens in the host indexer.
## Hiding the bindings
The bound-board badges on a band and the refinement pills on a stage form the
**Boards** layer (key `board-bindings`; the row was called "Board bindings" until
18.10.2026), the first row of the Filter menu. It is visible by default. On the
topology, units view and org chart the same row hides the board, route, topology and
flight-route markers of a box. Unchecking it hides the badges and the pills (and the binding rows in an
open drawer, which then shows the hidden-layer hint), so you can show a route first
without its boards and then switch them on: "this is how the stages are bound to
their boards". Nothing on the route moves when the layer is hidden: a refined
stage keeps its reserved lower area. SVG and PNG exports and the CLI's
`--hide board-bindings` prune the layer like every other one. The stage's
Flight Item Type row is never hidden, and the board markers of the topology and
units views do not follow this layer. The layer key is `board-bindings`.
48.1 D-19 supersedes Phase 34 D-06 for route bindings: Phase 34 had exempted the
route's bound-board badges from filtering; they are filterable now. The board
`fl-badge` and the child-count and unresolved badges stay unfiltered.
## Worked example — a bound route
The `binding.dwsd-flightroute` corpus fixture — an abstract two-band route whose
`Delivery` band is bound to a concrete board, with the `story` stage pinned to a
lane's column via the shared locator:
```dwsd.flightroute
# binding — a `bound-to:` layer maps a route band to a concrete board via the shared locator; `for-domain:` names the domain responsible for the route.
title: Bound Route
for: feature
for-domain: "[[Delivery Ops]]"
bands:
- name: Coordination
fl: 2
- name: Delivery
fl: 1
path:
- epic @ Coordination --> story @ Delivery
bound-to:
- band: Delivery
boards:
- board: "[[team-alpha]]"
stages:
- story: "lane:Frontend/col:Doing"
```
The abstract `path:` copies an `epic` in `Coordination` into a `story` in
`Delivery`. `bound-to:` then realizes the `Delivery` band on the `[[team-alpha]]`
board, pinning the `story` stage to that board's `Frontend` lane, `Doing`
column. Flow ⊆ Path holds: `Delivery` is a declared band, and `story` is a stage
that lives in `Delivery` per the path. Had the entry named a band not in `bands:`,
or pinned a `task` stage that the path never places in `Delivery`, it would be a
`route.flow-not-subset-path` error. Had `[[team-alpha]]` not existed in the
workspace, it would be a `route.unresolved-board-ref` warning — and the route
would still render.
`for-domain` names `Delivery Ops` as the domain responsible for the route; the
preview shows it as a chip next to the title, navigable when the workspace has
that work system or role.
See [The locator grammar](../locator-grammar.md) for the `lane:Frontend/col:Doing`
board-locator target syntax, and [Path & bands](./path-and-bands.md) for the path
elements a binding traces back to.