loom-spec 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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +96 -0
  5. package/dist/cli/index.js.map +1 -0
  6. package/dist/cli/init.d.ts +5 -0
  7. package/dist/cli/init.js +69 -0
  8. package/dist/cli/init.js.map +1 -0
  9. package/dist/cli/mcp.d.ts +4 -0
  10. package/dist/cli/mcp.js +17 -0
  11. package/dist/cli/mcp.js.map +1 -0
  12. package/dist/cli/validate.d.ts +5 -0
  13. package/dist/cli/validate.js +77 -0
  14. package/dist/cli/validate.js.map +1 -0
  15. package/dist/cli/view.d.ts +6 -0
  16. package/dist/cli/view.js +37 -0
  17. package/dist/cli/view.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/mcp/server.d.ts +4 -0
  22. package/dist/mcp/server.js +293 -0
  23. package/dist/mcp/server.js.map +1 -0
  24. package/dist/server/app.d.ts +9 -0
  25. package/dist/server/app.js +135 -0
  26. package/dist/server/app.js.map +1 -0
  27. package/dist/server/drift.d.ts +29 -0
  28. package/dist/server/drift.js +128 -0
  29. package/dist/server/drift.js.map +1 -0
  30. package/dist/server/fileOps.d.ts +13 -0
  31. package/dist/server/fileOps.js +56 -0
  32. package/dist/server/fileOps.js.map +1 -0
  33. package/dist/server/findLoomRoot.d.ts +9 -0
  34. package/dist/server/findLoomRoot.js +28 -0
  35. package/dist/server/findLoomRoot.js.map +1 -0
  36. package/dist/server/watch.d.ts +29 -0
  37. package/dist/server/watch.js +83 -0
  38. package/dist/server/watch.js.map +1 -0
  39. package/dist/types/diagram.d.ts +99 -0
  40. package/dist/types/diagram.js +7 -0
  41. package/dist/types/diagram.js.map +1 -0
  42. package/dist/types/node-types.d.ts +55 -0
  43. package/dist/types/node-types.js +7 -0
  44. package/dist/types/node-types.js.map +1 -0
  45. package/dist/validate.d.ts +11 -0
  46. package/dist/validate.js +47 -0
  47. package/dist/validate.js.map +1 -0
  48. package/dist/view/assets/index-Cst6HUW5.css +1 -0
  49. package/dist/view/assets/index-jlp2cU4j.js +205 -0
  50. package/dist/view/index.html +24 -0
  51. package/package.json +83 -0
  52. package/schema/diagram.schema.json +173 -0
  53. package/schema/node-types.schema.json +116 -0
  54. package/templates/.claude/skills/loom-spec/SKILL.md +278 -0
  55. package/templates/.loom/README.md +25 -0
  56. package/templates/.loom/diagrams/overview.flow.json +8 -0
  57. package/templates/.loom/node-types.json +56 -0
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>loom-spec</title>
7
+ <script>
8
+ // Apply persisted theme before paint to avoid flash.
9
+ try {
10
+ const t = localStorage.getItem("loom-theme");
11
+ if (t === "light" || t === "dark") {
12
+ document.documentElement.setAttribute("data-theme", t);
13
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
14
+ document.documentElement.setAttribute("data-theme", "dark");
15
+ }
16
+ } catch {}
17
+ </script>
18
+ <script type="module" crossorigin src="/assets/index-jlp2cU4j.js"></script>
19
+ <link rel="stylesheet" crossorigin href="/assets/index-Cst6HUW5.css">
20
+ </head>
21
+ <body>
22
+ <div id="root"></div>
23
+ </body>
24
+ </html>
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "loom-spec",
3
+ "version": "0.1.0",
4
+ "description": "Node-based architecture spec that lives in your repo. AI-readable, AI-writable, git-diffable.",
5
+ "type": "module",
6
+ "author": "René Jesser",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/renejes/loom-spec#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/renejes/loom-spec.git",
12
+ "directory": "packages/loom-spec"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/renejes/loom-spec/issues"
16
+ },
17
+ "keywords": [
18
+ "architecture",
19
+ "architecture-as-code",
20
+ "diagram",
21
+ "node-based",
22
+ "spec",
23
+ "specification",
24
+ "visual-programming",
25
+ "flow-chart",
26
+ "ai",
27
+ "ai-agent",
28
+ "claude",
29
+ "agent-skills",
30
+ "react-flow",
31
+ "xyflow",
32
+ "developer-tools"
33
+ ],
34
+ "bin": {
35
+ "loom-spec": "./dist/cli/index.js"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "templates",
40
+ "schema",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "exports": {
45
+ ".": "./dist/index.js",
46
+ "./schema": "./schema/diagram.schema.json",
47
+ "./node-types-schema": "./schema/node-types.schema.json"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json && vite build --config src/view/vite.config.ts",
51
+ "dev": "concurrently -n server,view -c blue,magenta \"pnpm dev:server\" \"pnpm dev:view\"",
52
+ "dev:server": "tsx src/cli/index.ts view --dev --root ../../examples/todo-app --port 7778",
53
+ "dev:view": "vite --config src/view/vite.config.ts",
54
+ "init:example": "tsx src/cli/index.ts init --path /tmp/loom-init-test --force",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p src/view/tsconfig.json --noEmit",
56
+ "generate-types": "tsx scripts/generate-types.ts",
57
+ "validate-examples": "tsx scripts/validate-examples.ts"
58
+ },
59
+ "dependencies": {
60
+ "@hono/node-server": "^1.13.7",
61
+ "@modelcontextprotocol/sdk": "^1.0.4",
62
+ "@xyflow/react": "^12.3.5",
63
+ "ajv": "^8.17.1",
64
+ "ajv-formats": "^3.0.1",
65
+ "chokidar": "^4.0.1",
66
+ "hono": "^4.6.13",
67
+ "lucide-react": "^0.460.0",
68
+ "react": "^18.3.1",
69
+ "react-dom": "^18.3.1",
70
+ "zod": "^3.23.8"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^22.10.0",
74
+ "@types/react": "^18.3.12",
75
+ "@types/react-dom": "^18.3.1",
76
+ "@vitejs/plugin-react": "^4.3.4",
77
+ "concurrently": "^9.1.0",
78
+ "json-schema-to-typescript": "^15.0.3",
79
+ "tsx": "^4.19.2",
80
+ "typescript": "^5.6.0",
81
+ "vite": "^5.4.11"
82
+ }
83
+ }
@@ -0,0 +1,173 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://loom-spec.dev/schema/diagram-v1.json",
4
+ "title": "Loom Diagram",
5
+ "description": "A node-based architecture spec file.",
6
+ "type": "object",
7
+ "required": ["version", "id", "title", "nodes", "edges"],
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": "Diagram identifier. Should match the filename without extension."
16
+ },
17
+ "title": { "type": "string", "minLength": 1 },
18
+ "description": { "type": "string" },
19
+ "nodes": {
20
+ "type": "array",
21
+ "items": { "$ref": "#/$defs/Node" }
22
+ },
23
+ "edges": {
24
+ "type": "array",
25
+ "items": { "$ref": "#/$defs/Edge" }
26
+ },
27
+ "groups": {
28
+ "type": "array",
29
+ "items": { "$ref": "#/$defs/Group" },
30
+ "default": []
31
+ }
32
+ },
33
+
34
+ "$defs": {
35
+ "Node": {
36
+ "type": "object",
37
+ "required": ["id", "type", "label", "position", "status"],
38
+ "additionalProperties": false,
39
+ "properties": {
40
+ "id": {
41
+ "type": "string",
42
+ "pattern": "^[a-z0-9-]+$",
43
+ "description": "Unique within this diagram."
44
+ },
45
+ "type": {
46
+ "type": "string",
47
+ "description": "References a key in node-types.json."
48
+ },
49
+ "label": { "type": "string", "minLength": 1, "maxLength": 60 },
50
+ "description": {
51
+ "type": "string",
52
+ "description": "Markdown allowed."
53
+ },
54
+ "position": {
55
+ "type": "object",
56
+ "required": ["x", "y"],
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "x": { "type": "number" },
60
+ "y": { "type": "number" }
61
+ }
62
+ },
63
+ "status": {
64
+ "type": "string",
65
+ "enum": ["planned", "implemented", "stale", "deprecated"],
66
+ "description": "planned = spec exists, code does not. implemented = both present. stale = code refs are broken, needs review. deprecated = kept for history, no longer active."
67
+ },
68
+ "code_refs": {
69
+ "type": "array",
70
+ "items": { "$ref": "#/$defs/CodeRef" },
71
+ "default": []
72
+ },
73
+ "properties": {
74
+ "type": "object",
75
+ "description": "Custom fields defined by the node's type in node-types.json.",
76
+ "default": {}
77
+ },
78
+ "tags": {
79
+ "type": "array",
80
+ "items": { "type": "string" },
81
+ "default": []
82
+ },
83
+ "drill_down": {
84
+ "type": "string",
85
+ "pattern": "^[a-z0-9-]+$",
86
+ "description": "ID of another diagram file to navigate into when this node is opened."
87
+ }
88
+ }
89
+ },
90
+
91
+ "Edge": {
92
+ "type": "object",
93
+ "required": ["id", "from", "to", "kind"],
94
+ "additionalProperties": false,
95
+ "properties": {
96
+ "id": { "type": "string", "minLength": 1 },
97
+ "from": {
98
+ "type": "string",
99
+ "pattern": "^[a-z0-9-]+(:[a-z0-9_-]+)?$",
100
+ "description": "Source node id, optionally with :port-name suffix."
101
+ },
102
+ "to": {
103
+ "type": "string",
104
+ "pattern": "^[a-z0-9-]+(:[a-z0-9_-]+)?$",
105
+ "description": "Target node id, optionally with :port-name suffix."
106
+ },
107
+ "kind": {
108
+ "type": "string",
109
+ "enum": ["request", "event", "data-read", "data-write", "signal", "dependency", "control"]
110
+ },
111
+ "label": { "type": "string", "maxLength": 60 },
112
+ "description": { "type": "string" },
113
+ "direction": {
114
+ "type": "string",
115
+ "enum": ["forward", "bidirectional"],
116
+ "default": "forward"
117
+ }
118
+ }
119
+ },
120
+
121
+ "Group": {
122
+ "type": "object",
123
+ "required": ["id", "label"],
124
+ "additionalProperties": false,
125
+ "properties": {
126
+ "id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
127
+ "label": { "type": "string", "minLength": 1 },
128
+ "children": {
129
+ "type": "array",
130
+ "items": { "type": "string" },
131
+ "description": "Node ids contained in this group.",
132
+ "default": []
133
+ },
134
+ "subgroups": {
135
+ "type": "array",
136
+ "items": { "type": "string" },
137
+ "description": "Group ids nested inside this group.",
138
+ "default": []
139
+ },
140
+ "color": {
141
+ "type": "string",
142
+ "pattern": "^#[0-9a-fA-F]{6}$"
143
+ },
144
+ "collapsed": { "type": "boolean", "default": false },
145
+ "drill_down": {
146
+ "type": "string",
147
+ "pattern": "^[a-z0-9-]+$"
148
+ }
149
+ }
150
+ },
151
+
152
+ "CodeRef": {
153
+ "type": "object",
154
+ "required": ["path"],
155
+ "additionalProperties": false,
156
+ "properties": {
157
+ "path": {
158
+ "type": "string",
159
+ "description": "Repo-relative file path."
160
+ },
161
+ "symbol": {
162
+ "type": "string",
163
+ "description": "Function, class, or component name. Preferred over lines because it survives refactors."
164
+ },
165
+ "lines": {
166
+ "type": "string",
167
+ "pattern": "^[0-9]+(-[0-9]+)?(,[0-9]+(-[0-9]+)?)*$",
168
+ "description": "Line range(s) like '1-80' or '12,45-50'. Use only when no symbol applies."
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,116 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://loom-spec.dev/schema/node-types-v1.json",
4
+ "title": "Loom Node Types",
5
+ "description": "Defines the node type vocabulary for a project.",
6
+ "type": "object",
7
+ "required": ["types"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "types": {
12
+ "type": "object",
13
+ "additionalProperties": { "$ref": "#/$defs/NodeType" },
14
+ "propertyNames": { "pattern": "^[a-z][a-z0-9-]*$" }
15
+ }
16
+ },
17
+
18
+ "$defs": {
19
+ "NodeType": {
20
+ "type": "object",
21
+ "required": ["label", "color"],
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "label": { "type": "string", "minLength": 1 },
25
+ "description": { "type": "string" },
26
+ "color": {
27
+ "type": "string",
28
+ "pattern": "^#[0-9a-fA-F]{6}$"
29
+ },
30
+ "icon": {
31
+ "type": "string",
32
+ "description": "Lucide icon name."
33
+ },
34
+ "fields": {
35
+ "type": "array",
36
+ "items": { "$ref": "#/$defs/Field" },
37
+ "default": []
38
+ },
39
+ "ports": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "in": {
44
+ "type": "array",
45
+ "items": { "$ref": "#/$defs/Port" },
46
+ "default": []
47
+ },
48
+ "out": {
49
+ "type": "array",
50
+ "items": { "$ref": "#/$defs/Port" },
51
+ "default": []
52
+ }
53
+ }
54
+ }
55
+ }
56
+ },
57
+
58
+ "Field": {
59
+ "type": "object",
60
+ "required": ["name", "type"],
61
+ "additionalProperties": false,
62
+ "properties": {
63
+ "name": {
64
+ "type": "string",
65
+ "pattern": "^[a-z][a-z0-9_]*$"
66
+ },
67
+ "type": {
68
+ "type": "string",
69
+ "enum": ["string", "number", "boolean", "enum", "markdown", "code-ref", "array"]
70
+ },
71
+ "required": { "type": "boolean", "default": false },
72
+ "description": { "type": "string" },
73
+ "default": {},
74
+ "values": {
75
+ "type": "array",
76
+ "description": "Required when type is 'enum'."
77
+ },
78
+ "items": {
79
+ "type": "string",
80
+ "enum": ["string", "number"],
81
+ "description": "Required when type is 'array'."
82
+ },
83
+ "min": { "type": "number" },
84
+ "max": { "type": "number" },
85
+ "pattern": { "type": "string" },
86
+ "max_length": { "type": "number", "minimum": 1 }
87
+ },
88
+ "allOf": [
89
+ {
90
+ "if": { "properties": { "type": { "const": "enum" } }, "required": ["type"] },
91
+ "then": { "required": ["values"] }
92
+ },
93
+ {
94
+ "if": { "properties": { "type": { "const": "array" } }, "required": ["type"] },
95
+ "then": { "required": ["items"] }
96
+ }
97
+ ]
98
+ },
99
+
100
+ "Port": {
101
+ "type": "object",
102
+ "required": ["name"],
103
+ "additionalProperties": false,
104
+ "properties": {
105
+ "name": {
106
+ "type": "string",
107
+ "pattern": "^[a-z][a-z0-9_-]*$"
108
+ },
109
+ "signal": {
110
+ "type": "string",
111
+ "description": "Free-form signal type tag, e.g. 'audio', 'midi', 'http', 'control'."
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,278 @@
1
+ ---
2
+ name: loom-spec
3
+ description: |
4
+ Use when modifying application architecture, components, services, data flow,
5
+ events, or any code with corresponding nodes in .loom/diagrams/. Reads and
6
+ updates the visual architecture spec, keeps code_refs accurate, and flags
7
+ stale nodes. Trigger on: new features, refactors, file moves, module
8
+ deletions, or when the user references "the diagram", "the architecture",
9
+ or "the spec".
10
+ ---
11
+
12
+ # loom-spec — Architecture Spec Maintenance
13
+
14
+ This project keeps a node-based architecture spec in `.loom/`. Treat it as
15
+ source-of-truth documentation that must stay in sync with code.
16
+
17
+ ## Quick-start workflow
18
+
19
+ For any task that touches structure:
20
+
21
+ 1. **Read first.** Find the relevant diagram(s) before editing code.
22
+ 2. **Plan in the spec.** Add planned nodes/edges for what you're about to build.
23
+ 3. **Implement the code.**
24
+ 4. **Update the spec.** Flip status to `implemented`, set accurate `code_refs`.
25
+ 5. **Validate.** Run `loom_validate` (MCP) or `loom-spec validate` (CLI) to
26
+ confirm no drift.
27
+
28
+ ## Rules
29
+
30
+ ### Before implementing a feature
31
+
32
+ - List `.loom/diagrams/` and read the relevant file(s).
33
+ - Check existing node IDs to avoid collisions within a diagram.
34
+ - Confirm available node types in `.loom/node-types.json`. If you need a type
35
+ that doesn't exist, add it there first (with a sensible color and icon).
36
+
37
+ ### When adding code
38
+
39
+ - New component / service / store → add a node with `status: "planned"` first
40
+ while scaffolding, then flip to `"implemented"` once it works.
41
+ - Always set `code_refs` to actual files. **Prefer `symbol` over `lines`** —
42
+ symbols survive refactors; line numbers do not.
43
+ - Use only types defined in `.loom/node-types.json`.
44
+
45
+ ### When editing code
46
+
47
+ - If you touch a file referenced by a node, verify the `symbol` still exists
48
+ and the `path` is still accurate. Update if not.
49
+ - If you rename or move a file, update every `code_refs` pointing at it.
50
+
51
+ ### When deleting code
52
+
53
+ - Don't delete the node. Set `status: "stale"`. Humans review staleness —
54
+ silent deletion loses architectural history.
55
+
56
+ ### When the user describes a new subsystem
57
+
58
+ - If it's clearly its own area (auth, billing, ingestion), create a new
59
+ `<name>.flow.json` instead of cramming it into an existing diagram.
60
+ - Link from the overview with a `drill_down` reference if appropriate.
61
+
62
+ ## Preferred tools (when the MCP server is wired up)
63
+
64
+ If a `loom-spec` MCP server is registered with the host (e.g. via
65
+ `.mcp.json`), prefer its tools over raw JSON edits:
66
+
67
+ - `loom_list_diagrams`, `loom_read_diagram`, `loom_read_node_types`
68
+ - `loom_add_node`, `loom_update_node`, `loom_mark_stale`, `loom_delete_node`
69
+ - `loom_add_edge`, `loom_delete_edge`
70
+ - `loom_validate` (schema + code-ref drift across every diagram)
71
+
72
+ They validate against the schema before writing, so invalid edits fail fast
73
+ instead of corrupting the file. They're also more token-efficient than
74
+ re-reading + re-writing JSON on every mutation.
75
+
76
+ If the MCP server is not available, edit the JSON files directly using the
77
+ rules above — the format is stable and tools-agnostic by design.
78
+
79
+ ---
80
+
81
+ ## Examples
82
+
83
+ ### 1. User asks for a new feature
84
+
85
+ > User: "Add a payments service. The checkout flow calls it to charge the card."
86
+
87
+ ```
88
+ # Step 1: Inspect the current state
89
+ loom_read_diagram("overview")
90
+
91
+ # Step 2: Add the new service as planned
92
+ loom_add_node({
93
+ diagram: "overview",
94
+ type: "service",
95
+ label: "Payments",
96
+ description: "Stripe wrapper. Handles charges, refunds, and the webhook.",
97
+ status: "planned",
98
+ code_refs: [{ path: "src/server/payments.ts" }],
99
+ properties: { language: "typescript", runtime: "node" }
100
+ })
101
+ # → { ok: true, id: "service-2" }
102
+
103
+ # Step 3: Connect it
104
+ loom_add_edge({
105
+ diagram: "overview",
106
+ from: "checkout-flow",
107
+ to: "service-2",
108
+ kind: "request",
109
+ label: "charge card"
110
+ })
111
+
112
+ # Step 4: Write the code (using your normal Write/Edit tools).
113
+
114
+ # Step 5: Update the node to reflect reality
115
+ loom_update_node({
116
+ diagram: "overview",
117
+ id: "service-2",
118
+ patch: {
119
+ status: "implemented",
120
+ code_refs: [
121
+ { path: "src/server/payments.ts", symbol: "createCharge" },
122
+ { path: "src/server/payments.ts", symbol: "handleWebhook" }
123
+ ]
124
+ }
125
+ })
126
+ ```
127
+
128
+ ### 2. User describes a multi-step agent (e.g. LangGraph)
129
+
130
+ > User: "agent.py has a LangGraph with three steps: decide_step, call_tool, format_response."
131
+
132
+ Two valid shapes, choose by **how much the flow between steps matters**:
133
+
134
+ **A) Tightly coupled, internal detail — one node with many refs:**
135
+
136
+ ```
137
+ loom_add_node({
138
+ diagram: "overview",
139
+ type: "service",
140
+ label: "Agent",
141
+ description: "LangGraph agent. Steps inside agent.py.",
142
+ status: "implemented",
143
+ code_refs: [
144
+ { path: "agent.py", symbol: "decide_step" },
145
+ { path: "agent.py", symbol: "call_tool" },
146
+ { path: "agent.py", symbol: "format_response" }
147
+ ]
148
+ })
149
+ ```
150
+
151
+ **B) Step flow itself is the architecture — drill down to a sub-diagram:**
152
+
153
+ ```
154
+ # Top-level: one node, drill_down to detail
155
+ loom_add_node({
156
+ diagram: "overview",
157
+ type: "service",
158
+ label: "Agent",
159
+ status: "implemented",
160
+ code_refs: [{ path: "agent.py" }],
161
+ drill_down: "agent-internals"
162
+ })
163
+
164
+ # Create the sub-diagram via the file system (no dedicated MCP tool):
165
+ # Write .loom/diagrams/agent-internals.flow.json
166
+ {
167
+ "version": "1",
168
+ "id": "agent-internals",
169
+ "title": "Agent — internal steps",
170
+ "nodes": [
171
+ { "id": "decide", "type": "service", "label": "decide_step",
172
+ "position": { "x": 80, "y": 100 }, "status": "implemented",
173
+ "code_refs": [{ "path": "agent.py", "symbol": "decide_step" }] },
174
+ { "id": "call", "type": "service", "label": "call_tool",
175
+ "position": { "x": 380, "y": 100 }, "status": "implemented",
176
+ "code_refs": [{ "path": "agent.py", "symbol": "call_tool" }] },
177
+ { "id": "format", "type": "service", "label": "format_response",
178
+ "position": { "x": 680, "y": 100 }, "status": "implemented",
179
+ "code_refs": [{ "path": "agent.py", "symbol": "format_response" }] }
180
+ ],
181
+ "edges": [
182
+ { "id": "e1", "from": "decide", "to": "call",
183
+ "kind": "control", "label": "if tool needed" },
184
+ { "id": "e2", "from": "decide", "to": "format",
185
+ "kind": "control", "label": "if final answer" },
186
+ { "id": "e3", "from": "call", "to": "format",
187
+ "kind": "control", "label": "after tool" }
188
+ ]
189
+ }
190
+ ```
191
+
192
+ **B is usually right for LangGraph** because the structure between steps *is*
193
+ the logic — the diagram makes routing errors visible at a glance.
194
+
195
+ ### 3. User renames or moves a function
196
+
197
+ > User refactored `validate_email` → `validateEmail` and moved it to `lib/validation.ts`.
198
+
199
+ ```
200
+ # Step 1: Find the drift
201
+ loom_validate()
202
+ # → reports nodes whose code_refs no longer resolve
203
+
204
+ # Step 2: For each affected node, update the ref
205
+ loom_update_node({
206
+ diagram: "overview",
207
+ id: "auth-form",
208
+ patch: {
209
+ code_refs: [{ path: "lib/validation.ts", symbol: "validateEmail" }]
210
+ }
211
+ })
212
+
213
+ # Step 3: Re-validate
214
+ loom_validate()
215
+ # → clean
216
+ ```
217
+
218
+ ### 4. User deletes a chunk of code
219
+
220
+ > User: "I removed the legacy /v1 API."
221
+
222
+ ```
223
+ loom_read_diagram("overview")
224
+ # → identify nodes whose code is gone
225
+
226
+ # Mark them stale instead of deleting:
227
+ loom_mark_stale({ diagram: "overview", id: "api-v1-router" })
228
+ loom_mark_stale({ diagram: "overview", id: "api-v1-auth" })
229
+ ```
230
+
231
+ The user will review and decide whether to truly remove, archive, or
232
+ re-purpose those nodes.
233
+
234
+ ### 5. The user wants a new domain
235
+
236
+ > User: "Build out billing — invoices, subscriptions, dunning."
237
+
238
+ Don't pile this into `overview.flow.json`. Create a dedicated diagram:
239
+
240
+ ```
241
+ # Write .loom/diagrams/billing.flow.json with the planned nodes/edges.
242
+
243
+ # Then, in overview, add a single placeholder node that drills into billing:
244
+ loom_add_node({
245
+ diagram: "overview",
246
+ type: "service",
247
+ label: "Billing",
248
+ status: "planned",
249
+ drill_down: "billing"
250
+ })
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Format reference
256
+
257
+ - Status enum: `planned`, `implemented`, `stale`, `deprecated`.
258
+ - Edge kinds: `request`, `event`, `data-read`, `data-write`, `signal`,
259
+ `dependency`, `control`.
260
+ - A node's `id` must match `^[a-z0-9-]+$`.
261
+ - Use `from`/`to` like `node-id:port-name` only when the node's type declares
262
+ ports in `node-types.json`.
263
+
264
+ ## Don't
265
+
266
+ - Don't create new top-level diagrams without checking if one already covers
267
+ the area.
268
+ - Don't move node `position` coordinates unless explicitly asked — the user
269
+ arranges the canvas.
270
+ - Don't invent node types — extend `node-types.json` first.
271
+ - Don't write invalid JSON. The validator (server-side or `loom_validate`)
272
+ will refuse it.
273
+ - Don't add a node for every function. A node is a **concept**; multiple
274
+ functions per node is normal (use `code_refs[]`).
275
+ - Don't create a diagram per directory. Create one per **subsystem** or
276
+ **flow**.
277
+ - Don't leave `drill_down` pointing at a non-existent diagram id.
278
+ - Don't `loom_delete_node` for code that was just removed — `mark_stale` it.