%% Werkbaum, planned with Werkbaum — what exists today and where it could go.
%% Distilled from docs/ROADMAP.md, docs/TASKS.md and docs/DECISIONS.md.
%% Every node carries an id; the block behind --- explains it. Hover a node.

[~] #wb: Werkbaum (XXL) https://github.com/mhoennig/werkbaum
  - [^] #not: Notation (XL)
    - [^] #not.line: Line format (M)
      - [^] #not.status: Status box, eight states (S)
      - [^] #not.size: T-shirt size and the "decompose from M" rule (S)
      - [^] #not.people: People tags, bare URLs, %% comments (XS)
      - [^] #not.cont: A trailing backslash continues the line (XS)  %% space before it is required
    - [^] #not.gates: And/or decomposition (S)
    - [^] #not.opt: Optional nodes — neither required nor an alternative (S)
    - [^] #not.id: Node IDs, (#auth:) before the title (S)  %% often the ticket number, see SPEC §11
    + [/] #not.tag: Free tags, (&tag) (M)  %% first consumer found: the Taiga project tag, D91
      - [?] #not.tag.lens: A lens: highlight every node tagged (&x) (S)
      - [^] #not.tag.project: (&taiga.slug) assigns the Taiga project, inherited (S)
    - [^] #not.dep: Dependencies across the tree, (:#auth,#api) (M)
      - [^] #not.dep.status: They constrain the status, not when work may start (S)
      - [^] #not.dep.eff: Effective status: intrinsic, plus what it depends on (S)
      - [^] #not.dep.cycle: Cycles are legal — they mean "finished together" (XS)
      - [^] #not.dep.warn: An id with no node behind it is a warning (XS)
    - [^] #not.xor: XOR, = — exactly one alternative, not at least one (S)
    - [^] #not.desc: Node descriptions, shown as a tooltip (M)
      - [^] #not.desc.short: A short text right at the node (S)  %% a " line below the node
      - [^] #not.desc.long: A long block at the end, addressed by its id (S)  %% behind a --- separator
    - [^] #not.sizes: Size conflict — sub-packages must fit the given size (S)  %% ranges, XXL open-ended, see D62
    - [-] #not.store: A separate storage format for the structure (L)  %% the text is the format
    - [ ] #not.ticket: Ticket references (#US-123,#T-1234) (S)  %% Werkbaum writes the US-/T- prefixes itself
    - [^] #not.llms: llms.md, the notation explained to AI agents (S)
      - [^] #not.llms.index: llms.txt, the signpost that points at it (XS)  %% the convention wants an index, not the content
    - [ ] #not.benefit: Benefit per node: a 0-9 digit in the size token (M)  %% (M/9), benefit alone as (/7) — see SPEC §11
      - [-] #not.benefit.sigil: A separate sigil next to the size (S)  %% one token in one corner won
      - [-] #not.benefit.points: Story points behind the T-shirt sizes (M)  %% false precision — the sizes stay ordinal
  - [~] #ed: Editor (XXL) https://werkbaum.javagil.de
    - [^] #ed.live: Live diagram from text (L)
      - [^] #ed.parser: Parser (S)
      - [^] #ed.render: Renderer, three layout modes (L)
        - [^] #ed.render.horiz: Horizontal fan (S)
        - [^] #ed.render.vert: Vertical, transposed (S)
        - [^] #ed.render.comp: Compact (S)
        - [^] #ed.render.wrap: Long titles wrap into balanced lines (S)  %% max ~40 chars, see D64
      - [^] #ed.deplink: Cross links for dependencies (M)
        - [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
        - [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
        - [^] #ed.deplink.fold: A hidden endpoint falls to its visible ancestor (S)  %% source and target alike, see D75
        - [^] #ed.deplink.toggle: A switch hides the cross links (XS)
    - [^] #ed.path: Cheapest path (XL)
      - [^] #ed.path.cost: Cost model, a size prices its subtree (S)
      - [^] #ed.path.est: Missing size estimated from the sub-packages (S)  %% see D66
      - [^] #ed.path.front: Only the open front: what is done costs nothing (S)  %% see D46
      - [^] #ed.path.started: Started work is on the path, extras included (S)  %% see D61
      - [^] #ed.path.line: Metro-map line through the leaves (S)
      - [^] #ed.path.step: Step from station to station, one button (S)  %% see D47
      - [^] #ed.path.load: Warn when one person is the bottleneck (S)  %% see D71
      - [ ] #ed.path.worth: The most worthwhile path: one button, three states (M) :#not.benefit
      - [^] #ed.closure: Count the whole dependency closure (M)
        - [^] #ed.closure.once: Shared dependencies counted only once (S)
        - [^] #ed.closure.nonlocal: The cheapest alternative is no longer the local one (M)  %% see D34, D42
          | [^] #ed.closure.exact: Search exactly, the trees are small enough (M)
            - [^] #ed.closure.enum: Enumerate the coupled any-of choices (S)
            - [^] #ed.closure.union: Cost is the size of the union (S)
          | [^] #ed.closure.greedy: Stay greedy, but say so (XS)  %% built as the named fallback
    - [^] #ed.export: Export (M)
      - [^] #ed.export.file: SVG and PNG download (S)
      - [^] #ed.export.clip: PNG to the clipboard (S)
      + [^] #ed.export.print: Print stylesheet (XS)
    - [^] #ed.i18n: Nine interface languages (M)
      - [^] #ed.i18n.texts: Translations (S)
      - [^] #ed.i18n.auto: Default taken from the browser (XS)
    - [^] #ed.a11y: Accessibility: aria labels, focus order (S)
    - [^] #ed.mobile: Works on a phone (S)
      - [^] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS)  %% no pointer, no tooltip
    - [^] #ed.tipwindow: The same window at the pointer and on keyboard focus (XS)  %% a title has no paragraphs and never shows on focus
    - [^] #ed.docs: Several documents in one editor (L)
      - [^] #ed.docs.picker: Breadcrumb picker in the app header (S)  %% Werkbaum › name, see D81
      - [^] #ed.docs.url: Load a document from ?sourceUrl= (S)
      - [^] #ed.docs.restore: Restore a shipped document from the menu (XS)
      - [^] #ed.docs.windows: Two windows on one storage: app and tab side by side (M)  %% docs/rfc/002-mehrfenster.md, D94
        - [^] #ed.docs.windows.keys: Every document owns its keys — text, meta, states; the index never deletes (S)
        - [^] #ed.docs.windows.sync: The storage event keeps the list, names and tombstones current in every window (S)
        - [^] #ed.docs.windows.lock: A per-document lock via the Web Locks API finds the one real loss case (S) :#ed.docs.windows.keys
        - [^] #ed.docs.windows.dialog: Three exits instead of continue anyway: open another, view only, edit anyway (XS) :#ed.docs.windows.lock
    - [^] #ed.jump: Jump between diagram and text (S)
      - [^] #ed.jump.dep: Ctrl+click follows a dependency to its id (XS)
    - [^] #ed.lineno: Line numbers in the text editor (XS)  %% the warnings name them
    - [^] #ed.shortid: Type #.kc under #prod-stage, get #prod-stage.kc (XS)  %% an input aid, not notation
    - [^] #ed.depcomplete: Typing :# suggests the known ids (S)  %% arrows choose, Enter inserts
    - [^] #ed.showids: A # button shows the ids before the titles (XS)  %% written as in the text, with a colon
      - [^] #ed.lineno.tip: The warning is the tooltip of its number (XS)  %% no hunting in the list
    - [^] #ed.fold: Collapsible subtrees (L)
      - [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S)
      - [^] #ed.fold.click: Fold and unfold in the diagram (S)
      - [^] #ed.fold.write: Folding writes the mark back into the text (S)  %% one undo step
      - [^] #ed.fold.preset: The fold button cycles through four presets (S)  %% M-and-smaller, off-path, all, none — see D44/D75
    + [?] #ed.textfold: Fold regions in the text editor, next to the line numbers  %% view-only, unrelated to the > marks; not sized yet
    - [^] #ed.lens: A people bar with a person lens (M)
      - [^] #ed.lens.fold: One person's nodes, everything else folds (S)  %% view-only, never written into the text
      - [^] #ed.lens.load: A load bar per person, shares of the open path work (S)  %% the D71 measure
    - [^] #ed.snaps: Earlier states of a document, every ten minutes (S)  %% only when something changed
      - [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS)  %% ten minutes is the wrong beat for that moment
    - [^] #ed.files: Open and save .werkbaum files (S)
      - [^] #ed.files.inplace: Saving writes back into the opened file (S)  %% File System Access, Chromium
      - [^] #ed.files.ctrls: Ctrl+S saves the document as a file (XS)  %% in place with a remembered handle
    + [^] #ed.fresh: Show what is new since your last visit (S)
      - [^] #ed.fresh.news: A star in the header, with the last few days (S)  %% see D58
    + [?] #ed.percolor: A pastel colour per person (S)
    - [?] #ed.dates: Dates and milestones (M)
      | [?] #ed.dates.attr: An attribute in the line (S)
      | [?] #ed.dates.view: A separate timeline view (L)
  - [^] #bld: Build and delivery (XL)
    - [^] #bld.bundle: One self-contained file, bundled by Vite (S)
    - [^] #bld.tests: Unit tests (S)
    - [^] #bld.pages: Published on every push (S)
    - [^] #bld.update: Says when a new build is out (S)  %% compares against the running one, see D45
    - [^] #bld.pwa: Installable as a progressive web app (M)
      - [^] #bld.pwa.manifest: A manifest with name, icons and standalone display (XS)
      - [^] #bld.pwa.sw: A service worker that serves the app offline (S)  %% network-first, cache as fallback
      - [^] #bld.pwa.update: The reload notice stays truthful under the worker (S)  %% see D45/D73
      - [^] #bld.pwa.files: Double-click a .werkbaum file, save without any dialog (S) :#ed.files.inplace  %% file_handlers + launchQueue
    - [^] #bld.backend: Ship the backend as a service of its own (L)
      - [^] #bld.backend.jdk: Its own JDK 21 in the server's home (S)  %% only 17 is installed there
      - [^] #bld.backend.unit: A systemd user unit that survives the session (S)
      - [^] #bld.backend.proxy: Apache passes /api/ on, in .htaccess (XS)  %% measured: the P flag is allowed
      - [^] #bld.backend.remote: One command for everything on the server (S)  %% remote <target> <action>
      - [^] #bld.backend.backup: Save the database, and prove it can be read back (XS)
    - [^] #bld.werkator: Also built by Werkator, the project's own CI (S)  %% .werkator.yml, backend + frontend
    - [^] #bld.gitea: The repository lives on Gitea, GitHub stays a clone (S)
    - [-] #bld.ghpages: Deploy through a gh-pages branch (S)  %% GitHub Actions instead
    - [-] #bld.gfonts: Fonts from Google (XS)  %% self-hosted, no third-party request
  - [ ] #be: Backend (XL)
    - [/] #be.scaffold: Kotlin and Spring Boot scaffold (M)
      - [^] #be.scaffold.gradle: Gradle Kotlin DSL, JDK 21 (S)
      - [^] #be.scaffold.info: An endpoint that says name and version (XS)
      - [ ] #be.scaffold.ci: Continuous integration for the backend (S)
    - [^] #be.docs: Documents on the server (L) :#be.scaffold
      - [^] #be.docs.model: Text plus metadata, no structure format (M)
        - [^] #be.docs.rest: REST endpoints (S)
        - [^] #be.docs.db: Persistence (S)
        - [^] #be.docs.history: History in two layers, and restore (S)
      - [^] #be.docs.share: Share by link (S)
      - [^] #be.docs.list: Master password for the document list (S)
    - [?] #be.auth: Accounts and permissions (L) :#be.docs
      | [?] #be.auth.token: Single user, one token (S)
      | [?] #be.auth.oidc: Log in with OIDC (L)
  - [~] #col: Collaborating on a Diagram (XXL)
    " Reading together is solved, changing together too — typing together at the
    " same instant is the expensive one.
    - [^] #col.point: A shared pointer: !!! focuses a node for everyone (XS)
    - [-] #col.pad: Borrow an Etherpad — it merges, we render (L)  %% built, then removed, see D78
    - [~] #col.git: Git as the shared store (L)
      | [ ] #col.git.pr: A file in a repository, changed by pull request (S)  %% works today, no code
      | [^] #col.git.pull: A script pulls the server document and commits it (S)  %% tools/pull-doc --git-commit, dated commits — see D88
        - [x] #col.git.timer: A timer on the backend host mirrors a list of documents into a branch (S)  %% tools/mirror-docs, systemd timer — see D88-Nachtrag 3
      | [?] #col.git.auto: The backend commits every change (L) :#be.scaffold
        - [?] #col.git.hist: History and restore (M)
          - [?] #col.git.diff: Diff between two versions (S)
          - [?] #col.git.blame: Who changed this line (S)
        - [?] #col.git.branch: A branch per scenario (S)
    - [/] #col.live: Live editing, several people at once (XXL) :#be.docs
      - [^] #col.live.http: Transport over plain HTTP (M) :#be.docs  %% no websocket needed
        - [^] #col.live.patch: Submit a change as a line diff (S)
        - [^] #col.live.feed: Long polling holds the other direction open (S)
      - [^] #col.live.merge: Merging simultaneous edits (L)  %% the actual work
        | [^] #col.live.diff: Line diffs against a base version; the server rebases (L)
        | [?] #col.live.crdt: Text CRDT — the plan is plain text, so it fits (L)
        | [?] #col.live.ot: Operational transformation (XL)
      - [^] #col.live.ask: On a real overlap, ask whose version wins (S)
      - [^] #col.live.upload: Share: put a local plan on the server (S)  %% a button in the editor title bar, see D81
      - [?] #col.live.cursors: Cursors and who else is here (S)
      - [ ] #col.live.owner: An owner password guards managing a shared plan (M)  %% returned once on creation, admin can reclaim
      - [^] #col.live.rename: Renaming reaches everyone (S)  %% PATCH /title + RENAMED in the feed, see D85
      - [^] #col.live.history: Earlier states come from the server (M)  %% milestones in the clock menu, rollback for everyone, see D86
      - [^] #col.live.name: A self-chosen display name (XS)  %% fills "changed by", a claim, not a proof
      - [^] #col.live.safe: Unsent work is loud, rescued and never silently discarded (M)  %% watchdog, local backups — see D89; the modal for a second window gives way to #ed.docs.windows (D94)
      - [-] #col.live.ws: Transport over a websocket (S)  %% plain HTTP turned out to be enough
      - [-] #col.live.own: A merge algorithm of our own (XL)  %% solved problem, do not reinvent
  - [?] #mmd: Mermaid plugin (XL)
    - [!] #mmd.layout: A layout engine of its own (XL)  %% measure, place, route — the real work
      - [ ] #mmd.measure: Measure node sizes (M)
        - [ ] #mmd.measure.text: Text metrics without a browser (S)
        - [ ] #mmd.measure.badge: Badges and tags inside the box (S)
      - [ ] #mmd.place: Place nodes and route connectors (L)
        - [ ] #mmd.place.and: All-of fan (M)
          - [ ] #mmd.place.horiz: Horizontal (S)
          - [ ] #mmd.place.vert: Transposed (S)
        - [ ] #mmd.place.or: Any-of rail (S)
    - [?] #mmd.reg: Registration with Mermaid (M)
      | [?] #mmd.reg.ext: External diagram, self-hosted pages only (M)
      | [?] #mmd.reg.pr: Pull request into Mermaid itself (L)
  - [?] > #idea: IntelliJ IDEA plugin (XL)
    - [?] #idea.view: Show the diagram in the IDE (L)
      | [?] #idea.view.mmd: Through Mermaid, once Werkbaum is part of Mermaid itself (M)
        - [?] #idea.view.fence: Fenced werkbaum blocks render in Markdown (S)
        - [-] #idea.view.selfmmd: A self-hosted Mermaid diagram type (S)  %% the IDE brings its own Mermaid
      | [?] #idea.view.jcef: Our own preview: the bundled single file in a JCEF window (M)
        - [?] #idea.view.push: Hand the editor text in (S)
        - [?] #idea.view.back: Get the clicked line back to the caret (S)
    + [?] #idea.edit: A real editor, not just a preview (XL)
      - [?] #idea.lang: Language support for .werkbaum (L)
        - [?] #idea.lang.psi: Lexer and parser to a PSI tree (M)
          - [?] #idea.lang.color: Syntax highlighting (S)
          - [?] #idea.lang.inspect: Warnings as inspections (S)  %% mixed gate, unknown status
        - [?] #idea.lang.complete: Completion for status, size and tags (S)
        - [?] #idea.lang.struct: Structure view and folding (S)
      - [?] #idea.build: Kotlin with the Gradle IntelliJ plugin (S)
      - [!] < #idea.drift: A second parser that must not drift from the SPEC (M)  %% exactly what D14 warns about
        | [?] #idea.drift.fixtures: Both parsers test against the same fixtures (S)
        | [?] #idea.drift.js: Run the one JS parser inside the IDE (M)
  - [~] #trk: Tracker integration (XL)  %% exactly one of these, hence =
    = [~] #trk.taiga: Taiga (XXL) https://taiga.io
      - [/] #trk.resolve: Resolve "#US-123" over the REST API (M)
        - [^] #trk.resolve.read: Read title, link and status (S)
        - [/] #trk.resolve.map: Map the workflow onto the states (S)
      - [^] #trk.write: Write the status back (M) :#trk.resolve
      - [^] #trk.bulk: Diverging tickets show up in the diagram (M) :#trk.resolve.read
        - [^] #trk.bulk.proxy: One request fans out into by_ref queries server-side (S)
        - [^] #trk.bulk.mark: A diverging ref turns amber on its node (S)
      - [^] #trk.create: Create tickets from nodes (XL)
        - [^] #trk.create.proxy: Backend proxy with named endpoints (M)
        - [^] #trk.create.login: Log in to Taiga, token stays in the browser (S) :#trk.create.proxy
        - [^] #trk.create.project: The project comes from the inherited tag (S) :#not.tag.project
        - [^] #trk.create.story: A "create story" action in the node window (S) :#trk.create.login
        - [^] #trk.create.tasks: Sub-packages become tasks, picked in a dialog (M) :#trk.create.story
        - [^] #trk.create.task: A dialog-free "create task" under a story ancestor (S) :#trk.create.tasks,#trk.resolve.read
        - [^] #trk.create.ref: The ref lands in the line beside the id (S) :#trk.create.story
      - [^] #trk.link: Link an existing ticket to a node (S) :#trk.resolve.read
      - [^] #trk.open: Open a ticket ref in Taiga (S) :#trk.create.ref,#not.tag.project
      - [-] #trk.plugin: A plugin inside Taiga itself (L)  %% legacy frontend, self-hosted only
    = [?] #trk.tenzu: Tenzu, the successor (XXL) https://tenzu.net
    = [ ] #trk.none: No tracker, plain text is enough (XS)
  - [?] #ai: AI integration (XL)
    - [?] #ai.dialog: Edit the plan in a dialogue with the AI (L)
    - [?] #ai.key: Bring your own API key, no server in between (M)
    - [?] #ai.ground: llms.md grounds the model in the notation (S)
    - [ ] #ai.mcp: An MCP server in the backend hands the plan to outside agents (XL) :#not.llms  %% docs/rfc/001-mcp-server.md
      - [ ] #ai.mcp.spike: GraalJS runs the one JS parser inside the JVM — measured before anything is built (S)  %% memory, same numbers as vitest, SSE through Apache
      - [ ] #ai.mcp.core: The tool layer as headless frontend modules, bundled into the jar (M) :#ai.mcp.spike
      - [ ] #ai.mcp.read: Resources: the agent guide and the server documents (S)
      - [ ] #ai.mcp.inspect: Tools that answer what the diagram answers, for a text or a document (M)  %% tree, warnings, cheapest path
      - [ ] #ai.mcp.write: Writes to documents go out as line diffs and are refused on conflict (M) :#col.live.diff
      - [ ] #ai.mcp.guard: Guard rails: no broken notation, no [^] — a block on documents, a finding on text (S)
      - [ ] #ai.mcp.verbs: Node verbs: add, move, size, remove — text to text, and on documents (M) :#ai.mcp.write
      - [ ] #ai.mcp.auth: A static bearer token via Spring Security; OAuth once there is an IdP (S)
      - [ ] #ai.mcp.mirror: pull-doc names who changed which version — run by cron or by the agent (S) :#col.git.pull
      - [-] #ai.mcp.node: A local Node package as stdio server (L)  %% a new runtime, and the text may go to the own server instead — D93
      - [-] #ai.mcp.kotlin: A parser of its own in Kotlin (L)  %% the second grammar D14 forbids

---
#wb
  The whole tool, planned with itself: a textual notation for work breakdown
  structures, a browser editor that draws it, and the integrations that could
  grow around both. What you are reading is that plan, written in its own
  notation.

#not
  The notation is the contract between every part of Werkbaum, and it is
  plain text — so a plan can be reviewed, diffed and blamed like code.
  docs/SPEC.md is normative: changes are written there before any code moves.

#not.line
  One line is one node: indentation, gate character, status box, fold mark,
  label, size, URL, tags, comment. The order in which those are extracted is
  fixed, which is what keeps them from colliding with each other.

#not.cont
  A long line may be spread over several text lines: ending one with a space
  and a backslash makes the next line part of it, so no new node begins. The
  space is required, or a label like C:\temp\ would silently swallow the node
  below it.

#not.status
  Eight states from vague idea to live in production, written as an extended
  Markdown task box. The colours follow risk rather than progress: money spent
  with the outcome still open is the loud one.

#not.size
  T-shirt sizes in parentheses, plus the rule that anything M or larger ought
  to be broken down further. A node that breaks the rule grows a dashed
  placeholder child — shown, not just reported. The last free-standing token
  of the line is the size, so parentheses in the title stay text.

#not.people
  Three conveniences that need no syntax of their own: @name for who is on it,
  a bare https URL that makes the whole node clickable, and %% for a comment.

#not.gates
  The heart of the notation: a dash means every sibling is required, a pipe
  means at least one alternative gets chosen. That single distinction is what
  turns a breakdown into something a cheapest path can be searched through.

#not.opt
  A plus node hangs off the same and-group but is dispensable. Without it,
  every nice-to-have is priced into the cheapest path and the estimate comes
  out systematically too high.

#not.id
  An id names a node across the whole document — the address that dependencies
  and description blocks point at. It goes before the title with a colon,
  which is also where a ticket number naturally sits.

#not.tag
  A tag names a set of nodes across the tree, whereas the tree itself is
  exactly one decomposition. Long reserved but unbuilt — tags without a
  consumer are only comments with extra syntax; the first consumer is now
  the Taiga project tag below, so the syntax gets built with it (D91).

#not.tag.lens
  The consumer that would justify tags: pick one and let everything else
  recede. The mechanics already exist — the cheapest path dims unneeded nodes
  in exactly that way.

#not.tag.project
  A tag of the form (&taiga.slug) names the Taiga project a subtree belongs
  to, inherited downwards like people tags in the bottleneck rule — plans
  that span several projects tag each subtree once, refs stay short.

#not.dep
  A node may need nodes outside its own subtree. That is the one thing a pure
  tree cannot express, and everything below this node follows from it.

#not.dep.status
  A dependency says nothing about order or start dates; it says the node
  cannot be further along than what it needs. That is what separates Werkbaum
  from a PERT chart.

#not.dep.eff
  What stands in the box is the node's own progress; effectively it is the
  minimum over everything it depends on. Computed, never written — the text
  stays the single source of truth.

#not.dep.cycle
  Two nodes that need each other are not an error but a statement: these
  finish together. A tool that forbids cycles forces you to lie about them.

#not.dep.warn
  A dependency pointing at nothing gets a warning naming its line, and the
  plan still renders. Fault tolerance throughout: one bad line never swallows
  the rest of the document.

#not.xor
  Where a pipe means at least one, an equals sign means exactly one. Nothing
  changes for the cost calculation, but a second realized alternative becomes
  a rule that can be broken — and is then reported.

#not.desc
  Explaining text for a node, shown in its tooltip. The work was never the
  display: indentation already means hierarchy, so a description needed a
  notation of its own.

#not.desc.short
  A line beginning with a quote mark belongs to the node above it. Convenient
  for one remark, tedious for a paragraph.

#not.desc.long
  Behind a --- separator an id opens a block, and the indented lines under it
  are its text. There is no closing fence, so there is none to forget — this
  section is its own example.

#not.sizes
  Every size is read as a range whose lower bounds double, XXL open-ended. A
  warning appears only when even the most optimistic reading of the children
  exceeds the most generous reading of the parent — nothing is corrected
  automatically, the badge just turns amber.

#not.store
  Rejected on purpose: the text is the format. A second representation of the
  structure would need syncing, and the first disagreement between the two
  would have no arbiter.

#not.ticket
  A reference into a tracker. Which token counts as a ticket comes from the
  connected tracker's pattern — for Taiga fixed as US-/T- plus number,
  prefixes written by Werkbaum itself since Taiga only counts per project.

#not.llms
  An English short form of the specification, served at the site root, so an
  AI agent can read and write Werkbaum without working through the German
  SPEC. It is not normative: the SPEC changes first and this follows.

#not.llms.index
  The llms.txt convention asks for a short index that links to the detailed
  files, not for the detailed file itself. This is that index, and it sits at
  the one address an agent tries on its own.

#not.benefit
  A benefit rating independent of the effort estimate, weighed against it
  only at prioritization time. Written inside the size token — (M/9), or
  (/7) without a size — one digit 0-9, where 0 marks a precondition that
  must be done but has no benefit of its own. A pure author statement: no
  aggregation, no conflict check, no estimation — benefit, unlike cost, is
  not additive. Spelled out in SPEC §11.

#not.benefit.sigil
  A separate character next to the size was the first idea; it lost to one
  token in one corner — (M/9) needs no new sigil, no new badge and leaves
  the node corners as they are.

#not.benefit.points
  Put numbers behind the T-shirt sizes and both benefit and effort become
  addable — at the price of a precision the ordinal sizes deliberately do
  not claim. The 0-9 digit keeps the benefit ordinal instead.

#ed
  The browser editor: text on one side, a live diagram on the other, no server
  involved. One self-contained HTML file that runs from a web server and from
  a file:// URL alike.

#ed.live
  Every keystroke re-parses the text and redraws the tree. That immediacy is
  the point — a notation is only pleasant to write if you can see what it
  means while writing it.

#ed.parser
  Around a hundred lines, no DOM, tested against the canonical example in the
  SPEC. Headless on purpose: an export or a plugin has to be able to use it
  without a browser around it.

#ed.render
  Three arrangements of the same tree. The mode changes the layout only, never
  what is drawn — the gates stay coded by line style in all of them.

#ed.render.horiz
  The classic organigram fan: children side by side, alternatives stacked
  below a dashed rail. Reads best on a wide screen.

#ed.render.vert
  The same fan turned by ninety degrees, children to the right. Fits beside
  the editor instead of above it.

#ed.render.comp
  Everything runs downward, so the tree stays narrow. Here the line style
  alone carries the gate: solid for and, dashed for or.

#ed.render.wrap
  A single long title used to cost a whole column of width. Titles beyond
  forty characters now break into evenly balanced lines, set by the renderer
  so the box shrinks to the longest line; connectors anchor on the first line.

#ed.deplink
  Dependencies drawn as curved dotted edges on their own layer behind the
  nodes — the first line type that does not follow the decomposition.

#ed.deplink.calm
  Thin, pale, dotted and curved: four ways of differing from the tree lines at
  once. The tree keeps the main statement; these are footnotes to it.

#ed.deplink.focus
  The selected node — keyboard focus in the diagram, otherwise the node of the
  caret line — pulls its incoming and outgoing edges forward in ink.

#ed.deplink.fold
  When an endpoint sits in a folded subtree, the edge ends at the nearest
  visible ancestor instead of vanishing — the collapsed node stands in for
  its subtree here too, for source and target alike. Edges whose two ends
  fall into the same visible node disappear, duplicates merge into one.

#ed.deplink.toggle
  A toggle next to the cheapest-path button hides the cross links entirely;
  export and print follow it like the other view filters.

#ed.path
  Highlight the cheapest way through the tree: with and, everything; with or,
  the cheapest alternative. This is the feature the whole notation exists for.

#ed.path.cost
  The cost of a node is its stated (or estimated) T-shirt size — the size
  prices the whole subtree, sub-packages are not added on top. Whether they
  fit is the size conflict check's job.

#ed.path.est
  A node without a size is estimated from its sub-packages: at least the
  largest child size, one step more when three or more children carry that
  size. Only a leaf without children still falls back to M.

#ed.path.front
  Done costs nothing any more, so the path prices what is left rather than
  what the plan would have cost from scratch. An alternative already built
  therefore wins its group even against a cheaper untouched one.

#ed.path.started
  An extra (`+`) joins the path while it is being worked on, and a started
  alternative wins its group. Optionality says the plan can do without it;
  the status says work has begun, and the front is made of exactly that.

#ed.path.line
  A dashed curve threads through the open leaves of the path, with a pale
  station dot at each. Borrowed from metro maps: the dot is large and faint so
  the label underneath stays readable.

#ed.path.step
  One button walks the stations in order, starting with the one to tackle
  next. Seeing the front was solved; walking it across a very wide tree was
  not.

#ed.path.load
  Warns when one @name carries more than half of the open work on the
  cheapest path while at least two people are on it: the next increment
  would serialize through that person. Untagged nodes inherit the nearest
  tagged ancestor, and the person's tag pills turn amber on open path nodes.

#ed.path.worth
  The path toggle becomes a three-state button: off, cheapest, most
  worthwhile. In the third state an alternative group picks the best
  benefit/cost ratio; rated beats unrated, a group with no ratings at all
  falls back to cost — without benefit ratings the most worthwhile path IS
  the cheapest. Realized stays realized, discarded never counts; line,
  stations and inversion keep their mechanics.

#ed.closure
  The cheapest path pulls every dependency of a needed node into the needed
  set, and shared targets count only once. That is why an alternative can win
  although it looks more expensive locally: its dependencies are already paid
  for elsewhere.

#ed.closure.once
  Two alternatives that need the same thing pay for it together, not twice.
  That is the whole reason the calculation works on a set rather than on a
  subtree.

#ed.closure.nonlocal
  Once dependencies count, the cheapest alternative in a group depends on what
  the rest of the plan buys anyway. The choice stops being decidable locally —
  the only place in Werkbaum with real algorithmics in it.

#ed.closure.exact
  Enumerate the choices instead of guessing: a plan has dozens of nodes, not
  millions. Only the groups that actually couple take part, so a plan without
  dependencies costs exactly what it did before.

#ed.closure.enum
  Coupled means: dependencies inside the subtree, or something elsewhere
  pointing at it. Every other group keeps choosing locally.

#ed.closure.union
  Cost is the size of the union of everything needed, so shared targets are
  counted once by construction rather than by a special rule.

#ed.closure.greedy
  Above a search limit the calculation falls back on the local choice and says
  so, in a warning. Being approximately right is fine; pretending to be exact
  is not.

#ed.export
  The diagram redrawn from the live geometry into a standalone SVG, shapes and
  text only. Nothing external is referenced, so the file works anywhere.

#ed.export.file
  SVG for scaling, PNG for the programs that cannot read SVG. The file route
  is the reliable one wherever the image clipboard is not.

#ed.export.clip
  PNG straight into the clipboard, for pasting into a chat or a slide. Falls
  back to the SVG source where the image clipboard is missing entirely.

#ed.export.print
  Printing reduces the page to the diagram. Status colours are printed on
  purpose, and nodes do not break across the page edge.

#ed.i18n
  The interface speaks nine languages. German is the source language: a new
  text is written there first and then translated into the rest.

#ed.i18n.texts
  All interface strings in one object in the source — no loading, no
  framework. The plan text itself is never translated; it is user data.

#ed.i18n.auto
  Without a stored choice the first matching browser language wins, with
  German as the fallback. A deliberate choice overrides it for good.

#ed.a11y
  Everything the diagram codes visually is spelled out per node in an
  aria-label, while the decorative badges are hidden from screen readers so
  nothing is read out twice. Focus order follows reading order.

#ed.mobile
  Below 640 px exactly one area is visible, diagram or text, and a button
  switches between them. Any split would leave both halves too small to use.

#ed.mobile.tip
  A tooltip needs a pointer, so without one the description could not be seen
  at all. A tap opens the same content as a window instead; on a linked node
  the link becomes a button inside it, because one tap cannot do both.

#ed.tipwindow
  A native title has no paragraphs, cannot draw a line, and never appears on
  keyboard focus. The window built for touch can do all three, so it now shows
  everywhere and the title is gone.

#ed.docs
  Several notation texts side by side, kept in the browser. Each is only text
  plus a name — no invented structure format, so this maps onto a server later
  without changing anything.

#ed.docs.picker
  The app header reads "Werkbaum > name": the chip opens the menu, grouped by
  kind (shipped, local, shared), with rename and delete on each row (shipped
  ones keep their delivered name, shared ones are left, not deleted). Restore
  lives on the editor title bar's reload button instead, acting only on the
  open document. The menu scrolls as a whole instead of being cut off on
  small screens, and on the phone it is reachable from both panes.

#ed.docs.url
  A link can carry the address of a text file, which is then fetched on every
  load. Good for a plan maintained elsewhere, in a wiki or a repository.

#ed.docs.restore
  Brings a shipped document back to the state it was delivered in. Without it
  an edited example would never see a newer version again.

#ed.docs.windows
  The installed app and a browser tab share one origin and thus one storage.
  Today a second window gets a modal dialog even where nothing collides, while
  the full flush behind it silently deletes documents the other window
  created — RFC 002 removes the collision instead of warning about it.

#ed.docs.windows.keys
  Text, metadata and earlier states live under their own key per document,
  and deleting leaves a tombstone. No window ever writes another window's
  key, so different documents in two windows cannot clash at all.

#ed.docs.windows.sync
  Windows learn about each other through the storage event: a document
  created, renamed or deleted elsewhere shows up without a reload. A document
  deleted elsewhere while open here stays until you type — typing recreates it.

#ed.docs.windows.lock
  One loss case remains: the same non-shared document in front in two windows.
  A Web Locks lock per document is taken while the document is active; it is
  atomic and falls by itself when the window closes — no heartbeat, no timer.

#ed.docs.windows.dialog
  The window that does not get the lock chooses: open another document, view
  this one read-only until the other window lets go, or edit anyway with a
  standing warning that names the document. Shared documents never ask.

#ed.jump
  Alt+click on a node selects its line in the text; Alt+click in the text
  centres the node. One gesture, both directions — and descriptions count as
  part of the node they describe.

#ed.jump.dep
  Ctrl+click (Cmd on macOS) on an id inside a dependency token like (:#target)
  selects the line that defines the id; Ctrl+Enter at the caret does the same.
  The first assignment wins for duplicate ids, an unknown id does nothing.

#ed.lineno
  A narrow strip beside the text field showing the logical line numbers, the
  same ones the warnings name. Measured rather than calculated: a wrapped line
  keeps exactly one number.

#ed.lineno.tip
  An amber number says that a line has a warning; its tooltip says which one,
  word for word as it stands below the diagram. Otherwise you read the number,
  then hunt for it in the list.

#ed.shortid
  The dotted prefix repeats what the indentation already says. Resolved while
  typing, not while parsing: the file keeps the full id, so it stays greppable
  and survives re-ordering, which is what the :#... references rely on.

#ed.depcomplete
  A dependency needs its target's id, and nobody remembers 180 of them. The
  list at the caret offers the document's ids filtered by what is typed, with
  the node title as context; an input aid like the shorthand, not notation.

#ed.showids
  The id is the address of a node, but the diagram only carried it in the
  tooltip. The switch writes it before the title the same way the text does,
  so both can be read side by side; the graphic export follows the switch.

#ed.fold
  A large plan is unreadable unless parts of it can be put away. Folding is
  view only — warnings from folded subtrees are still reported, because they
  are about the text.

#ed.fold.marks
  A > in a line means this subtree opens collapsed, a < brings one back into
  view. The author says how the document opens; the reader stays free to fold
  differently afterwards.

#ed.fold.click
  A small triangle before the label folds and unfolds; the arrow keys do the
  same from the keyboard. A collapsed node says how many nodes it is hiding.

#ed.fold.write
  Folding in the diagram writes the mark back into the text, as a single undo
  step. The text stays the one truth for the fold state too, so reloading
  restores exactly what you were looking at.

#ed.fold.preset
  One button cycles the whole tree through four complete fold states: size M
  and smaller closed, everything off the cheapest path closed, everything
  closed, everything open. Icon and tooltip show the next step; a manual fold
  restarts the cycle. Size rather than depth — depth only says how finely
  somebody happened to write it down.

#ed.textfold
  Collapse indentation regions and the --- description part in the text
  editor itself — pure view state, local to the session, independent of the
  > fold marks that describe the diagram. A plain textarea cannot hide
  lines, so this hinges on the parked editor-rebuild question (CodeMirror,
  D76); deliberately not sized yet.

#ed.lens
  A bar below the diagram, one pill per @person, shown only when the
  document carries tags at all. Tapping a pill filters the diagram to that
  person's work; an extra entry shows what is assigned to nobody.

#ed.lens.fold
  The lens keeps only the ancestor paths to the person's nodes open; their
  tagged packages stand collapsed. Deliberately view-only: written fold
  marks would push a personal filter to everyone in a shared document.

#ed.lens.load
  Each pill carries a bar with the person's share of the open work on the
  cheapest path — the same measure as the bottleneck warning, shares
  rather than absolute numbers because the sizes are ordinal.

#ed.snaps
  A safety net against mishaps: the text is put aside every ten minutes, but
  only when it differs from the last one kept, and the last twenty are offered
  beside the document picker. Loading one is a single undo step, so a wrong
  pick costs nothing.

#ed.snaps.manual
  Ten minutes is the wrong beat for the one moment a state is really wanted:
  right before a larger change. The button promises that the current state is
  kept, not that an entry was added, so it confirms even when the text is
  unchanged and already at the top of the list.

#ed.files
  Open a .werkbaum file and save one, from the document menu. The leading data
  format was the one thing that could not leave the browser as a file, while
  the diagram always could.

#ed.files.inplace
  Where the File System Access API exists (Chromium), opening keeps a file
  handle: saving writes back into the same file, and the same file reopens
  into the same document. Firefox and Safari keep the plain download.

#ed.files.ctrls
  The save gesture everyone tries first. Without it the browser offers to
  save the page — the one dialog nobody wants. With a remembered handle it
  writes straight back in place and flashes the document name as its answer;
  otherwise it behaves like the menu entry.

#ed.fresh
  For documents that come from outside, nodes that went to production since
  your last visit get a yellow halo. New means live, not "line added" — a line
  diff would be mostly noise.

#ed.fresh.news
  A star that is always in the header opens a popup with the changes of the
  last few days; every day can show its nodes in the diagram, in the very same
  halo view. Notes come from docs/CHANGELOG.md, the nodes from the plan's git
  history, both read at build time.

#ed.percolor
  Give every @name a colour derived from the name itself, so the same person
  is recognizable across the tree without looking anything up.

#ed.dates
  The one dimension the notation has nothing for. Deliberately open: dates are
  the point where a plan turns into a schedule, and that is a different tool.

#ed.dates.attr
  A date as another token in the line, like size and tags. Cheap to build, and
  the plan stays plain text.

#ed.dates.view
  A second view beside the tree, on a time axis. Considerably more work, and
  it would need the tree to say something about order — which dependencies
  deliberately do not.

#bld
  From source to a running page: one bundle, tests, and two deploy routes. The
  result stays a single file without runtime dependencies.

#bld.bundle
  Vite inlines every module, the stylesheet, the fonts and the favicon into
  one HTML file. It runs from a web server and from a local file alike.

#bld.tests
  The headless modules — parser, model, renderer — are tested against the
  canonical example in the SPEC and against every edge case decided along the
  way.

#bld.pages
  Every push to the main branch builds and publishes the preview instance. The
  stable installation is deployed deliberately, by hand.

#bld.update
  The running page compares its own build with the one the server delivers and
  offers a reload. It compares against itself, not against a remembered
  fetch — a remembered one survives the very reload that resolves it.

#bld.pwa
  Installable from the browser and startable like an app, in its own window
  and with its own icon. Everything it needs was already in one file — what
  was missing was mostly the declaration that says so.

#bld.pwa.manifest
  Name, icons, theme colour and a standalone display mode — the declaration a
  browser reads before it offers to install anything. The raster icons are
  rendered from the brand mark, with a padded maskable variant.

#bld.pwa.sw
  A worker lets the editor start without a network. It is deliberately dumb:
  navigations go network-first, and the cache only keeps the last version it
  has seen of the one self-contained file for the offline case. Texts pulled from
  a URL or a server pass through untouched and stay online by their nature.

#bld.pwa.update
  The feared migration turned out unnecessary: because the worker only
  touches navigations and answers them network-first, the update check still
  fetches the real server and a reload really loads the new build. The work
  here was proving that, not moving code.

#bld.pwa.files
  The installed app registers for the .werkbaum extension: double-clicking a
  file in the OS opens it here, with a ready file handle from the launch
  queue. Together with the persistent write permission an installed app can
  hold, editing becomes open, type, save — no dialog at all. The saving
  mechanics existed already; this added the manifest entry and the
  launch-queue receiver. Chromium only.

#bld.backend
  The editor is files, the backend is a service: a Java runtime, something that
  keeps it running, and a way in from outside. Its own deploy script, because
  none of that is rsync.

#bld.backend.jdk
  The measured target has Java 17 while the build asks for 21. Lowering the
  toolchain would be shorter and worse — development and production would run
  on different versions, and the difference would only show in operation. The
  archive's checksum is verified before anything is unpacked.

#bld.backend.unit
  Restart after a crash and a defined state, for free. The service listens on
  127.0.0.1 only; from outside there is exactly one way in.

#bld.backend.proxy
  A rewrite rule with the P flag, because ProxyPass is not allowed in an
  .htaccess and the vhost directory is out of reach. Measured on the target: a
  request is held open for 30 seconds, which is what the change feed needs.

#bld.backend.remote
  Deploying, reading the log, asking what is running and switching the service
  were four different incantations, three of them typed by hand as ssh plus
  systemctl. One front door names them all as target and action.

#bld.backend.backup
  The database is a file in a home directory, and H2 holds it open, so a copy
  taken while the service runs can be torn. Stop, fetch, start again — and read
  the archive back before trusting it.

#bld.werkator
  Werkator, the project's own declarative CI, builds every push from a
  committed .werkator.yml — backend and frontend as two named builds. It builds
  and tests only; deploying stays with the scripts.

#bld.gitea
  The repository moved to Gitea; GitHub stays a clone that carries the
  published main. The clone is not sentiment: the Pages instance hangs off it,
  and so do the example links, which need a raw endpoint that sends CORS
  headers.

#bld.ghpages
  Rejected: the official Actions route needs no extra branch and no deploy
  key.

#bld.gfonts
  Rejected: loading fonts from a third party sends every visitor's address
  there. The two families are self-hosted and inlined into the bundle instead.

#be
  Everything server-side. None of it exists yet, which makes it the largest
  untouched branch of this plan — and the one most of the rest waits for.

#be.scaffold
  A Spring Initializr project with Gradle Kotlin DSL on JDK 21. Everything
  server-side hangs off this one node, which is what the dependencies across
  the tree point at.

#be.scaffold.gradle
  The build itself: Kotlin DSL, one module, the current long-term-support
  Java.

#be.scaffold.info
  Something to ask "are you there, and which build is it?" — open and without
  side effects. Expecting a 404 from a document that does not exist would be a
  poor assurance: a misconfigured proxy returns one too.

#be.scaffold.ci
  A pipeline that builds and tests the backend on every push, like the editor
  has. The "is it alive" half moved out into its own node and is done.

#be.docs
  Documents on a server instead of in one browser. Text plus metadata —
  exactly the model the editor already uses locally.

#be.docs.model
  The server stores the notation text as a whole, with explicit metadata
  beside it. It does not parse the notation: one grammar, in one place.

#be.docs.rest
  Reading and writing documents over plain HTTP. Nothing more exotic is
  needed.

#be.docs.db
  Somewhere to keep them. Whether that also keeps their history is decided in
  the git branch of this plan, not here.

#be.docs.history
  Every change is kept as a snapshot. Two layers: milestones are the history
  people read, sync versions carry the diffs of live editing and are compacted
  after a while.

#be.docs.share
  A link that shows a plan to someone who has no account. It is no longer the
  read-only half: with live editing the same link is the way in.

#be.docs.list
  Listing all documents would make every document's address findable, so that
  one endpoint asks for a master password. Without a configured one the list
  stays shut rather than open.

#be.auth
  As soon as documents live on a server, the question is who may do what. Open
  rather than decided.

#be.auth.token
  The small answer: one user, one token, done in an afternoon. Enough for a
  private installation.

#be.auth.oidc
  The grown-up answer: log in with an existing identity provider. More work,
  and it presumes there are several people to tell apart.

#col
  Three steps that build on each other, and only the last needs real
  machinery. Reading together is solved, changing together mostly is, and
  typing together at the same instant is the expensive one.

#col.point
  Nobody sees anyone else's caret, so a mark in the text is the only way to
  point at a place — and it has something a caret has not: everyone sees the
  same spot.

#col.pad
  Removed. An Etherpad did the hard part for a while: it merged concurrent
  edits and Werkbaum only rendered the result, borrowed rather than rebuilt.
  The borrowing is what it cost — a rate-limited export, no writing from here,
  and an author cookie that a foreign frame never gets. The backend does it
  better and in our own editor, so this went out again.

#col.git
  Keep the plan in a repository. Because it is text, diff, blame, review and
  history come for free.

#col.git.pr
  Works today without a line of code: the file lives in a repository, changes
  go through a pull request, a link renders it. For a plan that changes weekly
  rather than per second this is often the better answer.

#col.git.pull
  The tools/pull-doc script fetches a server document (a ?live= link works too)
  and writes it to a file; with --git-commit it also commits it into the git
  worktree — date, title and version in the message, no commit when nothing
  changed. Run from cron, a shared plan archives itself.

#col.git.timer
  tools/mirror-docs reads a list of documents (uuid and path), runs pull-doc
  --git-commit for each and pushes once. As a systemd timer on the backend host
  it mirrors the shared plans every 15 minutes into a werkbaum-mirror branch —
  the net outside the browser and outside the backend, running whether or not
  anyone remembers.

#col.git.auto
  The server turns every change into a commit. History, restore and branches
  follow from that, with commit granularity as the open question.

#col.git.hist
  Being able to look back at what the plan said, and to go back to it.

#col.git.diff
  What changed between two versions of the plan.

#col.git.blame
  Who moved that node to high risk, and when.

#col.git.branch
  A branch per scenario: plan two futures side by side and compare their
  cheapest paths.

#col.live
  Everybody typing at once, cursors and all. The largest single piece of work
  in this plan, and the least necessary one.

#col.live.http
  Plain request and response, no websocket: a change goes up as a line diff,
  and one held-open request brings the others' changes down. It is testable
  with the same tools as everything else, needs no special treatment in
  proxies or service workers, and at ten onlookers the efficiency difference
  is beside the point.

#col.live.patch
  Every change is a diff against a known version, with a checksum of that
  version. A repeated submission is recognised and not applied twice — over a
  mobile connection the lost answer is the normal case, not the exception.

#col.live.feed
  The server holds the request open until something happens, then answers at
  once. Costs one open request per onlooker and runs only in the visible tab.

#col.live.merge
  What happens when two people change the same line. The actual work — and a
  solved problem, which is exactly why it should not be solved again here.

#col.live.diff
  The answer we took: changes are line diffs against a base version. Where two
  changes do not touch the same lines the server shifts them itself, so nobody
  has to retry; where they do, a human decides. No runtime dependency, and the
  same model on both sides.

#col.live.ask
  On a real overlap a bar asks whose version should win. Whoever loses loses
  nothing: the discarded state stays in the local snapshots, and every version
  stays in the server's history.

#col.live.crdt
  Because the plan is plain text, a text CRDT fits it without any merge logic
  of our own, and cursors come along as a by-product. The price would be the
  first runtime dependency this project has ever had.

#col.live.ot
  The other established answer, and the one Google Docs uses. More machinery
  for the same result.

#col.live.upload
  Without it the only way onto the server was a curl command. A share button
  in the editor title bar; the local plan stays where it is: whoever entrusts
  their only copy to a server should not lose it in the act. The picker names
  the host beside a server document, so the two entries are told apart.

#col.live.owner
  Today the unguessable link is the only guard, so holding it means holding
  everything, delete included. Creating a document shall return an owner
  password once — save it, the browser keeps a copy, there is no recovery on
  another browser. Management actions bind to it; an admin with the master
  password can still reclaim a document.

#col.live.rename
  The title is server metadata, so renaming patches it with a version check
  and the change feed hands the new title to every client in plain text. URL
  documents lost their pencil instead - their name is the URL.

#col.live.history
  On a shared document the clock button lists the server's milestone history
  instead of local snapshots - those would contain other people's work and
  overwrite everyone when loaded. The camera makes a server milestone, and
  loading an earlier state is a server-side rollback: a new version for
  everyone, nothing lost.

#col.live.name
  Asked once when joining or sharing, kept in the browser, sent with every
  patch. Without accounts it is a claim, not a proof.

#col.live.safe
  Born from a real loss: two hours of typing that never reached the server
  and vanished on reload. A second window raises a modal dialog until one is
  closed; a watchdog warns after 30 seconds of unsent changes; and unsent
  text is rescued into the local backups before anything overwrites it.

#col.live.cursors
  Seeing where the others are. A by-product of the merge layer rather than a
  feature in its own right.

#col.live.ws
  A socket and a backend to terminate it — long the obvious answer, and not
  needed after all: request and response carry a diff just as well, and
  everything around them stays simpler.

#col.live.own
  Rejected: writing a merge algorithm is a research project with a known
  answer.

#mmd
  Werkbaum diagrams inside Mermaid, so a fenced block renders wherever Mermaid
  already runs.

#mmd.layout
  Mermaid brings no layout for this shape: measuring nodes, placing them and
  routing the connectors all have to be written. Today the browser does all
  three for free, which is why the editor never needed any of it.

#mmd.measure
  How wide is a node, given its label, its badges and its tags. Without a
  browser, nothing answers that for you.

#mmd.measure.text
  Text metrics without a DOM — the part that decides how good the result
  looks.

#mmd.measure.badge
  Size badge, tags and marks have to be placed inside the box before the box
  has a size.

#mmd.place
  Positions for the nodes and paths for the connectors, for every arrangement
  the editor offers.

#mmd.place.and
  The fan for and-groups, in both orientations.

#mmd.place.horiz
  Children side by side, the classic organigram.

#mmd.place.vert
  The same, turned by ninety degrees.

#mmd.place.or
  The rail for alternatives, dashed, with its branches.

#mmd.reg
  Making Mermaid aware of a new diagram type at all.

#mmd.reg.ext
  Registering from outside works only where Mermaid is initialized by the page
  itself — not on GitHub, GitLab or Notion.

#mmd.reg.pr
  Getting it into Mermaid proper makes it available everywhere, at the price
  of a review by somebody else's project.

#idea
  Werkbaum inside the IDE, where the plan file already sits next to the code
  it describes.

#idea.view
  The smaller half: show the diagram for the file currently in the editor.

#idea.view.mmd
  If Werkbaum were part of Mermaid, the IDE would render it already — it
  brings its own Mermaid along.

#idea.view.fence
  Fenced blocks in Markdown would then render on the spot, with no plugin at
  all.

#idea.view.selfmmd
  Rejected: the IDE uses its own Mermaid build, so a self-hosted diagram type
  never reaches it.

#idea.view.jcef
  Or embed the bundled single file in a browser window inside the IDE. It is
  already self-contained, which is most of the work done.

#idea.view.push
  Hand the editor's text into the embedded page on every change.

#idea.view.back
  And get the clicked node back out, to move the caret to its line.

#idea.edit
  The larger half: editing the notation with everything an IDE brings, rather
  than only looking at it.

#idea.lang
  Registering .werkbaum as a language. Without that there is no highlighting,
  no completion and no structure view.

#idea.lang.psi
  A lexer and parser producing the IDE's own syntax tree. Everything below
  hangs off it.

#idea.lang.color
  Gate characters, status boxes, sizes and tags in colours of their own.

#idea.lang.inspect
  The warnings the editor shows, as inspections in the margin.

#idea.lang.complete
  Offering the status codes, the sizes and the known tags while typing.

#idea.lang.struct
  The tree as a structure view, with folding that follows it.

#idea.build
  Kotlin and the Gradle IntelliJ plugin — the same stack the backend uses.

#idea.drift
  A second parser is exactly what the specification warns about: two grammars
  that drift apart, and a plan that renders differently depending on where you
  open it.

#idea.drift.fixtures
  Keep them honest by testing both against the same examples, the canonical
  one included.

#idea.drift.js
  Or avoid the second parser altogether by running the one JavaScript parser
  inside the IDE. No drift is possible, at the cost of a runtime inside the
  plugin.

#trk
  Connecting nodes to a ticket tracker, so a status need not be maintained
  twice. Exactly one of these gets built, which is why the group is a XOR.

#trk.taiga
  The tracker actually in use here. Self-hosted, with a REST API that is
  enough for this.

#trk.resolve
  Turn a reference in a line into a real ticket: title, link and current
  state. Read-only, and already useful on its own.

#trk.resolve.read
  Fetching the ticket and showing what it says: subject, status and assignee
  in the node window, read through the backend proxy.

#trk.resolve.map
  Translating the tracker's workflow onto the eight states of the notation.
  The default five are mapped, an unknown column name stays unmapped and is
  shown as plain text; making the mapping configurable is still open.

#trk.bulk
  With a Taiga login, one bulk request per project and session reads the
  status of every referenced ticket, and a ref whose ticket no longer
  matches the node's status box turns amber. Details and both sync actions
  stay in the node window; the mark is session knowledge and stays out of
  export and print.

#trk.bulk.proxy
  GET /taiga/tickets takes the plan's refs and fans them out into by_ref
  queries next to the Taiga instance, in parallel with capped concurrency.
  The cost scales with the refs in the plan, never with a project holding
  thousands of tickets - the refs themselves are the pre-filter. A ref that
  no longer exists is silently absent; the bulk also pre-fills the node
  window's ticket cache.

#trk.bulk.mark
  A visible ref colours itself; where the ref is the node id and thus not
  part of the title, the renderer appends it as a small badge behind the
  label - the same construction as the description mark. An unmapped column
  name marks nothing: it says nothing about the plan.

#trk.write
  The other direction. Neither side wins by itself: a difference between the
  ticket and the status box is marked, and both directions are offered as
  explicit buttons — writing only for the five mapped states.

#trk.create
  One click in the node window turns a node into a Taiga ticket. The cut
  follows the Kanban view actually in use: stories are the cards and tasks
  live inside them — so a story per node, and tasks only under a node that
  already carries a story. Decisions in D91.

#trk.open
  Ctrl+click on a ticket ref — in the text or on its node — opens the
  ticket in Taiga's web UI; the node window carries an open button, the way
  on touch. Needs the instance's web address (server configuration,
  reported by /info) and the inherited &taiga.slug project (D91-Nachtrag 5).

#trk.link
  "Link ticket" in the node window binds an EXISTING Taiga ticket to a
  node: paste the ticket's URL — it carries project, type and number by
  itself — or enter a ref or bare number, resolved by probing story then
  task. The subject is shown before anything is written; linking appends
  the ref token and, where needed, the &taiga.slug tag, one undo step
  (D91-Nachtrag 11).

#trk.create.proxy
  A few named endpoints in the Werkbaum backend (auth, projects, user
  stories, tasks) relay to the Taiga instance configured server-side. No
  CORS trouble and no open relay — the target URL is never a request
  parameter — and /api/v1/info announces the feature, so the button only
  appears where a backend answers.

#trk.create.login
  Taiga's token API behind the proxy: the token lives in the browser and is
  only passed through, the backend stores nothing. The password crosses the
  proxy once at login and is never logged.

#trk.create.story
  "Create story" in the node window: confirm the project — asked on every
  creation, prefilled from the inherited project tag — and the story is
  created. A node that already carries a ticket ref is left alone.

#trk.create.tasks
  The story dialog also creates the direct sub-packages as tasks under the
  story, chosen in a checkbox dialog: preselected are required and optional
  children plus the realized alternatives of a |/= group; discarded ones
  never appear, finished ones start unchecked. Unchecking everything covers
  the story-only case, so there is just one create-story button.

#trk.create.task
  Where an ancestor node already carries a story ref, the node window offers
  "create task": the task lands in that story, without any dialog — project
  and story follow from the tree. Ancestors with a task ref are skipped
  (Taiga tasks have no subtasks), and the button is absent where a &taiga
  tag in between would resolve the new ref against the wrong project.

#trk.create.project
  The dialog's project choice is prefilled from the nearest (&taiga.slug)
  tag up the tree, and the first creation in a subtree without one writes
  the tag back, undoable — the assignment grows out of the work itself.

#trk.create.ref
  The returned ref is appended to the line as an extra token — #US-123 for
  stories, #T-1234 for tasks, prefixes written by Werkbaum since Taiga's own
  numbers are just per-project counters shared across all types. It never
  replaces a node id (only the first # token is the id, §1); on a node
  without one it simply becomes the id. Undoable, and the marker that keeps
  a second click from creating the ticket twice.

#trk.plugin
  Rejected: a plugin inside Taiga means its legacy frontend and works only in
  self-hosted installations. A companion application beside it is the better
  cut.

#trk.tenzu
  Taiga's successor, still young. Worth watching rather than building against;
  a later move should cost only the API adapter.

#trk.none
  The honest alternative: a plan in a text file may simply be enough. It is
  the cheapest node in this group, and the cheapest path duly picks it.

#ai
  Connect an LLM or an agentic AI to the editor, so the plan can be shaped in
  conversation instead of by hand alone. An idea for now: the pieces exist
  (the text is the whole state, and llms.md already teaches agents to read
  and write it), but nothing here is designed yet.

#ai.dialog
  A chat beside the diagram: ask for a decomposition, a size estimate or a
  restructuring, and the AI answers by editing the notation text — every
  change lands as an undoable text edit, with the human deciding what stays.

#ai.key
  The user brings their own API key and the browser talks directly to the
  chosen provider — no Werkbaum server in between, following the D20/D23
  line: requests only at the user's request, only to the host they configure.

#ai.ground
  The system prompt is essentially llms.md (D43): the agent guide already
  explains the notation for reading and writing, so the model needs no
  training — just the document and the request.

#ai.mcp
  The other direction of the AI integration: not an LLM inside the editor,
  but the plan handed to agents that live outside it — Claude Code,
  OpenCode, Codex, IDE agents — over the Model Context Protocol, served by
  the backend over streamable HTTP. No local process, no new runtime: a
  URL and a token in the host's configuration. Specified in
  docs/rfc/001-mcp-server.md before anything is built.

#ai.mcp.spike
  The whole design rests on the JVM running the frontend's JS modules
  through GraalJS. Before a line is built, a spike measures it: the bundle
  parses the shipped plan with the same numbers vitest reports, a context
  costs a known amount of memory on the tight production host, and Apache
  passes the SSE stream of a long tool call unbuffered.

#ai.mcp.core
  The tool layer — tree to JSON, guard rails, node edits — lives as headless
  modules in frontend/src, tested with vitest, and is bundled at build time
  into one ES module inside the jar. That keeps D14 intact: the one JS
  parser stays the only parser, and Node stays what it is here, a build tool.

#ai.mcp.read
  What an agent can read: llms.md as the notation guide, and the documents
  of this backend by UUID — with title, version and checksum. A local file
  the agent reads itself and hands the text to the tools.

#ai.mcp.inspect
  Tools that return the parsed tree with ids, statuses (own and effective),
  sizes (given and assumed), tags and dependencies, the warnings the editor
  would show, and the cheapest path with its stations and per-person load —
  for a text the agent passes in or a document it names — so the agent
  reasons on the model instead of re-implementing the SPEC.

#ai.mcp.write
  Edits to a document leave the server as the same line diffs the editor
  sends: against a base version with checksum, refused on a real conflict,
  visible to every open editor with "changed by" naming the agent's host.
  For a text, the tool returns the new text and the agent writes the file.

#ai.mcp.guard
  Guard rails on every write: the result must not break the notation (no new
  mixedGate, unknownStatus, descStray or duplicateId), and an agent never
  writes [^] — "in production" is a deploy's statement (D30), not a model's.
  On a document that blocks the write; on a text it is a finding the agent
  sees. Content warnings like a size conflict are written and reported, so a
  human sees them as the amber mark. Both rails can be lifted per call.

#ai.mcp.verbs
  Convenience for the model: add a node under a parent, move a subtree, set a
  size, remove or discard a node — by id, not by line index, which an agent
  gets wrong. The text-to-text rules live in frontend/src/edit.js beside the
  fold and short-id helpers, so exactly one place keeps knowing the line format.

#ai.mcp.auth
  One static bearer token per installation, checked by Spring Security like
  the master password; without it the endpoint is closed, not open. OAuth
  2.1, which MCP foresees for HTTP, waits for an identity provider.

#ai.mcp.mirror
  Both at once: the agent works on the shared document, where the people
  are, and the same plan stays under git control. pull-doc --git-commit
  archives the server state — run by a cron, or by the agent itself right
  after its change — and with --with-history the commit names who changed
  which version, as trailers, so blame never credits the agent with a
  human's lines. The server is the source, git the archive.

#ai.mcp.node
  A local Node package as stdio server was the first draft: files and the
  git mirror right at the process. Discarded because it would make Node a
  runtime — a new technology, one dimension heavier than a dependency — and
  the motives did not need it: the text may go to the own server instead.

#ai.mcp.kotlin
  A parser of its own in Kotlin would put the logic next to the documents —
  but it is exactly the second grammar D14 forbids. Discarded; the backend
  runs the JS parser instead of rebuilding it.
