@particle-academy/fancy-flow 0.65.2 → 0.66.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 (105) hide show
  1. package/README.md +100 -3
  2. package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
  3. package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
  4. package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
  5. package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
  6. package/dist/chunk-2QXKDLGT.js +58 -0
  7. package/dist/chunk-2QXKDLGT.js.map +1 -0
  8. package/dist/{chunk-77V4QC6Y.js → chunk-B7HKJJVV.js} +3 -3
  9. package/dist/{chunk-77V4QC6Y.js.map → chunk-B7HKJJVV.js.map} +1 -1
  10. package/dist/{chunk-MBVX4ZRB.js → chunk-FNYLKSFJ.js} +3 -3
  11. package/dist/{chunk-MBVX4ZRB.js.map → chunk-FNYLKSFJ.js.map} +1 -1
  12. package/dist/{chunk-JF6WCRBU.js → chunk-HQGOFGAL.js} +861 -40
  13. package/dist/chunk-HQGOFGAL.js.map +1 -0
  14. package/dist/{chunk-5H54OTKT.js → chunk-IEYCNFXZ.js} +3 -3
  15. package/dist/{chunk-5H54OTKT.js.map → chunk-IEYCNFXZ.js.map} +1 -1
  16. package/dist/{chunk-W5DPKJY4.js → chunk-MTONBM53.js} +4 -4
  17. package/dist/chunk-MTONBM53.js.map +1 -0
  18. package/dist/{chunk-RIAFHQT5.js → chunk-PNHZHEWE.js} +3 -3
  19. package/dist/{chunk-RIAFHQT5.js.map → chunk-PNHZHEWE.js.map} +1 -1
  20. package/dist/{chunk-EO6444T2.js → chunk-UCCTXJXL.js} +4 -4
  21. package/dist/{chunk-EO6444T2.js.map → chunk-UCCTXJXL.js.map} +1 -1
  22. package/dist/connectors.d.cts +2 -2
  23. package/dist/connectors.d.ts +2 -2
  24. package/dist/durable/index.d.cts +2 -2
  25. package/dist/durable/index.d.ts +2 -2
  26. package/dist/durable.cjs +837 -47
  27. package/dist/durable.cjs.map +1 -1
  28. package/dist/durable.js +2 -3
  29. package/dist/durable.js.map +1 -1
  30. package/dist/engine.cjs +843 -132
  31. package/dist/engine.cjs.map +1 -1
  32. package/dist/engine.d.cts +6 -7
  33. package/dist/engine.d.ts +6 -7
  34. package/dist/engine.js +5 -6
  35. package/dist/engine.js.map +1 -1
  36. package/dist/fields/react-fancy.d.cts +3 -3
  37. package/dist/fields/react-fancy.d.ts +3 -3
  38. package/dist/index.cjs +853 -140
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +65 -36
  41. package/dist/index.d.ts +65 -36
  42. package/dist/index.js +19 -18
  43. package/dist/index.js.map +1 -1
  44. package/dist/layout/index.d.cts +1 -1
  45. package/dist/layout/index.d.ts +1 -1
  46. package/dist/llm/prism.cjs.map +1 -1
  47. package/dist/llm/prism.d.cts +1 -2
  48. package/dist/llm/prism.d.ts +1 -2
  49. package/dist/llm/prism.js +1 -1
  50. package/dist/llm/vercel-ai.cjs.map +1 -1
  51. package/dist/llm/vercel-ai.d.cts +1 -2
  52. package/dist/llm/vercel-ai.d.ts +1 -2
  53. package/dist/llm/vercel-ai.js +1 -1
  54. package/dist/registry/index.d.cts +5 -6
  55. package/dist/registry/index.d.ts +5 -6
  56. package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
  57. package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
  58. package/dist/registry.cjs +867 -37
  59. package/dist/registry.cjs.map +1 -1
  60. package/dist/registry.js +3 -3
  61. package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
  62. package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
  63. package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
  64. package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
  65. package/dist/runtime/index.d.cts +5 -5
  66. package/dist/runtime/index.d.ts +5 -5
  67. package/dist/runtime.cjs +837 -36
  68. package/dist/runtime.cjs.map +1 -1
  69. package/dist/runtime.js +4 -4
  70. package/dist/schema/index.d.cts +19 -6
  71. package/dist/schema/index.d.ts +19 -6
  72. package/dist/schema.cjs +837 -36
  73. package/dist/schema.cjs.map +1 -1
  74. package/dist/schema.js +4 -4
  75. package/dist/screens.cjs +837 -36
  76. package/dist/screens.cjs.map +1 -1
  77. package/dist/screens.d.cts +2 -2
  78. package/dist/screens.d.ts +2 -2
  79. package/dist/screens.js +5 -5
  80. package/dist/terminal/fancy-term-host.cjs +99 -0
  81. package/dist/terminal/fancy-term-host.cjs.map +1 -0
  82. package/dist/terminal/fancy-term-host.d.cts +76 -0
  83. package/dist/terminal/fancy-term-host.d.ts +76 -0
  84. package/dist/terminal/fancy-term-host.js +92 -0
  85. package/dist/terminal/fancy-term-host.js.map +1 -0
  86. package/dist/types-B-Syk9-M.d.cts +620 -0
  87. package/dist/types-B-Syk9-M.d.ts +620 -0
  88. package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
  89. package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
  90. package/dist/ux.cjs +837 -36
  91. package/dist/ux.cjs.map +1 -1
  92. package/dist/ux.d.cts +2 -2
  93. package/dist/ux.d.ts +2 -2
  94. package/dist/ux.js +2 -2
  95. package/package.json +11 -1
  96. package/dist/capabilities-BYa5p5jw.d.cts +0 -112
  97. package/dist/capabilities-COOXRiNL.d.ts +0 -112
  98. package/dist/chunk-JF6WCRBU.js.map +0 -1
  99. package/dist/chunk-UM4C46AF.js +0 -103
  100. package/dist/chunk-UM4C46AF.js.map +0 -1
  101. package/dist/chunk-USL4FMFU.js +0 -41
  102. package/dist/chunk-USL4FMFU.js.map +0 -1
  103. package/dist/chunk-W5DPKJY4.js.map +0 -1
  104. package/dist/types-JFYjPJAG.d.cts +0 -333
  105. package/dist/types-JFYjPJAG.d.ts +0 -333
