DWSD.md

Locator Grammar — Syntax Reference

A locator is a string that addresses an addressable place: either a position inside a board or a route — a column, a lane, a group, a split section, a route stage, and the slot before, in, or after it — or an entity of the organization named by a bare [[Name]] wikilink — a work system, a unit, a person — resolved by the target’s type (Phase 42). Locators are the connective tissue of the Inside-Out / Outside-In model: an external entity (a meeting, an interaction, an agreement, a signal) points into a document at a specific place, and the document renders a marker there without the document itself declaring anything. A board marks the column, a topology marks the work-system node, an org chart marks the person’s box, a units view marks the unit’s box.

This document is the authoritative reference for the locator grammar. It is a sibling of topology/index.md (the topology anchor surface) and is written for both humans and AI agents. As with the Topology DSL, the governing principle is machine writability and readability above human terseness: field names are spelled out, the grammar is closed and documented, and the parser never throws. An AI coding agent should be able to author or repair a locator from this reference alone, without a UI.

The locator parser is packages/dwsd-dsl/src/common/parse-locator.ts (parseLocator). It lives in common/ so every DSL can reach it — the board segment kinds (col: / lane: / group: / split:) and the route stage: segment (added in Phase 23; landed in Phase 26, see §3). An entity is not a segment kind at all: it is addressed by the bare [[Name]] head alone (see §3). The parser is pure TypeScript — no filesystem access, no DOM, no vscode — and is tolerant: a malformed or partial locator degrades to whatever parts could be recovered rather than throwing.


How it works

A locator is built from up to four parts:

[[file]] #boardId / path :: side
  • head — [[file]], the canonical wikilink, resolved by lowercased basename (a board moved into a subfolder is still reachable by its unchanged filename).
  • #boardId — which board within the file when a file holds several; for a route the same slot is #Route Name (§3).
  • path — a /-separated chain of typed kind:Name segments that drills into the structure (§3).
  • side — a trailing ::before / ::in / ::after (§4).

Only the path and side concern resolution into the tree; the head + id select which board (or route) the path runs against.

Resolution is exact-one (D-07). The resolver walks the normalized tree segment by segment; at each step the candidate set must narrow to exactly one node to continue. There are no wildcards, predicates, or node-sets — one locator addresses one position. The walk produces one of four outcomes:

StatusMeaning
resolvedExactly one node matched — the marker is placed.
resolved-cellA synthetic lane×column cell coordinate (see the cell note in §3).
ambiguousMore than one node matched the same segment (e.g. a board with both Doing and doing).
not-foundZero matches, an unsupported segment kind, or an empty / over-long path.

One parser, both directions. The same parseLocator + resolver serve both directions of the Inside-Out / Outside-In model: read forward, a locator turns an entity’s frontmatter value into a position; read in reverse, the indexer builds a reverse index from those same locators so a board or route can surface what points at it. Because the parser lives in common/, the grammar is shared across DSLs — the board segment kinds plus the route stage: extension (§3) — only the recognized segment kinds widen.


1. Overview

A locator names one addressable place. There are two families of places:

FamilyWhat it addressesFormRendered by
Positionala place inside a board or a routea [[file]] head + typed segments — col: lane: group: split: (board) · stage: (route)the board / route that owns the position
Entitya thing of the organization — a work system, a unit, a persona bare [[Name]] wikilink, no #id, no path — resolved by the target’s type at lookup timeevery picture that draws the target: the topology node (a work system), the unit box (a unit), the person box (an individual)

A positional locator carries a [[file]] head to say which board or route (§2) and a path to say where inside it. An entity locator is only the head: [[Jonas Keller]] is complete on its own, because a person, a unit or a work system is one thing across the workspace. Which box the marker lands on is not spelled in the locator — it follows from what the target is: an individual → the person box in every org chart that draws them; a unit → the unit box in every units view that draws it; a unit wearing the work-system hat → the topology node and the unit box (one entity, one anchor set, both pictures); a work system → the topology node. A same-named person and unit both receive the bare form; the path form disambiguates (§2).

A locator is consumed in two places:

  1. The frontmatter of an external entity file — e.g. an interaction or agreement note that anchors itself to a single board position via an interaction-of: / agreement-of: value, or a signal that observes a person via observes:.
  2. A body table in a container entity (a meeting) — one row per anchored position, so a single Daily Standup file can anchor N board positions at once (see §7 Table-row container convention).

In both cases the locator string is resolved by the workspace indexer against the boards, routes, people and units it has indexed. When the locator resolves, the addressed document renders a type-specific marker at that place (an .interaction-chip for an interaction, a .policy-chip for an agreement, a pin for a signal). When it does not resolve, nothing happens — the document renders no marker there and never crashes (see §6 Graceful degradation).

The board file does not list its inbound anchors. Discovery is outside-in: the entities declare the positions, the indexer builds a reverse index, and the board renders what points at it. The board author may also declare internal agreements: / interactions: directly on a column/lane/group; when both an internal declaration and an external anchor target the same position, the renderer blends them into one surface with provenance badges (D-22.1-09).


2. Head forms

The head identifies the file (and optionally the board within the file) that the rest of the locator addresses.

locator := head ('#' boardId)? ('/' path)? ('::' position)?
Head formExampleMeaning
[[file]] (canonical wikilink)[[Sprint Board]]Reference the file Sprint Board (resolved by basename, case-insensitive). This is the canonical form (D-18).
[[file|label]][[sprint-board|Sprint]]Wikilink with a display label — only the part before the | is the file (sprint-board).

The head resolves by basename, case-insensitive — the workspace indexer keys files by lowercased basename, so [[Sprint Board]] and [[sprint board]] address the same file, and a board moved into a subfolder is still reachable by its unchanged basename.

The path form — [[individuals/Name]] / [[units/Name]] (same-named pair)

A head may carry a folder prefix: [[individuals/Aurora]] or [[units/Aurora]]. The indexer matches a path-form head by file-path suffix — the target’s own file path must end with /<the written path>.md (the D-WL-03 rule lookupNote already uses for notes). This is how a same-named person and unit are told apart on the entity surface (§3): a bare [[Aurora]] lands on both the person box and the unit box — documented, not diagnosed — while [[individuals/Aurora]] reaches only the person and [[units/Aurora]] only the unit. The folder is whatever folder the target file really sits in (matched case-insensitively); an unknown target still receives the bare form.

