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.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +69 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/mcp.d.ts +4 -0
- package/dist/cli/mcp.js +17 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/validate.d.ts +5 -0
- package/dist/cli/validate.js +77 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/view.d.ts +6 -0
- package/dist/cli/view.js +37 -0
- package/dist/cli/view.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +293 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/server/app.d.ts +9 -0
- package/dist/server/app.js +135 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/drift.d.ts +29 -0
- package/dist/server/drift.js +128 -0
- package/dist/server/drift.js.map +1 -0
- package/dist/server/fileOps.d.ts +13 -0
- package/dist/server/fileOps.js +56 -0
- package/dist/server/fileOps.js.map +1 -0
- package/dist/server/findLoomRoot.d.ts +9 -0
- package/dist/server/findLoomRoot.js +28 -0
- package/dist/server/findLoomRoot.js.map +1 -0
- package/dist/server/watch.d.ts +29 -0
- package/dist/server/watch.js +83 -0
- package/dist/server/watch.js.map +1 -0
- package/dist/types/diagram.d.ts +99 -0
- package/dist/types/diagram.js +7 -0
- package/dist/types/diagram.js.map +1 -0
- package/dist/types/node-types.d.ts +55 -0
- package/dist/types/node-types.js +7 -0
- package/dist/types/node-types.js.map +1 -0
- package/dist/validate.d.ts +11 -0
- package/dist/validate.js +47 -0
- package/dist/validate.js.map +1 -0
- package/dist/view/assets/index-Cst6HUW5.css +1 -0
- package/dist/view/assets/index-jlp2cU4j.js +205 -0
- package/dist/view/index.html +24 -0
- package/package.json +83 -0
- package/schema/diagram.schema.json +173 -0
- package/schema/node-types.schema.json +116 -0
- package/templates/.claude/skills/loom-spec/SKILL.md +278 -0
- package/templates/.loom/README.md +25 -0
- package/templates/.loom/diagrams/overview.flow.json +8 -0
- 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.
|