@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/README.md +104 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/derive.d.ts +17 -0
- package/dist/derive.js +67 -0
- package/dist/derive.js.map +1 -0
- package/dist/edit.d.ts +69 -0
- package/dist/edit.js +302 -0
- package/dist/edit.js.map +1 -0
- package/dist/format.d.ts +9 -0
- package/dist/format.js +35 -0
- package/dist/format.js.map +1 -0
- package/dist/generated/line-parser.d.ts +213 -0
- package/dist/generated/line-parser.js +1920 -0
- package/dist/history.d.ts +90 -0
- package/dist/history.js +182 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +33 -0
- package/dist/parse.js +468 -0
- package/dist/parse.js.map +1 -0
- package/dist/resolve.d.ts +41 -0
- package/dist/resolve.js +214 -0
- package/dist/resolve.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +376 -0
- package/dist/server.js.map +1 -0
- package/dist/threads.d.ts +93 -0
- package/dist/threads.js +128 -0
- package/dist/threads.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/rhumb-spec.md +289 -0
- package/docs/skill.md +320 -0
- package/package.json +78 -4
- package/web/dist/assets/index-BMGxaxGj.js +106 -0
- package/web/dist/assets/index-BjZBlA3n.css +1 -0
- package/web/dist/favicon.svg +16 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export type ThreadEvent = {
|
|
2
|
+
type: "open";
|
|
3
|
+
thread: string;
|
|
4
|
+
target: string;
|
|
5
|
+
author: string;
|
|
6
|
+
text: string;
|
|
7
|
+
time: string;
|
|
8
|
+
id: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "reply";
|
|
11
|
+
thread: string;
|
|
12
|
+
author: string;
|
|
13
|
+
text: string;
|
|
14
|
+
time: string;
|
|
15
|
+
id: string;
|
|
16
|
+
reply_to?: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "resolve";
|
|
19
|
+
thread: string;
|
|
20
|
+
author: string;
|
|
21
|
+
time: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: "reopen";
|
|
24
|
+
thread: string;
|
|
25
|
+
author: string;
|
|
26
|
+
time: string;
|
|
27
|
+
}
|
|
28
|
+
/** Written when a node ID is renamed, so threads follow the node. */
|
|
29
|
+
| {
|
|
30
|
+
type: "retarget";
|
|
31
|
+
thread: string;
|
|
32
|
+
target: string;
|
|
33
|
+
time: string;
|
|
34
|
+
};
|
|
35
|
+
export interface Message {
|
|
36
|
+
id: string;
|
|
37
|
+
author: string;
|
|
38
|
+
text: string;
|
|
39
|
+
time: string;
|
|
40
|
+
reply_to: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface Thread {
|
|
43
|
+
id: string;
|
|
44
|
+
/** Node ID the thread is attached to. */
|
|
45
|
+
target: string;
|
|
46
|
+
status: "open" | "resolved";
|
|
47
|
+
messages: Message[];
|
|
48
|
+
updated: string;
|
|
49
|
+
}
|
|
50
|
+
export interface FoldResult {
|
|
51
|
+
threads: Thread[];
|
|
52
|
+
/** 1-based sidecar lines that could not be applied. */
|
|
53
|
+
problems: {
|
|
54
|
+
line: number;
|
|
55
|
+
message: string;
|
|
56
|
+
}[];
|
|
57
|
+
}
|
|
58
|
+
export declare function foldThreads(events: {
|
|
59
|
+
event: unknown;
|
|
60
|
+
line: number;
|
|
61
|
+
}[]): FoldResult;
|
|
62
|
+
/** Input accepted from clients; the store fills in IDs and timestamps. */
|
|
63
|
+
export type ThreadAction = {
|
|
64
|
+
action: "open";
|
|
65
|
+
target: string;
|
|
66
|
+
author: string;
|
|
67
|
+
text: string;
|
|
68
|
+
} | {
|
|
69
|
+
action: "reply";
|
|
70
|
+
thread: string;
|
|
71
|
+
author: string;
|
|
72
|
+
text: string;
|
|
73
|
+
reply_to?: string;
|
|
74
|
+
} | {
|
|
75
|
+
action: "resolve";
|
|
76
|
+
thread: string;
|
|
77
|
+
author: string;
|
|
78
|
+
} | {
|
|
79
|
+
action: "reopen";
|
|
80
|
+
thread: string;
|
|
81
|
+
author: string;
|
|
82
|
+
};
|
|
83
|
+
export declare class ThreadStore {
|
|
84
|
+
readonly path: string;
|
|
85
|
+
constructor(rhumbPath: string);
|
|
86
|
+
load(): FoldResult;
|
|
87
|
+
/** Validates a client action, appends the event and returns it. */
|
|
88
|
+
apply(input: ThreadAction): ThreadEvent;
|
|
89
|
+
/** Makes threads follow a renamed node. */
|
|
90
|
+
retarget(from: string, to: string): void;
|
|
91
|
+
private requireThread;
|
|
92
|
+
private append;
|
|
93
|
+
}
|
package/dist/threads.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Annotation threads, stored in an append-only sidecar `<name>.threads.jsonl` next to the
|
|
2
|
+
// .rhumb file. One JSON event per line; state is rebuilt by folding events, so resolving
|
|
3
|
+
// or retargeting never rewrites old lines and concurrent writers rarely conflict.
|
|
4
|
+
//
|
|
5
|
+
// Open interfaces (not implemented in 0.1):
|
|
6
|
+
// - Delivering new human messages to an agent. The server emits a `threads` SSE event and
|
|
7
|
+
// `GET /api/threads?status=open` lists pending threads; a CLI poller, MCP tool or hook
|
|
8
|
+
// can be built on top of that.
|
|
9
|
+
// - Targets other than nodes (edges, anchors) and @mentions.
|
|
10
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
export function foldThreads(events) {
|
|
13
|
+
const threads = new Map();
|
|
14
|
+
const problems = [];
|
|
15
|
+
for (const { event, line } of events) {
|
|
16
|
+
const e = event;
|
|
17
|
+
const t = threads.get(e?.thread);
|
|
18
|
+
if (e?.type === "open") {
|
|
19
|
+
if (t) {
|
|
20
|
+
problems.push({ line, message: `Thread ${e.thread} opened twice` });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
threads.set(e.thread, {
|
|
24
|
+
id: e.thread, target: e.target, status: "open", updated: e.time,
|
|
25
|
+
messages: [{ id: e.id, author: e.author, text: e.text, time: e.time, reply_to: null }],
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!t) {
|
|
30
|
+
problems.push({ line, message: "Event for an unknown thread" });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
switch (e.type) {
|
|
34
|
+
case "reply":
|
|
35
|
+
t.messages.push({ id: e.id, author: e.author, text: e.text, time: e.time, reply_to: e.reply_to ?? null });
|
|
36
|
+
break;
|
|
37
|
+
case "resolve":
|
|
38
|
+
t.status = "resolved";
|
|
39
|
+
break;
|
|
40
|
+
case "reopen":
|
|
41
|
+
t.status = "open";
|
|
42
|
+
break;
|
|
43
|
+
case "retarget":
|
|
44
|
+
t.target = e.target;
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
problems.push({ line, message: "Unknown event type" });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
t.updated = e.time;
|
|
51
|
+
}
|
|
52
|
+
return { threads: [...threads.values()], problems };
|
|
53
|
+
}
|
|
54
|
+
const MAX_TEXT = 20_000;
|
|
55
|
+
const MAX_AUTHOR = 100;
|
|
56
|
+
export class ThreadStore {
|
|
57
|
+
path;
|
|
58
|
+
constructor(rhumbPath) {
|
|
59
|
+
this.path = rhumbPath.replace(/\.rhumb$/, "") + ".threads.jsonl";
|
|
60
|
+
}
|
|
61
|
+
load() {
|
|
62
|
+
if (!existsSync(this.path))
|
|
63
|
+
return { threads: [], problems: [] };
|
|
64
|
+
const events = [];
|
|
65
|
+
const problems = [];
|
|
66
|
+
readFileSync(this.path, "utf8").split("\n").forEach((text, i) => {
|
|
67
|
+
if (text.trim() === "")
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
events.push({ event: JSON.parse(text), line: i + 1 });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
problems.push({ line: i + 1, message: "Invalid JSON" });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const folded = foldThreads(events);
|
|
77
|
+
return { threads: folded.threads, problems: [...problems, ...folded.problems] };
|
|
78
|
+
}
|
|
79
|
+
/** Validates a client action, appends the event and returns it. */
|
|
80
|
+
apply(input) {
|
|
81
|
+
const a = input;
|
|
82
|
+
const str = (k, max) => {
|
|
83
|
+
const v = a[k];
|
|
84
|
+
if (typeof v !== "string" || v.trim() === "" || v.length > max)
|
|
85
|
+
throw new Error(`Invalid ${k}`);
|
|
86
|
+
return v;
|
|
87
|
+
};
|
|
88
|
+
const time = new Date().toISOString();
|
|
89
|
+
let event;
|
|
90
|
+
switch (a.action) {
|
|
91
|
+
case "open":
|
|
92
|
+
event = { type: "open", thread: randomUUID(), target: str("target", 100), author: str("author", MAX_AUTHOR),
|
|
93
|
+
text: str("text", MAX_TEXT), time, id: randomUUID() };
|
|
94
|
+
break;
|
|
95
|
+
case "reply":
|
|
96
|
+
this.requireThread(str("thread", 100));
|
|
97
|
+
event = { type: "reply", thread: a.thread, author: str("author", MAX_AUTHOR),
|
|
98
|
+
text: str("text", MAX_TEXT), time, id: randomUUID(),
|
|
99
|
+
...(typeof a.reply_to === "string" ? { reply_to: a.reply_to } : {}) };
|
|
100
|
+
break;
|
|
101
|
+
case "resolve":
|
|
102
|
+
case "reopen":
|
|
103
|
+
this.requireThread(str("thread", 100));
|
|
104
|
+
event = { type: a.action, thread: a.thread, author: str("author", MAX_AUTHOR), time };
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
throw new Error("Unknown action");
|
|
108
|
+
}
|
|
109
|
+
this.append(event);
|
|
110
|
+
return event;
|
|
111
|
+
}
|
|
112
|
+
/** Makes threads follow a renamed node. */
|
|
113
|
+
retarget(from, to) {
|
|
114
|
+
const time = new Date().toISOString();
|
|
115
|
+
for (const t of this.load().threads) {
|
|
116
|
+
if (t.target === from)
|
|
117
|
+
this.append({ type: "retarget", thread: t.id, target: to, time });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
requireThread(id) {
|
|
121
|
+
if (!this.load().threads.some((t) => t.id === id))
|
|
122
|
+
throw new Error(`Unknown thread ${id}`);
|
|
123
|
+
}
|
|
124
|
+
append(event) {
|
|
125
|
+
appendFileSync(this.path, JSON.stringify(event) + "\n");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=threads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threads.js","sourceRoot":"","sources":["../src/threads.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type Status = "todo" | "doing" | "done" | "dropped" | "blocked" | "idea";
|
|
2
|
+
export type EdgeKind = "needs" | "blocks" | "relates" | "replaces" | "from";
|
|
3
|
+
export type Level = "error" | "warning" | "info";
|
|
4
|
+
export interface Diagnostic {
|
|
5
|
+
code: string;
|
|
6
|
+
level: Level;
|
|
7
|
+
line: number;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Anchor {
|
|
11
|
+
text: string;
|
|
12
|
+
target: string;
|
|
13
|
+
line: number;
|
|
14
|
+
/** `prefix`: `diary:…` resolved via front matter `links`; `relative`: path from the .rhumb file; `url`: absolute URL. */
|
|
15
|
+
kind: "prefix" | "relative" | "url";
|
|
16
|
+
prefix: string | null;
|
|
17
|
+
path: string | null;
|
|
18
|
+
/** Heading slug (or unique slug prefix) the fragment starts from. */
|
|
19
|
+
fragment: string | null;
|
|
20
|
+
text_fragment: string | null;
|
|
21
|
+
/** `^=prefix`: first line in the section (or file) that starts with this text, ignoring indentation. */
|
|
22
|
+
line_prefix: string | null;
|
|
23
|
+
/** `+L3` / `-L2`: lines below / above the matched line or the heading. */
|
|
24
|
+
line_offset: number | null;
|
|
25
|
+
/** `#L42` or `#L42-L50`: absolute 1-based line range. */
|
|
26
|
+
lines: [number, number] | null;
|
|
27
|
+
/** Why the fragment could not be parsed; the resolver reports it as W003. */
|
|
28
|
+
fragment_error: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface Note {
|
|
31
|
+
text: string;
|
|
32
|
+
line: number;
|
|
33
|
+
/** Nesting depth below the owning node; 0 = direct note. */
|
|
34
|
+
depth: number;
|
|
35
|
+
}
|
|
36
|
+
export interface RhumbNode {
|
|
37
|
+
id: string | null;
|
|
38
|
+
generated_id: boolean;
|
|
39
|
+
/** null only when the checkbox symbol is unknown (E002). */
|
|
40
|
+
status: Status | null;
|
|
41
|
+
title: string;
|
|
42
|
+
attrs: Record<string, unknown>;
|
|
43
|
+
notes: Note[];
|
|
44
|
+
anchors: Anchor[];
|
|
45
|
+
children: RhumbNode[];
|
|
46
|
+
line: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Edge {
|
|
49
|
+
kind: EdgeKind;
|
|
50
|
+
from: string;
|
|
51
|
+
to: string;
|
|
52
|
+
label: string | null;
|
|
53
|
+
line: number;
|
|
54
|
+
}
|
|
55
|
+
export interface RhumbDocument {
|
|
56
|
+
rhumb: string;
|
|
57
|
+
title: string | null;
|
|
58
|
+
links: Record<string, string>;
|
|
59
|
+
nodes: RhumbNode[];
|
|
60
|
+
edges: Edge[];
|
|
61
|
+
diagnostics: Diagnostic[];
|
|
62
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Rhumb 0.1 Syntax Specification (draft)
|
|
2
|
+
|
|
3
|
+
Rhumb is Portolan's graph DSL. It describes the hierarchy, status and relations of work items. Each project has one `.rhumb` file. Details live in the diary, and Rhumb only links to them.
|
|
4
|
+
|
|
5
|
+
Design principle: every syntax element reuses a notation that LLMs and people already know. Rhumb only adds syntax for what those notations cannot express, namely edges.
|
|
6
|
+
|
|
7
|
+
| Element | Notation | Borrowed from |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| File metadata | YAML front matter | Markdown / Jekyll / Mermaid |
|
|
10
|
+
| Node | nested `- [ ] title` list | GFM task list |
|
|
11
|
+
| Extended statuses | `[/]` `[-]` `[!]` `[?]` | Obsidian Tasks / alternate checkboxes |
|
|
12
|
+
| Node ID | trailing `^id` | Obsidian block ID |
|
|
13
|
+
| Attributes | trailing `{key: value}` | YAML flow mapping |
|
|
14
|
+
| Anchor | `[text](target#heading)` | Markdown link, GitHub heading slug |
|
|
15
|
+
| Entry locator | `#heading:~:text=snippet` | URL Text Fragments |
|
|
16
|
+
| Comment | `%% ...` | Mermaid / Obsidian |
|
|
17
|
+
| Edge | `a needs b` | plain English verb sentence, see section 5 |
|
|
18
|
+
|
|
19
|
+
## 1. Example
|
|
20
|
+
|
|
21
|
+
```rhumb
|
|
22
|
+
---
|
|
23
|
+
rhumb: 0.1
|
|
24
|
+
title: Portolan
|
|
25
|
+
links:
|
|
26
|
+
diary: ../diary-job/{path}.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
- [/] Portolan project ^portolan
|
|
30
|
+
- [x] Prior-art survey ^survey
|
|
31
|
+
- [survey notes](diary:2026/1008#portolan)
|
|
32
|
+
- [/] Rhumb DSL design {owner: claude} ^dsl
|
|
33
|
+
- [/] Syntax spec ^dsl-spec
|
|
34
|
+
- [ ] Parser + formatter ^dsl-parser
|
|
35
|
+
- [ ] Visual frontend ^ui
|
|
36
|
+
- [?] Node annotations and threads ^threads
|
|
37
|
+
|
|
38
|
+
%% relations
|
|
39
|
+
dsl-parser needs dsl-spec
|
|
40
|
+
ui needs dsl-parser
|
|
41
|
+
threads relates ui
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 2. File structure
|
|
45
|
+
|
|
46
|
+
- Encoding is UTF-8, the extension is `.rhumb`, and line endings are LF. The parser accepts CRLF, and `fmt` writes LF.
|
|
47
|
+
- A file has optional front matter followed by a body. The body is parsed line by line, and each line has one of these types:
|
|
48
|
+
|
|
49
|
+
| Line type | Recognized by |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| Blank | whitespace only |
|
|
52
|
+
| Comment | starts with `%%` after indentation |
|
|
53
|
+
| Node | list item whose marker is followed by a `[S]` checkbox |
|
|
54
|
+
| Note | list item without a checkbox |
|
|
55
|
+
| Edge | starts at column 0, shaped `ref kind ref…` |
|
|
56
|
+
|
|
57
|
+
Any other line is error `E001`.
|
|
58
|
+
|
|
59
|
+
### 2.1 Front matter
|
|
60
|
+
|
|
61
|
+
If the first line is `---`, everything up to the next `---` is parsed as YAML. The block is optional.
|
|
62
|
+
|
|
63
|
+
| Key | Meaning | Default |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `rhumb` | syntax version | current version |
|
|
66
|
+
| `title` | graph title | file name |
|
|
67
|
+
| `links` | link prefix table, `prefix: template`, with a `{path}` placeholder in the template | empty |
|
|
68
|
+
|
|
69
|
+
Unknown keys are kept and reported as `I001`. A version newer than the parser supports is error `E013`. A missing closing `---`, invalid YAML, or a `links` value that is not a string map is error `E012`; an unterminated block makes the whole file unusable.
|
|
70
|
+
|
|
71
|
+
## 3. Nodes
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
node-line = indent ("-" | "*") SP "[" S "]" SP title [SP attrs] [SP "^" ID]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3.1 Statuses
|
|
78
|
+
|
|
79
|
+
| Symbol | Status | Meaning |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `[ ]` | todo | not started |
|
|
82
|
+
| `[/]` | doing | in progress |
|
|
83
|
+
| `[x]` | done | finished |
|
|
84
|
+
| `[-]` | dropped | abandoned |
|
|
85
|
+
| `[!]` | blocked | explicitly blocked; put the reason in a note |
|
|
86
|
+
| `[?]` | idea | proposed, not yet committed |
|
|
87
|
+
|
|
88
|
+
For tolerance the parser also accepts `[X]` and `[~]`, which `fmt` normalizes to `[x]` and `[/]`. Any other symbol is error `E002`.
|
|
89
|
+
|
|
90
|
+
### 3.2 IDs
|
|
91
|
+
|
|
92
|
+
- An ID goes at the end of the line as `^` plus the ID. Hand-written IDs match `[a-z0-9][a-z0-9-]{0,47}`.
|
|
93
|
+
- A readable ID is the default, and agents must always write one. If the ID is omitted, `fmt` or any write command generates `^_` plus 6 random base32 characters (for example `^_k3f7q2`) and writes it back. The ID never changes after that. IDs starting with `_` are reserved for generated IDs, so hand-written IDs cannot start with `_`.
|
|
94
|
+
- Generated IDs are random, not derived from the title, so renaming a node does not change its ID.
|
|
95
|
+
- IDs are unique within the file. A duplicate is error `E003`; the first occurrence is the one edges resolve to.
|
|
96
|
+
- A trailing `^token` that is not a valid ID (for example `^Not_Valid`) stays in the title and is warning `W009`.
|
|
97
|
+
- A node with an empty title (`- [ ] ^id`, `- [ ]`) is error `E011`.
|
|
98
|
+
- Edges can only reference IDs, so a node without an ID cannot be the target of an edge.
|
|
99
|
+
|
|
100
|
+
### 3.3 Title and attributes
|
|
101
|
+
|
|
102
|
+
The parser strips suffixes from the end of the line, in this order:
|
|
103
|
+
|
|
104
|
+
1. If the last token matches `^ID`, it is stripped as the ID.
|
|
105
|
+
2. If what remains ends with one complete `{…}` that parses as a YAML flow mapping, it is stripped as the attributes.
|
|
106
|
+
3. The rest is the title, which is parsed as inline Markdown (links, code, emphasis).
|
|
107
|
+
|
|
108
|
+
- `^` and `{` in the middle of a title need no escaping. Only a title that itself ends in `^word` or `{…}` must write `\^` or `\{`.
|
|
109
|
+
- Reserved attribute keys are `owner`, `due` (ISO date), `tags` (list), `priority` (`high` / `normal` / `low`) and `star` (boolean). Unknown keys are kept and reported as `I002`.
|
|
110
|
+
- `star: true` marks a node as a favorite of the project; it is shared like any other attribute and is independent of status. To unstar, remove the key. Any value other than `true`/`false` is warning `W010` and the node counts as not starred.
|
|
111
|
+
- Markdown links in a title are anchors, see section 4.
|
|
112
|
+
|
|
113
|
+
### 3.4 Hierarchy
|
|
114
|
+
|
|
115
|
+
Nesting follows Markdown nested lists:
|
|
116
|
+
|
|
117
|
+
- A list item's parent is the nearest preceding node with smaller indentation.
|
|
118
|
+
- Siblings must have the same indentation. A dedent must return to an existing level, otherwise it is error `E004`.
|
|
119
|
+
- A tab counts as 4 columns, as in CommonMark. `fmt` always writes 2 spaces per level.
|
|
120
|
+
|
|
121
|
+
### 3.5 Notes
|
|
122
|
+
|
|
123
|
+
A list item without a checkbox is a note that belongs to its parent node. A note is not a child node: the UI shows it as part of the node's description, not as a new branch.
|
|
124
|
+
|
|
125
|
+
```rhumb
|
|
126
|
+
- [!] K3 hidden-state capture ^capture
|
|
127
|
+
- Wait for the current 4-GPU training job to finish; TP8 needs the whole machine
|
|
128
|
+
- [record](diary:2026/1008#camelot-exp73)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- A note at the top level has no parent. It is ignored and reported as `W001`.
|
|
132
|
+
- A node indented under a note is attached to the node that owns the note, and reported as `W002`.
|
|
133
|
+
|
|
134
|
+
## 4. Anchors (links)
|
|
135
|
+
|
|
136
|
+
Every Markdown link in a node's title or in its notes is an anchor of that node. A node can have any number of anchors.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
[text](target) [text](<target with spaces>) <https://…>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Targets are resolved as follows:
|
|
143
|
+
|
|
144
|
+
| Form | Resolution |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `diary:2026/1008#frag` | look up prefix `diary` in the front matter `links`, replace `{path}` with `2026/1008`; the path is relative to the `.rhumb` file |
|
|
147
|
+
| `../notes/x.md#frag` | path relative to the `.rhumb` file |
|
|
148
|
+
| `https://…` | used as is |
|
|
149
|
+
|
|
150
|
+
The fragment (after `#`) is matched like this:
|
|
151
|
+
|
|
152
|
+
- Heading slugs follow GitHub rules: lowercase, punctuation removed, spaces replaced with `-`.
|
|
153
|
+
- A unique prefix of the slug is enough: `#portolan` matches `## Portolan: agent-human shared mind map …`.
|
|
154
|
+
- To point at a specific entry under a heading, append a Text Fragment: `#portolan:~:text=Backlog.md`. The resolver picks the first list entry under that heading that contains the text. If the text has spaces, wrap the whole target in angle brackets.
|
|
155
|
+
|
|
156
|
+
### 4.1 Lines inside a target
|
|
157
|
+
|
|
158
|
+
A fragment narrows left to right: a heading, then a line selector, then a line offset. An absolute line needs no heading and works in any text file.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
fragment = slug [":~:text=" text] ; existing form, text runs to the end
|
|
162
|
+
| [slug] "^=" prefix [offset] ; line that starts with prefix
|
|
163
|
+
| slug offset ; lines from the heading
|
|
164
|
+
| "L" n ["-L" m] ; absolute line or range
|
|
165
|
+
prefix = "`" chars "`" | chars ; backticks for spaces or + # : ~
|
|
166
|
+
offset = ("+" | "-") "L" n
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Form | Points at |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `#heading^=foo` | first line in the heading's section that starts with `foo`, ignoring indentation |
|
|
172
|
+
| ``#heading^=`* \> [host] Make` `` | same; backticks when the prefix has spaces or `+ # : ~` |
|
|
173
|
+
| `#^=export function parse` | first such line in the whole file (any text file) |
|
|
174
|
+
| `#heading+L3` / `#heading-L2` | 3 lines below / 2 lines above the heading line |
|
|
175
|
+
| `#heading^=foo+L2` | 2 lines below the matched line |
|
|
176
|
+
| `#L42`, `#L42-L50` | line 42, lines 42 to 50 (as on GitHub) |
|
|
177
|
+
|
|
178
|
+
- These separators never clash with slugs: GitHub slugs are lowercase and drop `+ ^ = \` : ~`, so `L` is always uppercase in an offset or line.
|
|
179
|
+
- Lines are counted as in an editor, blank lines included. The first match wins. An offset may not leave the heading's section (or the file).
|
|
180
|
+
- Prefer a `^=` or text match to a bare offset: inserting a line above shifts an offset to another line, and `check` cannot notice.
|
|
181
|
+
- Inside `<…>` a backslash escapes ASCII punctuation (CommonMark), so a diary entry line `* > [host] …` is written ``^=`* \> [host] …` ``. A prefix is percent-decoded after quoting, so a backtick inside it is written `%60`: a line `` * `src/edit.ts`: … `` is ``^=`* %60src/edit.ts%60` ``. An unescaped `>` ends the target early; the link is then not recognized and the line gets warning `W011`.
|
|
182
|
+
- The `:~:text=` form keeps its existing meaning and cannot take an offset (its text runs to the end of the fragment).
|
|
183
|
+
|
|
184
|
+
`rhumb check` resolves anchors against the actual files. An undefined prefix is error `E005`. A missing file, a missing heading, an ambiguous prefix, a text fragment or `^=` prefix that is not found, an offset that leaves its section, a line past the end of the file, or a fragment that cannot be parsed (such as `#L9-L3` or an unterminated backtick) is warning `W003`. The diary lives in another repository and may not always be present, so these are warnings, not errors.
|
|
185
|
+
|
|
186
|
+
## 5. Edges
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
edge-line = ref SP kind SP ref ("," [SP] ref)* [[SP] ":" [SP] label]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
An edge line starts at column 0 and can appear anywhere in the body. `fmt` does not reorder edges.
|
|
193
|
+
|
|
194
|
+
| kind | Meaning | Affects readiness |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| `needs` | a depends on b; a becomes ready once b is done | yes |
|
|
197
|
+
| `blocks` | a blocks b, same as `b needs a` | yes |
|
|
198
|
+
| `relates` | related; undirected without a label, directed with one (see 5.1) | no |
|
|
199
|
+
| `replaces` | a supersedes b | no |
|
|
200
|
+
| `from` | a was derived from b (split, follow-up, spin-off) | no |
|
|
201
|
+
|
|
202
|
+
- A `ref` in an edge is a node ID, with or without the `^` prefix (`^a needs ^b`). `fmt` removes the `^`.
|
|
203
|
+
- The label is optional free text: `ui needs dsl-parser: needs a stable AST`. With several targets, the label applies to each resulting edge.
|
|
204
|
+
- A line shaped like an edge with an unknown kind is `E008` only when its first word is a known ID; otherwise it is plain prose and `E001`. An indented edge line is also `E001`.
|
|
205
|
+
- Cross-file references are not supported in 0.1. The form `other.rhumb^id` is reserved and is error `E006` for now.
|
|
206
|
+
|
|
207
|
+
### 5.1 Domain relations go in labels
|
|
208
|
+
|
|
209
|
+
The five kinds are kept because each is general across domains and the tools treat it differently: `needs`/`blocks` drive readiness and cycle checks, `replaces` and `from` record supersession and provenance. A relation that only appears in some kinds of work (an experiment compared against a control, a task that uses a dataset, an investigation that tests a hypothesis) is written as a labeled `relates`, not as a new keyword:
|
|
210
|
+
|
|
211
|
+
```rhumb
|
|
212
|
+
a1 relates a0: compared against
|
|
213
|
+
train relates dataset: uses
|
|
214
|
+
probe relates lr-hypothesis: tests
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- The label is a verb phrase. The edge reads as one sentence, source + label + target: "a1 compared against a0".
|
|
218
|
+
- A labeled `relates` keeps the direction it is written in. An unlabeled `relates` stays undirected.
|
|
219
|
+
- Duplicates (`W004`): an unlabeled `relates` matches either direction; labeled edges match on direction and label, so `a relates b: uses` and `a relates b: tests` are two edges.
|
|
220
|
+
- Use `needs` only for execution order. A treatment that is compared against a control does not need the control to finish first; write `relates … : compared against`, otherwise readiness and progress become wrong.
|
|
221
|
+
- A label is promoted to a keyword only when it recurs across unrelated domains and a tool needs to treat it differently from `relates`.
|
|
222
|
+
|
|
223
|
+
Edges do not use Mermaid's `-->` because an arrow is ambiguous for dependencies: `a --> b` can be read as "a depends on b" or as "a comes before b". A plain English verb sentence leaves no room for an LLM to read the direction wrong.
|
|
224
|
+
|
|
225
|
+
| Problem | Code |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| reference to an unknown ID | `E007` |
|
|
228
|
+
| unknown kind | `E008` |
|
|
229
|
+
| cycle through `needs` / `blocks` | `E009` |
|
|
230
|
+
| self-edge | `E010` |
|
|
231
|
+
| duplicate edge | `W004` (`fmt` removes it) |
|
|
232
|
+
| `needs` / `blocks` between a parent and its descendant | `W005` (the hierarchy already expresses containment) |
|
|
233
|
+
|
|
234
|
+
## 6. Derived semantics
|
|
235
|
+
|
|
236
|
+
These values are computed by tools and never written to the file.
|
|
237
|
+
|
|
238
|
+
- **Progress**: done leaves in the subtree divided by (all leaves minus dropped and idea leaves). If the denominator is 0, no progress is shown.
|
|
239
|
+
- **Readiness**: a node is ready when its status is todo and every `needs` target (including reversed `blocks`) is done. A dependency on a dropped node is warning `W006` and does not block readiness.
|
|
240
|
+
- **Consistency checks**:
|
|
241
|
+
- a done parent whose subtree still has todo, doing or blocked nodes: `W007`
|
|
242
|
+
- a doing or done node with a `needs` target that is not done: `W008`
|
|
243
|
+
- a todo parent with a doing or done child: `I003` (suggests changing the parent to doing)
|
|
244
|
+
- **Parent status** is always written explicitly. 0.1 does not derive it, so the file never disagrees with what the UI shows.
|
|
245
|
+
|
|
246
|
+
## 7. Formatting (`rhumb fmt`)
|
|
247
|
+
|
|
248
|
+
Goal: when an agent edits one node, the diff touches only that line.
|
|
249
|
+
|
|
250
|
+
- 2 spaces per indent level, `-` as the list marker, status aliases normalized.
|
|
251
|
+
- One space between title, attributes and `^id`, with no column alignment. Alignment would rewrite every line in a sibling group whenever one sibling's title changes length.
|
|
252
|
+
- Attributes are written as `{k: v, k: v}` and keep their original key order.
|
|
253
|
+
- Missing IDs are generated.
|
|
254
|
+
- Edges lose the `^` prefix, duplicates are removed, and there is one space after each `,`.
|
|
255
|
+
- Runs of blank lines collapse to one. Comments and their positions are kept.
|
|
256
|
+
- Idempotent: `fmt(fmt(x)) == fmt(x)`.
|
|
257
|
+
|
|
258
|
+
UI write-back also goes through AST → `fmt`, never by splicing into the original text.
|
|
259
|
+
|
|
260
|
+
Implementation status (0.1 tooling): `rhumb fmt` currently only assigns missing IDs and normalizes status aliases. Programmatic changes go through the edit operations in `src/edit.ts`, which rewrite only the affected lines and reject edits that add new errors. The rest of this section needs a full CST printer and is not implemented yet.
|
|
261
|
+
|
|
262
|
+
## 8. AST (JSON)
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"rhumb": "0.1",
|
|
267
|
+
"title": "Portolan",
|
|
268
|
+
"links": {"diary": "../diary-job/{path}.md"},
|
|
269
|
+
"nodes": [
|
|
270
|
+
{
|
|
271
|
+
"id": "survey", "generated_id": false,
|
|
272
|
+
"status": "done", "title": "Prior-art survey",
|
|
273
|
+
"attrs": {}, "notes": [{"text": "[survey notes](diary:2026/1008#portolan)", "line": 9}],
|
|
274
|
+
"anchors": [{"text": "survey notes", "target": "diary:2026/1008#portolan", "line": 9}],
|
|
275
|
+
"children": [], "line": 8
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
"edges": [{"kind": "needs", "from": "dsl-parser", "to": "dsl-spec", "label": null, "line": 17}],
|
|
279
|
+
"diagnostics": [{"code": "W003", "level": "warning", "line": 9, "message": "…"}]
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`blocks` is kept as written in the AST, and the derivation layer converts it to `needs`. Comments exist only in the CST (used by `fmt`) and are not part of the AST.
|
|
284
|
+
|
|
285
|
+
## 9. Open items
|
|
286
|
+
|
|
287
|
+
- Reference-style links `[text][ref]` with definition lines are not supported in 0.1.
|
|
288
|
+
- Cross-file references `other.rhumb^id`.
|
|
289
|
+
- The sidecar format for node annotations and threads will get its own spec. The 0.1 implementation (`src/threads.ts`) is an append-only `<name>.threads.jsonl` with `open / reply / resolve / reopen / retarget` events.
|