@tuongaz/seeflow 0.1.25 → 0.1.27

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 (41) hide show
  1. package/dist/web/assets/index-BotEftAD.css +1 -0
  2. package/dist/web/assets/{index-BJ7xSozm.js → index-CdNWAi1U.js} +4 -4
  3. package/dist/web/assets/{index.es-B3xFOWmE.js → index.es-CPyvUCV3.js} +1 -1
  4. package/dist/web/assets/{jspdf.es.min-Dh_oxn-h.js → jspdf.es.min-Dkq0NSxE.js} +3 -3
  5. package/dist/web/index.html +2 -2
  6. package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +14 -77
  7. package/examples/ecommerce-platform/.seeflow/details/api-gateway.md +14 -0
  8. package/examples/ecommerce-platform/.seeflow/details/auth-service.md +9 -0
  9. package/examples/ecommerce-platform/.seeflow/details/cart-service.md +10 -0
  10. package/examples/ecommerce-platform/.seeflow/details/notification-service.md +13 -0
  11. package/examples/ecommerce-platform/.seeflow/details/order-service.md +16 -0
  12. package/examples/ecommerce-platform/.seeflow/details/payment-service.md +16 -0
  13. package/examples/ecommerce-platform/.seeflow/details/product-service.md +10 -0
  14. package/examples/ecommerce-platform/.seeflow/style.json +85 -0
  15. package/examples/order-pipeline/.seeflow/architecture.json +93 -0
  16. package/examples/order-pipeline/.seeflow/details/fulfillment-service.md +21 -0
  17. package/examples/order-pipeline/.seeflow/details/inventory-service.md +23 -0
  18. package/examples/order-pipeline/.seeflow/details/payment-service.md +23 -0
  19. package/examples/order-pipeline/.seeflow/details/post-orders.md +19 -0
  20. package/examples/order-pipeline/.seeflow/scripts/play.ts +2 -2
  21. package/examples/order-pipeline/.seeflow/style.json +42 -0
  22. package/package.json +1 -1
  23. package/src/api.ts +118 -118
  24. package/src/cli.ts +13 -13
  25. package/src/demo.ts +6 -6
  26. package/src/diagram.ts +4 -4
  27. package/src/events.ts +14 -14
  28. package/src/file-ref.ts +79 -0
  29. package/src/mcp.ts +117 -89
  30. package/src/merge.ts +190 -0
  31. package/src/operations.ts +415 -416
  32. package/src/proxy.ts +31 -31
  33. package/src/registry.ts +32 -20
  34. package/src/schema.ts +252 -8
  35. package/src/sdk-template.ts +2 -2
  36. package/src/sdk-writer.ts +2 -2
  37. package/src/server.ts +2 -2
  38. package/src/status-runner.ts +34 -38
  39. package/src/watcher.ts +165 -114
  40. package/dist/web/assets/index-Dwa7Bp5j.css +0 -1
  41. package/examples/order-pipeline/.seeflow/seeflow.json +0 -123
package/src/api.ts CHANGED
@@ -19,20 +19,22 @@ import {
19
19
  PositionBodySchema,
20
20
  RegisterBodySchema,
21
21
  ReorderBodySchema,
22
+ type ValidateBody,
22
23
  addConnectorImpl,
23
24
  addNodeImpl,
24
25
  createProjectImpl,
25
26
  deleteConnectorImpl,
26
- deleteDemoImpl,
27
+ deleteFlowImpl,
27
28
  deleteNodeImpl,
28
- getDemoImpl,
29
+ getFlowImpl,
29
30
  listDemosImpl,
30
31
  moveNodeImpl,
31
32
  patchConnectorImpl,
32
33
  patchNodeImpl,
33
- registerDemoImpl,
34
+ registerFlowImpl,
34
35
  reorderNodeImpl,
35
- resolveDemoPath,
36
+ resolveFilePath,
37
+ validateImpl,
36
38
  } from './operations.ts';