package/README.md CHANGED
@@ -339,9 +339,11 @@ function MyEditor() {
339
339
 
340
340
  ## Node kinds
341
341
 
342
- 27 builtins, grouped by category. Ids are namespaced (`@particle-academy/<name>`)
342
+ 31 builtins, grouped by category. Ids are namespaced (`@particle-academy/<name>`)
343
343
  and every bare name below is a permanent alias, so graphs saved against either
344
- keep resolving.
344
+ keep resolving. The count is checked against the registry by a test — a
345
+ hand-maintained mirror that nothing compares is the defect this kit keeps
346
+ finding, so it is compared.
345
347
 
346
348
  | Category | Kinds |
347
349
  |---|---|
@@ -352,7 +354,8 @@ keep resolving.
352
354
  | `data` | `variable`, `memory_store`, `data_store` |
353
355
  | `io` | `api_request`, `webhook_out` |
354
356
  | `output` | `output`, `log` |
355
- | `layout` / `annotation` | `lane`, `note` — visual only, never executed |
357
+ | `io` (terminal) | `terminal_run`, `terminal_send`, `terminal_await`inside a terminal lane |
358
+ | `layout` / `annotation` | `lane`, `terminal_lane`, `note` — visual only, never executed |
356
359
 
357
360
  Don't hand-copy this list into generated graphs — it moves. Enumerate at
358
361
  runtime with `getNodeKind()` / the registry, or ask the Fancy MCP's `list_nodes`.
@@ -363,6 +366,100 @@ Custom nodes plug in via xyflow's standard `nodeTypes` prop:
363
366
  <FlowCanvas nodeTypes={{ ...defaultNodeTypes, myNode: MyCustomNode }} ... />
364
367
  ```
365
368
 
369
+ ## Terminal lanes
370
+
371
+ A **terminal lane** owns one running terminal for the length of a workflow run,
372
+ so a graph can drive an interactive process — including an agent TUI like Claude
373
+ Code or Codex — instead of only firing one-shot commands.
374
+
375
+ Three promises, and all three are about WHEN rather than what:
376
+
377
+ 1. **One terminal per lane**, however many nodes are inside it. Two shells look
378
+ exactly like one that forgot a `cd`.
379
+ 2. **It opens at the first terminal node**, not at run start. A lane drawn
380
+ around nodes that mostly do other things costs nothing until something uses
381
+ it.
382
+ 3. **It stays open until the run finishes**, including when the run FAILS. A
383
+ leaked PTY looks like nothing at all until the machine is full of them.
384
+
385
+ Membership is the canvas's own `parentId`, which already persists into the
386
+ `WorkflowSchema` — so a headless runtime resolves exactly the grouping a person
387
+ drew, with no second association to keep in step.
388
+
389
+ ### The nodes
390
+
391
+ | Kind | For |
392
+ |---|---|
393
+ | `terminal_run` | Run a shell command and wait for its **exit code**. Shell only. |
394
+ | `terminal_send` | Type at whatever is running, without waiting. |
395
+ | `terminal_await` | Wait until the output matches. Returns capture groups in regex mode. |
396
+
397
+ `terminal_run` is the shell node and `terminal_send` + `terminal_await` are the
398
+ TUI pair, because a terminal is two different things depending on what is in it.
399
+ A shell answers and returns to a prompt, so the useful unit is "run this, tell me
400
+ what it said and whether it worked". A TUI never finishes — there is no exit code
401
+ to wait for — so there is only text going in and text coming out.
402
+
403
+ Matching runs against the **accumulated** output with escape sequences already
404
+ stripped, so you match what you SEE and a pattern split across PTY chunks still
405
+ resolves. Output that arrived before an await started is not lost. A wait
406
+ consumes through its match, so the same pattern does not resolve twice on one
407
+ line.
408
+
409
+ Three outcomes are reported distinctly — matched, timed out, and *the process
410
+ exited*. Collapsing the last two is how a dead shell gets reported as "timed out
411
+ waiting for X", sending whoever debugs it to lengthen a timeout on a process
412
+ that is not running.
413
+
414
+ ### Providing the terminal
415
+
416
+ Core declares the contract and imports no PTY — `node-pty` is a native addon
417
+ that would break every browser build. Same arrangement as `registerLlmClient`:
418
+
419
+ ```ts
420
+ import { registerTerminalHost } from "@particle-academy/fancy-flow/registry";
421
+
422
+ registerTerminalHost({
423
+ open: async ({ command, cwd, env, cols, rows }) => {
424
+ const pty = spawnYourPty(command ?? defaultShell(), { cwd, env, cols, rows });
425
+ return {
426
+ id: pty.pid.toString(),
427
+ write: (data) => pty.write(data),
428
+ onData: (listener) => { pty.onData(listener); return () => pty.dispose(); },
429
+ exited: new Promise((r) => pty.onExit(({ exitCode, signal }) => r({ exitCode, signal }))),
430
+ close: () => pty.kill(),
431
+ };
432
+ },
433
+ });
434
+ ```
435
+
436
+ There is deliberately **no `waitForOutput(pattern)` in the contract**. Matching
437
+ is derivable from `onData`, so putting it in the contract would mean every host
438
+ implements it — two implementations of one agreed rule, which is how matching
439
+ bugs end up differing per host. Core owns matching; the host owns the process.
440
+
441
+ ### The fancy-term-host adapter
442
+
443
+ If you already run `@particle-academy/fancy-term-host`, the adapter is on its
444
+ own subpath:
445
+
446
+ ```ts
447
+ import { terminalManager } from "@particle-academy/fancy-term-host";
448
+ import { useFancyTermHost } from "@particle-academy/fancy-flow/terminal/fancy-term-host";
449
+
450
+ useFancyTermHost(terminalManager(), { cwd: "/srv/app" });
451
+ ```
452
+
453
+ It takes the backend as an **argument** rather than importing the package.
454
+ `fancy-term-host` requires `node-pty` as a peer and imports it at module scope,
455
+ so depending on it here — even for types — would put a native build in every
456
+ fancy-flow install, including browser consumers who will never open a terminal.
457
+ Anyone who wants this adapter already holds a backend, so passing it in costs
458
+ them one argument and costs everyone else nothing.
459
+
460
+ **`fancy-flow-php` does not have this feature and is not going to.** It needs
461
+ desktop execution, which is not what the PHP runtime is for.
462
+
366
463
  ## LLM adapters
367
464
 
368
465
  `llm_router` (alias `llm_branch`) asks a model to pick one of a node's declared
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
- import { F as FlowGraph, a as FlowNode } from './types-JFYjPJAG.js';
3
+ import { F as FlowGraph, a as FlowNode } from './types-B-Syk9-M.js';
4
4
 
5
5
  /** Outcome of one node in a run, for annotating the graph after the fact. */
6
6
  type FlowNodeStatus = "ok" | "running" | "failed" | "skipped" | "pending";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
- import { F as FlowGraph, a as FlowNode } from './types-JFYjPJAG.cjs';
3
+ import { F as FlowGraph, a as FlowNode } from './types-B-Syk9-M.cjs';
4
4
 
5
5
  /** Outcome of one node in a run, for annotating the graph after the fact. */
6
6
  type FlowNodeStatus = "ok" | "running" | "failed" | "skipped" | "pending";
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { F as FlowGraph } from './types-JFYjPJAG.cjs';
4
- import { C as ConfigField } from './types-D71SKA5A.cjs';
3
+ import { F as FlowGraph } from './types-B-Syk9-M.cjs';
4
+ import { C as ConfigField } from './types-C2ATTmjN.cjs';
5
5
 
6
6
  /** What a host renderer is handed. */
7
7
  type ConfigFieldRenderContext = {
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { F as FlowGraph } from './types-JFYjPJAG.js';
4
- import { C as ConfigField } from './types-Ckhz-YwC.js';
3
+ import { F as FlowGraph } from './types-B-Syk9-M.js';
4
+ import { C as ConfigField } from './types-VunLGqrn.js';
5
5
 
6
6
  /** What a host renderer is handed. */
7
7
  type ConfigFieldRenderContext = {
@@ -0,0 +1,58 @@
1
+ // src/registry/capabilities.ts
2
+ var llmClient = null;
3
+ function registerLlmClient(client) {
4
+ llmClient = client;
5
+ return () => {
6
+ if (llmClient === client) llmClient = null;
7
+ };
8
+ }
9
+ function getLlmClient() {
10
+ return llmClient;
11
+ }
12
+ var terminalHost = null;
13
+ function registerTerminalHost(host) {
14
+ terminalHost = host;
15
+ return () => {
16
+ if (terminalHost === host) terminalHost = null;
17
+ };
18
+ }
19
+ function getTerminalHost() {
20
+ return terminalHost;
21
+ }
22
+ function isResolutionFailure(value) {
23
+ return typeof value === "object" && value !== null && "reason" in value && value.reason !== void 0;
24
+ }
25
+ var workflowResolver = null;
26
+ function registerWorkflowResolver(resolver) {
27
+ workflowResolver = resolver;
28
+ return () => {
29
+ if (workflowResolver === resolver) workflowResolver = null;
30
+ };
31
+ }
32
+ function getWorkflowResolver() {
33
+ return workflowResolver;
34
+ }
35
+ function capabilityStatus() {
36
+ let documentReady = false;
37
+ try {
38
+ documentReady = Boolean(globalThis.__fancyFlowDocumentAdapter);
39
+ } catch {
40
+ documentReady = false;
41
+ }
42
+ return {
43
+ llm: llmClient !== null,
44
+ workflow_resolver: workflowResolver !== null,
45
+ document: documentReady,
46
+ // Listed here for the reason this function exists at all. A graph with a
47
+ // terminal lane and no host registered fails PART WAY THROUGH — after the
48
+ // nodes before it have already run and possibly written somewhere — and the
49
+ // whole point of asking up front is to learn that before starting. Leaving
50
+ // it out would have made the one check designed to catch this report
51
+ // all-clear for exactly the case it was built for.
52
+ terminal: terminalHost !== null
53
+ };
54
+ }
55
+
56
+ export { capabilityStatus, getLlmClient, getTerminalHost, getWorkflowResolver, isResolutionFailure, registerLlmClient, registerTerminalHost, registerWorkflowResolver };
57
+ //# sourceMappingURL=chunk-2QXKDLGT.js.map
58
+ //# sourceMappingURL=chunk-2QXKDLGT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry/capabilities.ts"],"names":[],"mappings":";AAwDA,IAAI,SAAA,GAA8B,IAAA;AAG3B,SAAS,kBAAkB,MAAA,EAA+B;AAC/D,EAAA,SAAA,GAAY,MAAA;AACZ,EAAA,OAAO,MAAM;AACX,IAAA,IAAI,SAAA,KAAc,QAAQ,SAAA,GAAY,IAAA;AAAA,EACxC,CAAA;AACF;AAEO,SAAS,YAAA,GAAiC;AAC/C,EAAA,OAAO,SAAA;AACT;AAuEA,IAAI,YAAA,GAAoC,IAAA;AAGjC,SAAS,qBAAqB,IAAA,EAAgC;AACnE,EAAA,YAAA,GAAe,IAAA;AACf,EAAA,OAAO,MAAM;AACX,IAAA,IAAI,YAAA,KAAiB,MAAM,YAAA,GAAe,IAAA;AAAA,EAC5C,CAAA;AACF;AAEO,SAAS,eAAA,GAAuC;AACrD,EAAA,OAAO,YAAA;AACT;AAqDO,SAAS,oBAAoB,KAAA,EAA+D;AACjG,EAAA,OACE,OAAO,UAAU,QAAA,IACjB,KAAA,KAAU,QACV,QAAA,IAAY,KAAA,IACX,MAAoC,MAAA,KAAW,MAAA;AAEpD;AAEA,IAAI,gBAAA,GAA4C,IAAA;AAGzC,SAAS,yBAAyB,QAAA,EAAwC;AAC/E,EAAA,gBAAA,GAAmB,QAAA;AACnB,EAAA,OAAO,MAAM;AACX,IAAA,IAAI,gBAAA,KAAqB,UAAU,gBAAA,GAAmB,IAAA;AAAA,EACxD,CAAA;AACF;AAEO,SAAS,mBAAA,GAA+C;AAC7D,EAAA,OAAO,gBAAA;AACT;AAYO,SAAS,gBAAA,GAAkD;AAGhE,EAAA,IAAI,aAAA,GAAgB,KAAA;AACpB,EAAA,IAAI;AAEF,IAAA,aAAA,GAAgB,OAAA,CAAS,WAAmB,0BAA0B,CAAA;AAAA,EACxE,CAAA,CAAA,MAAQ;AACN,IAAA,aAAA,GAAgB,KAAA;AAAA,EAClB;AACA,EAAA,OAAO;AAAA,IACL,KAAK,SAAA,KAAc,IAAA;AAAA,IACnB,mBAAmB,gBAAA,KAAqB,IAAA;AAAA,IACxC,QAAA,EAAU,aAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOV,UAAU,YAAA,KAAiB;AAAA,GAC7B;AACF","file":"chunk-2QXKDLGT.js","sourcesContent":["import type { FlowGraph } from \"../types\";\n\n/**\n * Host capabilities — the services core nodes need but must never depend on.\n *\n * A node that imports a provider SDK forces every consumer to install it: a\n * workflow app that never calls a model should not inherit an LLM dependency.\n * So core declares the CONTRACT and the host supplies the implementation, the\n * same arrangement `renderDocumentField` already uses for documents.\n *\n * That keeps opinionated nodes in core without their opinions: `llm_branch`\n * ships the routing semantics, port derivation and config UI, while whichever\n * client the host registers — Prism, an OpenAI SDK, a local model, a fake in a\n * test — decides how the question actually gets asked.\n *\n * Registration is deliberately explicit and typed per capability rather than a\n * stringly-keyed bag, so a missing one is a clear error at the seam instead of\n * an undefined somewhere downstream.\n */\n\n// ── LLM ─────────────────────────────────────────────────────────────────────\n\nexport type LlmRoute = { port: string; description?: string };\n\nexport type LlmRouteRequest = {\n /** Optional framing for the decision. */\n system?: string;\n /** What the model is deciding about. */\n prompt: string;\n /** The ports it must choose between. */\n routes: LlmRoute[];\n provider?: string;\n model?: string;\n /** Host-resolved credential reference, never a raw key. */\n credential?: string;\n};\n\nexport type LlmRouteChoice = {\n /** Must be one of the requested route ports. */\n port: string;\n /** Why — carried down the chosen port so a run is explainable afterwards. */\n reason?: string;\n};\n\n/**\n * The only thing core asks of an LLM: given routes, pick one.\n *\n * Deliberately not a general chat interface. A narrow contract is one a host\n * can satisfy in a few lines over any SDK, and it keeps the choice\n * machine-checkable — an implementation should constrain the model to the\n * declared ports (structured output / enum) rather than parsing prose.\n */\nexport type LlmClient = {\n chooseRoute: (request: LlmRouteRequest) => Promise<LlmRouteChoice> | LlmRouteChoice;\n};\n\nlet llmClient: LlmClient | null = null;\n\n/** Install the host's LLM client. Returns an unregister function. */\nexport function registerLlmClient(client: LlmClient): () => void {\n llmClient = client;\n return () => {\n if (llmClient === client) llmClient = null;\n };\n}\n\nexport function getLlmClient(): LlmClient | null {\n return llmClient;\n}\n\n// ── Terminal ────────────────────────────────────────────────────────────────\n\n/** What to launch. Omit `command` for the host's default shell. */\nexport type TerminalSessionSpec = {\n command?: string;\n args?: string[];\n cwd?: string;\n env?: Record<string, string>;\n cols?: number;\n rows?: number;\n};\n\nexport type TerminalExit = { exitCode: number; signal?: string };\n\n/**\n * One live terminal, owned by the host for as long as the run needs it.\n *\n * Output arrives by SUBSCRIPTION rather than a `read()` the engine calls. A TUI\n * emits continuously and on its own schedule — repainting, streaming a reply,\n * redrawing a spinner — so a polled read either misses bytes between calls or\n * has to buffer them anyway, and every host would invent that buffer\n * differently.\n */\nexport type TerminalSession = {\n id: string;\n write: (data: string) => void | Promise<void>;\n /** Subscribe to output. Returns an unsubscribe. */\n onData: (listener: (chunk: string) => void) => () => void;\n /**\n * Resolves when the process exits.\n *\n * A promise rather than an `exited` flag the engine polls, so \"the agent\n * quit\" and \"the agent has not answered yet\" are distinguishable while\n * waiting. Without it a node awaiting output cannot tell a slow reply from a\n * dead process, and waits out its timeout either way.\n */\n exited: Promise<TerminalExit>;\n close: () => void | Promise<void>;\n};\n\n/**\n * The only thing core asks of a terminal: open one.\n *\n * ## Why the contract stops at the PTY\n *\n * There is deliberately no `waitForOutput(pattern)` here, though every terminal\n * node needs one. Matching is derivable from `onData`, so putting it in the\n * contract would mean every host implements it — and two implementations of one\n * agreed rule is precisely how a week of registry defects happened: both sides\n * agreed output matching should work, and would have disagreed on whether a\n * pattern spans chunk boundaries, whether ANSI escapes are stripped first, and\n * what a timeout returns.\n *\n * So core owns the matching and the host owns the process. A host satisfies\n * this in a few lines over `node-pty`; nothing about how a run interprets the\n * bytes is up to it.\n *\n * ## Why this exists at all\n *\n * A terminal cannot live in the engine. `node-pty` is a native addon, so\n * importing it would break every browser build of this package and force the\n * dependency on consumers who never run a terminal node — the same reason the\n * LLM client is a contract rather than an SDK import. The desktop app that CAN\n * spawn a PTY registers one; everyone else never notices the capability exists.\n */\nexport type TerminalHost = {\n open: (spec: TerminalSessionSpec) => Promise<TerminalSession> | TerminalSession;\n};\n\nlet terminalHost: TerminalHost | null = null;\n\n/** Install the host's terminal. Returns an unregister function. */\nexport function registerTerminalHost(host: TerminalHost): () => void {\n terminalHost = host;\n return () => {\n if (terminalHost === host) terminalHost = null;\n };\n}\n\nexport function getTerminalHost(): TerminalHost | null {\n return terminalHost;\n}\n\n// ── Workflow resolution ─────────────────────────────────────────────────────\n\n/**\n * Why a workflow reference could not be resolved.\n *\n * `missing` and `version-mismatch` are deliberately distinct. Collapsing them\n * into a bare null makes \"no such workflow\" indistinguishable from \"that\n * workflow exists, but it is not the one you pinned\" — and the second wants an\n * error naming both versions, because it is the interesting failure.\n */\nexport type WorkflowResolutionFailure = {\n reason: \"missing\" | \"version-mismatch\";\n /** The version the host actually holds, when it holds one. */\n available?: number;\n message?: string;\n};\n\nexport type WorkflowResolution = FlowGraph | WorkflowResolutionFailure | null;\n\n/**\n * Resolve a workflow reference to a runnable graph.\n *\n * `subflow` names another workflow rather than embedding it, so the host owns\n * where workflows live — a database, a file, an API.\n *\n * ## Why `version` is here\n *\n * A workflow another workflow depends on is an INTERFACE, and interfaces need\n * pins. Without a version, a parent goes on calling `invoice-triage`, someone\n * edits that child, and the parent now runs different logic having reported\n * success the whole time — correct-looking, no error, wrong behaviour. The same\n * failure family as the 0.9.0 routing divergence.\n *\n * The parameter lives on the resolver rather than being encoded into the ref\n * string (`invoice-triage@3`) because a stringly-typed protocol is one every\n * host invents differently — the \"three vocabularies for one node\" problem.\n *\n * Raised by the MOIC Suite consumer, whose `workflow_ref` pins versions and\n * fails loudly on mismatch. Their point: a host COULD NOT implement pinning\n * before this, because the node had no way to ask and the resolver no way to\n * receive.\n *\n * Returning `null` still means \"no such workflow\". Return a\n * {@link WorkflowResolutionFailure} to distinguish a version mismatch.\n */\nexport type WorkflowResolver = (\n ref: string,\n version?: number,\n) => Promise<WorkflowResolution> | WorkflowResolution;\n\n/** Narrow a resolver's return value to an explicit failure. */\nexport function isResolutionFailure(value: WorkflowResolution): value is WorkflowResolutionFailure {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"reason\" in value &&\n (value as WorkflowResolutionFailure).reason !== undefined\n );\n}\n\nlet workflowResolver: WorkflowResolver | null = null;\n\n/** Install the host's workflow resolver. Returns an unregister function. */\nexport function registerWorkflowResolver(resolver: WorkflowResolver): () => void {\n workflowResolver = resolver;\n return () => {\n if (workflowResolver === resolver) workflowResolver = null;\n };\n}\n\nexport function getWorkflowResolver(): WorkflowResolver | null {\n return workflowResolver;\n}\n\n// ── Introspection ───────────────────────────────────────────────────────────\n\nexport type CapabilityId = \"llm\" | \"workflow_resolver\" | \"document\" | \"terminal\";\n\n/**\n * Which capabilities are currently satisfied.\n *\n * Exists so a host (or the CLI, or an agent over MCP) can answer \"what does\n * this graph need that I haven't wired?\" BEFORE a run fails halfway through.\n */\nexport function capabilityStatus(): Record<CapabilityId, boolean> {\n // Imported lazily to avoid dragging the React-dependent rich-input module\n // into the headless engine.\n let documentReady = false;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef\n documentReady = Boolean((globalThis as any).__fancyFlowDocumentAdapter);\n } catch {\n documentReady = false;\n }\n return {\n llm: llmClient !== null,\n workflow_resolver: workflowResolver !== null,\n document: documentReady,\n // Listed here for the reason this function exists at all. A graph with a\n // terminal lane and no host registered fails PART WAY THROUGH — after the\n // nodes before it have already run and possibly written somewhere — and the\n // whole point of asking up front is to learn that before starting. Leaving\n // it out would have made the one check designed to catch this report\n // all-clear for exactly the case it was built for.\n terminal: terminalHost !== null,\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { getNodeKind, kindIds } from './chunk-JF6WCRBU.js';
1
+ import { getNodeKind, kindIds } from './chunk-HQGOFGAL.js';
2
2
 
3
3
  // src/analysis/graph-connectivity.ts
4
4
  function checkGraphConnectivity(graph) {
@@ -50,5 +50,5 @@ function isTerminator(node) {
50
50
  }
51
51
 
52
52
  export { checkGraphConnectivity, mayFloat };
53
- //# sourceMappingURL=chunk-77V4QC6Y.js.map
54
- //# sourceMappingURL=chunk-77V4QC6Y.js.map
53
+ //# sourceMappingURL=chunk-B7HKJJVV.js.map
54
+ //# sourceMappingURL=chunk-B7HKJJVV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/analysis/graph-connectivity.ts"],"names":[],"mappings":";;;AAsCO,SAAS,uBAAuB,KAAA,EAAiC;AACtE,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAM,GAAI,KAAA;AACzB,EAAA,MAAM,SAAwB,EAAC;AAE/B,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,WAAA,CAAY,GAAA,CAAI,KAAK,MAAM,CAAA;AAC3B,IAAA,WAAA,CAAY,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,EAC7B;AAMA,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,KAAW,CAAA;AAEhC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,MAAA,IAAU,QAAA,CAAS,IAAI,CAAA,EAAG;AAE9B,IAAA,IAAI,CAAC,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,EAAG;AAC1D,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,KAAA,EAAO,OAAA;AAAA,QACP,QAAQ,IAAA,CAAK,EAAA;AAAA,QACb,OAAA,EACE,CAAA,MAAA,EAAS,IAAA,CAAK,EAAE,CAAA,oSAAA;AAAA,OAInB,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAEhD,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,MAAA,IAAU,CAAC,YAAA,CAAa,MAAM,CAAA,EAAG;AAEtC,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,KAAA,EAAO,OAAA;AAAA,MACP,QAAQ,IAAA,CAAK,EAAA;AAAA,MACb,OAAA,EACE,SAAS,IAAA,CAAK,EAAE,iBAAiB,IAAA,CAAK,MAAM,CAAA,6JAAA,EAET,IAAA,CAAK,MAAM,CAAA,kCAAA;AAAA,KACjD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;AAyBO,SAAS,SAAS,IAAA,EAAyB;AAChD,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAClB,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,EAAA,IAAI,IAAA,KAAS,QAAQ,OAAO,IAAA;AAE5B,EAAA,MAAM,IAAA,GAAO,YAAY,IAAI,CAAA;AAC7B,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,OAAO,OAAA,CAAQ,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,KAAK,IAAA,CAAK,QAAA,KAAa,YAAA,IAAgB,IAAA,CAAK,QAAA,KAAa,QAAA;AAC/F;AAWA,SAAS,aAAa,IAAA,EAAyB;AAM7C,EAAA,MAAM,GAAA,GAAO,KAAK,IAAA,EAA8C,OAAA;AAChE,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,CAAA,EAAG,OAAO,IAAI,MAAA,KAAW,CAAA;AAE9C,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA,GAAO,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAKlD,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAElB,EAAA,OAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA,IAAK,IAAA,CAAK,QAAQ,MAAA,KAAW,CAAA;AAChE","file":"chunk-77V4QC6Y.js","sourcesContent":["import { getNodeKind, kindIds } from \"../registry/registry\";\nimport type { FlowGraph, FlowNode } from \"../types\";\nimport type { ImportIssue } from \"../schema/workflow-schema\";\n\n/**\n * Refuse a graph whose nodes cannot take part in the workflow's dataflow.\n *\n * Two shapes, both of which import cleanly and then quietly do nothing. Neither\n * FAILS — which is what makes them worth refusing at authoring time, because a\n * run that reports success is the worst way for a workflow to be wrong. Both\n * were measured against the PHP twin's engine before this was written, and both\n * behave the same way here:\n *\n * ## 1. A FLOATING node — no inbound and no outbound edge\n *\n * It is NOT skipped. A node with no incoming edge is a root, so the topo sort\n * runs it: a three-node graph with one floating `log` executed `t,lonely,o`. It\n * runs disconnected — receiving nothing from the graph and reaching nobody in\n * it — which is precisely the state an author cannot see on a canvas.\n *\n * ## 2. An edge leaving a TERMINATOR\n *\n * A terminal kind — `output`, `log` — declares an EMPTY output port list. It\n * ends a chain. Measured: `t -> output -> log` imported clean and the `log` DID\n * run, with `{{ input }}` resolving to `\"\"`. `collectInputs` binds a payload\n * only when `\"<sourceId>:<handle>\"` exists, and a node publishing no ports never\n * creates that key — so the edge does not fail, it delivers nothing, and the\n * node downstream operates on a hole.\n *\n * That is the same silent-nothing the undelivered-edge diagnostic reports at run\n * time, but this one is decidable FROM THE DOCUMENT ALONE.\n *\n * ## What may float\n *\n * See {@link mayFloat}. Not only `note`: any `annotation` or `layout` kind (a\n * swimlane is never wired to anything — that is what a lane IS), and any kind\n * the registry does not know.\n */\nexport function checkGraphConnectivity(graph: FlowGraph): ImportIssue[] {\n const { nodes, edges } = graph;\n const issues: ImportIssue[] = [];\n\n const hasIncoming = new Set<string>();\n const hasOutgoing = new Set<string>();\n for (const edge of edges) {\n hasIncoming.add(edge.target);\n hasOutgoing.add(edge.source);\n }\n\n // A single-node graph is not \"floating\" — it is a graph with one step, which\n // is a legitimate (if small) workflow and what every graph looks like on the\n // way to a bigger one. Refusing it would make the editor unusable from the\n // first node placed.\n const single = nodes.length === 1;\n\n for (const node of nodes) {\n if (single || mayFloat(node)) continue;\n\n if (!hasIncoming.has(node.id) && !hasOutgoing.has(node.id)) {\n issues.push({\n level: \"error\",\n nodeId: node.id,\n message:\n `Node \"${node.id}\" is connected to nothing — no inbound edge and no outbound edge. ` +\n `It still RUNS (a node with no inbound edge is a root), but it receives nothing from ` +\n `the graph and reaches nobody in it, so it is either unwired or left behind by a ` +\n `deletion. Only a note, an annotation or a lane may float.`,\n });\n }\n }\n\n const byId = new Map(nodes.map((n) => [n.id, n]));\n\n for (const edge of edges) {\n const source = byId.get(edge.source);\n if (!source || !isTerminator(source)) continue;\n\n issues.push({\n level: \"error\",\n edgeId: edge.id,\n message:\n `Edge \"${edge.id}\" reads from \"${edge.source}\", which is a TERMINAL node and publishes ` +\n `no output ports at all. Nothing can ever travel this edge: it does not fail at run ` +\n `time, it delivers nothing, and \"${edge.target}\" runs anyway with an empty input.`,\n });\n }\n\n return issues;\n}\n\n/**\n * Which nodes are allowed to sit unconnected.\n *\n * Three answers, and the third is the one that took a second pass — it was\n * missed in the PHP twin's first release and shipped as 0.48.1:\n *\n * 1. **`note`**, across every id the kind answers to, so a graph saved with the\n * canonical `@particle-academy/note` stays an annotation rather than becoming\n * an unwireable node.\n * 2. **Any `annotation` or `layout` kind.** A host may register its own note,\n * and `@particle-academy/lane` is a swimlane the engine walks straight past.\n * Neither is a step and neither is ever wired.\n * 3. **A kind the registry has never heard of.** Not a loophole — the honest\n * answer. An unknown kind already produces its own issue, and we cannot know\n * whether it is a step, an annotation or a lane. Claiming it must be wired\n * would assert something unverifiable, and it lands hardest on the graphs\n * that deserve it least: a laned graph loaded by a runtime without `lane`\n * registered would report every swimlane twice, the second time wrongly.\n *\n * The note/annotation/layout half matches the test `run-flow` uses to skip a\n * node. The unknown-kind half deliberately does NOT — see the closing comment in\n * this file for why the two must not become one helper.\n */\nexport function mayFloat(node: FlowNode): boolean {\n const type = node.type;\n if (!type) return false;\n if (type === \"note\") return true;\n\n const kind = getNodeKind(type);\n if (!kind) return true;\n\n return kindIds(kind).includes(\"note\") || kind.category === \"annotation\" || kind.category === \"layout\";\n}\n\n/**\n * A kind that declares an EMPTY output list ends a chain.\n *\n * `[]` and `undefined` are different answers and only the first means this.\n * `undefined` is \"nobody declared what this publishes\", which resolves to `out`\n * and is most nodes in most graphs; `[]` is an explicit claim that there is\n * nothing to connect from. Reading them alike would refuse nearly every\n * workflow ever written.\n */\nfunction isTerminator(node: FlowNode): boolean {\n // A node carrying its own ports overrides its kind — the engine reads these\n // first, so an author who has said what this node publishes is believed.\n // (The PHP twin's importer DROPS node-level ports, so this branch is reachable\n // there only for a hand-built graph. Noted rather than smoothed over: the two\n // importers genuinely differ here.)\n const own = (node.data as { outputs?: unknown[] } | undefined)?.outputs;\n if (Array.isArray(own)) return own.length === 0;\n\n const kind = node.type ? getNodeKind(node.type) : null;\n\n // An unregistered kind falls back to `out` in the engine, so it is not a\n // terminator. Refusing here would break a host mid-registration, and would\n // use \"I do not know\" as evidence.\n if (!kind) return false;\n\n return Array.isArray(kind.outputs) && kind.outputs.length === 0;\n}\n\n/**\n * NOT shared with `frontier`'s / `run-flow`'s annotation test, deliberately.\n *\n * The two predicates look identical and have OPPOSITE safe defaults, which is\n * exactly how a shared helper becomes a bug:\n *\n * - **Here** an unknown kind may float. Being permissive costs nothing — the\n * unknown-kind issue already fires, and we would otherwise assert something\n * unverifiable.\n * - **In the engine** an unknown kind must NOT be treated as an annotation.\n * Being permissive there means SKIPPING a node the host meant to run, which\n * is silent and unrecoverable.\n *\n * So the note/annotation/layout half is the same question and the unknown half\n * is a different one. Collapsing them into one function would read as tidier and\n * would make a host's unregistered kind stop executing.\n */\n"]}
1
+ {"version":3,"sources":["../src/analysis/graph-connectivity.ts"],"names":[],"mappings":";;;AAsCO,SAAS,uBAAuB,KAAA,EAAiC;AACtE,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAM,GAAI,KAAA;AACzB,EAAA,MAAM,SAAwB,EAAC;AAE/B,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,WAAA,CAAY,GAAA,CAAI,KAAK,MAAM,CAAA;AAC3B,IAAA,WAAA,CAAY,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,EAC7B;AAMA,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,KAAW,CAAA;AAEhC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,MAAA,IAAU,QAAA,CAAS,IAAI,CAAA,EAAG;AAE9B,IAAA,IAAI,CAAC,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,EAAG;AAC1D,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,KAAA,EAAO,OAAA;AAAA,QACP,QAAQ,IAAA,CAAK,EAAA;AAAA,QACb,OAAA,EACE,CAAA,MAAA,EAAS,IAAA,CAAK,EAAE,CAAA,oSAAA;AAAA,OAInB,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAEhD,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,MAAA,IAAU,CAAC,YAAA,CAAa,MAAM,CAAA,EAAG;AAEtC,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,KAAA,EAAO,OAAA;AAAA,MACP,QAAQ,IAAA,CAAK,EAAA;AAAA,MACb,OAAA,EACE,SAAS,IAAA,CAAK,EAAE,iBAAiB,IAAA,CAAK,MAAM,CAAA,6JAAA,EAET,IAAA,CAAK,MAAM,CAAA,kCAAA;AAAA,KACjD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;AAyBO,SAAS,SAAS,IAAA,EAAyB;AAChD,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAClB,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,EAAA,IAAI,IAAA,KAAS,QAAQ,OAAO,IAAA;AAE5B,EAAA,MAAM,IAAA,GAAO,YAAY,IAAI,CAAA;AAC7B,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,OAAO,OAAA,CAAQ,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,KAAK,IAAA,CAAK,QAAA,KAAa,YAAA,IAAgB,IAAA,CAAK,QAAA,KAAa,QAAA;AAC/F;AAWA,SAAS,aAAa,IAAA,EAAyB;AAM7C,EAAA,MAAM,GAAA,GAAO,KAAK,IAAA,EAA8C,OAAA;AAChE,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,CAAA,EAAG,OAAO,IAAI,MAAA,KAAW,CAAA;AAE9C,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA,GAAO,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAKlD,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAElB,EAAA,OAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA,IAAK,IAAA,CAAK,QAAQ,MAAA,KAAW,CAAA;AAChE","file":"chunk-B7HKJJVV.js","sourcesContent":["import { getNodeKind, kindIds } from \"../registry/registry\";\nimport type { FlowGraph, FlowNode } from \"../types\";\nimport type { ImportIssue } from \"../schema/workflow-schema\";\n\n/**\n * Refuse a graph whose nodes cannot take part in the workflow's dataflow.\n *\n * Two shapes, both of which import cleanly and then quietly do nothing. Neither\n * FAILS — which is what makes them worth refusing at authoring time, because a\n * run that reports success is the worst way for a workflow to be wrong. Both\n * were measured against the PHP twin's engine before this was written, and both\n * behave the same way here:\n *\n * ## 1. A FLOATING node — no inbound and no outbound edge\n *\n * It is NOT skipped. A node with no incoming edge is a root, so the topo sort\n * runs it: a three-node graph with one floating `log` executed `t,lonely,o`. It\n * runs disconnected — receiving nothing from the graph and reaching nobody in\n * it — which is precisely the state an author cannot see on a canvas.\n *\n * ## 2. An edge leaving a TERMINATOR\n *\n * A terminal kind — `output`, `log` — declares an EMPTY output port list. It\n * ends a chain. Measured: `t -> output -> log` imported clean and the `log` DID\n * run, with `{{ input }}` resolving to `\"\"`. `collectInputs` binds a payload\n * only when `\"<sourceId>:<handle>\"` exists, and a node publishing no ports never\n * creates that key — so the edge does not fail, it delivers nothing, and the\n * node downstream operates on a hole.\n *\n * That is the same silent-nothing the undelivered-edge diagnostic reports at run\n * time, but this one is decidable FROM THE DOCUMENT ALONE.\n *\n * ## What may float\n *\n * See {@link mayFloat}. Not only `note`: any `annotation` or `layout` kind (a\n * swimlane is never wired to anything — that is what a lane IS), and any kind\n * the registry does not know.\n */\nexport function checkGraphConnectivity(graph: FlowGraph): ImportIssue[] {\n const { nodes, edges } = graph;\n const issues: ImportIssue[] = [];\n\n const hasIncoming = new Set<string>();\n const hasOutgoing = new Set<string>();\n for (const edge of edges) {\n hasIncoming.add(edge.target);\n hasOutgoing.add(edge.source);\n }\n\n // A single-node graph is not \"floating\" — it is a graph with one step, which\n // is a legitimate (if small) workflow and what every graph looks like on the\n // way to a bigger one. Refusing it would make the editor unusable from the\n // first node placed.\n const single = nodes.length === 1;\n\n for (const node of nodes) {\n if (single || mayFloat(node)) continue;\n\n if (!hasIncoming.has(node.id) && !hasOutgoing.has(node.id)) {\n issues.push({\n level: \"error\",\n nodeId: node.id,\n message:\n `Node \"${node.id}\" is connected to nothing — no inbound edge and no outbound edge. ` +\n `It still RUNS (a node with no inbound edge is a root), but it receives nothing from ` +\n `the graph and reaches nobody in it, so it is either unwired or left behind by a ` +\n `deletion. Only a note, an annotation or a lane may float.`,\n });\n }\n }\n\n const byId = new Map(nodes.map((n) => [n.id, n]));\n\n for (const edge of edges) {\n const source = byId.get(edge.source);\n if (!source || !isTerminator(source)) continue;\n\n issues.push({\n level: \"error\",\n edgeId: edge.id,\n message:\n `Edge \"${edge.id}\" reads from \"${edge.source}\", which is a TERMINAL node and publishes ` +\n `no output ports at all. Nothing can ever travel this edge: it does not fail at run ` +\n `time, it delivers nothing, and \"${edge.target}\" runs anyway with an empty input.`,\n });\n }\n\n return issues;\n}\n\n/**\n * Which nodes are allowed to sit unconnected.\n *\n * Three answers, and the third is the one that took a second pass — it was\n * missed in the PHP twin's first release and shipped as 0.48.1:\n *\n * 1. **`note`**, across every id the kind answers to, so a graph saved with the\n * canonical `@particle-academy/note` stays an annotation rather than becoming\n * an unwireable node.\n * 2. **Any `annotation` or `layout` kind.** A host may register its own note,\n * and `@particle-academy/lane` is a swimlane the engine walks straight past.\n * Neither is a step and neither is ever wired.\n * 3. **A kind the registry has never heard of.** Not a loophole — the honest\n * answer. An unknown kind already produces its own issue, and we cannot know\n * whether it is a step, an annotation or a lane. Claiming it must be wired\n * would assert something unverifiable, and it lands hardest on the graphs\n * that deserve it least: a laned graph loaded by a runtime without `lane`\n * registered would report every swimlane twice, the second time wrongly.\n *\n * The note/annotation/layout half matches the test `run-flow` uses to skip a\n * node. The unknown-kind half deliberately does NOT — see the closing comment in\n * this file for why the two must not become one helper.\n */\nexport function mayFloat(node: FlowNode): boolean {\n const type = node.type;\n if (!type) return false;\n if (type === \"note\") return true;\n\n const kind = getNodeKind(type);\n if (!kind) return true;\n\n return kindIds(kind).includes(\"note\") || kind.category === \"annotation\" || kind.category === \"layout\";\n}\n\n/**\n * A kind that declares an EMPTY output list ends a chain.\n *\n * `[]` and `undefined` are different answers and only the first means this.\n * `undefined` is \"nobody declared what this publishes\", which resolves to `out`\n * and is most nodes in most graphs; `[]` is an explicit claim that there is\n * nothing to connect from. Reading them alike would refuse nearly every\n * workflow ever written.\n */\nfunction isTerminator(node: FlowNode): boolean {\n // A node carrying its own ports overrides its kind — the engine reads these\n // first, so an author who has said what this node publishes is believed.\n // (The PHP twin's importer DROPS node-level ports, so this branch is reachable\n // there only for a hand-built graph. Noted rather than smoothed over: the two\n // importers genuinely differ here.)\n const own = (node.data as { outputs?: unknown[] } | undefined)?.outputs;\n if (Array.isArray(own)) return own.length === 0;\n\n const kind = node.type ? getNodeKind(node.type) : null;\n\n // An unregistered kind falls back to `out` in the engine, so it is not a\n // terminator. Refusing here would break a host mid-registration, and would\n // use \"I do not know\" as evidence.\n if (!kind) return false;\n\n return Array.isArray(kind.outputs) && kind.outputs.length === 0;\n}\n\n/**\n * NOT shared with `frontier`'s / `run-flow`'s annotation test, deliberately.\n *\n * The two predicates look identical and have OPPOSITE safe defaults, which is\n * exactly how a shared helper becomes a bug:\n *\n * - **Here** an unknown kind may float. Being permissive costs nothing — the\n * unknown-kind issue already fires, and we would otherwise assert something\n * unverifiable.\n * - **In the engine** an unknown kind must NOT be treated as an annotation.\n * Being permissive there means SKIPPING a node the host meant to run, which\n * is silent and unrecoverable.\n *\n * So the note/annotation/layout half is the same question and the unknown half\n * is a different one. Collapsing them into one function would read as tidier and\n * would make a host's unregistered kind stop executing.\n */\n"]}
@@ -1,4 +1,4 @@
1
- import { runFlow, getNodeKind, applyNodeChanges, applyEdgeChanges, addEdge } from './chunk-JF6WCRBU.js';
1
+ import { runFlow, getNodeKind, applyNodeChanges, applyEdgeChanges, addEdge } from './chunk-HQGOFGAL.js';
2
2
  import { useState, useRef, useCallback, useMemo } from 'react';
