@tuongaz/seeflow 0.1.64 → 0.1.65

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/src/schema.ts CHANGED
@@ -5,11 +5,8 @@ const PositionSchema = z.object({
5
5
  y: z.number(),
6
6
  });
7
7
 
8
- // US-008: HttpAction was the original shape for both playAction and resetAction
9
- // in pre-script-action releases. After US-001 cut playAction to script-only and
10
- // US-008 cut resetAction the same way, no schema in this file uses HttpAction
11
- // anymore. `HttpMethodSchema` is still used for the optional `method` field on
12
- // connectors (documentation metadata).
8
+ // `HttpMethodSchema` is documentation metadata on connectors. No node schema
9
+ // uses it PlayAction/StatusAction are script-based.
13
10
  const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
14
11
 
15
12
  // Curated palette tokens. Stored on disk as readable names; the frontend maps
@@ -26,7 +23,8 @@ export const ColorTokenSchema = z.enum([
26
23
  ]);
27
24
 
28
25
  // Visual fields shared by every node type (functional + decorative). All
29
- // optional — existing demo files predate them and must continue to parse.
26
+ // optional — every visual must work without per-field opinions. Live on
27
+ // resolved nodes; the disk-side flow.json strips them into style.json.
30
28
  const NodeVisualBaseShape = {
31
29
  width: z.number().positive().optional(),
32
30
  height: z.number().positive().optional(),
@@ -39,19 +37,20 @@ const NodeVisualBaseShape = {
39
37
  cornerRadius: z.number().min(0).optional(),
40
38
  };
41
39
 
42
- // Consolidated three-field metadata shared by every node variant. `description`
43
- // is the short body text rendered on the canvas under the node header (and as
44
- // light-bold text in the sidebar). `detail` is the long-form free-text body
45
- // rendered only in the sidebar. Both optional so unset fields round-trip
46
- // unchanged. Spread into every node-data schema below since Icon doesn't
47
- // spread NodeVisualBaseShape.
48
- const NodeDescriptionBaseShape = {
40
+ // Semantic-data fields shared by every node type. `name` is optional —
41
+ // every visual works without a label. `icon` is decorative-by-default; the
42
+ // `type:'icon'` variant overrides it to required.
43
+ const NodeSemanticBaseShape = {
44
+ name: z.string().optional(),
49
45
  description: z.string().optional(),
50
46
  detail: z.string().optional(),
47
+ // Decorative header glyph. Lucide icon name (kebab-case) resolved by the
48
+ // canvas <Icon> primitive; falls back to a placeholder when unknown.
49
+ icon: z.string().optional(),
51
50
  };
52
51
 
53
- // US-001: relative-path safety refine (textual). Mirrors the same rule used
54
- // for image/html-node paths further down. Realpath verification is layered on
52
+ // Relative-path safety refine (textual). Mirrors the same rule used for
53
+ // image-node and html-node script paths. Realpath verification is layered on
55
54
  // top by the proxy/status-runner before any spawn (symlink-escape defense).
56
55
  const isCleanRelativePath = (s: string): boolean => {
57
56
  if (s.length === 0) return false;
@@ -81,11 +80,7 @@ const ScriptActionSchema = z.object({
81
80
 
82
81
  export const PlayActionSchema = ScriptActionSchema;
83
82
 
84
- // US-008: resetAction is a one-shot script action same shape as a play
85
- // script (interpreter + args + scriptPath + optional input/timeoutMs) but
86
- // invoked from the /reset endpoint. The studio kills every live play and
87
- // status script for the demo before running this script, so the running app
88
- // sees a clean baseline when wiping its state.
83
+ // resetAction is a one-shot script with the same shape as a play script.
89
84
  export const ResetActionSchema = ScriptActionSchema;
90
85
 
91
86
  // Long-running status script. Same spawn shape as ScriptAction (interpreter +
@@ -117,54 +112,23 @@ export const StateSourceSchema = z.discriminatedUnion('kind', [
117
112
  z.object({ kind: z.literal('event') }),
118
113
  ]);
119
114
 
120
- const NodeDataBaseSchema = z.object({
121
- name: z.string().min(1),
122
- stateSource: StateSourceSchema,
123
- // Reserved for v2: a module path resolved by future skills runtime.
124
- // Schema-only at v1 never read at runtime.
125
- handlerModule: z.string().optional(),
126
- // Decorative header glyph. Lucide icon name (kebab-case) resolved by the
127
- // canvas <Icon> primitive; falls back to a placeholder when unknown.
128
- icon: z.string().optional(),
129
- ...NodeVisualBaseShape,
130
- ...NodeDescriptionBaseShape,
131
- });
132
-
133
- const PlayNodeDataSchema = NodeDataBaseSchema.extend({
134
- playAction: PlayActionSchema,
135
- statusAction: StatusActionSchema.optional(),
136
- });
137
-
138
- const StateNodeDataSchema = NodeDataBaseSchema.extend({
115
+ // Capabilities any subset of these makes a node Playable / Stateful. All
116
+ // optional, valid on every node type. A node is Playable iff `playAction` is
117
+ // set; Stateful iff `statusAction` is set; Both iff both. `stateSource` is
118
+ // informational metadata that pairs with statusAction. `handlerModule` is
119
+ // reserved for a future skills runtime and is schema-only at v1.
120
+ const NodeCapabilitiesShape = {
139
121
  playAction: PlayActionSchema.optional(),
140
122
  statusAction: StatusActionSchema.optional(),
141
- });
142
-
143
- const NodeBaseShape = {
144
- id: z.string().min(1),
145
- position: PositionSchema,
123
+ stateSource: StateSourceSchema.optional(),
124
+ handlerModule: z.string().optional(),
146
125
  };
147
126
 
148
- const PlayNodeSchema = z.object({
149
- ...NodeBaseShape,
150
- type: z.literal('playNode'),
151
- data: PlayNodeDataSchema,
152
- });
153
-
154
- const StateNodeSchema = z.object({
155
- ...NodeBaseShape,
156
- type: z.literal('stateNode'),
157
- data: StateNodeDataSchema,
158
- });
159
-
160
- // Decorative annotation node — rectangle / ellipse / sticky. No semantic
161
- // payload (no kind/stateSource/playAction); reuses NodeVisualBaseShape so
162
- // users can theme it the same way as functional nodes.
163
- // US-009 added `database` as the first illustrative shape (cylinder rendered
164
- // via inline SVG inside shape-node.tsx). Illustrative shapes share the same
165
- // shapeNode wrapper and color/border fields but own their own visuals via a
166
- // per-shape component under `apps/web/src/components/nodes/shapes/`.
167
- export const ShapeKindSchema = z.enum([
127
+ // 12 flat node types. The first 9 are geometric/illustrative and share
128
+ // GeometricNodeData. `image`, `html`, `icon` carry per-type fields.
129
+ // The renderer picks the SVG / chrome by `type`; the schema treats them
130
+ // (apart from the per-type fields below) as identical.
131
+ export const GEOMETRIC_NODE_TYPES = [
168
132
  'rectangle',
169
133
  'ellipse',
170
134
  'sticky',
@@ -174,148 +138,102 @@ export const ShapeKindSchema = z.enum([
174
138
  'user',
175
139
  'queue',
176
140
  'cloud',
177
- ]);
141
+ ] as const;
178
142
 
179
- const ShapeNodeDataSchema = z.object({
180
- shape: ShapeKindSchema,
181
- name: z.string().optional(),
182
- ...NodeVisualBaseShape,
183
- ...NodeDescriptionBaseShape,
184
- });
143
+ export const NodeTypeSchema = z.enum([...GEOMETRIC_NODE_TYPES, 'image', 'html', 'icon']);
144
+
145
+ // ---- Resolved (in-memory) per-type data -------------------------------------
185
146
 
186
- const ShapeNodeSchema = z.object({
187
- ...NodeBaseShape,
188
- type: z.literal('shapeNode'),
189
- data: ShapeNodeDataSchema,
147
+ const ResolvedGeometricNodeData = z.object({
148
+ ...NodeSemanticBaseShape,
149
+ ...NodeVisualBaseShape,
150
+ ...NodeCapabilitiesShape,
190
151
  });
191
152
 
192
- // Decorative image node — references a file under the project root by
193
- // relative path (US-004 hard-cut from base64 data URLs to path-backed files).
194
- // `path` is a relative path under the project root for imageNode uploads:
195
- // no leading slash, no `..` segments. The renderer fetches via
196
- // `GET /api/projects/:id/files/:path`.
197
- const ImageNodeDataSchema = z.object({
153
+ // Image node — references a file under the project root by relative path.
154
+ // `path` is constrained to live under `nodes/<id>/` (post-validate refine on
155
+ // ResolvedFlowSchema below) so the delete_node cascade owns cleanup.
156
+ const ResolvedImageNodeData = z.object({
157
+ ...NodeSemanticBaseShape,
158
+ ...NodeVisualBaseShape,
159
+ ...NodeCapabilitiesShape,
198
160
  path: z.string().min(1).refine(isCleanRelativePath, {
199
161
  message: 'path must be a relative path under the project root (no absolute / traversal)',
200
162
  }),
201
163
  alt: z.string().optional(),
202
- ...NodeVisualBaseShape,
203
- ...NodeDescriptionBaseShape,
204
164
  borderWidth: z.number().min(1).max(8).optional(),
205
165
  });
206
166
 
207
- const ImageNodeSchema = z.object({
208
- ...NodeBaseShape,
209
- type: z.literal('imageNode'),
210
- data: ImageNodeDataSchema,
211
- });
212
-
213
- // US-011 (illustrative-shapes-htmlnode): htmlNode is the escape-hatch node type
214
- // for content the curated nodes don't cover — carries author-written HTML
215
- // inline via `data.html`. The studio externalizes the content to
216
- // `<project>/nodes/<id>/view.html` and stores a `file://` ref in flow.json;
217
- // the resolver inlines the content back on read so consumers see
218
- // the resolved HTML string. The renderer sanitizes before injection
219
- // (US-013/US-014). Spreads NodeVisualBaseShape so authors can theme the
220
- // wrapper (border / background / radius / font) with the same fields
221
- // available on every other visual node.
222
- export const HtmlNodeDataSchema = z.object({
167
+ // Html node — escape-hatch for content the curated visuals don't cover.
168
+ // `html` is externalized to `<project>/nodes/<id>/view.html` on write; the
169
+ // file-ref resolver inlines it back on read. The renderer sanitizes before
170
+ // injection. `autoSize:true` lets the renderer size around the content.
171
+ const ResolvedHtmlNodeData = z.object({
172
+ ...NodeSemanticBaseShape,
173
+ ...NodeVisualBaseShape,
174
+ ...NodeCapabilitiesShape,
223
175
  html: z.string().optional(),
224
- name: z.string().optional(),
225
- // Decorative caption glyph. Lucide icon name (kebab-case) resolved by the
226
- // canvas <Icon> primitive; rendered inline with the caption when set.
227
- icon: z.string().optional(),
228
- // When true (or absent), the renderer measures the HTML content and React
229
- // Flow sizes the wrapper around it (capped at 800×600 by the renderer's
230
- // measuring container styles). The studio adapter (`mergeNodeUpdates`)
231
- // enforces the invariant that `autoSize === true` and persisted
232
- // `width`/`height` never coexist: writing width/height flips autoSize to
233
- // false; writing autoSize: true strips width/height.
234
176
  autoSize: z.boolean().optional(),
235
- ...NodeVisualBaseShape,
236
- ...NodeDescriptionBaseShape,
237
- });
238
-
239
- const HtmlNodeSchema = z.object({
240
- ...NodeBaseShape,
241
- type: z.literal('htmlNode'),
242
- data: HtmlNodeDataSchema,
243
177
  });
244
178
 
245
- // Decorative icon node — renders a Lucide glyph on the canvas. Unboxed
246
- // (no border/cornerRadius/backgroundColor) so it does NOT spread
247
- // NodeVisualBaseShape; only `width` / `height` are reused for resizing.
248
- const IconNodeDataSchema = z.object({
179
+ // Icon node — renders a Lucide glyph as its main visual. `icon` is required
180
+ // here (overrides the optional decorative `icon` from NodeSemanticBaseShape).
181
+ const ResolvedIconNodeData = z.object({
182
+ ...NodeSemanticBaseShape,
183
+ ...NodeVisualBaseShape,
184
+ ...NodeCapabilitiesShape,
249
185
  icon: z.string().min(1),
250
186
  color: ColorTokenSchema.optional(),
251
187
  strokeWidth: z.number().min(0.5).max(4).optional(),
252
- width: z.number().positive().optional(),
253
- height: z.number().positive().optional(),
254
188
  alt: z.string().optional(),
255
- // US-002: optional visible caption rendered below the icon. Distinct from
256
- // `alt` (screen-reader text). Absent / empty → no caption rendered and the
257
- // node's bounding box is byte-identical to the unlabeled layout.
258
- name: z.string().optional(),
259
- ...NodeDescriptionBaseShape,
260
189
  });
261
190
 
262
- const IconNodeSchema = z.object({
263
- ...NodeBaseShape,
264
- type: z.literal('iconNode'),
265
- data: IconNodeDataSchema,
266
- });
191
+ const NodeBaseShape = {
192
+ id: z.string().min(1),
193
+ position: PositionSchema,
194
+ };
195
+
196
+ const makeResolvedGeometricSchema = (type: (typeof GEOMETRIC_NODE_TYPES)[number]) =>
197
+ z.object({
198
+ ...NodeBaseShape,
199
+ type: z.literal(type),
200
+ data: ResolvedGeometricNodeData,
201
+ });
267
202
 
268
203
  const NodeSchema = z.discriminatedUnion('type', [
269
- PlayNodeSchema,
270
- StateNodeSchema,
271
- ShapeNodeSchema,
272
- ImageNodeSchema,
273
- IconNodeSchema,
274
- HtmlNodeSchema,
204
+ makeResolvedGeometricSchema('rectangle'),
205
+ makeResolvedGeometricSchema('ellipse'),
206
+ makeResolvedGeometricSchema('sticky'),
207
+ makeResolvedGeometricSchema('text'),
208
+ makeResolvedGeometricSchema('database'),
209
+ makeResolvedGeometricSchema('server'),
210
+ makeResolvedGeometricSchema('user'),
211
+ makeResolvedGeometricSchema('queue'),
212
+ makeResolvedGeometricSchema('cloud'),
213
+ z.object({ ...NodeBaseShape, type: z.literal('image'), data: ResolvedImageNodeData }),
214
+ z.object({ ...NodeBaseShape, type: z.literal('html'), data: ResolvedHtmlNodeData }),
215
+ z.object({ ...NodeBaseShape, type: z.literal('icon'), data: ResolvedIconNodeData }),
275
216
  ]);
276
217
 
277
- // Connector is the edge between two nodes. The frontend derives a React Flow
278
- // Edge from each connector at render time (id/source/target are reused;
279
- // `label` becomes the edge label; visual style comes from optional
280
- // `style`/`color` fields). v1 has no separate `edges[]` array — connectors
281
- // are the sole source of truth for inter-node connections. Optional
282
- // metadata fields (`method`/`url`/`eventName`/`queueName`) may be present
283
- // for documentation purposes; the renderer does not branch on them.
218
+ // Connector unchanged by the flat-types refactor.
284
219
  const ConnectorStyleSchema = z.enum(['solid', 'dashed', 'dotted']);
285
220
  const ConnectorDirectionSchema = z.enum(['forward', 'backward', 'both', 'none']);
286
- // Path geometry — orthogonal to `style` (which means the dash pattern). Absent
287
- // → renders as today's smooth bezier curve. 'step' renders as a smoothstep
288
- // (right-angle / zigzag) path. (US-017)
289
221
  const ConnectorPathSchema = z.enum(['curve', 'step']);
290
222
 
291
- // Visual fields shared by every connector. All optional — existing
292
- // demo files predate them and must continue to parse. `direction` defaults
293
- // to 'forward' when absent (the historical behavior).
294
223
  const ConnectorVisualBaseShape = {
295
224
  style: ConnectorStyleSchema.optional(),
296
225
  color: ColorTokenSchema.optional(),
297
226
  direction: ConnectorDirectionSchema.optional(),
298
227
  borderSize: z.number().positive().optional(),
299
228
  path: ConnectorPathSchema.optional(),
300
- // US-018: per-connector label font size in CSS pixels. Absent → fall back to
301
- // the editable-edge default (11px). Mirrors NodeVisualBaseShape.fontSize.
302
229
  fontSize: z.number().positive().optional(),
303
230
  };
304
231
 
305
- // Handle ids — every node type in this codebase uses the same four-handle
306
- // layout: target-only on top + left, source-only on right + bottom (US-013).
307
- // `sourceHandle` MUST be a source-side id and `targetHandle` MUST be a
308
- // target-side id; sending the wrong role leaves a stranded endpoint at render
309
- // time, so the schema rejects it (US-022).
232
+ // Handle ids — every node type uses the same four-handle layout:
233
+ // target-only on top + left, source-only on right + bottom.
310
234
  export const SourceHandleIdSchema = z.enum(['r', 'b']);
311
235
  export const TargetHandleIdSchema = z.enum(['t', 'l']);
312
236
 
313
- // US-006: pinned endpoint position. `side` names one of the four perimeter
314
- // sides of the connected node; `t` is the parameterized position along that
315
- // side, [0, 1], measured from the top-left corner of the side (top/bottom →
316
- // left-to-right; left/right → top-to-bottom). Pins are persisted so they
317
- // survive node moves and resizes without drifting toward the other endpoint's
318
- // center the way floating endpoints do.
319
237
  const EdgePinSideSchema = z.enum(['top', 'right', 'bottom', 'left']);
320
238
  export const EdgePinSchema = z.object({
321
239
  side: EdgePinSideSchema,
@@ -326,21 +244,10 @@ const ConnectorBaseShape = {
326
244
  id: z.string().min(1),
327
245
  source: z.string().min(1),
328
246
  target: z.string().min(1),
329
- // Optional — connectors authored before the four-handle layout omit them and
330
- // React Flow falls back to the first matching handle.
331
247
  sourceHandle: SourceHandleIdSchema.optional(),
332
248
  targetHandle: TargetHandleIdSchema.optional(),
333
- // US-021: tracks whether each endpoint's handle was auto-picked by the
334
- // facing-handle picker (true) or pinned by an explicit user handle drop
335
- // (false / absent). Auto-picked endpoints get re-routed when nodes move so
336
- // the connector keeps facing the other end; user-pinned ones never do.
337
249
  sourceHandleAutoPicked: z.boolean().optional(),
338
250
  targetHandleAutoPicked: z.boolean().optional(),
339
- // US-006: optional explicit perimeter positions for each endpoint. When
340
- // set, the endpoint is computed from `(side, t)` against the connected
341
- // node's current bbox at render time — the position parameterizes with the
342
- // node so the pin survives moves and resizes. Absent → floating /
343
- // handle-based endpoint behavior (back-compat).
344
251
  sourcePin: EdgePinSchema.optional(),
345
252
  targetPin: EdgePinSchema.optional(),
346
253
  label: z.string().optional(),
@@ -363,9 +270,8 @@ export const ResolvedFlowSchema = z
363
270
  nodes: z.array(NodeSchema),
364
271
  connectors: z.array(ConnectorSchema),
365
272
  // Optional one-shot script the studio runs when the user clicks Restart.
366
- // Lets the running app wipe its own in-memory state. The studio kills
367
- // every live play + status script for the flow BEFORE invoking this
368
- // script (US-008), so the script sees no stragglers.
273
+ // The studio kills every live play + status script for the flow BEFORE
274
+ // invoking this script, so the script sees no stragglers.
369
275
  resetAction: ResetActionSchema.optional(),
370
276
  })
371
277
  .superRefine((resolved, ctx) => {
@@ -386,18 +292,18 @@ export const ResolvedFlowSchema = z
386
292
  });
387
293
  }
388
294
  });
389
- // imageNode upload paths must live under the node's own
295
+ // type:'image' upload paths must live under the node's own
390
296
  // `nodes/<id>/` folder so delete_node's removeNodeDir cascade is the
391
297
  // single source of cleanup.
392
298
  resolved.nodes.forEach((node, idx) => {
393
- if (node.type !== 'imageNode') return;
299
+ if (node.type !== 'image') return;
394
300
  const path = (node.data as { path?: string }).path;
395
301
  const expected = `nodes/${node.id}/`;
396
302
  if (typeof path === 'string' && !path.startsWith(expected)) {
397
303
  ctx.addIssue({
398
304
  code: z.ZodIssueCode.custom,
399
305
  path: ['nodes', idx, 'data', 'path'],
400
- message: `imageNode path must start with "${expected}"`,
306
+ message: `image node path must start with "${expected}"`,
401
307
  });
402
308
  }
403
309
  });
@@ -405,12 +311,7 @@ export const ResolvedFlowSchema = z
405
311
 
406
312
  export type ResolvedFlow = z.infer<typeof ResolvedFlowSchema>;
407
313
  export type ResolvedFlowNode = z.infer<typeof NodeSchema>;
408
- export type ShapeNode = z.infer<typeof ShapeNodeSchema>;
409
- export type ImageNode = z.infer<typeof ImageNodeSchema>;
410
- export type IconNode = z.infer<typeof IconNodeSchema>;
411
- export type HtmlNode = z.infer<typeof HtmlNodeSchema>;
412
- export type HtmlNodeData = z.infer<typeof HtmlNodeDataSchema>;
413
- export type ShapeKind = z.infer<typeof ShapeKindSchema>;
314
+ export type NodeType = z.infer<typeof NodeTypeSchema>;
414
315
  export type ColorToken = z.infer<typeof ColorTokenSchema>;
415
316
  export type Connector = z.infer<typeof ConnectorSchema>;
416
317
  export type ConnectorStyle = z.infer<typeof ConnectorStyleSchema>;
@@ -429,63 +330,38 @@ export type StateSource = z.infer<typeof StateSourceSchema>;
429
330
  // What lives on disk in <project>/flow.json after the split.
430
331
  // =============================================================================
431
332
 
432
- const FlowNodeDataBaseShape = {
433
- name: z.string().min(1),
434
- stateSource: StateSourceSchema,
435
- handlerModule: z.string().optional(),
436
- icon: z.string().optional(),
437
- ...NodeDescriptionBaseShape,
438
- };
439
-
440
- const FlowPlayNodeDataSchema = z
441
- .object({
442
- ...FlowNodeDataBaseShape,
443
- playAction: PlayActionSchema,
444
- statusAction: StatusActionSchema.optional(),
445
- })
446
- .strict();
447
-
448
- const FlowStateNodeDataSchema = z
333
+ const FlowGeometricNodeData = z
449
334
  .object({
450
- ...FlowNodeDataBaseShape,
451
- playAction: PlayActionSchema.optional(),
452
- statusAction: StatusActionSchema.optional(),
335
+ ...NodeSemanticBaseShape,
336
+ ...NodeCapabilitiesShape,
453
337
  })
454
338
  .strict();
455
339
 
456
- const FlowShapeNodeDataSchema = z
457
- .object({
458
- shape: ShapeKindSchema,
459
- name: z.string().optional(),
460
- ...NodeDescriptionBaseShape,
461
- })
462
- .strict();
463
-
464
- const FlowImageNodeDataSchema = z
340
+ const FlowImageNodeData = z
465
341
  .object({
342
+ ...NodeSemanticBaseShape,
343
+ ...NodeCapabilitiesShape,
466
344
  path: z.string().min(1).refine(isCleanRelativePath, {
467
345
  message: 'path must be a relative path under the project root (no absolute / traversal)',
468
346
  }),
469
347
  alt: z.string().optional(),
470
- ...NodeDescriptionBaseShape,
471
348
  })
472
349
  .strict();
473
350
 
474
- const FlowIconNodeDataSchema = z
351
+ const FlowHtmlNodeData = z
475
352
  .object({
476
- icon: z.string().min(1),
477
- alt: z.string().optional(),
478
- name: z.string().optional(),
479
- ...NodeDescriptionBaseShape,
353
+ ...NodeSemanticBaseShape,
354
+ ...NodeCapabilitiesShape,
355
+ html: z.string().optional(),
480
356
  })
481
357
  .strict();
482
358
 
483
- const FlowHtmlNodeDataSchema = z
359
+ const FlowIconNodeData = z
484
360
  .object({
485
- html: z.string().optional(),
486
- name: z.string().optional(),
487
- icon: z.string().optional(),
488
- ...NodeDescriptionBaseShape,
361
+ ...NodeSemanticBaseShape,
362
+ ...NodeCapabilitiesShape,
363
+ icon: z.string().min(1),
364
+ alt: z.string().optional(),
489
365
  })
490
366
  .strict();
491
367
 
@@ -493,61 +369,62 @@ const FlowNodeBaseShape = {
493
369
  id: z.string().min(1),
494
370
  };
495
371
 
496
- export const FlowPlayNodeSchema = z
497
- .object({
498
- ...FlowNodeBaseShape,
499
- type: z.literal('playNode'),
500
- data: FlowPlayNodeDataSchema,
501
- })
502
- .strict();
503
-
504
- export const FlowStateNodeSchema = z
505
- .object({
506
- ...FlowNodeBaseShape,
507
- type: z.literal('stateNode'),
508
- data: FlowStateNodeDataSchema,
509
- })
510
- .strict();
511
-
512
- export const FlowShapeNodeSchema = z
513
- .object({
514
- ...FlowNodeBaseShape,
515
- type: z.literal('shapeNode'),
516
- data: FlowShapeNodeDataSchema,
517
- })
518
- .strict();
372
+ const makeFlowGeometricSchema = (type: (typeof GEOMETRIC_NODE_TYPES)[number]) =>
373
+ z
374
+ .object({
375
+ ...FlowNodeBaseShape,
376
+ type: z.literal(type),
377
+ data: FlowGeometricNodeData,
378
+ })
379
+ .strict();
380
+
381
+ export const FlowRectangleNodeSchema = makeFlowGeometricSchema('rectangle');
382
+ export const FlowEllipseNodeSchema = makeFlowGeometricSchema('ellipse');
383
+ export const FlowStickyNodeSchema = makeFlowGeometricSchema('sticky');
384
+ export const FlowTextNodeSchema = makeFlowGeometricSchema('text');
385
+ export const FlowDatabaseNodeSchema = makeFlowGeometricSchema('database');
386
+ export const FlowServerNodeSchema = makeFlowGeometricSchema('server');
387
+ export const FlowUserNodeSchema = makeFlowGeometricSchema('user');
388
+ export const FlowQueueNodeSchema = makeFlowGeometricSchema('queue');
389
+ export const FlowCloudNodeSchema = makeFlowGeometricSchema('cloud');
519
390
 
520
391
  export const FlowImageNodeSchema = z
521
392
  .object({
522
393
  ...FlowNodeBaseShape,
523
- type: z.literal('imageNode'),
524
- data: FlowImageNodeDataSchema,
394
+ type: z.literal('image'),
395
+ data: FlowImageNodeData,
525
396
  })
526
397
  .strict();
527
398
 
528
- export const FlowIconNodeSchema = z
399
+ export const FlowHtmlNodeSchema = z
529
400
  .object({
530
401
  ...FlowNodeBaseShape,
531
- type: z.literal('iconNode'),
532
- data: FlowIconNodeDataSchema,
402
+ type: z.literal('html'),
403
+ data: FlowHtmlNodeData,
533
404
  })
534
405
  .strict();
535
406
 
536
- export const FlowHtmlNodeSchema = z
407
+ export const FlowIconNodeSchema = z
537
408
  .object({
538
409
  ...FlowNodeBaseShape,
539
- type: z.literal('htmlNode'),
540
- data: FlowHtmlNodeDataSchema,
410
+ type: z.literal('icon'),
411
+ data: FlowIconNodeData,
541
412
  })
542
413
  .strict();
543
414
 
544
415
  const FlowNodeSchema = z.discriminatedUnion('type', [
545
- FlowPlayNodeSchema,
546
- FlowStateNodeSchema,
547
- FlowShapeNodeSchema,
416
+ FlowRectangleNodeSchema,
417
+ FlowEllipseNodeSchema,
418
+ FlowStickyNodeSchema,
419
+ FlowTextNodeSchema,
420
+ FlowDatabaseNodeSchema,
421
+ FlowServerNodeSchema,
422
+ FlowUserNodeSchema,
423
+ FlowQueueNodeSchema,
424
+ FlowCloudNodeSchema,
548
425
  FlowImageNodeSchema,
549
- FlowIconNodeSchema,
550
426
  FlowHtmlNodeSchema,
427
+ FlowIconNodeSchema,
551
428
  ]);
552
429
 
553
430
  const FlowConnectorBaseShape = {
@@ -602,11 +479,10 @@ export type FlowNode = z.infer<typeof FlowNodeSchema>;
602
479
  export type FlowConnector = z.infer<typeof FlowConnectorSchema>;
603
480
 
604
481
  // Envelope-only flow shape for the `seeflow schema flow` surface. The full
605
- // FlowSchema validates the whole graph; this companion schema describes the
606
- // top-level shape without inlining every node variant or the connector
607
- // shape, so the runtime-introspectable JSON Schema stays compact. Authors
608
- // drill into `seeflow schema node` / `seeflow schema connector` for the
609
- // detailed shapes. Not used for validation — only the catalog reads it.
482
+ // FlowSchema validates the whole graph; this companion describes the top-level
483
+ // shape without inlining every node variant or the connector shape, so the
484
+ // runtime-introspectable JSON Schema stays compact. Authors drill into
485
+ // `seeflow schema node` / `seeflow schema connector` for the detailed shapes.
610
486
  export const FlowEnvelopeSchema = z
611
487
  .object({
612
488
  version: z.literal(2),
@@ -635,12 +511,12 @@ const NodeStyleSchema = z
635
511
  fontSize: z.number().positive().optional(),
636
512
  textColor: ColorTokenSchema.optional(),
637
513
  cornerRadius: z.number().min(0).optional(),
638
- // imageNode-specific
514
+ // type:'image'-specific
639
515
  borderWidth: z.number().min(1).max(8).optional(),
640
- // iconNode-specific
516
+ // type:'icon'-specific
641
517
  color: ColorTokenSchema.optional(),
642
518
  strokeWidth: z.number().min(0.5).max(4).optional(),
643
- // htmlNode-specific
519
+ // type:'html'-specific
644
520
  autoSize: z.boolean().optional(),
645
521
  })
646
522
  .strict();
package/src/server.ts CHANGED
@@ -100,7 +100,7 @@ export function createApp(options: CreateAppOptions = {}): Hono {
100
100
  // don't churn. Route is unauthenticated and stateless.
101
101
  app.get('/healthz', (c) => c.json({ status: 'ok' }));
102
102
 
103
- // Vendored runtime assets (e.g. @tailwindcss/browser@4 for htmlNode).
103
+ // Vendored runtime assets (e.g. @tailwindcss/browser@4 for type:'html').
104
104
  // Served identically in dev and prod so they don't depend on the web
105
105
  // bundle. The `{[A-Za-z0-9._-]+}` regex constrains :file to a single safe
106
106
  // segment, making traversal (`..`, `/`) impossible by construction.
@@ -159,7 +159,8 @@ interface StatusNode {
159
159
  function collectStatusNodes(demo: ResolvedFlow): StatusNode[] {
160
160
  const out: StatusNode[] = [];
161
161
  for (const node of demo.nodes) {
162
- if (node.type !== 'playNode' && node.type !== 'stateNode') continue;
162
+ // Capabilities are valid on every node type post-flat-types gate purely
163
+ // on `statusAction` presence, not on the visual kind.
163
164
  const action = node.data.statusAction;
164
165
  if (!action) continue;
165
166
  out.push({ nodeId: node.id, action });