loom-spec 0.1.0 → 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.
package/README.md CHANGED
@@ -2,34 +2,72 @@
2
2
 
3
3
  > A node-based architecture spec that lives in your repo. AI-readable, AI-writable, git-diffable.
4
4
 
5
- `loom-spec` keeps a structured visual spec of your application's architecture **inside your repo**, designed to be edited by both humans (in a browser-based node editor) and AI coding agents (directly via JSON files).
5
+ `loom-spec` keeps a structured visual spec of your application's architecture **inside your repo**, designed to be edited by both humans (in a browser-based node editor) and AI coding agents (directly via JSON files or MCP tool calls).
6
6
 
7
- It's a spec layer, not an execution layer. The nodes don't run — they describe.
7
+ It's a spec layer, not an execution layer. The nodes describe; they don't run.
8
8
 
9
- ## Install
9
+ ## Why
10
+
11
+ - **Architecture drift.** Mermaid diagrams in `docs/` go stale the moment you refactor.
12
+ - **Agents losing the forest for the trees.** An agent grepping through `src/` doesn't see the system. Every session rebuilds the mental model from scratch.
13
+ - **The mental-model gap.** People who think in signal flow but don't read code well get cut out.
14
+
15
+ `loom-spec` is one canonical, machine-readable file-set under `.loom/` that says what exists and how it connects. Humans edit it visually. The agent reads it before implementing and updates it when code changes. `code_refs` anchor each node to actual source, so [`loom-spec validate`](#loom-spec-validate---root-dir---json) catches drift instead of letting it accumulate.
16
+
17
+ ## Install + first run
10
18
 
11
19
  ```bash
12
- # No global install needed — use via npx
13
- npx loom-spec init
14
- npx loom-spec view
20
+ cd your-project
21
+ npx loom-spec init --mcp # scaffolds .loom/, the agent skill, and .mcp.json
22
+ npx loom-spec view # opens the editor on http://localhost:7777
15
23
  ```
16
24
 
17
- Or as a dev dependency:
25
+ `--mcp` is optional but recommended — it auto-registers the MCP server for Claude Code (and other MCP-capable agents). Skip it if you want to wire that up manually later via `npx loom-spec install-mcp`.
26
+
27
+ If you prefer it as a dev dependency:
18
28
 
19
29
  ```bash
20
30
  npm install --save-dev loom-spec
21
31
  ```
22
32
 
23
- Then in `package.json`:
24
-
25
33
  ```json
26
34
  {
27
35
  "scripts": {
28
- "loom": "loom-spec view"
36
+ "loom": "loom-spec view",
37
+ "loom:check": "loom-spec validate"
29
38
  }
30
39
  }
31
40
  ```
32
41
 
42
+ ## A typical workflow
43
+
44
+ 1. **Sketch the high-level architecture once.** Open `loom-spec view`, click **+ Add** to drop services, data stores, and UI components onto the canvas. Connect them with edges. Mark nodes as `planned` if you haven't built them yet, `implemented` once the code exists.
45
+ 2. **Let the agent grow it.** With Claude Code (or any MCP-capable agent) in the same repo, tell it what to build: *"add a payments service that the checkout flow calls"*. The agent calls `loom_add_node` and `loom_add_edge`, then writes the actual code. Your open editor updates live via SSE.
46
+ 3. **Anchor nodes to code.** When a feature is done, the agent (or you) adds `code_refs`: `{ "path": "src/server/payments.ts", "symbol": "chargeCard" }`. This is what makes drift detection work.
47
+ 4. **Catch drift in CI.** Add `loom-spec validate` as a pre-commit hook or CI step. It exits non-zero if any `code_refs` point at missing files or unresolved symbols.
48
+ 5. **Stale, don't delete.** When the underlying code goes away, the agent marks the node `status: stale` instead of deleting it. Humans review staleness — the architectural history stays.
49
+
50
+ ## What lives in your repo
51
+
52
+ ```
53
+ .loom/
54
+ ├── README.md Why this directory exists; for humans.
55
+ ├── node-types.json The vocabulary: ui, service, data, event, external (plus your customs).
56
+ └── diagrams/
57
+ └── overview.flow.json { nodes, edges, groups }
58
+
59
+ .claude/
60
+ └── skills/
61
+ └── loom-spec/
62
+ └── SKILL.md Tells Claude Code (or any Agent-Skills-aware tool)
63
+ when and how to maintain the spec, with five
64
+ worked examples.
65
+
66
+ .mcp.json Registers the MCP server, if you ran init --mcp.
67
+ ```
68
+
69
+ Nothing of the `loom-spec` package itself is committed — the npm install lives in `node_modules/`. Only the spec and the skill are tracked.
70
+
33
71
  ## Commands
34
72
 
35
73
  ### `loom-spec init [--path <dir>] [--force]`
@@ -45,10 +83,24 @@ Writes:
45
83
 
46
84
  Refuses to overwrite an existing `.loom/` unless `--force`.
47
85
 
86
+ Add `--mcp` to also register the MCP server in `.mcp.json` (idempotent merge — existing entries are preserved). Or run `npx loom-spec install-mcp` to register it after the fact.
87
+
48
88
  ### `loom-spec view [--root <dir>] [--port <n>]`
49
89
 
50
90
  Starts a local browser editor. Walks up from `--root` (default: cwd) to find the nearest `.loom/`. Opens on port 7777 by default.
51
91
 
92
+ In the editor you can:
93
+
94
+ - Drag nodes; edits debounce and write to disk within ~500ms
95
+ - Click a node or edge to inspect and edit fields, code refs, tags, type-specific properties
96
+ - Drag from a node's right handle to another node to create an edge
97
+ - Use the **+ Add** menu in the top bar to add a new node by type
98
+ - Use the diagram switcher (top-left dropdown) to navigate between diagrams or create new ones
99
+ - Use the "Drill into" chevron on any node or group with `drill_down` set to jump to a sub-diagram
100
+ - Toggle light/dark theme; preference is persisted
101
+
102
+ External edits to the JSON files (e.g. by an AI agent) propagate to the open UI live via Server-Sent Events — no reload needed.
103
+
52
104
  ### `loom-spec validate [--root <dir>] [--json]`
53
105
 
54
106
  Checks every diagram for schema validity plus **code-ref drift**: missing files, missing symbols, out-of-range line ranges. Skips nodes marked `planned` or `deprecated` (their code may legitimately not exist). Exit code is non-zero if any issue is found — useful as a CI step or pre-commit hook.
@@ -69,12 +121,14 @@ Starts a **Model Context Protocol** server on stdio. Wire it into Claude Code (o
69
121
  "mcpServers": {
70
122
  "loom-spec": {
71
123
  "command": "npx",
72
- "args": ["loom-spec", "mcp"]
124
+ "args": ["-y", "loom-spec", "mcp"]
73
125
  }
74
126
  }
75
127
  }
76
128
  ```
77
129
 
130
+ If you'd rather not hand-edit, `npx loom-spec install-mcp` writes this entry into `.mcp.json` for you (merging with any existing servers, idempotent).
131
+
78
132
  The server exposes semantic tools that validate against the schema before writing, more token-efficient than re-reading and re-writing the JSON on every change:
79
133
 
80
134
  - `loom_list_diagrams`, `loom_read_diagram`, `loom_read_node_types`
@@ -82,17 +136,9 @@ The server exposes semantic tools that validate against the schema before writin
82
136
  - `loom_add_edge`, `loom_delete_edge`
83
137
  - `loom_validate` (same drift check as the CLI)
84
138
 
85
- In the editor you can:
86
-
87
- - Drag nodes; edits debounce and write to disk within ~500ms
88
- - Click a node or edge to inspect and edit fields, code refs, tags, type-specific properties
89
- - Drag from a node's right handle to another node to create an edge
90
- - Use the "+ Add" menu in the top bar to add a new node by type
91
- - Use the diagram switcher (top-left dropdown) to navigate between diagrams or create new ones
92
- - Use the "Drill into" chevron on any node or group with `drill_down` set to jump to a sub-diagram
93
- - Toggle light/dark theme; preference is persisted
139
+ ### `loom-spec install-mcp [--path <dir>]`
94
140
 
95
- External edits to the JSON files (e.g. by an AI agent) propagate to the open UI live via Server-Sent Events no reload needed.
141
+ Writes the MCP-server entry into `.mcp.json` without touching anything else. Idempotent (safe to run repeatedly) and non-destructive (other MCP servers and unrelated top-level keys are preserved).
96
142
 
97
143
  ## File format
98
144
 
@@ -153,6 +199,68 @@ Edge kinds: `request`, `event`, `data-read`, `data-write`, `signal`, `dependency
153
199
 
154
200
  Full JSON Schemas ship with the package — see `schema/diagram.schema.json` and `schema/node-types.schema.json`.
155
201
 
202
+ ### Adding custom node types
203
+
204
+ `node-types.json` is yours to edit. Add a type for whatever domain you're modelling. A worker with typed ports:
205
+
206
+ ```json
207
+ {
208
+ "types": {
209
+ "worker": {
210
+ "label": "Worker",
211
+ "color": "#fb923c",
212
+ "icon": "server",
213
+ "fields": [
214
+ { "name": "queue", "type": "string", "required": true },
215
+ { "name": "concurrency", "type": "number" }
216
+ ],
217
+ "ports": {
218
+ "in": [{ "name": "jobs", "signal": "data" }],
219
+ "out": [{ "name": "results", "signal": "data" }, { "name": "errors", "signal": "data" }]
220
+ }
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ Once that's saved, the **+ Add** menu shows "Worker"; new worker nodes render with three labeled handles instead of one generic one. Edges can target a specific port via `from: "worker-1:results"`.
227
+
228
+ ### Drill-down between diagrams
229
+
230
+ For LangGraph-style multi-step agents or anything with non-trivial internal flow: one node in the overview, plus a sub-diagram with the steps inside.
231
+
232
+ ```json
233
+ // overview.flow.json
234
+ {
235
+ "id": "agent",
236
+ "type": "service",
237
+ "label": "Agent",
238
+ "drill_down": "agent-internals",
239
+ "code_refs": [{ "path": "agent.py" }]
240
+ }
241
+ ```
242
+
243
+ ```json
244
+ // agent-internals.flow.json
245
+ {
246
+ "nodes": [
247
+ { "id": "decide", "type": "service", "label": "decide_next_step",
248
+ "code_refs": [{ "path": "agent.py", "symbol": "decide_next_step" }] },
249
+ { "id": "call-tool", "type": "service", "label": "call_tool",
250
+ "code_refs": [{ "path": "agent.py", "symbol": "call_tool" }] },
251
+ { "id": "format", "type": "service", "label": "format_response",
252
+ "code_refs": [{ "path": "agent.py", "symbol": "format_response" }] }
253
+ ],
254
+ "edges": [
255
+ { "id": "e1", "from": "decide", "to": "call-tool", "kind": "control", "label": "if tool needed" },
256
+ { "id": "e2", "from": "call-tool", "to": "format", "kind": "control" },
257
+ { "id": "e3", "from": "decide", "to": "format", "kind": "control", "label": "if final" }
258
+ ]
259
+ }
260
+ ```
261
+
262
+ Click the chevron on the overview's `agent` node to navigate in.
263
+
156
264
  ## How AI agents use it
157
265
 
158
266
  `loom-spec init` writes a `SKILL.md` to `.claude/skills/loom-spec/` following the [Agent Skills open standard](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). Claude Code (and other tools that adopt the convention) auto-discovers it.
package/dist/cli/index.js CHANGED
@@ -3,12 +3,18 @@ import { runInit } from "./init.js";
3
3
  import { runView } from "./view.js";
4
4
  import { runValidate } from "./validate.js";
5
5
  import { runMcp } from "./mcp.js";
6
+ import { runInstallMcp } from "./installMcp.js";
6
7
  const HELP = `loom-spec — node-based architecture spec for your repo
7
8
 
8
9
  Usage:
9
- loom-spec init [--path <dir>] [--force]
10
+ loom-spec init [--path <dir>] [--force] [--mcp]
10
11
  Scaffold .loom/ and .claude/skills/loom-spec/ in the target directory.
11
- Defaults to current working directory.
12
+ With --mcp, also register the MCP server in .mcp.json (merging
13
+ with any existing entries). Defaults to current working directory.
14
+
15
+ loom-spec install-mcp [--path <dir>]
16
+ Register the loom-spec MCP server in .mcp.json without touching
17
+ anything else. Idempotent — safe to run multiple times.
12
18
 
13
19
  loom-spec view [--root <dir>] [--port <n>] [--dev]
14
20
  Start the local browser editor. Walks up from --root (default: cwd)
@@ -61,6 +67,13 @@ async function main() {
61
67
  await runInit({
62
68
  path: flags.path ?? process.cwd(),
63
69
  force: Boolean(flags.force),
70
+ mcp: Boolean(flags.mcp),
71
+ });
72
+ return;
73
+ }
74
+ if (subcommand === "install-mcp") {
75
+ await runInstallMcp({
76
+ path: flags.path ?? process.cwd(),
64
77
  });
65
78
  return;
66
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  export interface InitArgs {
2
2
  path: string;
3
3
  force: boolean;
4
+ mcp: boolean;
4
5
  }
5
6
  export declare function runInit(args: InitArgs): Promise<void>;
package/dist/cli/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, writeFile, readdir, stat } from "node:fs/promises";
2
2
  import { resolve, dirname, join, relative } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { installMcpEntry } from "./mcpConfig.js";
4
5
  const here = dirname(fileURLToPath(import.meta.url));
5
6
  // Templates ship alongside dist/ in the package: dist/cli/init.js → ../../templates/
6
7
  const templatesRoot = resolve(here, "../../templates");
@@ -60,10 +61,30 @@ export async function runInit(args) {
60
61
  for (const f of written) {
61
62
  console.log(` ${relative(target, f)}`);
62
63
  }
64
+ if (args.mcp) {
65
+ console.log();
66
+ try {
67
+ const report = await installMcpEntry(target);
68
+ const rel = relative(target, report.path);
69
+ if (report.action === "unchanged") {
70
+ console.log(`MCP entry already present in ${rel}.`);
71
+ }
72
+ else {
73
+ console.log(`${report.action === "created" ? "Created" : "Updated"} ${rel} with the loom-spec MCP server.`);
74
+ }
75
+ }
76
+ catch (e) {
77
+ console.error(`error registering MCP entry: ${e.message}`);
78
+ process.exitCode = 1;
79
+ }
80
+ }
63
81
  console.log();
64
82
  console.log("Next steps:");
65
83
  console.log(" 1. Run `npx loom-spec view` to open the editor.");
66
84
  console.log(" 2. Edit `.loom/node-types.json` to add project-specific node types.");
67
85
  console.log(" 3. Start populating `.loom/diagrams/overview.flow.json`.");
86
+ if (!args.mcp) {
87
+ console.log(" 4. To register the MCP server: re-run with `--mcp`, or `npx loom-spec install-mcp`.");
88
+ }
68
89
  }
69
90
  //# sourceMappingURL=init.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export interface InstallMcpArgs {
2
+ path: string;
3
+ }
4
+ export declare function runInstallMcp(args: InstallMcpArgs): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { resolve, relative } from "node:path";
2
+ import { installMcpEntry } from "./mcpConfig.js";
3
+ export async function runInstallMcp(args) {
4
+ const target = resolve(args.path);
5
+ try {
6
+ const report = await installMcpEntry(target);
7
+ const rel = relative(process.cwd(), report.path);
8
+ if (report.action === "unchanged") {
9
+ console.log(`MCP entry already up to date in ${rel}.`);
10
+ }
11
+ else if (report.action === "created") {
12
+ console.log(`Created ${rel} with the loom-spec MCP server.`);
13
+ }
14
+ else {
15
+ console.log(`Updated ${rel} — added loom-spec MCP server entry.`);
16
+ }
17
+ }
18
+ catch (e) {
19
+ console.error(`error: ${e.message}`);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ //# sourceMappingURL=installMcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installMcp.js","sourceRoot":"","sources":["../../src/cli/installMcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAMjD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAoB;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,mCAAmC,GAAG,GAAG,CAAC,CAAC;QACzD,CAAC;aAAM,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,iCAAiC,CAAC,CAAC;QAC/D,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,sCAAsC,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ export interface McpServerEntry {
2
+ command: string;
3
+ args?: string[];
4
+ env?: Record<string, string>;
5
+ cwd?: string;
6
+ }
7
+ /**
8
+ * Idempotently register the loom-spec MCP server in <projectPath>/.mcp.json.
9
+ *
10
+ * - If the file doesn't exist, creates it with just our entry.
11
+ * - If it exists and parses, merges our entry into mcpServers. Other servers
12
+ * and top-level keys are preserved.
13
+ * - If the existing entry is identical, reports "unchanged" and exits clean.
14
+ * - If the file exists but doesn't parse, errors out without writing.
15
+ *
16
+ * Returns a small report describing what happened.
17
+ */
18
+ export declare function installMcpEntry(projectPath: string): Promise<{
19
+ path: string;
20
+ action: "created" | "updated" | "unchanged";
21
+ }>;
@@ -0,0 +1,56 @@
1
+ import { readFile, writeFile, stat } from "node:fs/promises";
2
+ import { resolve, relative } from "node:path";
3
+ const SERVER_NAME = "loom-spec";
4
+ function loomSpecEntry() {
5
+ return {
6
+ command: "npx",
7
+ args: ["-y", "loom-spec", "mcp"],
8
+ };
9
+ }
10
+ /**
11
+ * Idempotently register the loom-spec MCP server in <projectPath>/.mcp.json.
12
+ *
13
+ * - If the file doesn't exist, creates it with just our entry.
14
+ * - If it exists and parses, merges our entry into mcpServers. Other servers
15
+ * and top-level keys are preserved.
16
+ * - If the existing entry is identical, reports "unchanged" and exits clean.
17
+ * - If the file exists but doesn't parse, errors out without writing.
18
+ *
19
+ * Returns a small report describing what happened.
20
+ */
21
+ export async function installMcpEntry(projectPath) {
22
+ const configPath = resolve(projectPath, ".mcp.json");
23
+ let existing = null;
24
+ try {
25
+ await stat(configPath);
26
+ const raw = await readFile(configPath, "utf8");
27
+ try {
28
+ existing = JSON.parse(raw);
29
+ }
30
+ catch (e) {
31
+ throw new Error(`${relative(process.cwd(), configPath)} exists but is not valid JSON: ${e.message}`);
32
+ }
33
+ }
34
+ catch (e) {
35
+ const code = e.code;
36
+ if (code && code !== "ENOENT")
37
+ throw e;
38
+ // fall through: existing remains null
39
+ }
40
+ const desired = loomSpecEntry();
41
+ const config = existing ?? {};
42
+ const servers = config.mcpServers ?? {};
43
+ const current = servers[SERVER_NAME];
44
+ // Compare normalized JSON to detect identical entries
45
+ if (current && JSON.stringify(current) === JSON.stringify(desired)) {
46
+ return { path: configPath, action: "unchanged" };
47
+ }
48
+ servers[SERVER_NAME] = desired;
49
+ config.mcpServers = servers;
50
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
51
+ return {
52
+ path: configPath,
53
+ action: existing ? "updated" : "created",
54
+ };
55
+ }
56
+ //# sourceMappingURL=mcpConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpConfig.js","sourceRoot":"","sources":["../../src/cli/mcpConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAc9C,MAAM,WAAW,GAAG,WAAW,CAAC;AAEhC,SAAS,aAAa;IACpB,OAAO;QACL,OAAO,EAAE,KAAK;QACd,IAAI,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC;KACjC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,WAAmB;IAIvD,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAErD,IAAI,QAAQ,GAAqB,IAAI,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC;QACvB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAc,CAAC;QAC1C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,kCAAmC,CAAW,CAAC,OAAO,EAAE,CAC/F,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,GAAI,CAA2B,CAAC,IAAI,CAAC;QAC/C,IAAI,IAAI,IAAI,IAAI,KAAK,QAAQ;YAAE,MAAM,CAAC,CAAC;QACvC,sCAAsC;IACxC,CAAC;IAED,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,MAAM,GAAc,QAAQ,IAAI,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAErC,sDAAsD;IACtD,IAAI,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACnD,CAAC;IAED,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC;IAE5B,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5E,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;KACzC,CAAC;AACJ,CAAC"}