%% 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) %% only together with a consumer, see D34 - [?] #not.tag.lens: A lens: highlight every node tagged &x (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 (#123,#US-123) (S) %% Taiga writes #US-123 for a user story - [^] #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, not only cost (M) | [?] #not.benefit.sigil: Another sigil next to the size (S) | [?] #not.benefit.points: Story points behind the T-shirt sizes (M) - [~] #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.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 (M) - [^] #ed.docs.picker: Switcher in the editor title bar (S) - [^] #ed.docs.url: Load a document from ?sourceUrl= (S) - [^] #ed.docs.restore: Restore a shipped document from the menu (XS) - [^] #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.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 (M) - [^] #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.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.ci: Health endpoint and CI (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) " The pad merges concurrent edits, Werkbaum renders the plan. - [^] #col.pad: Watch a shared Etherpad — it merges, we render (L) - [^] #col.pad.export: Load the pad's plain-text export (XS) - [^] #col.pad.reload: Reload on demand (XS) %% a timer only trips Etherpad's rate limit - [^] #col.pad.embed: Embed the pad, keep the text mirror beside it (M) - [^] #col.pad.views: Three views: pad and text, pad only, text only (S) - [^] #col.pad.split: Splitter between pad and mirror (XS) - [?] #col.pad.cookie: Editing in the frame stops after a while (S) %% SameSite=Lax, see D31 | [?] #col.pad.samesite: Ask the admin for cookie.sameSite = None (XS) | [ ] #col.pad.owntab: Live with it: edit in the pad's own tab (XS) - [^] #col.pad.point: A shared pointer: !!! focuses a node for everyone (XS) - [?] > #col.auto: Update by itself, within about 2 s (M) | [?] #col.own: Our own Etherpad beside Werkbaum (L) %% then every setting below is ours - [ ] #col.own.db: Node and a real database in userland (S) %% the flatfile store is not for production - [ ] #col.own.ram: Book the RAM quota for an own daemon (XS) - [ ] #col.own.proxy: Reverse proxy in .htaccess, websockets included (S) %% Mattermost does it, wiki shows how - [ ] #col.own.unit: systemd user unit so it survives a reboot (XS) - [!] #col.own.patch: Keeping a public service patched (S) %% indefinitely, that is the real cost | [?] #col.auto.limit: Admin raises the export rate limit (XS) %% default is 10 per 90 s | [?] #col.auto.plugin: Admin installs a plugin that pushes the text (S) %% postMessage to the page | [?] #col.auto.relay: Our own backend holds one socket and fans out (XXL) :#be.scaffold | [ ] #col.auto.poll: Poll inside the budget, at best every 9 s (S) %% legal, but not 2 s | [-] #col.auto.socket: Our own socket straight to the pad (L) %% measured: upgrade refused | [-] #col.auto.html: Read the pad's HTML instead of the export (M) %% measured: no CORS header - [ ] #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.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 (XL) :#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.cursors: Cursors and who else is here (S) - [-] #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 (L) https://taiga.io - [ ] #trk.resolve: Resolve "#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.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) --- #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. Reserved but deliberately unbuilt: with nothing to evaluate them, tags are only comments with extra syntax. #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.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 own pattern rather than from guessing at its shape, which is why this waits for the tracker work. #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 Sizes say what something costs; nothing yet says what it is worth. With both you could sort by value for money instead of only minimizing cost. #not.benefit.sigil A separate character next to the size, in the spirit of everything else in the line. Needs a collision-free sigil, and parentheses, @, # and %% are taken. #not.benefit.points Put numbers behind the T-shirt sizes and both benefit and effort become addable. That buys the effort rollup as well, at the price of a mapping convention the ordinal sizes deliberately avoid today. #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.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 name of the active document replaces the panel title and opens the menu for switching, creating, renaming and deleting. #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.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.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 pad 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.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.ci Enough to see that it lives: a health endpoint, and a pipeline that builds and tests it on every push. #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.pad Let an Etherpad do the hard part: it merges concurrent edits, and Werkbaum only renders the result. Borrowed rather than rebuilt. #col.pad.export Every pad offers its content as plain text, with the header that lets a foreign page read it. That one endpoint is the entire integration. #col.pad.reload Fetching happens on a button press, not on a timer. The export is rate limited, and a timer does nothing but trip it. #col.pad.embed The pad in a frame inside the editor, with our own text mirror beside it. The mirror keeps the jumps between diagram and text working, because a foreign frame gives up no access to its content. #col.pad.views Pad and text, pad only, text only. Text only is the view that tells the pad nothing about you — a loaded pad announces you in its list of people. #col.pad.split A splitter of its own between frame and mirror, remembered separately for side by side and stacked. #col.pad.cookie In a foreign frame the pad's author cookie is not sent, so a session can turn read-only after a while. Not fixable from this side. #col.pad.samesite One server setting resolves it. Presumes somebody who can change that setting. #col.pad.owntab Or live with it: read along in the frame, and write in the pad's own tab where the cookie works. #col.pad.point Nobody sees anybody else's cursor across two applications, so a mark in the text is the only way to point at something. It has one thing a cursor hasn't: everyone sees the same spot. #col.auto The view should follow the pad by itself, within a couple of seconds. Every route below was measured rather than guessed at. #col.own Running the pad ourselves makes every setting below it ours. It also makes every one of them our job. #col.own.db The default file store is not meant for production, so a real database and a runtime to host it are the price of admission. #col.own.ram A daemon of one's own needs memory booked for it. #col.own.proxy Reaching it needs a reverse proxy that passes websockets through as well. #col.own.unit And something that starts it again after a reboot. #col.own.patch The real cost is not the setup but keeping a public service patched, indefinitely. #col.auto.limit The cheapest effective change: raise the export limit on the pad server. Ten fetches per ninety seconds is the default, and the reason a timer fails. #col.auto.plugin A pad plugin can push the text to the embedding page. Live, no polling, no limit touched — and nothing to install on our side. #col.auto.relay Our own backend holds one connection to the pad and fans out to every viewer. Solves it for everybody at once, and costs a backend. #col.auto.poll Staying inside the budget means one fetch every nine seconds at best, shared by everyone behind the same address. Legal, but not two seconds. #col.auto.socket Measured and rejected: the pad server refuses the websocket upgrade when it comes from another origin. #col.auto.html Measured and rejected: the pad page carries the full text, but sends no header that would let a foreign page read it. #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.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.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. #trk.resolve.map Translating the tracker's workflow onto the eight states of the notation. Configurable, because no two projects use the same column names. #trk.write The other direction: change a status in the plan and the ticket follows. Only worth doing once reading it is trustworthy. #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.