37
39
  import type { ProcessSpawner } from './process-spawner.ts';
38
40
  import {
@@ -45,13 +47,14 @@ import {
45
47
  stopAllPlays as defaultStopAllPlays,
46
48
  } from './proxy.ts';
47
49
  import type { Registry } from './registry.ts';
48
- import { DemoSchema } from './schema.ts';
50
+ import { FlowSchema } from './schema.ts';
49
51
  import { type Spawner, defaultSpawner } from './shellout.ts';
50
52
  import type { StatusRunner } from './status-runner.ts';
51
- import type { DemoWatcher } from './watcher.ts';
53
+ import { readMergedFlow } from './watcher.ts';
54
+ import type { FlowWatcher } from './watcher.ts';
52
55
 
53
56
  const EmitBodySchema = z.object({
54
- demoId: z.string().min(1),
57
+ flowId: z.string().min(1),
55
58
  nodeId: z.string().min(1),
56
59
  status: z.enum(['running', 'done', 'error']),
57
60
  runId: z.string().optional(),
@@ -167,7 +170,7 @@ function pickUploadFilename(assetsDir: string, base: string, ext: string): strin
167
170
  export interface ApiOptions {
168
171
  registry: Registry;
169
172
  events?: EventBus;
170
- watcher?: DemoWatcher;
173
+ watcher?: FlowWatcher;
171
174
  /** Injectable shellout for tests; defaults to Bun.spawn fire-and-forget. */
172
175
  spawner?: Spawner;
173
176
  /** Override `process.platform` for tests covering darwin/win32/linux branches. */
@@ -195,7 +198,7 @@ export interface ApiOptions {
195
198
  export interface ProxyFacade {
196
199
  runPlay(options: RunPlayOptions): Promise<PlayResult>;
197
200
  runReset(options: RunResetOptions): Promise<ResetResult>;
198
- stopAllPlays(demoId: string): Promise<void>;
201
+ stopAllPlays(flowId: string): Promise<void>;
199
202
  }
200
203
 
201
204
  export const defaultProxyFacade: ProxyFacade = {
@@ -213,7 +216,7 @@ export function createApi(options: ApiOptions): Hono {
213
216
  const projectBaseDir = options.projectBaseDir;
214
217
  const api = new Hono();
215
218
 
216
- api.post('/demos/register', async (c) => {
219
+ api.post('/flows/register', async (c) => {
217
220
  let body: unknown;
218
221
  try {
219
222
  body = await c.req.json();
@@ -226,16 +229,16 @@ export function createApi(options: ApiOptions): Hono {
226
229
  return c.json({ error: 'Invalid register body', issues: parsed.error.issues }, 400);
227
230
  }
228
231
 
229
- const result = await registerDemoImpl({ registry, watcher }, parsed.data);
232
+ const result = await registerFlowImpl({ registry, watcher }, parsed.data);
230
233
  switch (result.kind) {
231
234
  case 'ok':
232
235
  return c.json(result.data);
233
236
  case 'fileNotFound':
234
- return c.json({ error: `Demo file not found: ${result.path}` }, 400);
237
+ return c.json({ error: `Flow file not found: ${result.path}` }, 400);
235
238
  case 'badJson':
236
- return c.json({ error: 'Demo file is not valid JSON', detail: result.detail }, 400);
239
+ return c.json({ error: 'Flow file is not valid JSON', detail: result.detail }, 400);
237
240
  case 'badSchema':
238
- return c.json({ error: 'Demo file failed schema validation', issues: result.issues }, 400);
241
+ return c.json({ error: 'Flow file failed schema validation', issues: result.issues }, 400);
239
242
  case 'sdkWriteFailed':
240
243
  return c.json(
241
244
  {
@@ -248,12 +251,12 @@ export function createApi(options: ApiOptions): Hono {
248
251
  }
249
252
  });
250
253
 
251
- // POST /api/demos/validate — dry-run validation. The skill's diagram
254
+ // POST /api/flows/validate — dry-run validation. The skill's diagram
252
255
  // pipeline calls this between assemble and register to decide whether to
253
256
  // rewire. Runs the Zod schema, the soft node cap, and the tier playability
254
257
  // check. Filesystem-bound checks (harness coverage, event emitter index)
255
258
  // stay in the skill since the studio doesn't see the user's $TARGET.
256
- api.post('/demos/validate', async (c) => {
259
+ api.post('/flows/validate', async (c) => {
257
260
  let body: unknown;
258
261
  try {
259
262
  body = await c.req.json();
@@ -267,6 +270,23 @@ export function createApi(options: ApiOptions): Hono {
267
270
  return c.json(validateDemo(parsed.data));
268
271
  });
269
272
 
273
+ // POST /api/validate — stateless schema validator for the architecture +
274
+ // optional style files. No flow id, no registry side-effects, no file://
275
+ // resolution (validation is structural only). Returns 200 even on
276
+ // validation failure — the result is the validation report itself.
277
+ api.post('/validate', async (c) => {
278
+ let body: unknown;
279
+ try {
280
+ body = await c.req.json();
281
+ } catch {
282
+ return c.json({ error: 'Invalid JSON body' }, 400);
283
+ }
284
+ if (!body || typeof body !== 'object' || !('architecture' in body)) {
285
+ return c.json({ error: 'Body must be { architecture, style? }' }, 400);
286
+ }
287
+ return c.json(validateImpl(body as ValidateBody));
288
+ });
289
+
270
290
  // POST /api/diagram/propose-scope — Phase 2 helper. The skill POSTs the
271
291
  // scan-result.json shape and gets back ranked entry-point candidates.
272
292
  // Pure compute; skill writes the response to intermediate/entry-candidates.json.
@@ -345,20 +365,20 @@ export function createApi(options: ApiOptions): Hono {
345
365
  }
346
366
  });
347
367
 
348
- api.get('/demos', (c) => {
368
+ api.get('/flows', (c) => {
349
369
  const result = listDemosImpl({ registry });
350
370
  return c.json(result.data);
351
371
  });
352
372
 
353
- api.get('/demos/:id', async (c) => {
354
- const result = await getDemoImpl({ registry, watcher }, c.req.param('id'));
373
+ api.get('/flows/:id', async (c) => {
374
+ const result = await getFlowImpl({ registry, watcher }, c.req.param('id'));
355
375
  switch (result.kind) {
356
376
  case 'ok':
357
377
  return c.json(result.data);
358
378
  case 'notFound':
359
379
  return c.json({ error: 'not found' }, 404);
360
380
  case 'fileNotFound':
361
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
381
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
362
382
  }
363
383
  });
364
384
 
@@ -548,8 +568,8 @@ export function createApi(options: ApiOptions): Hono {
548
568
  return c.json({ path: `assets/${finalName}` });
549
569
  });
550
570
 
551
- api.delete('/demos/:id', (c) => {
552
- const result = deleteDemoImpl({ registry, watcher }, c.req.param('id'));
571
+ api.delete('/flows/:id', (c) => {
572
+ const result = deleteFlowImpl({ registry, watcher }, c.req.param('id'));
553
573
  switch (result.kind) {
554
574
  case 'ok':
555
575
  return c.json({ ok: true });
@@ -558,7 +578,7 @@ export function createApi(options: ApiOptions): Hono {
558
578
  }
559
579
  });
560
580
 
561
- api.post('/demos/:id/play/:nodeId', async (c) => {
581
+ api.post('/flows/:id/play/:nodeId', async (c) => {
562
582
  const id = c.req.param('id');
563
583
  const nodeId = c.req.param('nodeId');
564
584
  const entry = registry.getById(id);
@@ -567,27 +587,18 @@ export function createApi(options: ApiOptions): Hono {
567
587
 
568
588
  // Always re-read from disk so the user's most recent edit (validated or
569
589
  // not yet observed by the watcher) drives the actual fetch.
570
- const fullPath = resolveDemoPath(entry.repoPath, entry.demoPath);
590
+ const fullPath = resolveFilePath(entry.repoPath, entry.architecturePath);
571
591
  if (!existsSync(fullPath)) {
572
- return c.json({ error: `Demo file not found: ${fullPath}` }, 404);
573
- }
574
- let raw: unknown;
575
- try {
576
- raw = await Bun.file(fullPath).json();
577
- } catch (err) {
578
- return c.json(
579
- {
580
- error: `Demo file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
581
- },
582
- 400,
583
- );
592
+ return c.json({ error: `Flow file not found: ${fullPath}` }, 404);
584
593
  }
585
- const parsed = DemoSchema.safeParse(raw);
586
- if (!parsed.success) {
587
- return c.json({ error: 'Demo failed schema validation', issues: parsed.error.issues }, 400);
594
+ const merged = readMergedFlow(fullPath);
595
+ if (!merged.flow) {
596
+ const error = merged.error ?? 'Flow read failed';
597
+ const status = error.startsWith('Invalid JSON in') ? 400 : 400;
598
+ return c.json({ error }, status);
588
599
  }
589
600
 
590
- const node = parsed.data.nodes.find((n) => n.id === nodeId);
601
+ const node = merged.flow.nodes.find((n) => n.id === nodeId);
591
602
  if (!node) return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
592
603
  if (
593
604
  node.type === 'shapeNode' ||
@@ -613,7 +624,7 @@ export function createApi(options: ApiOptions): Hono {
613
624
 
614
625
  const result = await proxy.runPlay({
615
626
  events,
616
- demoId: id,
627
+ flowId: id,
617
628
  nodeId,
618
629
  cwd: entry.repoPath,
619
630
  action: node.data.playAction,
@@ -628,40 +639,29 @@ export function createApi(options: ApiOptions): Hono {
628
639
  return c.json(result);
629
640
  });
630
641
 
631
- // POST /api/demos/:id/reset — the "Restart demo" workflow (US-008). Order:
642
+ // POST /api/flows/:id/reset — the "Restart demo" workflow (US-008). Order:
632
643
  // 1. Stop every live play-script + every long-running status-script for
633
644
  // this demo in parallel — both must complete before any reset script
634
645
  // spawns so the script sees no stragglers.
635
646
  // 2. Run the demo's `resetAction` script (if declared); any non-zero exit
636
647
  // becomes a 502 to the caller but does NOT suppress reload/restart.
637
- // 3. Broadcast `demo:reload` unconditionally so the canvas re-fetches.
648
+ // 3. Broadcast `flow:reload` unconditionally so the canvas re-fetches.
638
649
  // 4. Fire-and-forget `statusRunner.restart` so the next status batch is
639
650
  // spawning by the time the response lands. Individual spawn failures
640
651
  // surface via console.warn but never fail the /reset call.
641
- api.post('/demos/:id/reset', async (c) => {
652
+ api.post('/flows/:id/reset', async (c) => {
642
653
  const id = c.req.param('id');
643
654
  const entry = registry.getById(id);
644
655
  if (!entry) return c.json({ error: 'unknown demo' }, 404);
645
656
  if (!events) return c.json({ error: 'events not enabled' }, 500);
646
657
 
647
- const fullPath = resolveDemoPath(entry.repoPath, entry.demoPath);
658
+ const fullPath = resolveFilePath(entry.repoPath, entry.architecturePath);
648
659
  if (!existsSync(fullPath)) {
649
- return c.json({ error: `Demo file not found: ${fullPath}` }, 404);
650
- }
651
- let raw: unknown;
652
- try {
653
- raw = await Bun.file(fullPath).json();
654
- } catch (err) {
655
- return c.json(
656
- {
657
- error: `Demo file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
658
- },
659
- 400,
660
- );
660
+ return c.json({ error: `Flow file not found: ${fullPath}` }, 404);
661
661
  }
662
- const parsed = DemoSchema.safeParse(raw);
663
- if (!parsed.success) {
664
- return c.json({ error: 'Demo failed schema validation', issues: parsed.error.issues }, 400);
662
+ const merged = readMergedFlow(fullPath);
663
+ if (!merged.flow) {
664
+ return c.json({ error: merged.error ?? 'Flow read failed' }, 400);
665
665
  }
666
666
 
667
667
  // 1. Stop every play + status script in parallel. await BOTH before
@@ -672,7 +672,7 @@ export function createApi(options: ApiOptions): Hono {
672
672
  await Promise.all(stopPromises);
673
673
 
674
674
  // 2. Run resetAction (if declared).
675
- const resetAction = parsed.data.resetAction;
675
+ const resetAction = merged.flow.resetAction;
676
676
  let calledResetAction = false;
677
677
  let resetActionError: string | undefined;
678
678
 
@@ -680,7 +680,7 @@ export function createApi(options: ApiOptions): Hono {
680
680
  calledResetAction = true;
681
681
  const result = await proxy.runReset({
682
682
  events,
683
- demoId: id,
683
+ flowId: id,
684
684
  cwd: entry.repoPath,
685
685
  action: resetAction,
686
686
  });
@@ -693,8 +693,8 @@ export function createApi(options: ApiOptions): Hono {
693
693
  // the canvas should still refresh from disk in case the user just
694
694
  // edited the file.
695
695
  events.broadcast({
696
- type: 'demo:reload',
697
- demoId: id,
696
+ type: 'flow:reload',
697
+ flowId: id,
698
698
  payload: {},
699
699
  });
700
700
 
@@ -717,7 +717,7 @@ export function createApi(options: ApiOptions): Hono {
717
717
  // the second (and only other) place the studio mutates user files — the
718
718
  // first being the SDK helper write in `register`. Atomic write via tempfile
719
719
  // + rename keeps editor diffs clean and avoids corruption mid-write.
720
- api.patch('/demos/:id/nodes/:nodeId/position', async (c) => {
720
+ api.patch('/flows/:id/nodes/:nodeId/position', async (c) => {
721
721
  const id = c.req.param('id');
722
722
  const nodeId = c.req.param('nodeId');
723
723
 
@@ -736,14 +736,14 @@ export function createApi(options: ApiOptions): Hono {
736
736
  switch (result.kind) {
737
737
  case 'ok':
738
738
  return c.json({ ok: true, position: result.data.position });
739
- case 'demoNotFound':
739
+ case 'flowNotFound':
740
740
  return c.json({ error: 'unknown demo' }, 404);
741
741
  case 'fileNotFound':
742
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
742
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
743
743
  case 'badJson':
744
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
744
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
745
745
  case 'badSchema':
746
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
746
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
747
747
  case 'unknownNode':
748
748
  return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
749
749
  case 'writeFailed':
@@ -758,7 +758,7 @@ export function createApi(options: ApiOptions): Hono {
758
758
  // toBack (remove + push/unshift), and toIndex (pin to an absolute index)
759
759
  // which the undo path uses to faithfully revert forward/backward gestures
760
760
  // even if the array changed between the original op and the undo.
761
- api.patch('/demos/:id/nodes/:nodeId/order', async (c) => {
761
+ api.patch('/flows/:id/nodes/:nodeId/order', async (c) => {
762
762
  const id = c.req.param('id');
763
763
  const nodeId = c.req.param('nodeId');
764
764
 
@@ -777,14 +777,14 @@ export function createApi(options: ApiOptions): Hono {
777
777
  switch (result.kind) {
778
778
  case 'ok':
779
779
  return c.json({ ok: true });
780
- case 'demoNotFound':
780
+ case 'flowNotFound':
781
781
  return c.json({ error: 'unknown demo' }, 404);
782
782
  case 'fileNotFound':
783
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
783
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
784
784
  case 'badJson':
785
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
785
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
786
786
  case 'badSchema':
787
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
787
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
788
788
  case 'unknownNode':
789
789
  return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
790
790
  case 'writeFailed':
@@ -797,9 +797,9 @@ export function createApi(options: ApiOptions): Hono {
797
797
  // the high-frequency drag fast-path above) flows through here. The mutation
798
798
  // is performed against the raw parsed JSON (so unknown v2 fields the schema
799
799
  // doesn't yet recognize survive round-trips) and the WHOLE resulting demo
800
- // is re-validated through DemoSchema before commit, preventing partial
800
+ // is re-validated through FlowSchema before commit, preventing partial
801
801
  // writes from breaking invariants like the connector→node superRefine.
802
- api.patch('/demos/:id/nodes/:nodeId', async (c) => {
802
+ api.patch('/flows/:id/nodes/:nodeId', async (c) => {
803
803
  const id = c.req.param('id');
804
804
  const nodeId = c.req.param('nodeId');
805
805
 
@@ -818,14 +818,14 @@ export function createApi(options: ApiOptions): Hono {
818
818
  switch (result.kind) {
819
819
  case 'ok':
820
820
  return c.json({ ok: true });
821
- case 'demoNotFound':
821
+ case 'flowNotFound':
822
822
  return c.json({ error: 'unknown demo' }, 404);
823
823
  case 'fileNotFound':
824
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
824
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
825
825
  case 'badJson':
826
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
826
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
827
827
  case 'badSchema':
828
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
828
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
829
829
  case 'unknownNode':
830
830
  return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
831
831
  case 'writeFailed':
@@ -834,9 +834,9 @@ export function createApi(options: ApiOptions): Hono {
834
834
  });
835
835
 
836
836
  // POST a new node into the demo. Body is the node payload (id auto-generated
837
- // server-side if absent). Atomicity + final-DemoSchema validation match the
837
+ // server-side if absent). Atomicity + final-FlowSchema validation match the
838
838
  // PATCH path above, so a malformed node never produces a half-written file.
839
- api.post('/demos/:id/nodes', async (c) => {
839
+ api.post('/flows/:id/nodes', async (c) => {
840
840
  const id = c.req.param('id');
841
841
 
842
842
  let body: unknown;
@@ -853,25 +853,25 @@ export function createApi(options: ApiOptions): Hono {
853
853
  switch (result.kind) {
854
854
  case 'ok':
855
855
  return c.json({ ok: true, id: result.data.id, node: result.data.node });
856
- case 'demoNotFound':
856
+ case 'flowNotFound':
857
857
  return c.json({ error: 'unknown demo' }, 404);
858
858
  case 'fileNotFound':
859
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
859
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
860
860
  case 'badJson':
861
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
861
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
862
862
  case 'badSchema':
863
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
863
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
864
864
  case 'writeFailed':
865
865
  return c.json({ error: `Failed to write demo file: ${result.message}` }, 500);
866
866
  }
867
867
  });
868
868
 
869
869
  // DELETE a node and cascade-remove every connector with source === nodeId or
870
- // target === nodeId in the same atomic write. Final-DemoSchema validation
870
+ // target === nodeId in the same atomic write. Final-FlowSchema validation
871
871
  // is still run after the mutation — connector cascade closure means it
872
872
  // should always pass, but the check makes the failure mode honest if the
873
873
  // file had a pre-existing schema violation we'd otherwise paper over.
874
- api.delete('/demos/:id/nodes/:nodeId', async (c) => {
874
+ api.delete('/flows/:id/nodes/:nodeId', async (c) => {
875
875
  const id = c.req.param('id');
876
876
  const nodeId = c.req.param('nodeId');
877
877
 
@@ -879,14 +879,14 @@ export function createApi(options: ApiOptions): Hono {
879
879
  switch (result.kind) {
880
880
  case 'ok':
881
881
  return c.json({ ok: true });
882
- case 'demoNotFound':
882
+ case 'flowNotFound':
883
883
  return c.json({ error: 'unknown demo' }, 404);
884
884
  case 'fileNotFound':
885
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
885
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
886
886
  case 'badJson':
887
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
887
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
888
888
  case 'badSchema':
889
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
889
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
890
890
  case 'unknownNode':
891
891
  return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
892
892
  case 'writeFailed':
@@ -897,11 +897,11 @@ export function createApi(options: ApiOptions): Hono {
897
897
  // PATCH a single connector — partial update of label/style/color/direction
898
898
  // and (optionally) kind + per-kind payload fields. When `kind` changes,
899
899
  // stale kind-specific fields are dropped before the merge. The whole demo
900
- // is re-validated through DemoSchema before commit so the discriminated
900
+ // is re-validated through FlowSchema before commit so the discriminated
901
901
  // union catches missing-required-fields (e.g. kind='event' without
902
902
  // eventName) and the superRefine still gates source/target referential
903
903
  // integrity.
904
- api.patch('/demos/:id/connectors/:connId', async (c) => {
904
+ api.patch('/flows/:id/connectors/:connId', async (c) => {
905
905
  const id = c.req.param('id');
906
906
  const connId = c.req.param('connId');
907
907
 
@@ -920,14 +920,14 @@ export function createApi(options: ApiOptions): Hono {
920
920
  switch (result.kind) {
921
921
  case 'ok':
922
922
  return c.json({ ok: true });
923
- case 'demoNotFound':
923
+ case 'flowNotFound':
924
924
  return c.json({ error: 'unknown demo' }, 404);
925
925
  case 'fileNotFound':
926
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
926
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
927
927
  case 'badJson':
928
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
928
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
929
929
  case 'badSchema':
930
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
930
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
931
931
  case 'unknownConnector':
932
932
  return c.json({ error: `Unknown connectorId: ${connId}` }, 404);
933
933
  case 'writeFailed':
@@ -938,8 +938,8 @@ export function createApi(options: ApiOptions): Hono {
938
938
  // POST a new connector. Body is the connector payload; `id` is auto-generated
939
939
  // server-side if absent and `kind` defaults to 'default' (the no-semantics
940
940
  // user-drawn variant). Source/target referential integrity is enforced by
941
- // DemoSchema's superRefine on the post-mutation parse.
942
- api.post('/demos/:id/connectors', async (c) => {
941
+ // FlowSchema's superRefine on the post-mutation parse.
942
+ api.post('/flows/:id/connectors', async (c) => {
943
943
  const id = c.req.param('id');
944
944
 
945
945
  let body: unknown;
@@ -960,14 +960,14 @@ export function createApi(options: ApiOptions): Hono {
960
960
  switch (result.kind) {
961
961
  case 'ok':
962
962
  return c.json({ ok: true, id: result.data.id });
963
- case 'demoNotFound':
963
+ case 'flowNotFound':
964
964
  return c.json({ error: 'unknown demo' }, 404);
965
965
  case 'fileNotFound':
966
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
966
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
967
967
  case 'badJson':
968
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
968
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
969
969
  case 'badSchema':
970
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
970
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
971
971
  case 'writeFailed':
972
972
  return c.json({ error: `Failed to write demo file: ${result.message}` }, 500);
973
973
  }
@@ -975,7 +975,7 @@ export function createApi(options: ApiOptions): Hono {
975
975
 
976
976
  // DELETE a connector. Just removes the entry from demo.connectors — node
977
977
  // deletion is what cascades, not connector deletion.
978
- api.delete('/demos/:id/connectors/:connId', async (c) => {
978
+ api.delete('/flows/:id/connectors/:connId', async (c) => {
979
979
  const id = c.req.param('id');
980
980
  const connId = c.req.param('connId');
981
981
 
@@ -983,14 +983,14 @@ export function createApi(options: ApiOptions): Hono {
983
983
  switch (result.kind) {
984
984
  case 'ok':
985
985
  return c.json({ ok: true });
986
- case 'demoNotFound':
986
+ case 'flowNotFound':
987
987
  return c.json({ error: 'unknown demo' }, 404);
988
988
  case 'fileNotFound':
989
- return c.json({ error: `Demo file not found: ${result.path}` }, 404);
989
+ return c.json({ error: `Flow file not found: ${result.path}` }, 404);
990
990
  case 'badJson':
991
- return c.json({ error: `Demo file is not valid JSON: ${result.message}` }, 400);
991
+ return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
992
992
  case 'badSchema':
993
- return c.json({ error: 'Demo failed schema validation', issues: result.issues }, 400);
993
+ return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
994
994
  case 'unknownConnector':
995
995
  return c.json({ error: `Unknown connectorId: ${connId}` }, 404);
996
996
  case 'writeFailed':
@@ -1013,9 +1013,9 @@ export function createApi(options: ApiOptions): Hono {
1013
1013
  return c.json({ error: 'Invalid emit body', issues: parsed.error.issues }, 400);
1014
1014
  }
1015
1015
 
1016
- const { demoId, nodeId, status, runId, payload } = parsed.data;
1017
- if (!registry.getById(demoId)) {
1018
- return c.json({ error: `Unknown demoId: ${demoId}` }, 404);
1016
+ const { flowId, nodeId, status, runId, payload } = parsed.data;
1017
+ if (!registry.getById(flowId)) {
1018
+ return c.json({ error: `Unknown flowId: ${flowId}` }, 404);
1019
1019
  }
1020
1020
 
1021
1021
  const extras =
@@ -1027,7 +1027,7 @@ export function createApi(options: ApiOptions): Hono {
1027
1027
 
1028
1028
  events.broadcast({
1029
1029
  type: EMIT_STATUS_TO_EVENT[status],
1030
- demoId,
1030
+ flowId,
1031
1031
  payload: eventPayload,
1032
1032
  });
1033
1033
 
@@ -1035,9 +1035,9 @@ export function createApi(options: ApiOptions): Hono {
1035
1035
  });
1036
1036
 
1037
1037
  api.get('/events', (c) => {
1038
- const demoId = c.req.query('demoId');
1039
- if (!demoId) return c.json({ error: 'demoId query param required' }, 400);
1040
- if (!registry.getById(demoId)) return c.json({ error: 'unknown demoId' }, 404);
1038
+ const flowId = c.req.query('flowId');
1039
+ if (!flowId) return c.json({ error: 'flowId query param required' }, 400);
1040
+ if (!registry.getById(flowId)) return c.json({ error: 'unknown flowId' }, 404);
1041
1041
  if (!events) return c.json({ error: 'events not enabled' }, 500);
1042
1042
 
1043
1043
  return streamSSE(c, async (stream) => {
@@ -1053,7 +1053,7 @@ export function createApi(options: ApiOptions): Hono {
1053
1053
  }
1054
1054
  };
1055
1055
 
1056
- const unsubscribe = events.subscribe(demoId, (e) => {
1056
+ const unsubscribe = events.subscribe(flowId, (e) => {
1057
1057
  queue.push({ event: e.type, data: JSON.stringify({ ts: e.ts, ...(e.payload as object) }) });
1058
1058
  wake();
1059
1059
  });
@@ -1068,7 +1068,7 @@ export function createApi(options: ApiOptions): Hono {
1068
1068
  // and trigger a re-fetch on the frontend.
1069
1069
  await stream.writeSSE({
1070
1070
  event: 'hello',
1071
- data: JSON.stringify({ demoId, ts: Date.now() }),
1071
+ data: JSON.stringify({ flowId, ts: Date.now() }),
1072
1072
  });
1073
1073
 
1074
1074
  try {