3
3
 
4
4
  function useFlowRun({ maxFeed = 200 } = {}) {
@@ -234,5 +234,5 @@ function useFlowHistory(flow) {
234
234
  }
235
235
 
236
236
  export { applyOutputsToNodes, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState };
237
- //# sourceMappingURL=chunk-MBVX4ZRB.js.map
238
- //# sourceMappingURL=chunk-MBVX4ZRB.js.map
237
+ //# sourceMappingURL=chunk-FNYLKSFJ.js.map
238
+ //# sourceMappingURL=chunk-FNYLKSFJ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runtime/use-flow-run.ts","../src/runtime/use-flow-state.ts","../src/runtime/history.ts","../src/runtime/use-flow-history.ts"],"names":["useState","useCallback","useRef"],"mappings":"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","file":"chunk-MBVX4ZRB.js","sourcesContent":["import { useCallback, useRef, useState } from \"react\";\nimport { runFlow, type RunOptions, type RunResult } from \"./run-flow\";\nimport { getNodeKind } from \"../registry/registry\";\nimport type {\n ExecutorRegistry,\n FlowGraph,\n NodeRunStatus,\n RunEvent,\n} from \"../types\";\n\nexport type FlowRunFeedEntry = {\n id: string;\n at: number;\n /**\n * `message` is a node's own human-facing announcement (`startingMsg` /\n * `stoppingMsg`), kept distinct from `status` and `info` so a progress UI can\n * show ONLY the narration. Folded in with diagnostics it would be unfindable,\n * which is the same as not having it.\n */\n level: \"info\" | \"warn\" | \"error\" | \"status\" | \"message\";\n text: string;\n nodeId?: string;\n detail?: unknown;\n};\n\nexport type UseFlowRunReturn = {\n /** Status keyed by nodeId — drive the UI overlay from this. */\n statuses: Record<string, NodeRunStatus>;\n /** Per-node status text (e.g. error message). */\n statusText: Record<string, string | undefined>;\n /** Latest computed output value per node (for reactive kinds). */\n outputs: Record<string, unknown>;\n /**\n * Latest status message per nodeId, from the node's own `startingMsg` /\n * `stoppingMsg`. Undefined for the nodes that declared none, which is most\n * of them by design.\n */\n nodeMessages: Record<string, string | undefined>;\n /** Live event log (capped to last N). */\n feed: FlowRunFeedEntry[];\n /** Whether a run is currently in progress. */\n running: boolean;\n /** Last run result, or null. */\n lastResult: RunResult | null;\n /** Kick off a run with the provided graph + executors. */\n run: (graph: FlowGraph, executors: ExecutorRegistry, options?: RunOptions) => Promise<RunResult>;\n /** Cancel the current run (if any). */\n cancel: () => void;\n /** Reset all runtime state (statuses, feed, lastResult). */\n reset: () => void;\n};\n\nexport type UseFlowRunOptions = {\n /** Cap the in-memory feed to this many entries. Default 200. */\n maxFeed?: number;\n};\n\n/**\n * useFlowRun — drives `runFlow` + maintains observability state. Pair with\n * `applyStatusesToNodes` (below) before passing nodes to `<FlowCanvas>` so\n * the per-node status badge renders.\n */\nexport function useFlowRun({ maxFeed = 200 }: UseFlowRunOptions = {}): UseFlowRunReturn {\n const [statuses, setStatuses] = useState<Record<string, NodeRunStatus>>({});\n const [statusText, setStatusText] = useState<Record<string, string | undefined>>({});\n const [nodeMessages, setNodeMessages] = useState<Record<string, string | undefined>>({});\n const [outputs, setOutputs] = useState<Record<string, unknown>>({});\n const [feed, setFeed] = useState<FlowRunFeedEntry[]>([]);\n const [running, setRunning] = useState(false);\n const [lastResult, setLastResult] = useState<RunResult | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n\n const handleEvent = useCallback(\n (e: RunEvent) => {\n switch (e.type) {\n case \"node-status\":\n setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));\n setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));\n appendFeed({ level: \"status\", text: `${e.nodeId} → ${e.status}${e.text ? ` (${e.text})` : \"\"}`, nodeId: e.nodeId });\n break;\n case \"node-message\":\n setNodeMessages((m) => ({ ...m, [e.nodeId]: e.message }));\n appendFeed({ level: \"message\", text: e.message, nodeId: e.nodeId });\n break;\n case \"node-output\":\n setOutputs((o) => ({ ...o, [e.nodeId]: e.value }));\n appendFeed({ level: \"info\", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });\n break;\n case \"log\":\n appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });\n break;\n case \"run-start\":\n appendFeed({ level: \"info\", text: \"▶ run started\" });\n break;\n case \"run-end\":\n appendFeed({ level: e.ok ? \"info\" : \"error\", text: e.ok ? \"✓ run complete\" : \"✗ run failed\" });\n break;\n case \"run-error\":\n appendFeed({ level: \"error\", text: e.error });\n break;\n }\n function appendFeed(partial: Omit<FlowRunFeedEntry, \"id\" | \"at\">) {\n setFeed((f) => {\n const entry: FlowRunFeedEntry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };\n const next = [...f, entry];\n return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;\n });\n }\n },\n [maxFeed],\n );\n\n const run = useCallback(\n async (graph: FlowGraph, executors: ExecutorRegistry, options: RunOptions = {}) => {\n if (running) {\n return { ok: false, outputs: {}, error: \"another run is already in progress\" } satisfies RunResult;\n }\n const controller = new AbortController();\n abortRef.current = controller;\n // Reset previous statuses for the nodes we're about to run.\n const idleStatuses: Record<string, NodeRunStatus> = {};\n for (const n of graph.nodes) idleStatuses[n.id] = \"idle\";\n setStatuses(idleStatuses);\n setStatusText({});\n setOutputs({});\n setRunning(true);\n try {\n const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });\n setLastResult(result);\n return result;\n } finally {\n setRunning(false);\n abortRef.current = null;\n }\n },\n [handleEvent, running],\n );\n\n const cancel = useCallback(() => abortRef.current?.abort(), []);\n\n const reset = useCallback(() => {\n setStatuses({});\n setStatusText({});\n setOutputs({});\n setFeed([]);\n setLastResult(null);\n }, []);\n\n return { statuses, statusText, nodeMessages, outputs, feed, running, lastResult, run, cancel, reset };\n}\n\n/**\n * Write each reactive kind's latest run output into its `data.output`, so its\n * card can render the computed value live. Non-reactive kinds are returned\n * untouched. Pair with `applyStatusesToNodes` before rendering.\n */\nexport function applyOutputsToNodes<TNode extends { id: string; type?: string; data: any }>(\n nodes: TNode[],\n outputs: Record<string, unknown>,\n): TNode[] {\n return nodes.map((n) => {\n const kind = getNodeKind((n.data as any)?.kind ?? n.type ?? \"\");\n if (!kind?.reactive || !(n.id in outputs)) return n;\n return { ...n, data: { ...n.data, output: outputs[n.id] } };\n });\n}\n\n/** Merge runtime statuses into nodes for rendering. */\nexport function applyStatusesToNodes<TNode extends { id: string; data: any }>(\n nodes: TNode[],\n statuses: Record<string, NodeRunStatus>,\n statusText: Record<string, string | undefined>,\n): TNode[] {\n return nodes.map((n) => ({\n ...n,\n data: {\n ...n.data,\n status: statuses[n.id] ?? n.data?.status ?? \"idle\",\n statusText: statusText[n.id] ?? n.data?.statusText,\n },\n }));\n}\n\nfunction preview(v: unknown): string {\n try {\n const s = JSON.stringify(v);\n return s && s.length > 60 ? s.slice(0, 57) + \"…\" : (s ?? String(v));\n } catch {\n return String(v);\n }\n}\n","import { useCallback, useState } from \"react\";\nimport {\n addEdge,\n applyEdgeChanges,\n applyNodeChanges,\n type Connection,\n type Edge,\n type EdgeChange,\n type NodeChange,\n} from \"@xyflow/react\";\nimport type { FlowEdge, FlowGraph, FlowNode } from \"../types\";\n\nexport type UseFlowStateReturn = {\n nodes: FlowNode[];\n edges: FlowEdge[];\n setNodes: React.Dispatch<React.SetStateAction<FlowNode[]>>;\n setEdges: React.Dispatch<React.SetStateAction<FlowEdge[]>>;\n /**\n * Replace nodes AND edges atomically in a single commit. Use this for any op\n * that touches both (delete-with-edge-prune, undo/redo restore, setGraph):\n * calling `setNodes` then `setEdges` is NOT atomic in controlled mode (each\n * closes over a stale `value`), so the second write clobbers the first.\n */\n setGraph: (graph: FlowGraph) => void;\n onNodesChange: (changes: NodeChange[]) => void;\n onEdgesChange: (changes: EdgeChange[]) => void;\n onConnect: (connection: Connection) => void;\n /** Snapshot the current graph (suitable for serialization). */\n toGraph: () => FlowGraph;\n};\n\n/**\n * useFlowState — React Flow's standard controlled-state plumbing in one hook.\n * Spread into <FlowCanvas>.\n */\nexport function useFlowState(initial: FlowGraph): UseFlowStateReturn {\n const [nodes, setNodes] = useState<FlowNode[]>(initial.nodes);\n const [edges, setEdges] = useState<FlowEdge[]>(initial.edges);\n\n const onNodesChange = useCallback((changes: NodeChange[]) => {\n setNodes((ns) => applyNodeChanges(changes, ns) as FlowNode[]);\n }, []);\n const onEdgesChange = useCallback((changes: EdgeChange[]) => {\n setEdges((es) => applyEdgeChanges(changes, es));\n }, []);\n const onConnect = useCallback((connection: Connection) => {\n setEdges((es) => addEdge(connection, es) as Edge[]);\n }, []);\n\n const setGraph = useCallback((graph: FlowGraph) => {\n // Two useState writes in one event are batched, so this IS atomic here.\n setNodes(graph.nodes);\n setEdges(graph.edges);\n }, []);\n\n const toGraph = useCallback(() => ({ nodes, edges }), [nodes, edges]);\n\n return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };\n}\n","import type { FlowGraph } from \"../types\";\n\n/**\n * Undo/redo history — a pure, React-free snapshot controller. The editor pushes\n * a graph snapshot BEFORE each committing mutation; `undo`/`redo` swap snapshots\n * between the past/future stacks. Kept free of React so it can be unit-tested\n * and reused by any host (or a headless driver).\n *\n * Coalescing (collapsing a burst of setState calls from one logical op into a\n * single undo step) is the caller's job — see `useFlowHistory`.\n */\nexport type HistoryController = {\n /** Record `graph` as an undo point. Clears the redo stack (new branch). */\n push: (graph: FlowGraph) => void;\n /** Pop the last undo point; `current` is pushed onto the redo stack. */\n undo: (current: FlowGraph) => FlowGraph | null;\n /** Pop the last redo point; `current` is pushed back onto the undo stack. */\n redo: (current: FlowGraph) => FlowGraph | null;\n canUndo: () => boolean;\n canRedo: () => boolean;\n clear: () => void;\n /** Test/inspection helper — sizes of the two stacks. */\n size: () => { past: number; future: number };\n};\n\nexport function createHistory(limit = 100): HistoryController {\n let past: FlowGraph[] = [];\n let future: FlowGraph[] = [];\n\n return {\n push(graph) {\n past.push(graph);\n if (past.length > limit) past.shift();\n future = [];\n },\n undo(current) {\n const prev = past.pop();\n if (prev === undefined) return null;\n future.push(current);\n return prev;\n },\n redo(current) {\n const next = future.pop();\n if (next === undefined) return null;\n past.push(current);\n return next;\n },\n canUndo: () => past.length > 0,\n canRedo: () => future.length > 0,\n clear() {\n past = [];\n future = [];\n },\n size: () => ({ past: past.length, future: future.length }),\n };\n}\n","import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { EdgeChange, NodeChange } from \"@xyflow/react\";\nimport type { FlowGraph } from \"../types\";\nimport { createHistory } from \"./history\";\nimport type { UseFlowStateReturn } from \"./use-flow-state\";\n\nexport type UseFlowHistoryReturn = {\n /** The flow sink wrapped so committing mutations snapshot for undo first. */\n flow: UseFlowStateReturn;\n undo: () => void;\n redo: () => void;\n canUndo: boolean;\n canRedo: boolean;\n /** Snapshot the current graph as an undo point (before a programmatic edit). */\n capture: () => void;\n /** Wire to `<FlowCanvas onNodeDragStart>` — snapshots the pre-drag graph. */\n onNodeDragStart: () => void;\n};\n\n/**\n * useFlowHistory — the commit/undo pipeline. Wraps a flow sink (the uncontrolled\n * `useFlowState` hook OR the controlled adapter) so every *committing* mutation\n * records a snapshot first, giving undo/redo without the editor threading\n * history through each call site. This is the single interception point the\n * two-sink architecture otherwise lacks.\n *\n * Granularity: a burst of setState calls from one logical op (e.g. a\n * delete = setNodes+setEdges) is coalesced into ONE undo step; transient\n * interactive changes (drag-move, dimension-measure, selection) are NOT captured\n * — a drag is captured once at `onNodeDragStart` instead.\n */\nexport function useFlowHistory(flow: UseFlowStateReturn): UseFlowHistoryReturn {\n const history = useRef(createHistory()).current;\n const restoring = useRef(false);\n const coalesced = useRef(false);\n const [, bump] = useState(0);\n const rerender = useCallback(() => bump((n) => n + 1), []);\n\n // Latest graph via a ref, so capture never reads a stale snapshot.\n const graphRef = useRef<FlowGraph>({ nodes: flow.nodes, edges: flow.edges });\n graphRef.current = { nodes: flow.nodes, edges: flow.edges };\n\n const capture = useCallback(() => {\n if (restoring.current || coalesced.current) return;\n coalesced.current = true;\n queueMicrotask(() => {\n coalesced.current = false;\n });\n history.push(graphRef.current);\n rerender();\n }, [history, rerender]);\n\n const restore = useCallback(\n (g: FlowGraph | null) => {\n if (!g) return;\n restoring.current = true;\n flow.setGraph(g);\n queueMicrotask(() => {\n restoring.current = false;\n });\n rerender();\n },\n [flow, rerender],\n );\n\n const undo = useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);\n const redo = useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);\n\n const wrapped = useMemo<UseFlowStateReturn>(\n () => ({\n ...flow,\n setNodes: (next) => {\n capture();\n flow.setNodes(next);\n },\n setEdges: (next) => {\n capture();\n flow.setEdges(next);\n },\n setGraph: (g) => {\n capture();\n flow.setGraph(g);\n },\n onNodesChange: (changes: NodeChange[]) => {\n // Only structural removes commit; position/dimensions/select are transient.\n if (!restoring.current && changes.some((c) => c.type === \"remove\")) capture();\n flow.onNodesChange(changes);\n },\n onEdgesChange: (changes: EdgeChange[]) => {\n if (!restoring.current && changes.some((c) => c.type === \"remove\")) capture();\n flow.onEdgesChange(changes);\n },\n onConnect: (conn) => {\n capture();\n flow.onConnect(conn);\n },\n }),\n [flow, capture],\n );\n\n return {\n flow: wrapped,\n undo,\n redo,\n canUndo: history.canUndo(),\n canRedo: history.canRedo(),\n capture,\n onNodeDragStart: capture,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/runtime/use-flow-run.ts","../src/runtime/use-flow-state.ts","../src/runtime/history.ts","../src/runtime/use-flow-history.ts"],"names":["useState","useCallback","useRef"],"mappings":"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","file":"chunk-FNYLKSFJ.js","sourcesContent":["import { useCallback, useRef, useState } from \"react\";\nimport { runFlow, type RunOptions, type RunResult } from \"./run-flow\";\nimport { getNodeKind } from \"../registry/registry\";\nimport type {\n ExecutorRegistry,\n FlowGraph,\n NodeRunStatus,\n RunEvent,\n} from \"../types\";\n\nexport type FlowRunFeedEntry = {\n id: string;\n at: number;\n /**\n * `message` is a node's own human-facing announcement (`startingMsg` /\n * `stoppingMsg`), kept distinct from `status` and `info` so a progress UI can\n * show ONLY the narration. Folded in with diagnostics it would be unfindable,\n * which is the same as not having it.\n */\n level: \"info\" | \"warn\" | \"error\" | \"status\" | \"message\";\n text: string;\n nodeId?: string;\n detail?: unknown;\n};\n\nexport type UseFlowRunReturn = {\n /** Status keyed by nodeId — drive the UI overlay from this. */\n statuses: Record<string, NodeRunStatus>;\n /** Per-node status text (e.g. error message). */\n statusText: Record<string, string | undefined>;\n /** Latest computed output value per node (for reactive kinds). */\n outputs: Record<string, unknown>;\n /**\n * Latest status message per nodeId, from the node's own `startingMsg` /\n * `stoppingMsg`. Undefined for the nodes that declared none, which is most\n * of them by design.\n */\n nodeMessages: Record<string, string | undefined>;\n /** Live event log (capped to last N). */\n feed: FlowRunFeedEntry[];\n /** Whether a run is currently in progress. */\n running: boolean;\n /** Last run result, or null. */\n lastResult: RunResult | null;\n /** Kick off a run with the provided graph + executors. */\n run: (graph: FlowGraph, executors: ExecutorRegistry, options?: RunOptions) => Promise<RunResult>;\n /** Cancel the current run (if any). */\n cancel: () => void;\n /** Reset all runtime state (statuses, feed, lastResult). */\n reset: () => void;\n};\n\nexport type UseFlowRunOptions = {\n /** Cap the in-memory feed to this many entries. Default 200. */\n maxFeed?: number;\n};\n\n/**\n * useFlowRun — drives `runFlow` + maintains observability state. Pair with\n * `applyStatusesToNodes` (below) before passing nodes to `<FlowCanvas>` so\n * the per-node status badge renders.\n */\nexport function useFlowRun({ maxFeed = 200 }: UseFlowRunOptions = {}): UseFlowRunReturn {\n const [statuses, setStatuses] = useState<Record<string, NodeRunStatus>>({});\n const [statusText, setStatusText] = useState<Record<string, string | undefined>>({});\n const [nodeMessages, setNodeMessages] = useState<Record<string, string | undefined>>({});\n const [outputs, setOutputs] = useState<Record<string, unknown>>({});\n const [feed, setFeed] = useState<FlowRunFeedEntry[]>([]);\n const [running, setRunning] = useState(false);\n const [lastResult, setLastResult] = useState<RunResult | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n\n const handleEvent = useCallback(\n (e: RunEvent) => {\n switch (e.type) {\n case \"node-status\":\n setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));\n setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));\n appendFeed({ level: \"status\", text: `${e.nodeId} → ${e.status}${e.text ? ` (${e.text})` : \"\"}`, nodeId: e.nodeId });\n break;\n case \"node-message\":\n setNodeMessages((m) => ({ ...m, [e.nodeId]: e.message }));\n appendFeed({ level: \"message\", text: e.message, nodeId: e.nodeId });\n break;\n case \"node-output\":\n setOutputs((o) => ({ ...o, [e.nodeId]: e.value }));\n appendFeed({ level: \"info\", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });\n break;\n case \"log\":\n appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });\n break;\n case \"run-start\":\n appendFeed({ level: \"info\", text: \"▶ run started\" });\n break;\n case \"run-end\":\n appendFeed({ level: e.ok ? \"info\" : \"error\", text: e.ok ? \"✓ run complete\" : \"✗ run failed\" });\n break;\n case \"run-error\":\n appendFeed({ level: \"error\", text: e.error });\n break;\n }\n function appendFeed(partial: Omit<FlowRunFeedEntry, \"id\" | \"at\">) {\n setFeed((f) => {\n const entry: FlowRunFeedEntry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };\n const next = [...f, entry];\n return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;\n });\n }\n },\n [maxFeed],\n );\n\n const run = useCallback(\n async (graph: FlowGraph, executors: ExecutorRegistry, options: RunOptions = {}) => {\n if (running) {\n return { ok: false, outputs: {}, error: \"another run is already in progress\" } satisfies RunResult;\n }\n const controller = new AbortController();\n abortRef.current = controller;\n // Reset previous statuses for the nodes we're about to run.\n const idleStatuses: Record<string, NodeRunStatus> = {};\n for (const n of graph.nodes) idleStatuses[n.id] = \"idle\";\n setStatuses(idleStatuses);\n setStatusText({});\n setOutputs({});\n setRunning(true);\n try {\n const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });\n setLastResult(result);\n return result;\n } finally {\n setRunning(false);\n abortRef.current = null;\n }\n },\n [handleEvent, running],\n );\n\n const cancel = useCallback(() => abortRef.current?.abort(), []);\n\n const reset = useCallback(() => {\n setStatuses({});\n setStatusText({});\n setOutputs({});\n setFeed([]);\n setLastResult(null);\n }, []);\n\n return { statuses, statusText, nodeMessages, outputs, feed, running, lastResult, run, cancel, reset };\n}\n\n/**\n * Write each reactive kind's latest run output into its `data.output`, so its\n * card can render the computed value live. Non-reactive kinds are returned\n * untouched. Pair with `applyStatusesToNodes` before rendering.\n */\nexport function applyOutputsToNodes<TNode extends { id: string; type?: string; data: any }>(\n nodes: TNode[],\n outputs: Record<string, unknown>,\n): TNode[] {\n return nodes.map((n) => {\n const kind = getNodeKind((n.data as any)?.kind ?? n.type ?? \"\");\n if (!kind?.reactive || !(n.id in outputs)) return n;\n return { ...n, data: { ...n.data, output: outputs[n.id] } };\n });\n}\n\n/** Merge runtime statuses into nodes for rendering. */\nexport function applyStatusesToNodes<TNode extends { id: string; data: any }>(\n nodes: TNode[],\n statuses: Record<string, NodeRunStatus>,\n statusText: Record<string, string | undefined>,\n): TNode[] {\n return nodes.map((n) => ({\n ...n,\n data: {\n ...n.data,\n status: statuses[n.id] ?? n.data?.status ?? \"idle\",\n statusText: statusText[n.id] ?? n.data?.statusText,\n },\n }));\n}\n\nfunction preview(v: unknown): string {\n try {\n const s = JSON.stringify(v);\n return s && s.length > 60 ? s.slice(0, 57) + \"…\" : (s ?? String(v));\n } catch {\n return String(v);\n }\n}\n","import { useCallback, useState } from \"react\";\nimport {\n addEdge,\n applyEdgeChanges,\n applyNodeChanges,\n type Connection,\n type Edge,\n type EdgeChange,\n type NodeChange,\n} from \"@xyflow/react\";\nimport type { FlowEdge, FlowGraph, FlowNode } from \"../types\";\n\nexport type UseFlowStateReturn = {\n nodes: FlowNode[];\n edges: FlowEdge[];\n setNodes: React.Dispatch<React.SetStateAction<FlowNode[]>>;\n setEdges: React.Dispatch<React.SetStateAction<FlowEdge[]>>;\n /**\n * Replace nodes AND edges atomically in a single commit. Use this for any op\n * that touches both (delete-with-edge-prune, undo/redo restore, setGraph):\n * calling `setNodes` then `setEdges` is NOT atomic in controlled mode (each\n * closes over a stale `value`), so the second write clobbers the first.\n */\n setGraph: (graph: FlowGraph) => void;\n onNodesChange: (changes: NodeChange[]) => void;\n onEdgesChange: (changes: EdgeChange[]) => void;\n onConnect: (connection: Connection) => void;\n /** Snapshot the current graph (suitable for serialization). */\n toGraph: () => FlowGraph;\n};\n\n/**\n * useFlowState — React Flow's standard controlled-state plumbing in one hook.\n * Spread into <FlowCanvas>.\n */\nexport function useFlowState(initial: FlowGraph): UseFlowStateReturn {\n const [nodes, setNodes] = useState<FlowNode[]>(initial.nodes);\n const [edges, setEdges] = useState<FlowEdge[]>(initial.edges);\n\n const onNodesChange = useCallback((changes: NodeChange[]) => {\n setNodes((ns) => applyNodeChanges(changes, ns) as FlowNode[]);\n }, []);\n const onEdgesChange = useCallback((changes: EdgeChange[]) => {\n setEdges((es) => applyEdgeChanges(changes, es));\n }, []);\n const onConnect = useCallback((connection: Connection) => {\n setEdges((es) => addEdge(connection, es) as Edge[]);\n }, []);\n\n const setGraph = useCallback((graph: FlowGraph) => {\n // Two useState writes in one event are batched, so this IS atomic here.\n setNodes(graph.nodes);\n setEdges(graph.edges);\n }, []);\n\n const toGraph = useCallback(() => ({ nodes, edges }), [nodes, edges]);\n\n return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };\n}\n","import type { FlowGraph } from \"../types\";\n\n/**\n * Undo/redo history — a pure, React-free snapshot controller. The editor pushes\n * a graph snapshot BEFORE each committing mutation; `undo`/`redo` swap snapshots\n * between the past/future stacks. Kept free of React so it can be unit-tested\n * and reused by any host (or a headless driver).\n *\n * Coalescing (collapsing a burst of setState calls from one logical op into a\n * single undo step) is the caller's job — see `useFlowHistory`.\n */\nexport type HistoryController = {\n /** Record `graph` as an undo point. Clears the redo stack (new branch). */\n push: (graph: FlowGraph) => void;\n /** Pop the last undo point; `current` is pushed onto the redo stack. */\n undo: (current: FlowGraph) => FlowGraph | null;\n /** Pop the last redo point; `current` is pushed back onto the undo stack. */\n redo: (current: FlowGraph) => FlowGraph | null;\n canUndo: () => boolean;\n canRedo: () => boolean;\n clear: () => void;\n /** Test/inspection helper — sizes of the two stacks. */\n size: () => { past: number; future: number };\n};\n\nexport function createHistory(limit = 100): HistoryController {\n let past: FlowGraph[] = [];\n let future: FlowGraph[] = [];\n\n return {\n push(graph) {\n past.push(graph);\n if (past.length > limit) past.shift();\n future = [];\n },\n undo(current) {\n const prev = past.pop();\n if (prev === undefined) return null;\n future.push(current);\n return prev;\n },\n redo(current) {\n const next = future.pop();\n if (next === undefined) return null;\n past.push(current);\n return next;\n },\n canUndo: () => past.length > 0,\n canRedo: () => future.length > 0,\n clear() {\n past = [];\n future = [];\n },\n size: () => ({ past: past.length, future: future.length }),\n };\n}\n","import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { EdgeChange, NodeChange } from \"@xyflow/react\";\nimport type { FlowGraph } from \"../types\";\nimport { createHistory } from \"./history\";\nimport type { UseFlowStateReturn } from \"./use-flow-state\";\n\nexport type UseFlowHistoryReturn = {\n /** The flow sink wrapped so committing mutations snapshot for undo first. */\n flow: UseFlowStateReturn;\n undo: () => void;\n redo: () => void;\n canUndo: boolean;\n canRedo: boolean;\n /** Snapshot the current graph as an undo point (before a programmatic edit). */\n capture: () => void;\n /** Wire to `<FlowCanvas onNodeDragStart>` — snapshots the pre-drag graph. */\n onNodeDragStart: () => void;\n};\n\n/**\n * useFlowHistory — the commit/undo pipeline. Wraps a flow sink (the uncontrolled\n * `useFlowState` hook OR the controlled adapter) so every *committing* mutation\n * records a snapshot first, giving undo/redo without the editor threading\n * history through each call site. This is the single interception point the\n * two-sink architecture otherwise lacks.\n *\n * Granularity: a burst of setState calls from one logical op (e.g. a\n * delete = setNodes+setEdges) is coalesced into ONE undo step; transient\n * interactive changes (drag-move, dimension-measure, selection) are NOT captured\n * — a drag is captured once at `onNodeDragStart` instead.\n */\nexport function useFlowHistory(flow: UseFlowStateReturn): UseFlowHistoryReturn {\n const history = useRef(createHistory()).current;\n const restoring = useRef(false);\n const coalesced = useRef(false);\n const [, bump] = useState(0);\n const rerender = useCallback(() => bump((n) => n + 1), []);\n\n // Latest graph via a ref, so capture never reads a stale snapshot.\n const graphRef = useRef<FlowGraph>({ nodes: flow.nodes, edges: flow.edges });\n graphRef.current = { nodes: flow.nodes, edges: flow.edges };\n\n const capture = useCallback(() => {\n if (restoring.current || coalesced.current) return;\n coalesced.current = true;\n queueMicrotask(() => {\n coalesced.current = false;\n });\n history.push(graphRef.current);\n rerender();\n }, [history, rerender]);\n\n const restore = useCallback(\n (g: FlowGraph | null) => {\n if (!g) return;\n restoring.current = true;\n flow.setGraph(g);\n queueMicrotask(() => {\n restoring.current = false;\n });\n rerender();\n },\n [flow, rerender],\n );\n\n const undo = useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);\n const redo = useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);\n\n const wrapped = useMemo<UseFlowStateReturn>(\n () => ({\n ...flow,\n setNodes: (next) => {\n capture();\n flow.setNodes(next);\n },\n setEdges: (next) => {\n capture();\n flow.setEdges(next);\n },\n setGraph: (g) => {\n capture();\n flow.setGraph(g);\n },\n onNodesChange: (changes: NodeChange[]) => {\n // Only structural removes commit; position/dimensions/select are transient.\n if (!restoring.current && changes.some((c) => c.type === \"remove\")) capture();\n flow.onNodesChange(changes);\n },\n onEdgesChange: (changes: EdgeChange[]) => {\n if (!restoring.current && changes.some((c) => c.type === \"remove\")) capture();\n flow.onEdgesChange(changes);\n },\n onConnect: (conn) => {\n capture();\n flow.onConnect(conn);\n },\n }),\n [flow, capture],\n );\n\n return {\n flow: wrapped,\n undo,\n redo,\n canUndo: history.canUndo(),\n canRedo: history.canRedo(),\n capture,\n onNodeDragStart: capture,\n };\n}\n"]}