loom-spec 0.1.1 → 0.2.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.
@@ -15,8 +15,8 @@
15
15
  }
16
16
  } catch {}
17
17
  </script>
18
- <script type="module" crossorigin src="/assets/index-Cdrw4Ya7.js"></script>
19
- <link rel="stylesheet" crossorigin href="/assets/index-Cst6HUW5.css">
18
+ <script type="module" crossorigin src="/assets/index-DAM9J2qS.js"></script>
19
+ <link rel="stylesheet" crossorigin href="/assets/index-B18EbiQt.css">
20
20
  </head>
21
21
  <body>
22
22
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loom-spec",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Node-based architecture spec that lives in your repo. AI-readable, AI-writable, git-diffable.",
5
5
  "type": "module",
6
6
  "author": "René Jesser",
@@ -0,0 +1,135 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://loom-spec.dev/schema/timeline-v1.json",
4
+ "title": "Loom Timeline",
5
+ "description": "A time-axis overlay describing when events happen on the nodes of a diagram. Same node universe, different render.",
6
+ "type": "object",
7
+ "required": ["version", "id", "title", "diagram", "events"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "version": { "const": "1" },
12
+ "id": {
13
+ "type": "string",
14
+ "pattern": "^[a-z0-9-]+$",
15
+ "description": "Timeline identifier. Should match the filename without extension."
16
+ },
17
+ "title": { "type": "string", "minLength": 1 },
18
+ "description": { "type": "string" },
19
+ "diagram": {
20
+ "type": "string",
21
+ "pattern": "^[a-z0-9-]+$",
22
+ "description": "Id of the diagram whose nodes this timeline overlays."
23
+ },
24
+ "events": {
25
+ "type": "array",
26
+ "items": { "$ref": "#/$defs/TimelineEvent" }
27
+ },
28
+ "tracks": {
29
+ "type": "array",
30
+ "items": { "$ref": "#/$defs/TimelineTrack" },
31
+ "description": "Optional explicit track definitions (label, color). If omitted, tracks are inferred from events[].track values."
32
+ }
33
+ },
34
+
35
+ "$defs": {
36
+ "TimelineEvent": {
37
+ "type": "object",
38
+ "required": ["id", "node", "start_ms", "duration_ms"],
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "id": {
42
+ "type": "string",
43
+ "pattern": "^[a-z0-9-]+$",
44
+ "description": "Unique within this timeline."
45
+ },
46
+ "node": {
47
+ "type": "string",
48
+ "pattern": "^[a-z0-9-]+$",
49
+ "description": "Node id from the referenced diagram."
50
+ },
51
+ "track": {
52
+ "type": "string",
53
+ "description": "Track label. Events on the same track are interpreted as sequential; events on different tracks at overlapping times are concurrent. If omitted, the renderer auto-assigns one track per node."
54
+ },
55
+ "start_ms": {
56
+ "type": "number",
57
+ "minimum": 0,
58
+ "description": "Start time in milliseconds from t=0."
59
+ },
60
+ "duration_ms": {
61
+ "type": "number",
62
+ "minimum": 0,
63
+ "description": "Duration in milliseconds. May be 0 for instantaneous events."
64
+ },
65
+ "label": {
66
+ "type": "string",
67
+ "maxLength": 60,
68
+ "description": "Short text rendered inside the clip."
69
+ },
70
+ "description": { "type": "string" },
71
+ "kind": {
72
+ "type": "string",
73
+ "enum": ["compute", "io", "wait", "error"],
74
+ "description": "Optional category for clip styling. Distinct from edge.kind in the diagram — this describes what the node is doing during this clip, not what flows between nodes."
75
+ },
76
+ "code_refs": {
77
+ "type": "array",
78
+ "items": { "$ref": "#/$defs/CodeRef" },
79
+ "description": "Optional anchors to the specific function(s) running during this clip. Like node-level code_refs but scoped to a moment in time. Drift detection checks these too. Use for function-level granularity inside a node (e.g. 'validate' vs. 'issue jwt' both happen on the auth-service node)."
80
+ },
81
+ "triggered_by": {
82
+ "type": "string",
83
+ "pattern": "^[a-z0-9-]+$",
84
+ "description": "Optional id of another event that caused this one. Used for explicit causation chains and to preserve OTel span.parent_id when importing traces. The renderer can draw a connecting arrow between the two clips."
85
+ },
86
+ "tags": {
87
+ "type": "array",
88
+ "items": { "type": "string" },
89
+ "description": "Free-form labels for filtering or grouping clips. Examples: 'critical-path', 'billable-time', 'background'."
90
+ }
91
+ }
92
+ },
93
+
94
+ "CodeRef": {
95
+ "type": "object",
96
+ "required": ["path"],
97
+ "additionalProperties": false,
98
+ "description": "Same shape as the CodeRef in diagram.schema.json. Inlined here to keep the schema self-contained — no cross-file $ref required to validate.",
99
+ "properties": {
100
+ "path": {
101
+ "type": "string",
102
+ "description": "Repo-relative file path."
103
+ },
104
+ "symbol": {
105
+ "type": "string",
106
+ "description": "Function, class, or component name. Preferred over lines because it survives refactors."
107
+ },
108
+ "lines": {
109
+ "type": "string",
110
+ "pattern": "^[0-9]+(-[0-9]+)?(,[0-9]+(-[0-9]+)?)*$",
111
+ "description": "Line range(s) like '1-80' or '12,45-50'. Use only when no symbol applies."
112
+ }
113
+ }
114
+ },
115
+
116
+ "TimelineTrack": {
117
+ "type": "object",
118
+ "required": ["id", "label"],
119
+ "additionalProperties": false,
120
+ "properties": {
121
+ "id": {
122
+ "type": "string",
123
+ "pattern": "^[a-z0-9-]+$",
124
+ "description": "Track id, matches event.track values."
125
+ },
126
+ "label": { "type": "string", "minLength": 1 },
127
+ "color": {
128
+ "type": "string",
129
+ "pattern": "^#[0-9a-fA-F]{6}$",
130
+ "description": "Optional background tint for the track lane."
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
@@ -67,6 +67,8 @@ If a `loom-spec` MCP server is registered with the host (e.g. via
67
67
  - `loom_list_diagrams`, `loom_read_diagram`, `loom_read_node_types`
68
68
  - `loom_add_node`, `loom_update_node`, `loom_mark_stale`, `loom_delete_node`
69
69
  - `loom_add_edge`, `loom_delete_edge`
70
+ - `loom_list_timelines`, `loom_read_timeline`
71
+ - `loom_add_event`, `loom_update_event`, `loom_delete_event`
70
72
  - `loom_validate` (schema + code-ref drift across every diagram)
71
73
 
72
74
  They validate against the schema before writing, so invalid edits fail fast
@@ -250,6 +252,92 @@ loom_add_node({
250
252
  })
251
253
  ```
252
254
 
255
+ ### 6. User wants to document a sequence as a timeline
256
+
257
+ > User: "Document the checkout flow on a timeline so I can see the latency
258
+ > breakdown."
259
+
260
+ A timeline is a horizontal time-axis overlay on a specific diagram. Each
261
+ event references a node in that diagram and a `[start_ms, duration_ms]`
262
+ interval. The view plays the events back like a DAW edit, lighting up the
263
+ referenced nodes in a mini graph beside the timeline.
264
+
265
+ ```
266
+ # Step 1: Pick the diagram this sequence belongs to.
267
+ loom_list_diagrams()
268
+ loom_read_diagram("overview")
269
+
270
+ # Step 2: Create the timeline file. There's no MCP tool for creating one
271
+ # from scratch — write the empty shell directly:
272
+ #
273
+ # .loom/timelines/checkout.timeline.json
274
+ {
275
+ "version": "1",
276
+ "id": "checkout",
277
+ "title": "Checkout — happy path",
278
+ "description": "Click 'pay' → Stripe charge → confirmation render.",
279
+ "diagram": "overview",
280
+ "tracks": [
281
+ { "id": "client", "label": "Browser", "color": "#dbeafe" },
282
+ { "id": "server", "label": "API", "color": "#dcfce7" },
283
+ { "id": "data", "label": "Postgres", "color": "#ede9fe" }
284
+ ],
285
+ "events": []
286
+ }
287
+
288
+ # Step 3: Append events with the MCP tool — it validates that each `node`
289
+ # actually exists in the referenced diagram, so typos fail fast.
290
+ loom_add_event({
291
+ timeline: "checkout",
292
+ node: "checkout-page",
293
+ track: "client",
294
+ start_ms: 0,
295
+ duration_ms: 8,
296
+ kind: "compute",
297
+ label: "click pay",
298
+ code_refs: [{ path: "src/views/Checkout.tsx", symbol: "handlePay" }],
299
+ tags: ["critical-path", "user-input"]
300
+ })
301
+ # → { ok: true, id: "ev1" }
302
+
303
+ loom_add_event({
304
+ timeline: "checkout",
305
+ node: "payments-api",
306
+ track: "server",
307
+ start_ms: 12,
308
+ duration_ms: 320,
309
+ kind: "io",
310
+ label: "POST /checkout (Stripe)",
311
+ triggered_by: "ev1", # explicit causation arrow
312
+ tags: ["critical-path", "external-io"]
313
+ })
314
+ # → { ok: true, id: "ev2" }
315
+
316
+ # Step 4: Adjust if you got something wrong.
317
+ loom_update_event({
318
+ timeline: "checkout",
319
+ id: "ev2",
320
+ patch: { duration_ms: 280 }
321
+ })
322
+
323
+ # Step 5: Validate.
324
+ loom_validate()
325
+ ```
326
+
327
+ **When to reach for a timeline rather than extra diagram detail:**
328
+
329
+ - The *order* and *latency* matter (perf review, race conditions, end-to-end
330
+ flow). The diagram alone shows topology, not sequencing.
331
+ - You want one node referenced multiple times because it does different
332
+ things at different moments (e.g. an auth-service that validates *then
333
+ later* re-issues a JWT). Use `code_refs` on each event for function-level
334
+ granularity inside the same node.
335
+ - The user describes a *trace*, a *user journey*, or a *failure case* that
336
+ has a clock.
337
+
338
+ **Don't create a timeline for static structure** — that's what diagrams are
339
+ for. A timeline of "the app boots, then runs forever" adds noise.
340
+
253
341
  ---
254
342
 
255
343
  ## Format reference