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,56 @@
1
+ import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
2
+ import { resolve, basename } from "node:path";
3
+ function diagramsDir(loomPath) {
4
+ return resolve(loomPath, "diagrams");
5
+ }
6
+ function diagramFilePath(loomPath, id) {
7
+ return resolve(diagramsDir(loomPath), `${id}.flow.json`);
8
+ }
9
+ export async function listDiagrams(loomPath) {
10
+ const dir = diagramsDir(loomPath);
11
+ try {
12
+ const entries = await readdir(dir);
13
+ const summaries = [];
14
+ for (const file of entries) {
15
+ if (!file.endsWith(".flow.json"))
16
+ continue;
17
+ const id = basename(file, ".flow.json");
18
+ try {
19
+ const raw = await readFile(resolve(dir, file), "utf8");
20
+ const diagram = JSON.parse(raw);
21
+ summaries.push({
22
+ id,
23
+ title: diagram.title,
24
+ description: diagram.description,
25
+ nodeCount: diagram.nodes?.length ?? 0,
26
+ edgeCount: diagram.edges?.length ?? 0,
27
+ });
28
+ }
29
+ catch {
30
+ // skip unreadable files; the validate step elsewhere flags them
31
+ }
32
+ }
33
+ return summaries.sort((a, b) => a.id.localeCompare(b.id));
34
+ }
35
+ catch (e) {
36
+ if (e.code === "ENOENT")
37
+ return [];
38
+ throw e;
39
+ }
40
+ }
41
+ export async function readDiagram(loomPath, id) {
42
+ const raw = await readFile(diagramFilePath(loomPath, id), "utf8");
43
+ return JSON.parse(raw);
44
+ }
45
+ export async function writeDiagram(loomPath, id, data, onWritten) {
46
+ await mkdir(diagramsDir(loomPath), { recursive: true });
47
+ const serialized = JSON.stringify(data, null, 2) + "\n";
48
+ const path = diagramFilePath(loomPath, id);
49
+ await writeFile(path, serialized, "utf8");
50
+ onWritten?.(path);
51
+ }
52
+ export async function readNodeTypes(loomPath) {
53
+ const raw = await readFile(resolve(loomPath, "node-types.json"), "utf8");
54
+ return JSON.parse(raw);
55
+ }
56
+ //# sourceMappingURL=fileOps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileOps.js","sourceRoot":"","sources":["../../src/server/fileOps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAY9C,SAAS,WAAW,CAAC,QAAgB;IACnC,OAAO,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB,EAAE,EAAU;IACnD,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,SAAS,GAAqB,EAAE,CAAC;QACvC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;gBAAE,SAAS;YAC3C,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;YACxC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;gBACvD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;gBAC/C,SAAS,CAAC,IAAI,CAAC;oBACb,EAAE;oBACF,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;oBACrC,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;iBACtC,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,gEAAgE;YAClE,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAC9D,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB,EAAE,EAAU;IAC5D,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,eAAe,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAClE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAAgB,EAChB,EAAU,EACV,IAAiB,EACjB,SAAkC;IAElC,MAAM,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACxD,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC3C,MAAM,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAC1C,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAgB;IAClD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,9 @@
1
+ export interface LoomRoot {
2
+ rootPath: string;
3
+ loomPath: string;
4
+ }
5
+ /**
6
+ * Walks up from `startDir` looking for a `.loom/` directory.
7
+ * Throws if none is found.
8
+ */
9
+ export declare function findLoomRoot(startDir: string): Promise<LoomRoot>;
@@ -0,0 +1,28 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ /**
4
+ * Walks up from `startDir` looking for a `.loom/` directory.
5
+ * Throws if none is found.
6
+ */
7
+ export async function findLoomRoot(startDir) {
8
+ let dir = resolve(startDir);
9
+ while (true) {
10
+ const candidate = resolve(dir, ".loom");
11
+ try {
12
+ const s = await stat(candidate);
13
+ if (s.isDirectory()) {
14
+ return { rootPath: dir, loomPath: candidate };
15
+ }
16
+ }
17
+ catch {
18
+ // not here, keep walking
19
+ }
20
+ const parent = dirname(dir);
21
+ if (parent === dir) {
22
+ throw new Error(`No .loom/ directory found in ${startDir} or any parent. ` +
23
+ `Run \`loom-spec init\` first.`);
24
+ }
25
+ dir = parent;
26
+ }
27
+ }
28
+ //# sourceMappingURL=findLoomRoot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findLoomRoot.js","sourceRoot":"","sources":["../../src/server/findLoomRoot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAO7C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5B,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;YAChC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpB,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;YAChD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,gCAAgC,QAAQ,kBAAkB;gBACxD,+BAA+B,CAClC,CAAC;QACJ,CAAC;QACD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { EventEmitter } from "node:events";
2
+ export interface LoomChangeEvent {
3
+ type: "diagram-changed" | "node-types-changed";
4
+ id?: string;
5
+ }
6
+ /**
7
+ * Watches `.loom/` for changes from external editors (humans, AI agents).
8
+ * Self-writes (from our own PUT endpoint) are suppressed within a short
9
+ * grace window so the UI doesn't react to its own edits.
10
+ *
11
+ * The watcher does not throw on startup if the target dir is missing — it
12
+ * just logs a warning. chokidar errors are caught and logged so a transient
13
+ * filesystem hiccup doesn't crash the server.
14
+ */
15
+ export declare class LoomWatcher extends EventEmitter {
16
+ private loomPath;
17
+ private watcher;
18
+ private recentSelfWrites;
19
+ private static SUPPRESS_MS;
20
+ constructor(loomPath: string);
21
+ private start;
22
+ /**
23
+ * Mark a path as having been just written by us. Subsequent chokidar
24
+ * events on this path within the grace window are suppressed.
25
+ */
26
+ markSelfWrite(path: string): void;
27
+ private isSelfWrite;
28
+ close(): Promise<void>;
29
+ }
@@ -0,0 +1,83 @@
1
+ import { FSWatcher, watch as chokidarWatch } from "chokidar";
2
+ import { EventEmitter } from "node:events";
3
+ import { basename, resolve } from "node:path";
4
+ /**
5
+ * Watches `.loom/` for changes from external editors (humans, AI agents).
6
+ * Self-writes (from our own PUT endpoint) are suppressed within a short
7
+ * grace window so the UI doesn't react to its own edits.
8
+ *
9
+ * The watcher does not throw on startup if the target dir is missing — it
10
+ * just logs a warning. chokidar errors are caught and logged so a transient
11
+ * filesystem hiccup doesn't crash the server.
12
+ */
13
+ export class LoomWatcher extends EventEmitter {
14
+ loomPath;
15
+ watcher = null;
16
+ recentSelfWrites = new Map();
17
+ static SUPPRESS_MS = 1500;
18
+ constructor(loomPath) {
19
+ super();
20
+ this.loomPath = loomPath;
21
+ this.start();
22
+ }
23
+ start() {
24
+ try {
25
+ this.watcher = chokidarWatch([
26
+ resolve(this.loomPath, "diagrams"),
27
+ resolve(this.loomPath, "node-types.json"),
28
+ ], {
29
+ ignoreInitial: true,
30
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
31
+ });
32
+ this.watcher.on("all", (_eventName, path) => {
33
+ if (this.isSelfWrite(path))
34
+ return;
35
+ if (path.endsWith("/node-types.json")) {
36
+ this.emit("change", { type: "node-types-changed" });
37
+ }
38
+ else if (path.endsWith(".flow.json")) {
39
+ const id = basename(path, ".flow.json");
40
+ this.emit("change", { type: "diagram-changed", id });
41
+ }
42
+ });
43
+ this.watcher.on("error", (err) => {
44
+ console.warn(`[loom-spec watcher] error: ${err.message}. ` +
45
+ "Live sync may be degraded; server continues serving.");
46
+ });
47
+ }
48
+ catch (err) {
49
+ console.warn(`[loom-spec watcher] failed to start: ${err.message}. ` +
50
+ "Live sync disabled; server continues serving.");
51
+ this.watcher = null;
52
+ }
53
+ }
54
+ /**
55
+ * Mark a path as having been just written by us. Subsequent chokidar
56
+ * events on this path within the grace window are suppressed.
57
+ */
58
+ markSelfWrite(path) {
59
+ this.recentSelfWrites.set(path, Date.now());
60
+ }
61
+ isSelfWrite(path) {
62
+ const t = this.recentSelfWrites.get(path);
63
+ if (t === undefined)
64
+ return false;
65
+ if (Date.now() - t > LoomWatcher.SUPPRESS_MS) {
66
+ this.recentSelfWrites.delete(path);
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+ async close() {
72
+ if (this.watcher) {
73
+ try {
74
+ await this.watcher.close();
75
+ }
76
+ catch {
77
+ // ignore close errors on shutdown
78
+ }
79
+ this.watcher = null;
80
+ }
81
+ }
82
+ }
83
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/server/watch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * AUTOGENERATED — do not edit.
3
+ * Source: schema/diagram.schema.json
4
+ */
5
+ /**
6
+ * A node-based architecture spec file.
7
+ */
8
+ export interface LoomDiagram {
9
+ $schema?: string;
10
+ version: "1";
11
+ /**
12
+ * Diagram identifier. Should match the filename without extension.
13
+ */
14
+ id: string;
15
+ title: string;
16
+ description?: string;
17
+ nodes: Node[];
18
+ edges: Edge[];
19
+ groups?: Group[];
20
+ }
21
+ export interface Node {
22
+ /**
23
+ * Unique within this diagram.
24
+ */
25
+ id: string;
26
+ /**
27
+ * References a key in node-types.json.
28
+ */
29
+ type: string;
30
+ label: string;
31
+ /**
32
+ * Markdown allowed.
33
+ */
34
+ description?: string;
35
+ position: {
36
+ x: number;
37
+ y: number;
38
+ };
39
+ /**
40
+ * planned = spec exists, code does not. implemented = both present. stale = code refs are broken, needs review. deprecated = kept for history, no longer active.
41
+ */
42
+ status: "planned" | "implemented" | "stale" | "deprecated";
43
+ code_refs?: CodeRef[];
44
+ /**
45
+ * Custom fields defined by the node's type in node-types.json.
46
+ */
47
+ properties?: {
48
+ [k: string]: unknown;
49
+ };
50
+ tags?: string[];
51
+ /**
52
+ * ID of another diagram file to navigate into when this node is opened.
53
+ */
54
+ drill_down?: string;
55
+ }
56
+ export interface CodeRef {
57
+ /**
58
+ * Repo-relative file path.
59
+ */
60
+ path: string;
61
+ /**
62
+ * Function, class, or component name. Preferred over lines because it survives refactors.
63
+ */
64
+ symbol?: string;
65
+ /**
66
+ * Line range(s) like '1-80' or '12,45-50'. Use only when no symbol applies.
67
+ */
68
+ lines?: string;
69
+ }
70
+ export interface Edge {
71
+ id: string;
72
+ /**
73
+ * Source node id, optionally with :port-name suffix.
74
+ */
75
+ from: string;
76
+ /**
77
+ * Target node id, optionally with :port-name suffix.
78
+ */
79
+ to: string;
80
+ kind: "request" | "event" | "data-read" | "data-write" | "signal" | "dependency" | "control";
81
+ label?: string;
82
+ description?: string;
83
+ direction?: "forward" | "bidirectional";
84
+ }
85
+ export interface Group {
86
+ id: string;
87
+ label: string;
88
+ /**
89
+ * Node ids contained in this group.
90
+ */
91
+ children?: string[];
92
+ /**
93
+ * Group ids nested inside this group.
94
+ */
95
+ subgroups?: string[];
96
+ color?: string;
97
+ collapsed?: boolean;
98
+ drill_down?: string;
99
+ }
@@ -0,0 +1,7 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * AUTOGENERATED — do not edit.
4
+ * Source: schema/diagram.schema.json
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=diagram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagram.js","sourceRoot":"","sources":["../../src/types/diagram.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;GAGG"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * AUTOGENERATED — do not edit.
3
+ * Source: schema/node-types.schema.json
4
+ */
5
+ export type Field = {
6
+ [k: string]: unknown;
7
+ } & {
8
+ name: string;
9
+ type: "string" | "number" | "boolean" | "enum" | "markdown" | "code-ref" | "array";
10
+ required?: boolean;
11
+ description?: string;
12
+ default?: unknown;
13
+ /**
14
+ * Required when type is 'enum'.
15
+ */
16
+ values?: unknown[];
17
+ /**
18
+ * Required when type is 'array'.
19
+ */
20
+ items?: "string" | "number";
21
+ min?: number;
22
+ max?: number;
23
+ pattern?: string;
24
+ max_length?: number;
25
+ };
26
+ /**
27
+ * Defines the node type vocabulary for a project.
28
+ */
29
+ export interface LoomNodeTypes {
30
+ $schema?: string;
31
+ types: {
32
+ [k: string]: NodeType;
33
+ };
34
+ }
35
+ export interface NodeType {
36
+ label: string;
37
+ description?: string;
38
+ color: string;
39
+ /**
40
+ * Lucide icon name.
41
+ */
42
+ icon?: string;
43
+ fields?: Field[];
44
+ ports?: {
45
+ in?: Port[];
46
+ out?: Port[];
47
+ };
48
+ }
49
+ export interface Port {
50
+ name: string;
51
+ /**
52
+ * Free-form signal type tag, e.g. 'audio', 'midi', 'http', 'control'.
53
+ */
54
+ signal?: string;
55
+ }
@@ -0,0 +1,7 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * AUTOGENERATED — do not edit.
4
+ * Source: schema/node-types.schema.json
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=node-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-types.js","sourceRoot":"","sources":["../../src/types/node-types.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;GAGG"}
@@ -0,0 +1,11 @@
1
+ import type { LoomDiagram } from "./types/diagram.js";
2
+ import type { LoomNodeTypes } from "./types/node-types.js";
3
+ export type ValidationResult = {
4
+ ok: true;
5
+ } | {
6
+ ok: false;
7
+ errors: string[];
8
+ };
9
+ export declare function validateDiagram(data: unknown): Promise<ValidationResult>;
10
+ export declare function validateNodeTypes(data: unknown): Promise<ValidationResult>;
11
+ export type { LoomDiagram, LoomNodeTypes };
@@ -0,0 +1,47 @@
1
+ import Ajv from "ajv/dist/2020.js";
2
+ import addFormats from "ajv-formats";
3
+ import { readFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ const schemaDir = resolve(here, "../schema");
8
+ const ajv = new Ajv({ allErrors: true, strict: false });
9
+ addFormats(ajv);
10
+ let diagramValidator = null;
11
+ let nodeTypesValidator = null;
12
+ async function loadDiagramValidator() {
13
+ if (diagramValidator)
14
+ return diagramValidator;
15
+ const schema = JSON.parse(await readFile(resolve(schemaDir, "diagram.schema.json"), "utf8"));
16
+ diagramValidator = ajv.compile(schema);
17
+ return diagramValidator;
18
+ }
19
+ async function loadNodeTypesValidator() {
20
+ if (nodeTypesValidator)
21
+ return nodeTypesValidator;
22
+ const schema = JSON.parse(await readFile(resolve(schemaDir, "node-types.schema.json"), "utf8"));
23
+ nodeTypesValidator = ajv.compile(schema);
24
+ return nodeTypesValidator;
25
+ }
26
+ function formatErrors(errors) {
27
+ return errors.map((e) => {
28
+ const err = e;
29
+ const path = err.instancePath || "(root)";
30
+ return `${path}: ${err.message ?? "invalid"}`;
31
+ });
32
+ }
33
+ export async function validateDiagram(data) {
34
+ const validator = await loadDiagramValidator();
35
+ const ok = validator(data);
36
+ if (ok)
37
+ return { ok: true };
38
+ return { ok: false, errors: formatErrors(validator.errors ?? []) };
39
+ }
40
+ export async function validateNodeTypes(data) {
41
+ const validator = await loadNodeTypesValidator();
42
+ const ok = validator(data);
43
+ if (ok)
44
+ return { ok: true };
45
+ return { ok: false, errors: formatErrors(validator.errors ?? []) };
46
+ }
47
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AAE7C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AACxD,UAAU,CAAC,GAAG,CAAC,CAAC;AAEhB,IAAI,gBAAgB,GAA0C,IAAI,CAAC;AACnE,IAAI,kBAAkB,GAA0C,IAAI,CAAC;AAErE,KAAK,UAAU,oBAAoB;IACjC,IAAI,gBAAgB;QAAE,OAAO,gBAAgB,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,MAAM,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,qBAAqB,CAAC,EAAE,MAAM,CAAC,CAClE,CAAC;IACF,gBAAgB,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,KAAK,UAAU,sBAAsB;IACnC,IAAI,kBAAkB;QAAE,OAAO,kBAAkB,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,MAAM,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,wBAAwB,CAAC,EAAE,MAAM,CAAC,CACrE,CAAC;IACF,kBAAkB,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAMD,SAAS,YAAY,CAAC,MAAiB;IACrC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACtB,MAAM,GAAG,GAAG,CAAkE,CAAC;QAC/E,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,IAAI,QAAQ,CAAC;QAC1C,OAAO,GAAG,IAAI,KAAK,GAAG,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAa;IACjD,MAAM,SAAS,GAAG,MAAM,oBAAoB,EAAE,CAAC;IAC/C,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAa;IACnD,MAAM,SAAS,GAAG,MAAM,sBAAsB,EAAE,CAAC;IACjD,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;AACrE,CAAC"}
@@ -0,0 +1 @@
1
+ .react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}:root[data-theme=light]{--bg: #fafafa;--bg-elevated: #ffffff;--bg-canvas: #f4f4f5;--bg-hover: #f4f4f5;--text: #18181b;--text-muted: #71717a;--text-subtle: #a1a1aa;--border: #e4e4e7;--border-strong: #d4d4d8;--accent: #6366f1;--accent-fg: #ffffff;--shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 12px rgba(0,0,0,.04);--grid-dot: #d4d4d8}:root[data-theme=dark]{--bg: #0a0a0a;--bg-elevated: #18181b;--bg-canvas: #09090b;--bg-hover: #27272a;--text: #fafafa;--text-muted: #a1a1aa;--text-subtle: #71717a;--border: #27272a;--border-strong: #3f3f46;--accent: #818cf8;--accent-fg: #0a0a0a;--shadow: 0 1px 2px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.3);--grid-dot: #3f3f46}:root{--status-planned: #f59e0b;--status-implemented: #10b981;--status-stale: #ef4444;--status-deprecated: #71717a;--edge-request: #6366f1;--edge-event: #f59e0b;--edge-data-read: #06b6d4;--edge-data-write: #a855f7;--edge-signal: #ec4899;--edge-dependency: #71717a;--edge-control: #10b981}*{box-sizing:border-box}html,body,#root{margin:0;height:100%;width:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Inter,Segoe UI,sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;transition:background-color .15s,color .15s}button{font-family:inherit;font-size:inherit;cursor:pointer;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text);padding:6px 10px;border-radius:6px;display:inline-flex;align-items:center;gap:6px;transition:background-color .1s,border-color .1s}button:hover{background:var(--bg-hover);border-color:var(--border-strong)}code,pre{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px}.app{display:grid;grid-template-rows:auto 1fr;grid-template-columns:1fr 320px;grid-template-areas:"topbar topbar" "canvas inspector";height:100vh}.topbar{grid-area:topbar;height:48px;border-bottom:1px solid var(--border);background:var(--bg-elevated);display:flex;align-items:center;padding:0 16px;gap:12px}.topbar .title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}.breadcrumb-back{background:transparent;border:1px solid transparent;color:var(--text-muted);padding:4px 8px 4px 4px;font-size:13px}.breadcrumb-back:hover{background:var(--bg-canvas);border-color:var(--border);color:var(--text)}.topbar .subtitle{color:var(--text-muted);font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.canvas-wrap{grid-area:canvas;position:relative;background:var(--bg-canvas);overflow:hidden}.inspector{grid-area:inspector;border-left:1px solid var(--border);background:var(--bg-elevated);overflow-y:auto;padding:16px}.inspector .empty{color:var(--text-muted);font-style:italic;padding:20px 0;text-align:center}.inspector h2{margin:0 0 4px;font-size:16px}.inspector .type-tag{display:inline-block;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;padding:2px 8px;border-radius:4px;margin-bottom:12px}.inspector .field{margin:12px 0}.inspector .field-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:4px}.inspector .field-value{color:var(--text)}.inspector .field-value.muted{color:var(--text-muted);font-style:italic}.inspector .code-ref{font-family:ui-monospace,monospace;font-size:12px;padding:4px 8px;background:var(--bg-canvas);border:1px solid var(--border);border-radius:4px;margin-top:4px;word-break:break-all}.inspector .tag{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:2px 4px 2px 6px;background:var(--bg-canvas);border:1px solid var(--border);border-radius:3px;margin-right:4px;margin-bottom:4px;color:var(--text)}.inspector .tag-x{display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;padding:0;color:var(--text-muted);cursor:pointer}.inspector .tag-x:hover{color:var(--status-stale)}.code-ref-row{display:grid;grid-template-columns:1fr 80px 60px 22px;gap:4px;margin-bottom:4px;align-items:center}.code-ref-row .input.small,.code-ref-row .input{padding:4px 6px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11px}.row-delete{width:22px;height:22px;padding:0;background:transparent;border:1px solid var(--border);border-radius:4px;color:var(--text-muted);display:inline-flex;align-items:center;justify-content:center}.row-delete:hover{color:var(--status-stale);border-color:var(--status-stale)}.add-row{background:transparent;border:1px dashed var(--border);color:var(--text-muted);width:100%;margin-top:4px;padding:6px;font-size:12px;display:flex;align-items:center;justify-content:center;gap:6px}.add-row:hover{color:var(--accent);border-color:var(--accent)}.tag-chips{margin-bottom:6px;min-height:1px}.input{width:100%;background:var(--bg-canvas);color:var(--text);border:1px solid var(--border);border-radius:5px;padding:6px 8px;font-family:inherit;font-size:13px;outline:none;transition:border-color .1s,box-shadow .1s}.input:focus{border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in oklab,var(--accent) 25%,transparent)}.input.textarea{resize:vertical;min-height:56px;font-family:inherit;line-height:1.4}.status-line{height:2px;margin-top:6px;border-radius:2px}.save-indicator{display:inline-flex;align-items:center;gap:4px;font-size:12px;padding:3px 8px;border-radius:4px;font-weight:500}.save-indicator.dirty{color:var(--text-muted);background:var(--bg-canvas)}.save-indicator.saving{color:var(--accent);background:color-mix(in oklab,var(--accent) 15%,transparent)}.save-indicator.saved{color:var(--status-implemented);background:color-mix(in oklab,var(--status-implemented) 15%,transparent)}.save-indicator.error{color:var(--status-stale);background:color-mix(in oklab,var(--status-stale) 15%,transparent)}.conn-dot{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:4px}.conn-dot.connected{color:var(--status-implemented)}.conn-dot.connecting{color:var(--text-muted)}.conn-dot.disconnected{color:var(--status-stale)}.validation-chip{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;padding:3px 8px;border-radius:4px;color:var(--status-stale);background:color-mix(in oklab,var(--status-stale) 15%,transparent)}.field-error{color:var(--status-stale);font-size:11px;margin-top:4px;line-height:1.3}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.add-node-menu{position:fixed;z-index:100;background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow);padding:6px;min-width:220px}.add-node-menu-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);padding:6px 8px 4px}.add-node-item{display:flex;align-items:center;gap:10px;width:100%;padding:8px;border:none;background:transparent;text-align:left;border-radius:5px;cursor:pointer;color:var(--text)}.add-node-item:hover{background:var(--bg-hover)}.add-node-color{width:10px;height:10px;border-radius:50%;background:var(--node-color);flex-shrink:0}.add-node-text{display:flex;flex-direction:column;gap:2px}.add-node-label{font-size:13px;font-weight:500}.add-node-key{font-size:11px;color:var(--text-muted)}.diagram-switcher{position:relative;flex-shrink:0}.switcher-trigger{background:transparent;border:1px solid transparent;padding:4px 6px 4px 8px;display:inline-flex;align-items:center;gap:6px;font-weight:600;color:var(--text);border-radius:6px;max-width:320px}.switcher-trigger:hover{background:var(--bg-canvas);border-color:var(--border)}.switcher-trigger .switcher-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px}.switcher-trigger .flip{transform:rotate(180deg)}.switcher-menu{position:absolute;top:100%;left:0;margin-top:6px;z-index:100;background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow);padding:6px;min-width:280px;max-width:380px;max-height:70vh;overflow-y:auto}.switcher-empty{padding:12px;color:var(--text-muted);font-style:italic;font-size:13px}.switcher-item{display:flex;align-items:center;gap:10px;width:100%;padding:8px;border:none;background:transparent;text-align:left;border-radius:5px;cursor:pointer;color:var(--text);font-weight:400}.switcher-item:hover{background:var(--bg-hover)}.switcher-item.active{background:color-mix(in oklab,var(--accent) 14%,transparent)}.switcher-item.active .switcher-item-title{color:var(--accent)}.switcher-item-icon{color:var(--text-muted);flex-shrink:0}.switcher-item-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}.switcher-item-title{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.switcher-item-meta{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.switcher-item-meta code{font-size:11px}.switcher-divider{height:1px;background:var(--border);margin:6px 0}.switcher-item.new{color:var(--accent);font-weight:500}.node-card{background:var(--bg-elevated);border:2px solid var(--node-color, var(--border));border-radius:8px;min-width:180px;padding:10px 12px;box-shadow:var(--shadow);position:relative}.node-card.status-planned{border-style:dashed;opacity:.85}.node-card.status-stale{border-color:var(--status-stale)}.node-card.status-deprecated{opacity:.5}.node-card .node-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}.node-card .node-icon{width:14px;height:14px;color:var(--node-color);flex-shrink:0}.node-card .node-type{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted)}.node-card .node-label{font-weight:600;font-size:14px;color:var(--text);line-height:1.3}.node-card .status-dot{position:absolute;top:8px;right:8px;width:8px;height:8px;border-radius:50%;background:var(--status-color, var(--text-subtle))}.node-card .refs-badge{position:absolute;bottom:6px;left:8px;display:inline-flex;align-items:center;gap:3px;font-size:10px;font-weight:500;color:var(--text-muted);background:var(--bg-canvas);border:1px solid var(--border);border-radius:3px;padding:1px 5px 1px 4px;font-family:ui-monospace,monospace;line-height:1.2;pointer-events:auto;white-space:pre}.node-card .refs-badge:hover{color:var(--text);border-color:var(--border-strong)}.drill-down-btn{position:absolute;bottom:6px;right:6px;width:20px;height:20px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-canvas);color:var(--text-muted);border:1px solid var(--border);border-radius:4px;cursor:pointer;transition:background-color .1s,color .1s,border-color .1s}.drill-down-btn:hover{background:var(--node-color, var(--accent));color:var(--accent-fg);border-color:var(--node-color, var(--accent))}.drill-down-btn.inline{position:static;width:16px;height:16px;margin-left:6px;vertical-align:middle}.drill-down-btn.inline:hover{background:var(--group-color, var(--accent));border-color:var(--group-color, var(--accent))}.group-frame{position:relative;border:1.5px dashed color-mix(in oklab,var(--group-color) 70%,var(--text-subtle));border-radius:12px;background:color-mix(in oklab,var(--group-color) 15%,transparent);pointer-events:none}.group-label{position:absolute;top:-10px;left:12px;padding:1px 8px;background:var(--bg-elevated);border:1px solid color-mix(in oklab,var(--group-color) 60%,var(--border));border-radius:4px;font-size:11px;font-weight:600;color:var(--text);text-transform:uppercase;letter-spacing:.04em;pointer-events:auto;white-space:nowrap}.react-flow__handle{background:var(--node-color, var(--accent));border:2px solid var(--bg-elevated);width:10px;height:10px}.node-card.has-ports{padding-top:14px;padding-bottom:14px;min-width:220px}.port-row{position:absolute;display:flex;align-items:center;gap:6px;transform:translateY(-50%);pointer-events:none}.port-row .react-flow__handle{pointer-events:all;position:static!important;transform:none!important}.port-in{left:-5px}.port-out{right:-5px;flex-direction:row}.port-label{font-size:10px;color:var(--text-muted);font-family:ui-monospace,monospace;background:var(--bg-elevated);padding:1px 4px;border-radius:2px;white-space:nowrap}.port-in .port-label{margin-left:4px}.port-out .port-label{margin-right:4px}.react-flow{background:var(--bg-canvas)}.react-flow__background{background-color:var(--bg-canvas)}.react-flow__controls{background:var(--bg-elevated);border:1px solid var(--border);border-radius:6px;overflow:hidden}.react-flow__controls-button{background:var(--bg-elevated);border-bottom:1px solid var(--border);color:var(--text)}.react-flow__controls-button:hover{background:var(--bg-hover)}.react-flow__controls-button svg{fill:currentColor}.react-flow__edge-text{fill:var(--text-muted);font-size:11px}.react-flow__edge-textbg{fill:var(--bg-elevated)}