A locator with no head (e.g. a bare #col:Doing::in in a container body table) is a relative locator — it inherits its file/board from the container entity’s frontmatter. See §7.

Inline (position-only) form — same-doc, no head (D-02)

There is a second head-less form that is not relative-by-inheritance: the position-only (inline) locator. It carries no [[file]] head and no #boardId — just a typed path and an optional ::side:

inline-locator := path ('::' position)? (no head, no '#boardId')

It is used by an annotation authored inside the very document it addresses — the inline notes: surface (Phase 33). Because the note lives in the same board / route / topology document, there is nothing to point at by filename: the locator resolves against the same doc, so the wikilink head is dropped entirely rather than inherited from a container. This is the deliberate same-doc simplification versus the external-entity model.

FormHeadResolves againstExample
Full[[file]]#Idthe addressed file/board[[Sprint Board]]#Sprint Board/col:Done::in
Relativenone (inherits container frontmatter)the container’s declared headcol:Done::in in a meeting body table
Inline (position-only)none (same-doc)the document the locator is authored incol:Done::in inside that board’s own notes:

The path grammar and the ::side qualifier are identical across all three forms — the inline form simply reuses the same parseLocator typed segments (§3) and position qualifier (§4) with the head omitted. Each DSL accepts the segment kinds its structure supports:

  • Board — col: / lane: / group: / split: (e.g. col:Doing::in, lane:Alice/col:Review::before).
  • Route — stage: only for v1 (e.g. stage:epic @ Coordination::before). A non-stage route position (a band, a trigger, an edge) has no locator segment yet and stays out until the NAV-05 / v2 grammar extension — it degrades to no marker, never a blank.
  • Topology — the work-system node id (the node name).
  • Org chart — [[Name]] or a bare Name (the person’s file basename); no . doc-note yet — the header is painted, but neither structure view has a doc-note bubble.
  • Units — [[Name]] or a bare Name (the unit’s file basename).

The inline form inherits the same exact-one resolution and graceful degradation contract (§6): a position-only locator that no longer resolves is orphaned. Unlike the external surfaces (which silently drop an unresolved marker), the inline note surface preserves an orphaned note — see board/notes.md and its route / topology siblings for the orphan tray + diagnostic behavior.


3. Typed path segments

After the head (and optional #boardId), a /-separated path drills into the board’s structure. Every segment is kind:Name:

path := segment ('/' segment)*
segment := kind ':' name (kind ∈ lane | group | col | split | stage)
SegmentAddresses
col:NameA column named Name
lane:NameA lane (horizontal swimlane) named Name
group:NameA group named Name
split:NameA split section named Name
stage:NameA route stage — addresses a Flight Route, not a board. Name is the full itemType @ Band pair (see The stage: route segment below).

Every segment kind is positional. A person, a unit or a work system is not reached through a segment — it is the bare [[Name]] head with no path (see Entity wikilinks below).

Segments chain to reach nested positions. Examples:

  • col:Done — the Done column.
  • lane:Alice/col:Doing — the Doing column within the Alice lane.
  • group:Delivery/lane:Bob/col:Review — the Review column in the Bob lane inside the Delivery group.
  • col:Build/split:QA — the QA split section inside the Build column.

Case-insensitive matching (D-12). Both the kind and the Name match case-insensitively, so col:done, COL:Done, and col:Done all address the same column. A name may legitimately contain an interior colon (col:Review: Notes) — only the first colon separates the kind from the name.

A segment whose kind is not one of the five recognized kinds, or whose name is empty, is dropped (tolerant degradation) — the rest of the path still parses.

There is no authorable cell: segment. A cell is a resolution result, not something you write. When a lane:L/col:C path descends into a lane that owns no column node C, but C is a board-global column on a shared-columns grid, the resolver returns a synthetic lane×column cell coordinate ({ laneName, columnName }) so the marker lands in the same visible cell regardless of which board structure the author used — this is the resolved-cell outcome in How it works. You still author lane:L/col:C; the cell is what the resolver hands back.

The . document-root path (whole-document scope)

A path may begin with a . document-root token. It names the position from the root of the document itself rather than as an implicit same-document path — the XPath / filesystem root convention. / stays the path separator, so . (not /) is the free root token:

path := '.' | '.' '/' segment ('/' segment)* | segment ('/' segment)*
Path formResolves toEquivalent to
.the whole document — an empty path, document scope(the document itself; no per-position marker)
./col:Doinga path from the document rootthe bare col:Doing
col:Doingan implicit same-document path (unchanged)—
  • . alone = the whole document. The empty path resolves to document scope: the marker is not pinned to any column / lane / group / split (or stage / node) but to the document as a whole. On the inline notes: surface this renders as one marker in the document header, to the left of the doc-tag chips; when two or more . doc-notes exist they collapse to one header marker with a count badge (see board/notes.md and its route / topology siblings).
  • ./<path> = a path from the root, equal to the bare path. A leading . on a longer path (./col:Doing) is exactly equivalent to writing the bare path (col:Doing) — the . names the root the path descends from and is optional. No YAML quoting is strictly required for ., but the convention is to quote it (on: ".") for clarity.

The stage: route segment (landed Phase 26)

stage: is the route extension of the locator grammar — the one segment kind that addresses a Flight Route instead of a board. The route DSL reuses the very same parseLocator (D-26-01 / D-26-02 / D-26-03), so a route position is just another typed path segment. It behaves like the four board kinds, with two route-specific specifics:

  • Name = the full itemType @ Band pair. A route stage is identified by its (itemType, band) pair, spelled itemType @ Band with a single space each side of the @ — the same spelling used in path: edges (see Path & bands). So stage:epic @ Coordination names the epic stage in the Coordination band. The name is opaque to parseLocator: interior spaces and the @ are fine, and the name runs to the next / (or to the ::side qualifier). A whitespace drift simply never matches → no marker (graceful). The item-type half is the stage’s name: its token, or for a stage written as a wikilink ("[[Epic]] @ Coordination") its link target — stage:Epic @ Coordination, never with [[…]] inside the locator. The match is exact (the band half stays case-insensitive); the old lowercase spelling of a stage that became a wikilink still lands for one release and is reported as a deprecation (see Route: a stage’s item type).
  • Multi-route disambiguation reuses the #Name slot. In a Markdown file hosting several route fences, [[file]]#Route Name/stage:epic @ Coordination selects the route by name — exactly like #boardId selects a board (§2). The bare [[file]]/stage:epic @ Coordination form also resolves for the common one-route-per-file case.

The ::before / ::in / ::after side (§4) applies unchanged. The route-side delta and its markers are documented in route/anchors.md; this page is the authority for the locator string itself.

Board scope, and a board locator without #Title

Three board-locator forms differ only in what they address; all use the head resolution above.

FormExampleAddresses
board scope[[Order Studio Board]]#Order Studio::inthe whole board — a #Title and no path. Shown in the board header (see board anchors).
board position[[Order Studio Board]]#Order Studio/col:Flagged::beforeone unit; the ::before / ::in / ::after side picks the set at that unit
bare entity[[Order Studio Board]]::innot a board anchor: no #Title and no path make it an entity locator (the entity named Order Studio Board)

The two-slot rule. A board locator written without #Title ([[Sprint Board]]/col:Doing::in) lands on a titled board the same way a route locator without a title lands on a titled route: the resolver tries the board’s title and the empty title. In a Markdown file that hosts several titled dwsd.board fences it lands on every fence that has the unit. A locator with #Title stays exact — it lands only on the board of that title. A titled locator against an untitled board does not land.

A locator that lands nowhere is a warning. VS Code marks a board locator that lands on no board unit with a warning on the line that writes it, naming one of three reasons: the addressed document is not in the workspace, the document has no board of that title, or the board has no unit at that path.

An entity wikilink is the bare head with nothing after it — [[Name]], no #id, no path, no ::side. It addresses a thing of the organization rather than a position inside a document, and it is the one form for doing so: a work system, a unit and a person are all written the same way. parseLocator yields { file: 'Name', segments: [] }; the indexer keeps such values in name-keyed reverse maps without asking what the target is. The picture that draws the target decides at lookup time:

The target [[Name]] is …The marker lands on …
an individualthe person’s box, in every org chart that draws them
a unitthe unit’s box, in every units view that draws it
a unit wearing the work-system hatthe topology node and the unit box — one entity, one anchor set, both pictures
a work systemthe topology node
nothing the indexer knowsnowhere — no marker, no error (§6)

Because resolution happens when a picture asks, file order never matters: a signal indexed before the person it observes still lands once the person’s file is known.

Casing does not matter either. Like the head form (§2), an entity wikilink matches the target’s basename case-insensitively: observes: "[[jonas keller]]" reaches Jonas Keller.md, for-domain: "[[head of operations]]" still bridges through the role, and two links that differ only by case address one target. The path form ([[individuals/jonas keller]]) compares the same way. The structure keys reports-to: and role-keeper: are not locators and still expect the file’s authored spelling.

Which frontmatter keys accept an entity wikilink:

KeyEntity typesWhat renders
interaction-of:interaction · meetingan interaction marker; a drawer row under “Interactions”
agreement-of:agreementan agreement marker; a drawer row under “Agreements”
bound-to:ai-agentan agent marker; a drawer row under “AI Agents”
mandate-of:mandatea mandate chip or marker, labeled with the holder; see Mandates
observes:signal · insighta pin on the box / node, listed under the drawer’s “Change conversation” band
doc-level notes: - on:inline notes in an org chart / units documentthe note marker on the box (org chart notes, units notes)

The observes: row lists the two pin types whose observes: is read (format 1.3), which is what the code accepts: isObservesPinType in common/anchor-registry.ts tests membership in ANCHOR_OBSERVES_PIN_TYPE_KEYS (signal, insight), and the indexer gates every observes: pin on it. That accept-set is the authority — if it and this row ever disagree, the row is the one that is wrong. A driver or proposal is a pin too (the change pill has four stages), but its observes: is not read at all: neither a pin nor the ? fall-through. It names its domain with for-domain:, a plain wikilink. An entity whose type: is outside the pin family does not vanish: its observes: falls through to the unknown-anchor affordance and renders a dashed ? marker instead of a pin.

These keys are the value-polymorphic ones of §5; the entity wikilink is their “bare [[…]]” shape. mandate-of: takes the same value forms as interaction-of: and agreement-of:. The Mandate of cell of a mandate row on a holder’s file takes them too, but it must be absolute (it starts with [[ or @): a relative locator in that cell inherits nothing, because a holder is not a board, and the editor warns about it (mandate.relative-locator). A list value puts the same entity under each name it lists (interaction-of: ["[[Jonas Keller]]", "[[Mara Holt]]"] is a 1:1 on both boxes).

for-domain is different — it names a domain, not a target. for-domain: is not value-polymorphic (§5); it points up at a domain, and a domain is a work system or a role:

  • for-domain: [[Work System]] — the topology’s anchor surface for agreements, drivers, proposals, decision records and flight routes; a unit with the work-system hat shows the same agreements / drivers / proposals / decision records on its unit box. A role’s own for-domain: [[Work System]] (a single wikilink or a list) puts a role marker on that work system’s topology node and on the unit box of a unit that wears the work-system hat; a plain team (no hat) and a person get none.
  • for-domain: [[Role]] — the role bridge: the entity lands on the box of every keeper of that role (a shared role reaches each keeper), and the drawer row says via . It is one hop: the role’s own for-domain: [[Work System]] is never followed further, so the topology learns nothing from a role-scoped record. The bridge applies to every type that carries for-domain — agreement, interaction / meeting, AI agent, decision record, driver, proposal (not flight routes, not roles). A record that names the person directly (through a polymorphic key) and their role shows once, without via.
  • for-domain: [[Person]] lands nowhere — a person is not a domain. The org chart validator says so with an info diagnostic on the view’s mode line: orgchart.for-domain-names-individual (point it at the person’s role instead); only an anchoring document is reported — a file whose type: the indexer does not know is never named by this diagnostic. Roles are anchoring documents since Phase 48, so a type: role scoped to a person is reported too (its marker has nowhere to land). An unresolved target keeps today’s behavior — silence.
  • for-domain: [[Plain Unit]] (a unit without the hat) is a valid target and raises no finding: format 1.1 counts a unit as a valid for-domain target (a mandate can name one, see Mandates). Its box in the units view shows the document.

The path form. [[individuals/Aurora]] / [[units/Aurora]] tells a same-named person and unit apart (§2); the bare [[Aurora]] lands on both.

A leftover type prefix is skipped. The grammar recognizes only the five positional kinds above; a value such as a type word, a colon and a name is an unknown kind and is dropped by the tolerant parser — an anchor lands nowhere, a note becomes an orphan (orgchart.note-orphan / units.note-orphan). Write the wikilink.


4. Position qualifier

A trailing ::before / ::in / ::after selects where, relative to the addressed position, the marker sits:

position := 'before' | 'in' | 'after'
QualifierSlot
::beforeImmediately before the position
::inIn / on the position itself (the default)
::afterImmediately after the position

Bare = in (D-04). Omitting the qualifier is identical to writing ::in: [[Sprint Board]]#Sprint Board/col:Doing and [[Sprint Board]]#Sprint Board/col:Doing::in resolve to the same position. An explicit ::in is accepted and normalizes to the same in — authoring it does not produce a doubled key (a real bug fixed in Plan 22-08 / RC3).

The qualifier is peeled from the end of the string before the path is split, so col:Review::before yields a column named Review (not Review::before).

Runtime target forms: chip: and @doc are not authored

Marks (Phase 52) address a chip, a pin or a drawer entry with two further forms that exist only at runtime, in a host’s mark set and in an agent’s request:

<position>[::before|in|after]/chip:<bucket>[:<entry>] e.g. col:doing/chip:agreement:team-working
@doc/chip:<bucket>[:<entry>] e.g. @doc/chip:domain

They are not accepted in bound-to:, in a note’s on: or in any other field this grammar governs: parseLocator does not know a chip: segment, and it silently drops an unknown segment kind, so a chip: written into an authored locator would quietly address the position in front of it. Nothing to author, nothing new to migrate.

@doc is a reserved head for the document scope (the whole diagram: a route’s header chips, a board’s header chips). A legacy @<file> head whose name is literally doc is therefore not addressable as a mark target. The contract for both forms (buckets, entry slugs, the ::side qualifier, limits) is docs/host-integration/marks.md in the source repository.


5. Value-polymorphic keys

The two relationship keys interaction-of: and agreement-of: are value-polymorphic (D-22.1-01): the same key accepts either of two value shapes, and the renderer dispatches on the shape, not on a different key.

Value shapeMeaningHow to recognize it
Entity wikilink — [[X]]An entity anchor: the entity relates to the whole thing X names — a work system (the topology anchor surface), a unit (the unit box), a person (the person box); resolved by X’s type (§3).A bare [[…]] with no #… / /col: / /lane: … path segments.
Board-position locator — [[board]]#Title/col:Name::inA position anchor: the entity points at one specific board position.Presence of a #… board id and/or /col: · /lane: · /group: · /split: path segments.

At a glance, the indexer branches route → board → bare wikilink: a stage: segment makes it a route-position locator; otherwise, if the value carries a # boardId or any kind: path segment, it is a board-position locator; otherwise it is an entity wikilink. Route and board values go to the route / board reverse indexes and render a chip at the position; a bare wikilink goes to the name-keyed entity reverse index, and the picture that draws the named thing renders the marker.

# Entity anchor (value = bare wikilink, no path) — a work system here:
interaction-of: [[Delivery Squad]]
# Entity anchor — a person (a 1:1) and a unit:
interaction-of: ["[[Jonas Keller]]", "[[Wholesale]]"]
# Position anchor (value = board-position locator, has #boardId + /col:…):
interaction-of: "[[Sprint Board]]#Sprint Board/col:Done::in"

interaction-of:, agreement-of: and bound-to: accept both shapes; observes: (the pin types, Phase 31) is value-polymorphic the same way — a bare observes: [[X]] pins on X’s box or node. for-domain: (the domain key) is unrelated and is not value-polymorphic — it remains the canonical domain key, a plain wikilink naming a work system or a role, and is untouched by this surface (D-22.1-02; the role bridge is described in §3).


6. Graceful degradation

The locator surface never crashes and never blanks the board (D-10). Concretely:

  • An unresolved locator (the file does not exist, the board id is wrong, the column name does not match) simply renders no marker at that position. The rest of the board renders normally.
  • A malformed locator degrades to whatever parts the tolerant parser could recover — an unrecognized segment kind is dropped, an empty path yields the board-level position, an empty string yields { segments: [], position: 'in' }.
  • The parser performs no filesystem access — resolution happens in the host indexer, and a failed resolution is a silent no-op, not an error.

A future diagnostic surface (the B3 work, currently deferred) will surface unresolved locators back to the author so they are not silently lost. Until then, the principle is “show as much as possible, never blank, never break” — an unresolved anchor costs you one missing chip, nothing more.


7. Table-row container convention

A container entity (a type: meeting) can anchor N board positions at once via a body table — one row per position. This is the Inside-Out / Outside-In workhorse: a Daily Standup meeting whose individual agenda items each anchor a different corner of one board.

The container declares a ## Interactions (for meetings/interactions) or ## Agreements (for agreements) heading, followed by a table whose “Interaction of” / “Agreement of” column holds one locator per row:

---
type: meeting
title: Daily Standup
interaction-of: [[Sprint Board]]
---
## Interactions
| Name | Outcome | Interaction of |
|---------------|----------------------|-------------------------------------------------|
| What's done? | Items moved to Done | [[Sprint Board]]#Sprint Board/col:Done::in |
| Where blocked?| Blocked items visible| [[Sprint Board]]#Sprint Board/col:Blocked::in |
  • One row = one position anchor. N rows = N anchors. The two rows above produce two markers on the Sprint Board — one on Done, one on Blocked.
  • The extractor scans the full body (not a head-byte cap), so a long meeting with many rows is fully indexed.

Locator-inheritance chain (D-22.1-12)

Each row’s locator follows a full / relative / bare trichotomy:

Row locator formResolution
Full — [[Sprint Board]]#Sprint Board/col:Done::inUsed as-is; overrides the container frontmatter (the row names its own file + board).
Relative — #col:Done::in or col:Done::in (no [[file]])Inherits the container’s frontmatter interaction-of: / agreement-of: head, which is grafted on to form a full locator before resolution.
Bare / unresolved — a position with no inheritable headSkipped gracefully — no anchor, no crash (D-10).

The relative form keeps container tables terse: declare the board once in interaction-of: frontmatter, then list only the in-board positions in the rows.


7a. Outcome channel (D-09 / D-10)

Each anchored entity can carry an outcome — a short line of context shown in the unified drawer next to the marker. There are two sources, with a strict precedence (D-09):

  1. Primary — the body-table Outcome cell. In a container table (§7), the per-row “Outcome” column is the primary outcome channel: one outcome per anchored position, row-granular.
  2. Fallback — a ## Outcome(s) section. When an anchored entity has no body-table Outcome cell, the renderer falls back to a markdown ## Outcome(s) H2 section in the entity body and uses its first paragraph as the drawer outcome. The fallback fires only when no table outcome exists; a table Outcome cell always wins.

The recognized-heading allowlist (D-10)

The fallback matches a closed allowlist of H2 headings — not “any heading containing the word outcome”. After stripping the ## marker and trailing punctuation/whitespace, the heading must match, case-insensitively:

Recognized headingNotes
## Outcomesingular
## Outcomesplural
## What is the outcome?the question form; the trailing ? is optional

The exact literal (mirrored verbatim from the workspace indexer’s OUTCOME_HEADING_RE, so a reviewer can diff the two and see they agree):

/^(?:outcomes?|what is the outcome)\??$/i

Deliberately rejected — these do not match, so unrelated prose never leaks into the drawer:

  • ## Outcome history
  • ## Bad outcomes to avoid
  • any other …outcome… heading.

A broad /##.*outcome/i match is intentionally avoided: it would false-positive on the rejected headings above. The allowlist is a single anchored, non-backtracking alternation (no nested quantifier), so it is also ReDoS-safe on hostile workspace input (T-22.3-01). The captured first paragraph is truncated to 280 characters; on overflow a single … affordance is appended (renderer-resilience — never a hard mid-word clip), and the text always reaches the drawer as a plain text node, never as HTML.


8. Worked examples per DSL

(a) Interaction anchored to a column

An interaction note that points at the Done column of the Sprint Board:

---
type: interaction
title: Done Review
interaction-of: "[[Sprint Board]]#Sprint Board/col:Done::in"
---

Renders an .interaction-chip in the Done column.

(b) Agreement anchored to a split section

A working agreement that points at the QA split section inside the Build column:

---
type: agreement
title: QA Gate
agreement-of: "[[Sprint Board]]#Sprint Board/col:Build/split:QA::before"
---
## Summary
No story leaves Build until QA signs off.

Renders a .policy-chip before the QA split section.

(c) Meeting container with N rows

A Daily Standup meeting anchoring two positions of one board via its body table — see the full example in §7. One file → two markers (Done + Blocked), demonstrating the one container → N anchors invariant.

(d) Interaction anchored to a route stage

An interaction note that points at the epic @ Coordination stage of the Referenced Route (the route refs fixture):

---
type: interaction
title: Epic Kickoff
interaction-of: "[[Referenced Route]]/stage:epic @ Coordination::before"
---

Renders an interaction marker before the epic @ Coordination stage. The stage: segment is the route extension of the grammar (§3); the route-side marker + drawer behavior is documented in route/anchors.md.


9. Out of scope

Not covered (yet)Where it lives
Wildcard / predicate queries — e.g. “every column whose name matches *Done*” or “all lanes in group X”Future work; the current grammar addresses one exact position per locator.
Topology anchors — anchoring to a Work System rather than a board positionSee topology/index.md; the value-polymorphic interaction-of: / agreement-of: keys cover the WS-wikilink shape (see §5).

Reference parser: packages/dwsd-dsl/src/common/parse-locator.ts. Sibling reference: topology/index.md.

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

locator-grammar.md
# Locator Grammar — Syntax Reference
A **locator** is a string that addresses an **addressable place**: either a
**position** inside a board or a route — a column, a lane, a group, a split
section, a route stage, and the slot *before*, *in*, or *after* it — or an
**entity** of the organization named by a bare `[[Name]]` wikilink — a work
system, a unit, a person — resolved by the **target's** type (Phase 42). Locators
are the connective tissue of the **Inside-Out / Outside-In** model: an external
entity (a meeting, an interaction, an agreement, a signal) points *into* a
document at a specific place, and the document renders a marker there without
the document itself declaring anything. A board marks the column, a topology
marks the work-system node, an org chart marks the person's box, a units view
marks the unit's box.
This document is the **authoritative reference** for the locator grammar. It is a
sibling of [`topology/index.md`](./topology/index.md) (the topology anchor surface)
and is written for **both humans and AI agents**. As with the Topology DSL, the
governing principle is **machine writability and readability above human
terseness**: field names are spelled out, the grammar is closed and documented,
and the parser never throws. An AI coding agent should be able to author or
repair a locator from this reference alone, without a UI.
The locator parser is `packages/dwsd-dsl/src/common/parse-locator.ts`
(`parseLocator`). It lives in `common/` so every DSL can reach it — the board
segment kinds (`col:` / `lane:` / `group:` / `split:`) and the route `stage:`
segment (added in Phase 23; landed in Phase 26, see
[§3](#the-stage-route-segment-landed-phase-26)). An **entity** is not a segment
kind at all: it is addressed by the bare `[[Name]]` head alone (see
[§3](#entity-wikilinks--resolution-by-target-type-the-role-bridge-the-path-form-phase-42)).
The parser is pure TypeScript — no filesystem access, no DOM, no `vscode` — and
is **tolerant**: a malformed or partial locator degrades to whatever parts could
be recovered rather than throwing.
---
## How it works
A locator is built from up to four parts:
```
[[file]] #boardId / path :: side
```
- **head** — `[[file]]`, the canonical wikilink, resolved by **lowercased
basename** (a board moved into a subfolder is still reachable by its unchanged
filename).
- **`#boardId`** — which board *within* the file when a file holds several; for a
route the same slot is `#Route Name` ([§3](#3-typed-path-segments)).
- **path** — a `/`-separated chain of typed `kind:Name` segments that drills into
the structure ([§3](#3-typed-path-segments)).
- **side** — a trailing `::before` / `::in` / `::after`
([§4](#4-position-qualifier)).
Only the path and side concern resolution into the tree; the head + id select
*which* board (or route) the path runs against.
**Resolution is exact-one (D-07).** The resolver walks the normalized tree
segment by segment; at each step the candidate set must narrow to **exactly one**
node to continue. There are **no wildcards, predicates, or node-sets** — one
locator addresses one position. The walk produces one of four outcomes:
| Status | Meaning |
|--------|---------|
| `resolved` | Exactly one node matched — the marker is placed. |
| `resolved-cell` | A synthetic lane×column **cell** coordinate (see the cell note in [§3](#3-typed-path-segments)). |
| `ambiguous` | More than one node matched the same segment (e.g. a board with both `Doing` and `doing`). |
| `not-found` | Zero matches, an unsupported segment kind, or an empty / over-long path. |
**One parser, both directions.** The *same* `parseLocator` + resolver serve
**both** directions of the
[Inside-Out / Outside-In](../concepts/inside-out-outside-in.md) model: read
forward, a locator turns an entity's frontmatter value into a position; read in
reverse, the indexer builds a **reverse index** from those same locators so a
board or route can surface *what points at it*. Because the parser lives in
`common/`, the grammar is **shared across DSLs** — the board segment kinds plus
the route `stage:` extension ([§3](#3-typed-path-segments)) — only the recognized
segment kinds widen.
---
## 1. Overview
A locator names **one addressable place**. There are two families of places:
| Family | What it addresses | Form | Rendered by |
|--------|-------------------|------|-------------|
| **Positional** | a place *inside* a board or a route | a `[[file]]` head + typed segments — `col:` `lane:` `group:` `split:` (board) · `stage:` (route) | the board / route that owns the position |
| **Entity** | a thing *of the organization* — a work system, a unit, a person | a **bare `[[Name]]` wikilink**, no `#id`, no path — resolved by the **target's** type at lookup time | every picture that draws the target: the topology node (a work system), the unit box (a unit), the person box (an individual) |
A positional locator carries a `[[file]]` head to say *which* board or route
(§2) and a path to say *where inside it*. An entity locator is **only** the head:
`[[Jonas Keller]]` is complete on its own, because a person, a unit or a work
system is one thing across the workspace. Which box the marker lands on is not
spelled in the locator — it follows from what the target **is**: an `individual`
→ the person box in every org chart that draws them; a `unit` → the unit box in
every units view that draws it; a unit wearing the `work-system` hat → the
topology node **and** the unit box (one entity, one anchor set, both pictures);
a work system → the topology node. A same-named person and unit both receive the
bare form; the path form disambiguates (§2).
A locator is consumed in two places:
1. The **frontmatter** of an external entity file — e.g. an interaction or
agreement note that anchors itself to a single board position via an
`interaction-of:` / `agreement-of:` value, or a signal that observes a person
via `observes:`.
2. A **body table** in a container entity (a meeting) — one row per anchored
position, so a single Daily Standup file can anchor *N* board positions at
once (see [§7 Table-row container convention](#7-table-row-container-convention)).
In both cases the locator string is resolved by the workspace indexer against
the boards, routes, people and units it has indexed. When the locator resolves,
the addressed document renders a **type-specific marker** at that place (an
`.interaction-chip` for an interaction, a `.policy-chip` for an agreement, a pin
for a signal). When it does **not** resolve, nothing happens — the document
renders no marker there and never crashes (see
[§6 Graceful degradation](#6-graceful-degradation)).
The board file does **not** list its inbound anchors. Discovery is *outside-in*:
the entities declare the positions, the indexer builds a reverse index, and the
board renders what points at it. The board author may **also** declare *internal*
`agreements:` / `interactions:` directly on a column/lane/group; when both an
internal declaration and an external anchor target the same position, the
renderer blends them into one surface with provenance badges (D-22.1-09).
---
## 2. Head forms
The **head** identifies the *file* (and optionally the *board within the file*)
that the rest of the locator addresses.
```
locator := head ('#' boardId)? ('/' path)? ('::' position)?
```
| Head form | Example | Meaning |
|-----------|---------|---------|
| `[[file]]` (canonical wikilink) | `[[Sprint Board]]` | Reference the file `Sprint Board` (resolved by basename, case-insensitive). **This is the canonical form** (D-18). |
| `[[file\|label]]` | `[[sprint-board\|Sprint]]` | Wikilink with a display label — only the part **before** the `\|` is the file (`sprint-board`). |
The head resolves by **basename, case-insensitive** — the workspace indexer keys
files by lowercased basename, so `[[Sprint Board]]` and `[[sprint board]]`
address the same file, and a board moved into a subfolder is still reachable by
its unchanged basename.
### The path form — `[[individuals/Name]]` / `[[units/Name]]` (same-named pair)
A head may carry a **folder prefix**: `[[individuals/Aurora]]` or
`[[units/Aurora]]`. The indexer matches a path-form head by **file-path suffix**
— the target's own file path must end with `/<the written path>.md` (the
D-WL-03 rule `lookupNote` already uses for notes). This is how a **same-named
person and unit** are told apart on the entity surface (§3): a bare
`[[Aurora]]` lands on **both** the person box and the unit box — documented, not
diagnosed — while `[[individuals/Aurora]]` reaches only the person and
`[[units/Aurora]]` only the unit. The folder is whatever folder the target file
really sits in (matched case-insensitively); an unknown target still receives the
bare form.
A locator with **no head** (e.g. a bare `#col:Doing::in` in a container body
table) is a **relative** locator — it inherits its file/board from the container
entity's frontmatter. See [§7](#7-table-row-container-convention).
### Inline (position-only) form — same-doc, no head (D-02)
There is a second head-less form that is **not** relative-by-inheritance: the
**position-only** (inline) locator. It carries **no `[[file]]` head and no
`#boardId`** — just a typed `path` and an optional `::side`:
```
inline-locator := path ('::' position)? (no head, no '#boardId')
```
It is used by an annotation **authored inside the very document it addresses** —
the inline `notes:` surface (Phase 33). Because the note lives *in* the same
board / route / topology document, there is nothing to point *at* by filename:
the locator resolves against **the same doc**, so the wikilink head is dropped
entirely rather than inherited from a container. This is the deliberate same-doc
simplification versus the external-entity model.
| Form | Head | Resolves against | Example |
|------|------|------------------|---------|
| **Full** | `[[file]]#Id` | the addressed file/board | `[[Sprint Board]]#Sprint Board/col:Done::in` |
| **Relative** | none (inherits container frontmatter) | the container's declared head | `col:Done::in` in a meeting body table |
| **Inline (position-only)** | none (same-doc) | **the document the locator is authored in** | `col:Done::in` inside that board's own `notes:` |
The **path grammar and the `::side` qualifier are identical** across all three
forms — the inline form simply reuses the same `parseLocator` typed segments
([§3](#3-typed-path-segments)) and position qualifier
([§4](#4-position-qualifier)) with the head omitted. Each DSL accepts the segment
kinds its structure supports:
- **Board** — `col:` / `lane:` / `group:` / `split:` (e.g. `col:Doing::in`,
`lane:Alice/col:Review::before`).
- **Route** — **`stage:` only** for v1 (e.g. `stage:epic @ Coordination::before`).
A non-stage route position (a band, a trigger, an edge) has no locator segment
yet and stays **out** until the NAV-05 / v2 grammar extension — it degrades to
no marker, never a blank.
- **Topology** — the work-system node id (the node name).
- **Org chart** — `[[Name]]` or a bare `Name` (the person's file basename); no
`.` doc-note yet — the header is painted, but neither structure view has a
doc-note bubble.
- **Units** — `[[Name]]` or a bare `Name` (the unit's file basename).
The inline form inherits the same **exact-one** resolution and **graceful
degradation** contract ([§6](#6-graceful-degradation)): a position-only locator
that no longer resolves is *orphaned*. Unlike the external surfaces (which
silently drop an unresolved marker), the inline note surface **preserves an
orphaned note** — see [`board/notes.md`](./board/notes.md) and its route /
topology siblings for the orphan tray + diagnostic behavior.
---
## 3. Typed path segments
After the head (and optional `#boardId`), a `/`-separated **path** drills into
the board's structure. Every segment is `kind:Name`:
```
path := segment ('/' segment)*
segment := kind ':' name (kind ∈ lane | group | col | split | stage)
```
| Segment | Addresses |
|---------|-----------|
| `col:Name` | A column named `Name` |
| `lane:Name` | A lane (horizontal swimlane) named `Name` |
| `group:Name` | A group named `Name` |
| `split:Name` | A split section named `Name` |
| `stage:Name` | A **route stage** — addresses a Flight Route, not a board. `Name` is the full `itemType @ Band` pair (see [The `stage:` route segment](#the-stage-route-segment-landed-phase-26) below). |
Every segment kind is **positional**. A person, a unit or a work system is not
reached through a segment — it is the bare `[[Name]]` head with no path (see
[Entity wikilinks](#entity-wikilinks--resolution-by-target-type-the-role-bridge-the-path-form-phase-42)
below).
Segments **chain** to reach nested positions. Examples:
- `col:Done` — the `Done` column.
- `lane:Alice/col:Doing` — the `Doing` column *within* the `Alice` lane.
- `group:Delivery/lane:Bob/col:Review` — the `Review` column in the `Bob` lane
inside the `Delivery` group.
- `col:Build/split:QA` — the `QA` split section inside the `Build` column.
**Case-insensitive matching (D-12).** Both the `kind` and the `Name` match
case-insensitively, so `col:done`, `COL:Done`, and `col:Done` all address the
same column. A name may legitimately contain an interior colon
(`col:Review: Notes`) — only the **first** colon separates the kind from the name.
A segment whose `kind` is not one of the five recognized kinds, or whose name is
empty, is **dropped** (tolerant degradation) — the rest of the path still parses.
> **There is no authorable `cell:` segment.** A *cell* is a **resolution
> result**, not something you write. When a `lane:L/col:C` path descends into a
> lane that owns no column node `C`, but `C` *is* a board-global column on a
> shared-columns grid, the resolver returns a **synthetic lane×column cell
> coordinate** (`{ laneName, columnName }`) so the marker lands in the same
> visible cell regardless of which board structure the author used — this is the
> `resolved-cell` outcome in [How it works](#how-it-works). You still author
> `lane:L/col:C`; the cell is what the resolver hands back.
### The `.` document-root path (whole-document scope)
A path may begin with a `.` **document-root token**. It names the position
**from the root of the document itself** rather than as an implicit same-document
path — the XPath / filesystem root convention. `/` stays the path **separator**,
so `.` (not `/`) is the free root token:
```
path := '.' | '.' '/' segment ('/' segment)* | segment ('/' segment)*
```
| Path form | Resolves to | Equivalent to |
|-----------|-------------|---------------|
| `.` | the **whole document** — an empty path, document scope | (the document itself; no per-position marker) |
| `./col:Doing` | a path **from the document root** | the bare `col:Doing` |
| `col:Doing` | an implicit same-document path (unchanged) | — |
- **`.` alone = the whole document.** The empty path resolves to **document
scope**: the marker is not pinned to any column / lane / group / split (or
stage / node) but to the document as a whole. On the inline `notes:` surface
this renders as **one marker in the document header**, to the **left of the
doc-tag chips**; when two or more `.` doc-notes exist they **collapse to one
header marker with a count badge** (see [`board/notes.md`](./board/notes.md)
and its route / topology siblings).
- **`./<path>` = a path from the root, equal to the bare path.** A leading `.`
on a longer path (`./col:Doing`) is **exactly equivalent** to writing the bare
path (`col:Doing`) — the `.` names the root the path descends from and is
optional. No YAML quoting is strictly required for `.`, but the convention is
to quote it (`on: "."`) for clarity.
### The `stage:` route segment (landed Phase 26)
`stage:` is the **route** extension of the locator grammar — the one segment kind
that addresses a **Flight Route** instead of a board. The route DSL reuses the
very same `parseLocator` (D-26-01 / D-26-02 / D-26-03), so a route position is
just another typed path segment. It behaves like the four board kinds, with two
route-specific specifics:
- **Name = the full `itemType @ Band` pair.** A route stage is identified by its
`(itemType, band)` pair, spelled `itemType @ Band` with a single space each
side of the `@` — the **same** spelling used in `path:` edges (see
[Path & bands](./route/path-and-bands.md)). So `stage:epic @ Coordination`
names the `epic` stage in the `Coordination` band. The name is **opaque** to
`parseLocator`: interior spaces and the `@` are fine, and the name runs to the
next `/` (or to the `::side` qualifier). A whitespace drift simply never
matches → no marker (graceful). The item-type half is the stage's **name**: its
token, or for a stage written as a wikilink (`"[[Epic]] @ Coordination"`) its link
target — `stage:Epic @ Coordination`, never with `[[…]]` inside the locator. The
match is exact (the band half stays case-insensitive); the old lowercase spelling of
a stage that became a wikilink still lands for one release and is reported as a
deprecation (see [Route: a stage's item type](./route/index.md#responsibility-and-references)).
- **Multi-route disambiguation reuses the `#Name` slot.** In a Markdown file
hosting several route fences, `[[file]]#Route Name/stage:epic @ Coordination`
selects the route by name — exactly like `#boardId` selects a board (§2). The
bare `[[file]]/stage:epic @ Coordination` form also resolves for the common
one-route-per-file case.
The `::before` / `::in` / `::after` side ([§4](#4-position-qualifier)) applies
unchanged. The route-side delta and its markers are documented in
[`route/anchors.md`](./route/anchors.md); **this page is the authority for the
locator string itself.**
### Board scope, and a board locator without `#Title`
Three board-locator forms differ only in what they address; all use the head
resolution above.
| Form | Example | Addresses |
|------|---------|-----------|
| board scope | `[[Order Studio Board]]#Order Studio::in` | the **whole board** — a `#Title` and no path. Shown in the board header (see [board anchors](./board/anchors.md)). |
| board position | `[[Order Studio Board]]#Order Studio/col:Flagged::before` | one unit; the `::before` / `::in` / `::after` side picks the set at that unit |
| bare entity | `[[Order Studio Board]]::in` | **not** a board anchor: no `#Title` and no path make it an entity locator (the entity named `Order Studio Board`) |
**The two-slot rule.** A board locator written **without** `#Title`
(`[[Sprint Board]]/col:Doing::in`) lands on a **titled** board the same way a route
locator without a title lands on a titled route: the resolver tries the board's
title and the empty title. In a Markdown file that hosts several titled `dwsd.board`
fences it lands on **every fence that has the unit**. A locator **with** `#Title`
stays exact — it lands only on the board of that title. A titled locator against an
*untitled* board does not land.
**A locator that lands nowhere is a warning.** VS Code marks a board locator that
lands on no board unit with a warning on the line that writes it, naming one of
three reasons: the addressed document is not in the workspace, the document has no
board of that title, or the board has no unit at that path.
### Entity wikilinks — resolution by target type, the role bridge, the path form (Phase 42)
An **entity wikilink** is the bare head with nothing after it — `[[Name]]`, no
`#id`, no path, no `::side`. It addresses a thing *of the organization* rather
than a position *inside a document*, and it is the **one** form for doing so:
a work system, a unit and a person are all written the same way. `parseLocator`
yields `{ file: 'Name', segments: [] }`; the indexer keeps such values in
**name-keyed** reverse maps without asking what the target is. The picture that
draws the target decides at **lookup time**:
| The target `[[Name]]` is … | The marker lands on … |
|----------------------------|-----------------------|
| an `individual` | the person's box, in every org chart that draws them |
| a `unit` | the unit's box, in every units view that draws it |
| a unit wearing the `work-system` hat | the topology node **and** the unit box — one entity, one anchor set, both pictures |
| a work system | the topology node |
| nothing the indexer knows | nowhere — no marker, no error ([§6](#6-graceful-degradation)) |
Because resolution happens when a picture asks, file order never matters: a
signal indexed before the person it observes still lands once the person's file
is known.
Casing does not matter either. Like the head form ([§2](#2-head-forms)), an
entity wikilink matches the target's basename **case-insensitively**:
`observes: "[[jonas keller]]"` reaches `Jonas Keller.md`,
`for-domain: "[[head of operations]]"` still bridges through the role, and two
links that differ only by case address one target. The path form
(`[[individuals/jonas keller]]`) compares the same way.
The structure keys `reports-to:` and `role-keeper:` are not locators and still expect the file's authored spelling.
**Which frontmatter keys accept an entity wikilink:**
| Key | Entity types | What renders |
|-----|--------------|--------------|
| `interaction-of:` | `interaction` · `meeting` | an interaction marker; a drawer row under "Interactions" |
| `agreement-of:` | `agreement` | an agreement marker; a drawer row under "Agreements" |
| `bound-to:` | `ai-agent` | an agent marker; a drawer row under "AI Agents" |
| `mandate-of:` | `mandate` | a mandate chip or marker, labeled with the holder; see [Mandates](./mandates.md) |
| `observes:` | `signal` · `insight` | a pin on the box / node, listed under the drawer's "Change conversation" band |
| doc-level `notes: - on:` | inline notes in an org chart / units document | the note marker on the box ([org chart notes](./orgchart/notes.md), [units notes](./units/notes.md)) |
The `observes:` row lists the **two pin types whose `observes:` is read** (format 1.3), which is what the
code accepts: `isObservesPinType` in `common/anchor-registry.ts` tests membership in
`ANCHOR_OBSERVES_PIN_TYPE_KEYS` (`signal`, `insight`), and the indexer gates every `observes:` pin on it.
**That accept-set is the authority** — if it and this row ever disagree, the row is the one that is wrong.
A `driver` or `proposal` is a pin too (the change pill has four stages), but its `observes:` is not
read at all: neither a pin nor the `?` fall-through. It names its domain with `for-domain:`, a plain
wikilink. An entity whose `type:` is outside the pin family does not vanish: its `observes:` falls through
to the unknown-anchor affordance and renders a dashed `?` marker instead of a pin.
These keys are the value-polymorphic ones of [§5](#5-value-polymorphic-keys);
the entity wikilink is their "bare `[[…]]`" shape. `mandate-of:` takes the same value forms as
`interaction-of:` and `agreement-of:`. The `Mandate of` cell of a mandate row on a holder's file takes
them too, but it must be **absolute** (it starts with `[[` or `@`): a relative locator in that cell
inherits nothing, because a holder is not a board, and the editor warns about it (`mandate.relative-locator`). A list value puts the same
entity under **each** name it lists (`interaction-of: ["[[Jonas Keller]]",
"[[Mara Holt]]"]` is a 1:1 on both boxes).
**`for-domain` is different — it names a domain, not a target.** `for-domain:`
is **not** value-polymorphic ([§5](#5-value-polymorphic-keys)); it points *up* at
a **domain**, and a domain is **a work system or a role**:
- `for-domain: [[Work System]]` — the topology's anchor surface for agreements,
drivers, proposals, decision records and flight routes; a unit with the
`work-system` hat shows the same agreements / drivers / proposals / decision
records on its unit box. A **role**'s own `for-domain: [[Work System]]` (a single
wikilink or a list) puts a **role marker** on that work system's topology node and
on the unit box of a unit that wears the `work-system` hat; a plain team (no hat)
and a person get none.
- `for-domain: [[Role]]` — the **role bridge**: the entity lands on the box of
**every keeper** of that role (a shared role reaches each keeper), and the
drawer row says *via <Role>*. It is **one hop**: the role's own
`for-domain: [[Work System]]` is never followed further, so the topology learns
nothing from a role-scoped record. The bridge applies to every type that
carries `for-domain` — agreement, interaction / meeting, AI agent, decision
record, driver, proposal (not flight routes, not roles). A record that names
the person **directly** (through a polymorphic key) *and* their role shows
once, without *via*.
- `for-domain: [[Person]]` **lands nowhere** — a person is not a domain. The org
chart validator says so with an `info` diagnostic on the view's `mode` line:
`orgchart.for-domain-names-individual` (*point it at the person's role
instead*); only an anchoring document is reported — a file whose `type:` the indexer
does not know is never named by this diagnostic. Roles are anchoring
documents since Phase 48, so a `type: role` scoped to a person is reported too
(its marker has nowhere to land). An **unresolved** target keeps today's behavior —
silence.
- `for-domain: [[Plain Unit]]` (a unit without the hat) is **a valid target**
and raises no finding: format 1.1 counts a unit as a valid `for-domain` target (a mandate
can name one, see [Mandates](./mandates.md)). Its box in the units view shows the document.
**The path form.** `[[individuals/Aurora]]` / `[[units/Aurora]]` tells a
same-named person and unit apart ([§2](#the-path-form--individualsname--unitsname-same-named-pair));
the bare `[[Aurora]]` lands on both.
**A leftover type prefix is skipped.** The grammar recognizes only the five
positional kinds above; a value such as a type word, a colon and a name is an
unknown kind and is dropped by the tolerant parser — an anchor lands nowhere, a
note becomes an orphan (`orgchart.note-orphan` / `units.note-orphan`). Write the
wikilink.
---
## 4. Position qualifier
A trailing `::before` / `::in` / `::after` selects **where, relative to the
addressed position**, the marker sits:
```
position := 'before' | 'in' | 'after'
```
| Qualifier | Slot |
|-----------|------|
| `::before` | Immediately **before** the position |
| `::in` | **In** / on the position itself (the default) |
| `::after` | Immediately **after** the position |
**Bare = `in` (D-04).** Omitting the qualifier is identical to writing `::in`:
`[[Sprint Board]]#Sprint Board/col:Doing` and
`[[Sprint Board]]#Sprint Board/col:Doing::in` resolve to the **same** position.
An explicit `::in` is accepted and normalizes to the same `in` — authoring it
does not produce a doubled key (a real bug fixed in Plan 22-08 / RC3).
The qualifier is peeled from the **end** of the string before the path is split,
so `col:Review::before` yields a column named `Review` (not `Review::before`).
### Runtime target forms: `chip:` and `@doc` are not authored
Marks (Phase 52) address a **chip**, a pin or a drawer entry with two further forms that exist **only at
runtime**, in a host's mark set and in an agent's request:
```
<position>[::before|in|after]/chip:<bucket>[:<entry>] e.g. col:doing/chip:agreement:team-working
@doc/chip:<bucket>[:<entry>] e.g. @doc/chip:domain
```
They are **not accepted** in `bound-to:`, in a note's `on:` or in any other field this grammar governs:
`parseLocator` does not know a `chip:` segment, and it silently drops an unknown segment kind, so a
`chip:` written into an authored locator would quietly address the position in front of it. Nothing to
author, nothing new to migrate.
`@doc` is a **reserved head** for the document scope (the whole diagram: a route's header chips, a
board's header chips). A legacy `@<file>` head whose name is literally `doc` is therefore not addressable
as a mark target. The contract for both forms (buckets, entry slugs, the `::side` qualifier, limits) is
`docs/host-integration/marks.md` in the source repository.
---
## 5. Value-polymorphic keys
The two relationship keys `interaction-of:` and `agreement-of:` are
**value-polymorphic** (D-22.1-01): the *same key* accepts **either** of two value
shapes, and the renderer dispatches on the shape, not on a different key.
| Value shape | Meaning | How to recognize it |
|-------------|---------|---------------------|
| **Entity wikilink** — `[[X]]` | An *entity anchor*: the entity relates to the whole thing X names — a work system (the topology anchor surface), a unit (the unit box), a person (the person box); resolved by X's type ([§3](#entity-wikilinks--resolution-by-target-type-the-role-bridge-the-path-form-phase-42)). | A bare `[[…]]` with **no** `#…` / `/col:` / `/lane:` … path segments. |
| **Board-position locator** — `[[board]]#Title/col:Name::in` | A *position anchor*: the entity points at one specific board position. | Presence of a `#…` board id and/or `/col:` · `/lane:` · `/group:` · `/split:` path segments. |
At a glance, the indexer branches **route → board → bare wikilink**: a
`stage:` segment makes it a route-position locator; otherwise, if the value
carries a `#` boardId or any `kind:` path segment, it is a board-position
locator; **otherwise it is an entity wikilink.** Route and board values go to
the route / board reverse indexes and render a chip at the position; a bare
wikilink goes to the name-keyed entity reverse index, and the picture that draws
the named thing renders the marker.
```yaml
# Entity anchor (value = bare wikilink, no path) — a work system here:
interaction-of: [[Delivery Squad]]
# Entity anchor — a person (a 1:1) and a unit:
interaction-of: ["[[Jonas Keller]]", "[[Wholesale]]"]
# Position anchor (value = board-position locator, has #boardId + /col:…):
interaction-of: "[[Sprint Board]]#Sprint Board/col:Done::in"
```
`interaction-of:`, `agreement-of:` and `bound-to:` accept both shapes;
`observes:` (the pin types, Phase 31) is value-polymorphic the same way — a
bare `observes: [[X]]` pins on X's box or node. `for-domain:` (the domain key)
is **unrelated** and is **not** value-polymorphic — it remains the canonical
domain key, a plain wikilink naming a work system or a role, and is untouched by
this surface (D-22.1-02; the role bridge is described in
[§3](#entity-wikilinks--resolution-by-target-type-the-role-bridge-the-path-form-phase-42)).
---
## 6. Graceful degradation
The locator surface **never crashes** and **never blanks the board** (D-10).
Concretely:
- An **unresolved** locator (the file does not exist, the board id is wrong, the
column name does not match) simply renders **no marker** at that position. The
rest of the board renders normally.
- A **malformed** locator degrades to whatever parts the tolerant parser could
recover — an unrecognized segment kind is dropped, an empty path yields the
board-level position, an empty string yields `{ segments: [], position: 'in' }`.
- The parser performs **no filesystem access** — resolution happens in the host
indexer, and a failed resolution is a silent no-op, not an error.
A future **diagnostic surface** (the B3 work, currently *deferred*) will surface
unresolved locators back to the author so they are not silently lost. Until then,
the principle is *"show as much as possible, never blank, never break"* — an
unresolved anchor costs you one missing chip, nothing more.
---
## 7. Table-row container convention
A **container entity** (a `type: meeting`) can anchor **N** board positions at
once via a body table — one row per position. This is the
**Inside-Out / Outside-In** workhorse: a Daily Standup meeting whose individual
agenda items each anchor a different corner of one board.
The container declares a `## Interactions` (for meetings/interactions) or
`## Agreements` (for agreements) heading, followed by a table whose **"Interaction
of"** / **"Agreement of"** column holds one locator per row:
```markdown
---
type: meeting
title: Daily Standup
interaction-of: [[Sprint Board]]
---
## Interactions
| Name | Outcome | Interaction of |
|---------------|----------------------|-------------------------------------------------|
| What's done? | Items moved to Done | [[Sprint Board]]#Sprint Board/col:Done::in |
| Where blocked?| Blocked items visible| [[Sprint Board]]#Sprint Board/col:Blocked::in |
```
- **One row = one position anchor. N rows = N anchors.** The two rows above
produce two markers on the `Sprint Board` — one on `Done`, one on `Blocked`.
- The extractor scans the **full body** (not a head-byte cap), so a long meeting
with many rows is fully indexed.
### Locator-inheritance chain (D-22.1-12)
Each row's locator follows a **full / relative / bare** trichotomy:
| Row locator form | Resolution |
|------------------|------------|
| **Full** — `[[Sprint Board]]#Sprint Board/col:Done::in` | Used **as-is**; overrides the container frontmatter (the row names its own file + board). |
| **Relative** — `#col:Done::in` or `col:Done::in` (no `[[file]]`) | **Inherits** the container's frontmatter `interaction-of:` / `agreement-of:` head, which is grafted on to form a full locator before resolution. |
| **Bare / unresolved** — a position with no inheritable head | **Skipped gracefully** — no anchor, no crash (D-10). |
The relative form keeps container tables terse: declare the board once in
`interaction-of:` frontmatter, then list only the in-board positions in the rows.
---
## 7a. Outcome channel (D-09 / D-10)
Each anchored entity can carry an **outcome** — a short line of context shown in
the unified drawer next to the marker. There are two sources, with a strict
precedence (D-09):
1. **Primary — the body-table `Outcome` cell.** In a container table (§7), the
per-row **"Outcome"** column is the primary outcome channel: one outcome per
anchored position, row-granular.
2. **Fallback — a `## Outcome(s)` section.** When an anchored entity has **no**
body-table Outcome cell, the renderer falls back to a markdown
`## Outcome(s)` H2 **section** in the entity body and uses its **first
paragraph** as the drawer outcome. The fallback fires *only* when no table
outcome exists; a table Outcome cell always wins.
### The recognized-heading allowlist (D-10)
The fallback matches a **closed allowlist** of H2 headings — not "any heading
containing the word outcome". After stripping the `## ` marker and trailing
punctuation/whitespace, the heading must match, **case-insensitively**:
| Recognized heading | Notes |
|--------------------|-------|
| `## Outcome` | singular |
| `## Outcomes` | plural |
| `## What is the outcome?` | the question form; the trailing `?` is optional |
The exact literal (mirrored verbatim from the workspace indexer's
`OUTCOME_HEADING_RE`, so a reviewer can diff the two and see they agree):
```js
/^(?:outcomes?|what is the outcome)\??$/i
```
**Deliberately rejected** — these do **not** match, so unrelated prose never
leaks into the drawer:
- `## Outcome history`
- `## Bad outcomes to avoid`
- any other `…outcome…` heading.
A broad `/##.*outcome/i` match is **intentionally avoided**: it would
false-positive on the rejected headings above. The allowlist is a single
**anchored, non-backtracking** alternation (no nested quantifier), so it is also
ReDoS-safe on hostile workspace input (T-22.3-01). The captured first paragraph
is truncated to **280 characters**; on overflow a single `…` affordance is
appended (renderer-resilience — never a hard mid-word clip), and the text always
reaches the drawer as a plain text node, never as HTML.
---
## 8. Worked examples per DSL
### (a) Interaction anchored to a column
An interaction note that points at the `Done` column of the `Sprint Board`:
```yaml
---
type: interaction
title: Done Review
interaction-of: "[[Sprint Board]]#Sprint Board/col:Done::in"
---
```
Renders an `.interaction-chip` *in* the `Done` column.
### (b) Agreement anchored to a split section
A working agreement that points at the `QA` split section inside the `Build`
column:
```yaml
---
type: agreement
title: QA Gate
agreement-of: "[[Sprint Board]]#Sprint Board/col:Build/split:QA::before"
---
## Summary
No story leaves Build until QA signs off.
```
Renders a `.policy-chip` *before* the `QA` split section.
### (c) Meeting container with N rows
A Daily Standup meeting anchoring two positions of one board via its body table —
see the full example in [§7](#7-table-row-container-convention). One file → two
markers (`Done` + `Blocked`), demonstrating the *one container → N anchors*
invariant.
### (d) Interaction anchored to a route stage
An interaction note that points at the `epic @ Coordination` stage of the
`Referenced Route` (the route `refs` fixture):
```yaml
---
type: interaction
title: Epic Kickoff
interaction-of: "[[Referenced Route]]/stage:epic @ Coordination::before"
---
```
Renders an interaction marker *before* the `epic @ Coordination` stage. The
`stage:` segment is the route extension of the grammar
([§3](#the-stage-route-segment-landed-phase-26)); the route-side marker + drawer
behavior is documented in [`route/anchors.md`](./route/anchors.md).
---
## 9. Out of scope
| Not covered (yet) | Where it lives |
|-------------------|----------------|
| **Wildcard / predicate queries** — e.g. "every column whose name matches `*Done*`" or "all lanes in group X" | Future work; the current grammar addresses one exact position per locator. |
| **Topology anchors** — anchoring to a Work System rather than a board position | See [`topology/index.md`](./topology/index.md); the value-polymorphic `interaction-of:` / `agreement-of:` keys cover the WS-wikilink shape (see [§5](#5-value-polymorphic-keys)). |
---
*Reference parser: `packages/dwsd-dsl/src/common/parse-locator.ts`. Sibling
reference: [`topology/index.md`](./topology/index.md).*