@k-l-lambda/portolan 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/skill.md ADDED
@@ -0,0 +1,320 @@
1
+ ---
2
+ name: rhumb
3
+ description: Record and maintain project work in a Portolan Rhumb (.rhumb) map - nodes, statuses, diary anchors, dependency edges, and the check/fmt/serve tooling.
4
+ ---
5
+
6
+ # Maintaining a Rhumb map
7
+
8
+ A `.rhumb` file is the structural map of one project: work items, their status, hierarchy and relations. Details (what was tried, results, commands, decisions) live in the daily diary. The map links to diary entries and never copies them. Syntax source of truth: `docs/rhumb-spec.md` (0.1). Run commands from the portolan repo root.
9
+
10
+ ## 1. When to update the map
11
+
12
+ Update the map in the same turn as the work, whenever:
13
+
14
+ - a new work item appears (planned, proposed, split off) → add a node
15
+ - an item starts, finishes, gets blocked or is abandoned → change its status
16
+ - you learn that one item depends on another → add an edge
17
+ - you finish a diary entry about an item → add an anchor note linking to it
18
+
19
+ Keep the map structural. A note is one line: a blocked reason, a key decision, a pointer. If you need more, write it in the diary and link to it.
20
+
21
+ ### Write for a human reading the big picture
22
+
23
+ The map is read by people scanning the whole project; the diary is where the work is reproduced. Write each node so that someone who never saw the code understands what it is and why it matters, and put the technical record behind a precise link.
24
+
25
+ - **Title: a short name in plain words**, usually two to four words, naming the thing or outcome, not a file, function or flag. "Hot reload", not "`fs.watch` per directory + 2 s rescan". Do not turn titles into sentences; brevity is part of readability, and the `Why:` note carries the explanation. Keep a title that is already short and clear.
26
+ - **First note: `Why:` and how the node serves the larger goal.** Name the user need or the parent goal it advances, so the line connects the node to the picture around it, not just restates the title.
27
+ - **Other notes: decisions, outcomes and open questions** a reader needs to judge progress, in one plain sentence each.
28
+ - **Technical details go to the diary**: commands, error messages, numbers, file paths, commit hashes, parameter lists, how a bug was found. Link to the exact entry, or the exact line with `^=` / `+L` (section 3), so a reader can jump straight to the evidence instead of searching.
29
+ - If a note needs a code span to be understood, it probably belongs in the diary.
30
+
31
+ ```rhumb
32
+ ---
33
+ links:
34
+ diary: ../{path}.md
35
+ ---
36
+
37
+ %% Too technical for the map: a reader learns how it was built, not what it is for.
38
+ - [x] fs.watch per dir + 2s rescan, SSE `change` on recreate (05b336e) ^hot-reload-raw
39
+ - Node 22 recursive watch drops files after rm+create; mtime/size shortcut
40
+
41
+ %% Readable: what it gives the user and why, with the details one click away.
42
+ - [x] Hot reload ^hot-reload
43
+ - Why: agents and editors rewrite maps while a page is open; the view must stay current or people act on stale plans.
44
+ - Saves that replace the file (as many editors do) no longer stop updates.
45
+ - [how it was fixed](<diary:2026/1008#rhumb-tooling:~:text=Implement hot reload>)
46
+ ```
47
+
48
+ ## 2. Nodes
49
+
50
+ ```
51
+ - [S] Title {attrs} ^id
52
+ ```
53
+
54
+ | Status | Meaning |
55
+ | --- | --- |
56
+ | `[ ]` | todo, not started |
57
+ | `[/]` | doing, in progress |
58
+ | `[x]` | done |
59
+ | `[-]` | dropped, abandoned (keep the node) |
60
+ | `[!]` | blocked; add a note with the reason |
61
+ | `[?]` | idea, proposed but not committed |
62
+
63
+ - Always write a readable kebab-case ID matching `[a-z0-9][a-z0-9-]{0,47}`. Prefix child IDs with the parent's (`threads`, `threads-model`). Never write `^_xxxxxx`: the `_` prefix is reserved for IDs that `fmt` generates when a human omits one.
64
+ - IDs are permanent. Edges and threads reference them; change one only with `rename-id` (section 7).
65
+ - Attributes are optional: `owner`, `due` (ISO date), `tags` (list), `priority` (`high`/`normal`/`low`), `star` (boolean). Other keys are kept but reported as `I002`.
66
+ - `{star: true}` is the project's shared favorite mark, shown with a dedicated star in the web view. Humans usually set it; add or remove a star only when the user asks. Unstar by removing the key, not by writing `false`. Any other value is `W010`.
67
+ - Hierarchy is 2 spaces per level. A node's parent is the nearest preceding node with smaller indentation; siblings share one indentation.
68
+ - Notes are list items without a checkbox. They belong to the node above them and are not children. Use them for blocked reasons, short context and links.
69
+ - Titles are inline Markdown (code spans, links). A title that itself ends in `^word` or `{...}` must escape it as `\^` or `\{`.
70
+
71
+ ```rhumb
72
+ - [/] Visual frontend {owner: claude, priority: high} ^ui
73
+ - Read-only viewer first; editing goes through the server
74
+ - [x] Renderer spike ^ui-spike
75
+ - [!] Mind map view ^ui-tree
76
+ - Blocked: renderer license review pending
77
+ - [?] Dependency graph view {tags: [dag, xyflow]} ^ui-dag
78
+ - [-] Canvas prototype ^ui-canvas
79
+ - Dropped: the license requires a production key
80
+ ```
81
+
82
+ ## 3. Anchors
83
+
84
+ Every Markdown link in a node's title or notes is an anchor of that node. Prefer an anchor note over a link in the title.
85
+
86
+ - Define link prefixes in front matter `links:`; `{path}` is replaced and the result is relative to the `.rhumb` file. Then write `diary:2026/1008#frag`.
87
+ - Plain relative paths (`../notes/x.md#frag`) and `https://` URLs also work.
88
+ - The fragment is a GitHub heading slug (lowercase, punctuation removed, spaces → `-`). A unique prefix is enough: `#portolan` matches `## Portolan: agent-human shared mind map ...`.
89
+ - To point at one entry under the heading, append a text fragment: `#portolan:~:text=Backlog.md`. The resolver picks the first top-level list item under that heading whose lines contain the text. Choose a distinctive phrase from the entry's first line or summary.
90
+ - To point at a line, use `^=` (line starts with, indentation ignored) and offsets: ``#map-layout^=`* \> [host][portolan] Design a vector`+L2`` is the summary line two lines below that entry. `#heading+L3` counts from the heading. `#L42` / `#L42-L50` point at absolute lines in any file (code: `repo:src/edit.ts#L120`). Prefer `^=` or `:~:text=` to bare offsets; offsets shift when lines are inserted above.
91
+ - If the target contains spaces, wrap the whole target in `<...>`. Inside it, write `>` as `\>` (diary entries start with `* > [host]`); an unescaped `>` breaks the link and `check` reports `W011`. A backtick inside a `^=` prefix is written `%60`.
92
+ - When you write a diary heading, start it with a word no other heading in that day's file starts with, so a short prefix like `#rhumb-tooling` stays unique. An ambiguous prefix is `W003`.
93
+
94
+ ```rhumb
95
+ ---
96
+ rhumb: 0.1
97
+ title: Portolan
98
+ links:
99
+ diary: ../{path}.md
100
+ ---
101
+
102
+ - [x] [Prior-art survey](diary:2026/1008#portolan) ^survey
103
+ - [Backlog.md analysis](<diary:2026/1008#portolan:~:text=Explain Backlog.md>)
104
+ - [Backlog.md repo](https://github.com/MrLesk/Backlog.md)
105
+ - [x] Parser (Peggy) ^rhumb-parser
106
+ - [parser record](<diary:2026/1008#rhumb-dsl:~:text=Parser: chose Peggy>)
107
+ ```
108
+
109
+ ## 4. Edges
110
+
111
+ ```
112
+ a needs b, c: optional label
113
+ ```
114
+
115
+ Edge lines start at column 0, reference IDs (no `^` needed) and can go anywhere in the body; convention is a `%%`-commented group after the tree.
116
+
117
+ | Kind | Use when | Affects readiness |
118
+ | --- | --- | --- |
119
+ | `needs` | a cannot finish (or start) before b is done | yes |
120
+ | `blocks` | same as `b needs a`; use only when the sentence reads better that way | yes |
121
+ | `relates` | any other relation; add a verb-phrase label for domain meaning (see below) | no |
122
+ | `replaces` | a supersedes b (usually b is `[-]`) | no |
123
+ | `from` | a was split off, followed up or derived from b | no |
124
+
125
+ - Do not add `needs`/`blocks` between a node and its own ancestor or descendant (`W005`); the hierarchy already says that.
126
+ - Never create a dependency cycle (`E009`). If two items depend on each other, split one or use `relates`.
127
+ - A label applies to every target on the line. Write one only when the reason is not obvious.
128
+ - Use `needs` only for execution order: a really cannot finish before b is done. Do not use it for "a is compared against b", "a uses b" or "a tests b".
129
+ - Domain relations are labeled `relates`, never new keywords. The label is a verb phrase and the edge reads source + label + target. A labeled `relates` is directed; an unlabeled one is not.
130
+
131
+ ```rhumb
132
+ - [x] A0 baseline loss ^loss-a0
133
+ - [/] A1 forward KL ^loss-a1
134
+ - [ ] Shared fixed50 cohort ^cohort
135
+ - [?] LR/data-size mismatch ^lr-hypothesis
136
+
137
+ loss-a1 relates loss-a0: compared against
138
+ loss-a1 relates cohort: uses
139
+ loss-a1 relates lr-hypothesis: tests
140
+ ```
141
+
142
+ ```rhumb
143
+ - [/] Rhumb DSL ^rhumb
144
+ - [x] Parser ^rhumb-parser
145
+ - [ ] Edit operations API ^rhumb-edit
146
+ - [ ] Local server ^server
147
+ - [ ] Agent interface ^agent-api
148
+ - [-] Old JSON format ^json-format
149
+ - [ ] Sidecar thread format ^threads-format
150
+
151
+ %% dependencies
152
+ rhumb-edit needs rhumb-parser
153
+ server needs rhumb-edit, rhumb-parser: serves the AST and applies edits
154
+ agent-api needs rhumb-edit
155
+ agent-api relates server
156
+
157
+ %% provenance
158
+ rhumb replaces json-format
159
+ threads-format from server
160
+ ```
161
+
162
+ ## 5. Status discipline
163
+
164
+ - Parent status is written, not derived. When the first child starts, set the parent to `[/]` (otherwise `I003`).
165
+ - Do not mark a parent `[x]` while its subtree has `[ ]`, `[/]` or `[!]` nodes (`W007`). Finish, drop (`[-]`) or move the open children first.
166
+ - Do not mark a node `[/]` or `[x]` while one of its `needs` targets is not done (`W008`). Either finish the dependency, or the edge is wrong and should be removed or changed to `relates`.
167
+ - Use `[!]` only with a note that says what it is waiting for.
168
+ - Never delete abandoned work. Set it to `[-]` and add a note with the reason. A dependency on a dropped node is `W006` and no longer blocks readiness; retarget or remove that edge.
169
+ - `[?]` ideas do not count toward progress. Promote to `[ ]` when the work is committed.
170
+
171
+ ## 6. Editing rules (minimal diffs)
172
+
173
+ - Change only the lines you mean to change. A status change is a one-character diff.
174
+ - One space between title, `{attrs}` and `^id`. Never align columns.
175
+ - Keep `%%` comments, blank lines and the existing order.
176
+ - Insert new nodes at the end of their sibling group unless order matters.
177
+ - Append new edges next to related edges (same `%%` group) or at the end of the file. Do not reorder edges.
178
+ - Re-read the file before editing; a human or the UI may have changed it.
179
+
180
+ ## 7. Tooling
181
+
182
+ ```sh
183
+ node src/cli.ts check <file> # parse, derived and anchor diagnostics
184
+ node src/cli.ts fmt <file> # in-place: assign missing IDs, normalize [X]→[x], [~]→[/]
185
+ node src/cli.ts serve <file> [--port N] # local API on 127.0.0.1, default port 4310
186
+ ```
187
+
188
+ - Run `check` after every edit. Output is `<file>:<line>: <level> <code> <message>`; exit code 1 means at least one error. Fix all errors and every warning you caused. `W003` for a diary that is not checked out is acceptable.
189
+ - `fmt` in 0.1 does only the two things above (it prints `assigned ^_xxxxxx` per ID). It does not re-indent, dedupe edges or collapse blank lines. Use it when a human left nodes without IDs; you should never need it for your own lines.
190
+
191
+ ### Edit API (`serve`)
192
+
193
+ The server has no authentication and accepts only loopback `Host` headers. Get the current version, then post one operation:
194
+
195
+ ```sh
196
+ v=$(curl -s http://127.0.0.1:4310/api/doc | jq -r .version)
197
+ curl -s -X POST http://127.0.0.1:4310/api/edit -H 'content-type: application/json' \
198
+ -d "{\"version\":\"$v\",\"edit\":{\"op\":\"set-status\",\"id\":\"rhumb-fmt\",\"status\":\"done\"}}"
199
+ ```
200
+
201
+ | `op` | Fields | Effect |
202
+ | --- | --- | --- |
203
+ | `set-status` | `id`, `status` | rewrites the checkbox only |
204
+ | `set-title` | `id`, `title` | rewrites the node line, keeps attrs and ID |
205
+ | `set-attr` | `id`, `key`, `value` (string, number, boolean or list; `null` removes) | rewrites only that node line; for `star`, `false` also removes the key |
206
+ | `add-node` | `parent` (ID or `null`), `title`, `status?`, `id?` | inserts after the parent's subtree; ID defaults to a slug of the title, so pass a readable `id` |
207
+ | `remove-node` | `id`, `recursive?` | deletes the node, its notes and (with `recursive`) its subtree, plus edges touching them |
208
+ | `rename-id` | `id`, `to` | renames the ID and every edge reference; threads are retargeted |
209
+ | `add-edge` | `from`, `kind`, `to`, `label?` | appends one edge line at the end of the file |
210
+ | `remove-edge` | `from`, `kind`, `to` | removes that target from matching edge lines |
211
+
212
+ - `status` values are words: `todo`, `doing`, `done`, `dropped`, `blocked`, `idea`.
213
+ - `409` means the file changed since your `version`; re-fetch `/api/doc` and retry. `400` means the edit is invalid or would introduce a new error.
214
+ - The response is `{version}` plus `id` (add-node), `renamed` or `removed` when relevant.
215
+ - The API cannot add notes or anchors, and `move-node` is not implemented. Make those edits by hand. The server watches the file, so hand edits show up in the UI.
216
+ - Prefer `set-status dropped` over `remove-node`.
217
+
218
+ ### Threads
219
+
220
+ Human/agent discussion threads live in the sidecar `<name>.threads.jsonl` next to the `.rhumb` file (append-only, one event per line). Never write discussion into the `.rhumb` file. Use `GET /api/threads?status=open` to list open threads (`orphan: true` means the target node is gone) and `POST /api/threads` with `{action: "open"|"reply"|"resolve"|"reopen", ...}` to write. Renaming an ID by hand instead of with `rename-id` orphans its threads.
221
+
222
+ ## 8. Worked example
223
+
224
+ You finish work and write this diary entry in `2026/1009.md`:
225
+
226
+ ```markdown
227
+ ## Rhumb tooling: fmt and edit API
228
+
229
+ * > [host][portolan] Implement `rhumb fmt` and the edit operations API
230
+ <details>
231
+ <summary>fmt assigns IDs and normalizes status aliases; edit API started</summary>
232
+
233
+ * fmt 0.1 subset only; the full CST printer (re-indent, edge dedup) is postponed
234
+ * edit API: set-status, add-node, add-edge work; move-node not started
235
+ </details>
236
+ ```
237
+
238
+ Map before (`memo/portolan.rhumb`):
239
+
240
+ ```rhumb
241
+ ---
242
+ rhumb: 0.1
243
+ title: Portolan
244
+ links:
245
+ diary: ../{path}.md
246
+ ---
247
+
248
+ - [/] Rhumb DSL ^rhumb
249
+ - [x] Parser (Peggy) ^rhumb-parser
250
+ - [parser record](<diary:2026/1008#rhumb-dsl:~:text=Parser: chose Peggy>)
251
+ - [ ] Formatter `rhumb fmt` ^rhumb-fmt
252
+ - [ ] Edit operations API ^rhumb-edit
253
+
254
+ %% Rhumb tooling
255
+ rhumb-fmt needs rhumb-parser
256
+ rhumb-edit needs rhumb-fmt
257
+ ```
258
+
259
+ Updates: `rhumb-fmt` → done with an anchor to the entry; `rhumb-edit` → doing (allowed now that `rhumb-fmt` is done); the postponed work becomes a new node under `rhumb`, linked to its origin with `from`.
260
+
261
+ ```diff
262
+ @@ -8,9 +8,13 @@
263
+ - [/] Rhumb DSL ^rhumb
264
+ - [x] Parser (Peggy) ^rhumb-parser
265
+ - [parser record](<diary:2026/1008#rhumb-dsl:~:text=Parser: chose Peggy>)
266
+ - - [ ] Formatter `rhumb fmt` ^rhumb-fmt
267
+ - - [ ] Edit operations API ^rhumb-edit
268
+ + - [x] Formatter `rhumb fmt` ^rhumb-fmt
269
+ + - [fmt record](<diary:2026/1009#rhumb-tooling:~:text=fmt assigns IDs>)
270
+ + - [/] Edit operations API ^rhumb-edit
271
+ + - [ ] Full CST printer for fmt ^rhumb-fmt-cst
272
+ + - Re-indent, edge dedup, blank-line collapsing
273
+
274
+ %% Rhumb tooling
275
+ rhumb-fmt needs rhumb-parser
276
+ rhumb-edit needs rhumb-fmt
277
+ +rhumb-fmt-cst from rhumb-fmt
278
+ ```
279
+
280
+ Then run `node src/cli.ts check memo/portolan.rhumb`: no output, exit 0. If the diary repo is not checked out, the anchors report `W003` only.
281
+
282
+ ## 9. Common mistakes
283
+
284
+ | Mistake | Code | Fix |
285
+ | --- | --- | --- |
286
+ | Unknown checkbox, e.g. `[>]` | `E002` | use one of the six statuses |
287
+ | Duplicate `^id` | `E003` | pick a new readable ID |
288
+ | Dedent to a level that was never opened, odd indentation | `E004` | 2 spaces per level, align siblings |
289
+ | `foo:` link prefix missing from front matter | `E005` | add it under `links:` |
290
+ | Edge to a typo or missing ID | `E007` | check the ID exists |
291
+ | Unknown edge kind (`a depends b`, `a compares b`) | `E008` | use needs/blocks/relates/replaces/from; put domain meaning in a `relates` label |
292
+ | Indented edge line, or prose at column 0 | `E001` | edges at column 0; prose goes in a note |
293
+ | `a needs b` plus `b needs a` | `E009` | break the cycle |
294
+ | Node without a title | `E011` | write a title |
295
+ | Note at top level | `W001` | put it under a node |
296
+ | Node indented under a note | `W002` | indent it under the node instead |
297
+ | Missing diary file, heading, ambiguous prefix, text not found | `W003` | fix the link or the heading |
298
+ | Same edge twice | `W004` | delete the duplicate |
299
+ | `needs` between parent and descendant | `W005` | remove it |
300
+ | `needs` a dropped node | `W006` | retarget or remove the edge |
301
+ | `[x]` parent with open children | `W007` | close or drop the children first |
302
+ | `[/]`/`[x]` node whose `needs` target is not done | `W008` | fix the status or the edge |
303
+ | `^Upper_Case` or `^_handmade` ID | `W009` | lowercase kebab-case, no leading `_` |
304
+ | `[ ]` parent with started children | `I003` | set the parent to `[/]` |
305
+
306
+ This file triggers `W005`, `W006`, `W007`, `W008` and `I003`:
307
+
308
+ ```rhumb
309
+ - [x] Release 1.0 ^release
310
+ - [ ] Write changelog ^changelog
311
+ - [/] Deploy ^deploy
312
+ - [ ] Smoke test ^smoke
313
+ - [ ] Docs ^docs
314
+ - [/] API reference ^docs-api
315
+ - [-] Old installer ^old-installer
316
+
317
+ deploy needs changelog
318
+ deploy needs smoke
319
+ docs needs old-installer
320
+ ```
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@k-l-lambda/portolan",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A shared map of work for humans and AI agents: a mind map that is also a TODO list and task log, written in the Rhumb DSL.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "mind-map",
8
+ "task-graph",
9
+ "dsl",
10
+ "agents",
11
+ "todo",
12
+ "diary"
13
+ ],
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "bin": {
24
+ "portolan": "dist/cli.js",
25
+ "rhumb": "dist/cli.js"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "web/dist",
30
+ "docs"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22.12"
34
+ },
35
+ "scripts": {
36
+ "build:grammar": "peggy --format es --dts --allowed-start-rules Line,Inline -o src/generated/line-parser.js src/grammar.peggy",
37
+ "pretest": "npm run build:grammar",
38
+ "test": "vitest run",
39
+ "typecheck": "npm run build:grammar && tsc --noEmit && tsc --noEmit -p web",
40
+ "portolan": "node src/cli.ts",
41
+ "build:lib": "npm run build:grammar && tsc -p tsconfig.build.json && mkdir -p dist/generated && cp src/generated/line-parser.js src/generated/line-parser.d.ts dist/generated/",
42
+ "build:web": "vite build web",
43
+ "dev:web": "vite web",
44
+ "build": "npm run build:lib && npm run build:web",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/k-l-lambda/portolan.git"
50
+ },
51
+ "homepage": "https://github.com/k-l-lambda/portolan#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/k-l-lambda/portolan/issues"
54
+ },
55
+ "author": "k.l.lambda",
56
+ "license": "ISC",
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "packageManager": "pnpm@10.23.0",
61
+ "dependencies": {
62
+ "yaml": "2.9.1"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "22.20.4",
66
+ "@types/react": "19.3.0",
67
+ "@types/react-dom": "19.3.0",
68
+ "@vitejs/plugin-react": "6.1.1",
69
+ "@xyflow/react": "12.11.6",
70
+ "dompurify": "3.4.16",
71
+ "elkjs": "0.12.0",
72
+ "marked": "18.0.14",
73
+ "peggy": "5.1.0",
74
+ "react": "19.3.0",
75
+ "react-dom": "19.3.0",
76
+ "typescript": "6.0.3",
77
+ "vite": "8.3.3",
78
+ "vitest": "5.0.1"
79
+ }
80
+ }