@particle-academy/fancy-flow 0.14.0 → 0.15.1

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 (42) hide show
  1. package/README.md +48 -0
  2. package/dist/{capabilities-D-V9Rxv1.d.cts → capabilities-BEbLqnJU.d.cts} +1 -1
  3. package/dist/{capabilities-B6r1Snfj.d.ts → capabilities-CI3gw0C7.d.ts} +1 -1
  4. package/dist/chunk-UEOE6B52.js +39 -0
  5. package/dist/chunk-UEOE6B52.js.map +1 -0
  6. package/dist/{chunk-7U3EP4Q5.js → chunk-ZQBWPYTI.js} +5 -2
  7. package/dist/chunk-ZQBWPYTI.js.map +1 -0
  8. package/dist/engine.cjs +299 -0
  9. package/dist/engine.cjs.map +1 -1
  10. package/dist/engine.d.cts +205 -27
  11. package/dist/engine.d.ts +205 -27
  12. package/dist/engine.js +258 -2
  13. package/dist/engine.js.map +1 -1
  14. package/dist/index.cjs +45 -0
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +7 -6
  17. package/dist/index.d.ts +7 -6
  18. package/dist/index.js +3 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/llm/vercel-ai.d.cts +2 -2
  21. package/dist/llm/vercel-ai.d.ts +2 -2
  22. package/dist/pause-9iT4tCEV.d.cts +96 -0
  23. package/dist/pause-9iT4tCEV.d.ts +96 -0
  24. package/dist/registry/index.d.cts +6 -5
  25. package/dist/registry/index.d.ts +6 -5
  26. package/dist/registry.cjs +45 -0
  27. package/dist/registry.cjs.map +1 -1
  28. package/dist/registry.js +2 -1
  29. package/dist/run-flow-ChBi6q-W.d.ts +35 -0
  30. package/dist/run-flow-VKm57Hcj.d.cts +35 -0
  31. package/dist/runtime/index.d.cts +3 -3
  32. package/dist/runtime/index.d.ts +3 -3
  33. package/dist/schema/index.d.cts +1 -1
  34. package/dist/schema/index.d.ts +1 -1
  35. package/dist/{types-H60tQAxr.d.cts → types-D_jOR3Z2.d.cts} +12 -1
  36. package/dist/{types-CnnKPnpt.d.ts → types-NerkPtHS.d.ts} +12 -1
  37. package/dist/{types-fc0fFKkf.d.cts → types-sOmpCitB.d.cts} +1 -1
  38. package/dist/{types-fc0fFKkf.d.ts → types-sOmpCitB.d.ts} +1 -1
  39. package/dist/ux.d.cts +3 -2
  40. package/dist/ux.d.ts +3 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-7U3EP4Q5.js.map +0 -1
package/dist/engine.js CHANGED
@@ -1,5 +1,261 @@
1
+ import { decodePause } from './chunk-UEOE6B52.js';
2
+ export { LEGACY_PAUSE_PREFIXES, PAUSE_PREFIX, decodePause, encodePause, isPause, pauseForHuman } from './chunk-UEOE6B52.js';
3
+ import { runFlow } from './chunk-L4AX73Q6.js';
1
4
  export { runFlow } from './chunk-L4AX73Q6.js';
2
- import './chunk-TITD5W4Y.js';
3
- import './chunk-U5F22BHV.js';
5
+ import { resolveNodePorts } from './chunk-TITD5W4Y.js';
6
+ import { getNodeKind } from './chunk-U5F22BHV.js';
7
+
8
+ // src/marketplace/manifest.ts
9
+ var NODE_MANIFEST_SCHEMA_VERSION = 1;
10
+ var FIRST_PARTY_SCOPE = "@particle-academy/";
11
+ var NAMESPACED_KIND = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/i;
12
+ function err(field, message) {
13
+ return { level: "error", field, message };
14
+ }
15
+ function warn(field, message) {
16
+ return { level: "warning", field, message };
17
+ }
18
+ function validateNodeManifest(input) {
19
+ const problems = [];
20
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
21
+ return { ok: false, problems: [err("", "Manifest must be a JSON object.")] };
22
+ }
23
+ const m = input;
24
+ if (m.schemaVersion !== NODE_MANIFEST_SCHEMA_VERSION) {
25
+ if (typeof m.schemaVersion !== "number") {
26
+ problems.push(err("schemaVersion", `Required, and must be ${NODE_MANIFEST_SCHEMA_VERSION}.`));
27
+ } else {
28
+ return {
29
+ ok: false,
30
+ problems: [
31
+ err(
32
+ "schemaVersion",
33
+ `Unsupported manifest version ${m.schemaVersion}; this fancy-flow understands ${NODE_MANIFEST_SCHEMA_VERSION}. Upgrade fancy-flow to install this node.`
34
+ )
35
+ ]
36
+ };
37
+ }
38
+ }
39
+ if (typeof m.name !== "string" || m.name.trim() === "") {
40
+ problems.push(err("name", "Required \u2014 the package name as installed."));
41
+ }
42
+ if (typeof m.kind !== "string" || m.kind.trim() === "") {
43
+ problems.push(err("kind", "Required \u2014 the canonical kind id this package provides."));
44
+ } else if (!NAMESPACED_KIND.test(m.kind)) {
45
+ problems.push(
46
+ err("kind", `"${m.kind}" must be namespaced as @scope/name \u2014 a bare id makes stored graphs ambiguous, and that is unfixable once documents carry it.`)
47
+ );
48
+ } else if (m.kind.startsWith(FIRST_PARTY_SCOPE)) {
49
+ problems.push(
50
+ warn("kind", `${FIRST_PARTY_SCOPE}* is reserved for first-party nodes; the registry will reject this unless the package is first-party.`)
51
+ );
52
+ }
53
+ if (typeof m.fancyFlow !== "string" || m.fancyFlow.trim() === "") {
54
+ problems.push(err("fancyFlow", "Required \u2014 the semver range of fancy-flow this node targets."));
55
+ }
56
+ if (typeof m.runtimes !== "object" || m.runtimes === null || Array.isArray(m.runtimes)) {
57
+ problems.push(err("runtimes", "Required \u2014 an object of runtime id to entrypoint."));
58
+ } else {
59
+ const entries = Object.entries(m.runtimes);
60
+ if (entries.length === 0) {
61
+ problems.push(err("runtimes", "A node that implements no runtime cannot execute anywhere."));
62
+ }
63
+ for (const [runtime, entry] of entries) {
64
+ if (typeof entry !== "string" || entry.trim() === "") {
65
+ problems.push(err(`runtimes.${runtime}`, "Entrypoint must be a non-empty string."));
66
+ }
67
+ }
68
+ }
69
+ if (typeof m.fixtures !== "string" || m.fixtures.trim() === "") {
70
+ problems.push(
71
+ err("fixtures", "Required \u2014 path to the node's golden fixtures. Every claimed runtime runs them, which is what makes cross-runtime parity verified rather than claimed.")
72
+ );
73
+ }
74
+ if (m.capabilities !== void 0) {
75
+ if (!Array.isArray(m.capabilities) || m.capabilities.some((c) => typeof c !== "string")) {
76
+ problems.push(err("capabilities", "Must be an array of capability id strings."));
77
+ }
78
+ }
79
+ if (m.verified !== void 0) {
80
+ problems.push(
81
+ err("verified", "Assigned by the registry, not the package. Remove it \u2014 a package cannot vouch for itself.")
82
+ );
83
+ }
84
+ const ok = !problems.some((p) => p.level === "error");
85
+ return ok ? { ok, manifest: m, problems } : { ok, problems };
86
+ }
87
+ function checkRuntimeSupport(manifest, hostRuntimes) {
88
+ const provided = Object.keys(manifest.runtimes ?? {});
89
+ const missing = hostRuntimes.filter((r) => !provided.includes(r));
90
+ if (missing.length === 0) return [];
91
+ return [
92
+ err(
93
+ "runtimes",
94
+ `${manifest.kind} implements ${provided.join(", ") || "no runtime"} but this project executes on ${missing.join(", ")}. The node would install, appear in the palette, and then fail to run.`
95
+ )
96
+ ];
97
+ }
98
+ function checkCapabilities(manifest, available) {
99
+ const needed = manifest.capabilities ?? [];
100
+ const missing = needed.filter((c) => available[c] !== true);
101
+ if (missing.length === 0) return [];
102
+ return [
103
+ warn(
104
+ "capabilities",
105
+ `${manifest.kind} needs ${missing.join(", ")} wired on the host. Until then the node will fail at run time rather than at install.`
106
+ )
107
+ ];
108
+ }
109
+
110
+ // src/marketplace/fixtures.ts
111
+ var SUBJECT = "subject";
112
+ var TRIGGER = "trigger";
113
+ var probeId = (port) => `probe:${port}`;
114
+ function buildGraph(kindId, testCase) {
115
+ const subject = {
116
+ id: SUBJECT,
117
+ type: kindId,
118
+ position: { x: 0, y: 100 },
119
+ data: { kind: kindId, config: testCase.config ?? {} }
120
+ };
121
+ const kind = getNodeKind(kindId) ?? void 0;
122
+ const ports = testCase.ports ?? (resolveNodePorts(subject, kind).outputs ?? []).map((p) => p.id);
123
+ const effective = ports.length ? ports : ["out"];
124
+ const nodes = [
125
+ { id: TRIGGER, type: "manual_trigger", position: { x: 0, y: 0 }, data: {} },
126
+ subject,
127
+ ...effective.map(
128
+ (port, i) => ({
129
+ id: probeId(port),
130
+ type: "@particle-academy/transform",
131
+ position: { x: i * 200, y: 200 },
132
+ data: {}
133
+ })
134
+ )
135
+ ];
136
+ const edges = [
137
+ { id: `e:trigger`, source: TRIGGER, target: SUBJECT },
138
+ ...effective.map((port) => ({
139
+ id: `e:${port}`,
140
+ source: SUBJECT,
141
+ sourceHandle: port,
142
+ target: probeId(port)
143
+ }))
144
+ ];
145
+ return { graph: { nodes, edges }, ports: effective };
146
+ }
147
+ function deepEqual(a, b) {
148
+ return JSON.stringify(a) === JSON.stringify(b);
149
+ }
150
+ async function runFixtures(file, executor) {
151
+ const failures = [];
152
+ let passed = 0;
153
+ for (const testCase of file.cases) {
154
+ const { graph } = buildGraph(file.kind, testCase);
155
+ const fired = [];
156
+ let carried;
157
+ const executors = {
158
+ manual_trigger: () => testCase.inputs ?? {},
159
+ "@particle-academy/manual_trigger": () => testCase.inputs ?? {},
160
+ [file.kind]: executor
161
+ };
162
+ for (const node of graph.nodes) {
163
+ if (!node.id.startsWith("probe:")) continue;
164
+ const port = node.id.slice("probe:".length);
165
+ executors[node.id] = ((ctx) => {
166
+ fired.push(port);
167
+ carried = ctx.inputs?.in ?? ctx.inputs;
168
+ return void 0;
169
+ });
170
+ }
171
+ const result = await runFlow(graph, executors, () => {
172
+ });
173
+ const fail = (message) => failures.push({ case: testCase.name, message });
174
+ const expected = testCase.expect;
175
+ let caseOk = true;
176
+ if (expected.pause) {
177
+ const paused = decodePause(result.error);
178
+ if (!paused) {
179
+ caseOk = false;
180
+ fail(`expected a pause awaiting "${expected.pause.awaiting}", got ${result.ok ? "a completed run" : `error: ${result.error}`}`);
181
+ } else {
182
+ if (paused.awaiting !== expected.pause.awaiting) {
183
+ caseOk = false;
184
+ fail(`expected pause awaiting "${expected.pause.awaiting}", got "${paused.awaiting}"`);
185
+ }
186
+ if ("detail" in expected.pause && !deepEqual(paused.detail, expected.pause.detail)) {
187
+ caseOk = false;
188
+ fail(`pause detail mismatch: expected ${JSON.stringify(expected.pause.detail)}, got ${JSON.stringify(paused.detail)}`);
189
+ }
190
+ }
191
+ } else if (expected.error !== void 0) {
192
+ if (result.ok) {
193
+ caseOk = false;
194
+ fail(`expected an error containing "${expected.error}", but the run succeeded`);
195
+ } else if (!String(result.error ?? "").includes(expected.error)) {
196
+ caseOk = false;
197
+ fail(`expected an error containing "${expected.error}", got "${result.error}"`);
198
+ }
199
+ } else {
200
+ if (expected.ports !== void 0) {
201
+ const got = [...fired].sort();
202
+ const want = [...expected.ports].sort();
203
+ if (!deepEqual(got, want)) {
204
+ caseOk = false;
205
+ fail(
206
+ `expected these ports to reach a downstream node: [${want.join(", ")}], but [${got.join(", ")}] did` + (result.ok ? "" : ` (run error: ${result.error})`)
207
+ );
208
+ }
209
+ }
210
+ if ("value" in expected && !deepEqual(carried, expected.value)) {
211
+ caseOk = false;
212
+ fail(`expected the value carried downstream to be ${JSON.stringify(expected.value)}, got ${JSON.stringify(carried)}`);
213
+ }
214
+ }
215
+ if (caseOk) passed += 1;
216
+ }
217
+ return { ok: failures.length === 0, passed, failures };
218
+ }
219
+ function validateFixtureFile(input, expectedKind) {
220
+ const problems = [];
221
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
222
+ return ["Fixture file must be a JSON object."];
223
+ }
224
+ const f = input;
225
+ if (typeof f.kind !== "string" || f.kind.trim() === "") {
226
+ problems.push("`kind` is required \u2014 the kind these cases exercise.");
227
+ } else if (expectedKind && f.kind !== expectedKind) {
228
+ problems.push(`\`kind\` is "${f.kind}" but the manifest declares "${expectedKind}".`);
229
+ }
230
+ if (!Array.isArray(f.cases) || f.cases.length === 0) {
231
+ problems.push("`cases` must contain at least one case \u2014 an empty fixture file proves nothing.");
232
+ return problems;
233
+ }
234
+ f.cases.forEach((c, i) => {
235
+ const label = `cases[${i}]`;
236
+ if (typeof c !== "object" || c === null) {
237
+ problems.push(`${label} must be an object.`);
238
+ return;
239
+ }
240
+ const testCase = c;
241
+ if (typeof testCase.name !== "string" || testCase.name.trim() === "") {
242
+ problems.push(`${label}.name is required \u2014 a failure report names it.`);
243
+ }
244
+ if (typeof testCase.expect !== "object" || testCase.expect === null) {
245
+ problems.push(`${label}.expect is required \u2014 a case that asserts nothing passes vacuously.`);
246
+ return;
247
+ }
248
+ const expect = testCase.expect;
249
+ if (expect.ports === void 0 && expect.value === void 0 && expect.pause === void 0 && expect.error === void 0) {
250
+ problems.push(`${label}.expect must assert at least one of: ports, value, pause, error.`);
251
+ }
252
+ if (expect.ports !== void 0 && (!Array.isArray(expect.ports) || expect.ports.some((p) => typeof p !== "string"))) {
253
+ problems.push(`${label}.expect.ports must be an array of port id strings.`);
254
+ }
255
+ });
256
+ return problems;
257
+ }
258
+
259
+ export { NODE_MANIFEST_SCHEMA_VERSION, checkCapabilities, checkRuntimeSupport, runFixtures, validateFixtureFile, validateNodeManifest };
4
260
  //# sourceMappingURL=engine.js.map
5
261
  //# sourceMappingURL=engine.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"engine.js"}
1
+ {"version":3,"sources":["../src/marketplace/manifest.ts","../src/marketplace/fixtures.ts"],"names":[],"mappings":"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","file":"engine.js","sourcesContent":["/**\n * The node package manifest — what a marketplace node declares about itself.\n *\n * A node is not one artifact. It is a kind definition (palette entry, config\n * schema, ports, canvas renderer) plus an executor for EACH runtime the\n * consumer actually runs. A package shipping only a TS executor is unusable to\n * anyone executing on PHP, and today that is invisible until a run fails.\n *\n * So the manifest states which runtimes it implements, and the CLI checks that\n * against the host before installing rather than after. Requested by the MOIC\n * Suite consumer (fancy-flow#2 §2), who runs the editor in TS and executes in\n * PHP and hit exactly this.\n *\n * The manifest is data, not code: the registry, the CLI, and the MCP all read\n * it without executing anything a package author wrote.\n */\n\nimport type { PauseAwaiting } from \"../registry/pause\";\n\n/** The current manifest schema version. Bump only on a breaking shape change. */\nexport const NODE_MANIFEST_SCHEMA_VERSION = 1;\n\n/**\n * A runtime a node can implement.\n *\n * Open rather than a closed union — the PHP and Node twins are what exist\n * today, but the point of a manifest is that a runtime we haven't written can\n * declare itself without a release here.\n */\nexport type NodeRuntimeId = \"ts\" | \"php\" | (string & {});\n\nexport type NodePackageManifest = {\n /** Must equal `NODE_MANIFEST_SCHEMA_VERSION`. */\n schemaVersion: number;\n /** Package name, as installed (`@acme/fancy-flow-salesforce`). */\n name: string;\n /**\n * The canonical kind id this package provides — namespaced, and the string\n * that gets persisted into every document using it.\n */\n kind: string;\n /** Semver range of fancy-flow this node's contract targets. */\n fancyFlow: string;\n /**\n * Per-runtime entrypoints. TS is a module path within the package; PHP is a\n * Composer requirement, because the two ecosystems install differently and\n * pretending otherwise just moves the problem into the CLI.\n */\n runtimes: Partial<Record<NodeRuntimeId, string>>;\n /**\n * Host capabilities this node needs wired before it can run — `llm`,\n * `workflow_resolver`, `document`, or a host-specific one.\n *\n * Declared so the CLI can tell an author what to wire BEFORE install, rather\n * than the node silently no-opping or crashing mid-run.\n */\n capabilities?: string[];\n /**\n * Path to this node's golden fixtures, relative to the package root.\n *\n * REQUIRED. Every runtime the package claims runs these same cases, which is\n * what makes \"behaves identically on both runtimes\" verified rather than\n * asserted. See `./fixtures`.\n */\n fixtures: string;\n /** Declares the node halts for a person. Mirrors `NodeKindDefinition`. */\n pausesForHuman?: PauseAwaiting;\n /** One-line summary — what `search_nodes` matches against. */\n description?: string;\n /**\n * Assigned by the registry, never by the author. Present in a manifest being\n * submitted for publication, it is a claim to a trust signal the author does\n * not get to make.\n */\n verified?: boolean;\n};\n\nexport type ManifestProblem = {\n level: \"error\" | \"warning\";\n field: string;\n message: string;\n};\n\nexport type ManifestValidation = {\n /** True when there are no `error`-level problems. Warnings do not block. */\n ok: boolean;\n manifest?: NodePackageManifest;\n problems: ManifestProblem[];\n};\n\n/** Reserved for first-party packages; the registry rejects other claimants. */\nconst FIRST_PARTY_SCOPE = \"@particle-academy/\";\n\n/** `@scope/name` — the shape 0.11.0 made canonical for kind ids. */\nconst NAMESPACED_KIND = /^@[a-z0-9][a-z0-9._-]*\\/[a-z0-9][a-z0-9._-]*$/i;\n\nfunction err(field: string, message: string): ManifestProblem {\n return { level: \"error\", field, message };\n}\n\nfunction warn(field: string, message: string): ManifestProblem {\n return { level: \"warning\", field, message };\n}\n\n/**\n * Validate a manifest read from disk or a registry.\n *\n * Returns every problem rather than throwing on the first, because an author\n * fixing a package wants the whole list — a validator that reveals one error\n * per run turns a five-minute fix into five round trips.\n */\nexport function validateNodeManifest(input: unknown): ManifestValidation {\n const problems: ManifestProblem[] = [];\n\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { ok: false, problems: [err(\"\", \"Manifest must be a JSON object.\")] };\n }\n\n const m = input as Record<string, unknown>;\n\n // Version first: an unknown version means every other check below is\n // guessing at a shape we do not know, so say so plainly instead of\n // half-reading it and reporting confident nonsense about the rest.\n if (m.schemaVersion !== NODE_MANIFEST_SCHEMA_VERSION) {\n if (typeof m.schemaVersion !== \"number\") {\n problems.push(err(\"schemaVersion\", `Required, and must be ${NODE_MANIFEST_SCHEMA_VERSION}.`));\n } else {\n return {\n ok: false,\n problems: [\n err(\n \"schemaVersion\",\n `Unsupported manifest version ${m.schemaVersion}; this fancy-flow understands ${NODE_MANIFEST_SCHEMA_VERSION}. Upgrade fancy-flow to install this node.`,\n ),\n ],\n };\n }\n }\n\n if (typeof m.name !== \"string\" || m.name.trim() === \"\") {\n problems.push(err(\"name\", \"Required — the package name as installed.\"));\n }\n\n if (typeof m.kind !== \"string\" || m.kind.trim() === \"\") {\n problems.push(err(\"kind\", \"Required — the canonical kind id this package provides.\"));\n } else if (!NAMESPACED_KIND.test(m.kind)) {\n // Un-namespaced ids are the one mistake that cannot be fixed after the\n // fact: the ambiguous string is already written into saved documents.\n problems.push(\n err(\"kind\", `\"${m.kind}\" must be namespaced as @scope/name — a bare id makes stored graphs ambiguous, and that is unfixable once documents carry it.`),\n );\n } else if (m.kind.startsWith(FIRST_PARTY_SCOPE)) {\n problems.push(\n warn(\"kind\", `${FIRST_PARTY_SCOPE}* is reserved for first-party nodes; the registry will reject this unless the package is first-party.`),\n );\n }\n\n if (typeof m.fancyFlow !== \"string\" || m.fancyFlow.trim() === \"\") {\n problems.push(err(\"fancyFlow\", \"Required — the semver range of fancy-flow this node targets.\"));\n }\n\n // Runtimes: the check the whole manifest exists for.\n if (typeof m.runtimes !== \"object\" || m.runtimes === null || Array.isArray(m.runtimes)) {\n problems.push(err(\"runtimes\", \"Required — an object of runtime id to entrypoint.\"));\n } else {\n const entries = Object.entries(m.runtimes as Record<string, unknown>);\n if (entries.length === 0) {\n problems.push(err(\"runtimes\", \"A node that implements no runtime cannot execute anywhere.\"));\n }\n for (const [runtime, entry] of entries) {\n if (typeof entry !== \"string\" || entry.trim() === \"\") {\n problems.push(err(`runtimes.${runtime}`, \"Entrypoint must be a non-empty string.\"));\n }\n }\n }\n\n // Fixtures: the publish gate. Cross-runtime drift does not fail loudly —\n // it completes, down one path, with no error — so it has to be caught by\n // something that runs, not by review.\n if (typeof m.fixtures !== \"string\" || m.fixtures.trim() === \"\") {\n problems.push(\n err(\"fixtures\", \"Required — path to the node's golden fixtures. Every claimed runtime runs them, which is what makes cross-runtime parity verified rather than claimed.\"),\n );\n }\n\n if (m.capabilities !== undefined) {\n if (!Array.isArray(m.capabilities) || m.capabilities.some((c) => typeof c !== \"string\")) {\n problems.push(err(\"capabilities\", \"Must be an array of capability id strings.\"));\n }\n }\n\n if (m.verified !== undefined) {\n problems.push(\n err(\"verified\", \"Assigned by the registry, not the package. Remove it — a package cannot vouch for itself.\"),\n );\n }\n\n const ok = !problems.some((p) => p.level === \"error\");\n return ok ? { ok, manifest: m as unknown as NodePackageManifest, problems } : { ok, problems };\n}\n\n/**\n * Check a node against the runtimes a host actually executes on.\n *\n * This is what makes a TS-only package visible to someone running PHP BEFORE\n * they install it rather than at the first run. A missing runtime is an error,\n * not a warning: the node genuinely cannot execute there.\n */\nexport function checkRuntimeSupport(\n manifest: Pick<NodePackageManifest, \"kind\" | \"runtimes\">,\n hostRuntimes: readonly string[],\n): ManifestProblem[] {\n const provided = Object.keys(manifest.runtimes ?? {});\n const missing = hostRuntimes.filter((r) => !provided.includes(r));\n\n if (missing.length === 0) return [];\n\n return [\n err(\n \"runtimes\",\n `${manifest.kind} implements ${provided.join(\", \") || \"no runtime\"} but this project executes on ${missing.join(\", \")}. The node would install, appear in the palette, and then fail to run.`,\n ),\n ];\n}\n\n/**\n * Check that every capability a node needs is actually wired.\n *\n * Pass `capabilityStatus()` from the host. Unwired capabilities are a warning\n * rather than an error — install is the right time to learn what to wire, not\n * a reason to refuse, since wiring usually happens after install.\n */\nexport function checkCapabilities(\n manifest: Pick<NodePackageManifest, \"kind\" | \"capabilities\">,\n available: Readonly<Record<string, boolean>>,\n): ManifestProblem[] {\n const needed = manifest.capabilities ?? [];\n const missing = needed.filter((c) => available[c] !== true);\n\n if (missing.length === 0) return [];\n\n return [\n warn(\n \"capabilities\",\n `${manifest.kind} needs ${missing.join(\", \")} wired on the host. Until then the node will fail at run time rather than at install.`,\n ),\n ];\n}\n","/**\n * Golden fixtures — the parity guarantee, and a publishing requirement.\n *\n * Every runtime a node package claims runs these same JSON cases. That is what\n * makes \"this node behaves identically on TS and PHP\" verified rather than\n * asserted, and it is the one thing a loose collection of repos can never\n * offer.\n *\n * WHY IT IS REQUIRED RATHER THAN ENCOURAGED: cross-runtime drift does not fail\n * loudly. The 0.9.0 port-resolution divergence produced flows that routed\n * correctly in the editor and silently down one path on the server — status\n * `completed`, no error, no exception, nothing to alert on. A guarantee that\n * only holds when an author opts in is not a guarantee.\n *\n * WHAT A CASE ASSERTS, and why it matters more than it looks:\n *\n * A fixture asserts THE DOWNSTREAM NODE EXECUTED — not the port the node\n * recorded. The distinction is the whole lesson of that incident. A test\n * reading back `outputs.router.__port` stays green while no edge fires and\n * the run halts at the branch: the node faithfully recorded its choice, and\n * nothing downstream ever ran. So the runner wires a real probe to every\n * declared port and reports which probes actually executed.\n *\n * The format is plain JSON with no expressions or callbacks, so the PHP runner\n * executes byte-identical cases without an embedded JS engine.\n */\n\nimport { runFlow } from \"../runtime/run-flow\";\nimport { getNodeKind } from \"../registry/registry\";\nimport { resolveNodePorts } from \"../registry/ports\";\nimport { decodePause, type PauseAwaiting } from \"../registry/pause\";\nimport type { FlowGraph, FlowNode, NodeExecutor } from \"../types\";\n\n/** What a case expects to have happened. */\nexport type FixtureExpectation = {\n /**\n * Output ports whose downstream node must have executed — the assertion that\n * catches routing drift. Order-insensitive; the set must match exactly, so a\n * node that fires an extra port fails just as loudly as one that fires none.\n */\n ports?: string[];\n /** The value carried downstream, deep-compared when present. */\n value?: unknown;\n /** The run halted for a person. */\n pause?: { awaiting: PauseAwaiting; detail?: unknown };\n /** The run failed, and the message contains this substring. */\n error?: string;\n};\n\nexport type FixtureCase = {\n /** Human-readable, and what a failure report names. */\n name: string;\n /** Config for the node under test — drives config-derived ports. */\n config?: Record<string, unknown>;\n /**\n * The node's resolved output ports, stated rather than derived.\n *\n * NOT a convenience. TS derives config-driven ports by running a JavaScript\n * function (`switch_case`'s cases, `llm_router`'s routes); PHP cannot, and\n * falls back to the kind's static declaration. Left there, the identical\n * fixture would build a DIFFERENT graph on each runtime — the fixtures would\n * silently stop comparing like with like, which is the exact failure they\n * exist to catch.\n *\n * So a case states its ports the same way an exported document does (see\n * 0.10.1, \"serialize resolved ports\"), and both runners honour it. Omit it\n * only for a node whose ports are static.\n */\n ports?: string[];\n /** Inputs delivered on the node's input ports. */\n inputs?: Record<string, unknown>;\n expect: FixtureExpectation;\n};\n\nexport type FixtureFile = {\n /** The kind under test. Must match the manifest's `kind`. */\n kind: string;\n cases: FixtureCase[];\n};\n\nexport type FixtureFailure = {\n case: string;\n message: string;\n};\n\nexport type FixtureRunResult = {\n ok: boolean;\n passed: number;\n failures: FixtureFailure[];\n};\n\nconst SUBJECT = \"subject\";\nconst TRIGGER = \"trigger\";\nconst probeId = (port: string) => `probe:${port}`;\n\n/**\n * Build the graph a case runs in: a trigger, the node under test, and one\n * probe per declared output port.\n *\n * The probes are the point. Reading the subject's return value would tell us\n * what it *recorded*; only a probe tells us what actually reached a downstream\n * node through a real edge.\n */\nfunction buildGraph(kindId: string, testCase: FixtureCase): { graph: FlowGraph; ports: string[] } {\n const subject: FlowNode = {\n id: SUBJECT,\n type: kindId,\n position: { x: 0, y: 100 },\n data: { kind: kindId, config: testCase.config ?? {} },\n } as unknown as FlowNode;\n\n const kind = getNodeKind(kindId) ?? undefined;\n // An explicitly declared port list wins, so both runtimes build the same\n // graph even where one can derive config-driven ports and the other cannot.\n const ports = testCase.ports ?? (resolveNodePorts(subject, kind).outputs ?? []).map((p) => p.id);\n const effective = ports.length ? ports : [\"out\"];\n\n const nodes: FlowNode[] = [\n { id: TRIGGER, type: \"manual_trigger\", position: { x: 0, y: 0 }, data: {} } as unknown as FlowNode,\n subject,\n ...effective.map(\n (port, i) =>\n ({\n id: probeId(port),\n type: \"@particle-academy/transform\",\n position: { x: i * 200, y: 200 },\n data: {},\n }) as unknown as FlowNode,\n ),\n ];\n\n const edges = [\n { id: `e:trigger`, source: TRIGGER, target: SUBJECT },\n ...effective.map((port) => ({\n id: `e:${port}`,\n source: SUBJECT,\n sourceHandle: port,\n target: probeId(port),\n })),\n ];\n\n return { graph: { nodes, edges } as FlowGraph, ports: effective };\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * Run one fixture file against a kind's executor.\n *\n * `executor` is the node's own; everything else in the graph is supplied here,\n * so a case exercises the node and nothing but the node.\n */\nexport async function runFixtures(\n file: FixtureFile,\n executor: NodeExecutor,\n): Promise<FixtureRunResult> {\n const failures: FixtureFailure[] = [];\n let passed = 0;\n\n for (const testCase of file.cases) {\n const { graph } = buildGraph(file.kind, testCase);\n const fired: string[] = [];\n let carried: unknown;\n\n const executors: Record<string, NodeExecutor> = {\n manual_trigger: () => testCase.inputs ?? {},\n \"@particle-academy/manual_trigger\": () => testCase.inputs ?? {},\n [file.kind]: executor,\n };\n\n // A probe per port, bound by NODE ID — `pickExecutor` checks that before\n // kind, so each probe reports which specific port reached it. Recording\n // here, rather than inspecting the subject's result, is what makes this\n // assert reachability instead of intent.\n for (const node of graph.nodes) {\n if (!node.id.startsWith(\"probe:\")) continue;\n const port = node.id.slice(\"probe:\".length);\n executors[node.id] = ((ctx: { inputs: Record<string, unknown> }) => {\n fired.push(port);\n carried = ctx.inputs?.in ?? ctx.inputs;\n return undefined;\n }) as unknown as NodeExecutor;\n }\n\n const result = await runFlow(graph, executors, () => {});\n const fail = (message: string) => failures.push({ case: testCase.name, message });\n const expected = testCase.expect;\n let caseOk = true;\n\n if (expected.pause) {\n const paused = decodePause(result.error);\n if (!paused) {\n caseOk = false;\n fail(`expected a pause awaiting \"${expected.pause.awaiting}\", got ${result.ok ? \"a completed run\" : `error: ${result.error}`}`);\n } else {\n if (paused.awaiting !== expected.pause.awaiting) {\n caseOk = false;\n fail(`expected pause awaiting \"${expected.pause.awaiting}\", got \"${paused.awaiting}\"`);\n }\n if (\"detail\" in expected.pause && !deepEqual(paused.detail, expected.pause.detail)) {\n caseOk = false;\n fail(`pause detail mismatch: expected ${JSON.stringify(expected.pause.detail)}, got ${JSON.stringify(paused.detail)}`);\n }\n }\n } else if (expected.error !== undefined) {\n if (result.ok) {\n caseOk = false;\n fail(`expected an error containing \"${expected.error}\", but the run succeeded`);\n } else if (!String(result.error ?? \"\").includes(expected.error)) {\n caseOk = false;\n fail(`expected an error containing \"${expected.error}\", got \"${result.error}\"`);\n }\n } else {\n if (expected.ports !== undefined) {\n const got = [...fired].sort();\n const want = [...expected.ports].sort();\n if (!deepEqual(got, want)) {\n caseOk = false;\n // Name the failure in terms of reachability, because that is what\n // the assertion means and what a reader needs to act on.\n fail(\n `expected these ports to reach a downstream node: [${want.join(\", \")}], but [${got.join(\", \")}] did` +\n (result.ok ? \"\" : ` (run error: ${result.error})`),\n );\n }\n }\n if (\"value\" in expected && !deepEqual(carried, expected.value)) {\n caseOk = false;\n fail(`expected the value carried downstream to be ${JSON.stringify(expected.value)}, got ${JSON.stringify(carried)}`);\n }\n }\n\n if (caseOk) passed += 1;\n }\n\n return { ok: failures.length === 0, passed, failures };\n}\n\n/**\n * Validate a fixture file's shape before running it.\n *\n * A package that publishes an empty or malformed fixture file has satisfied the\n * letter of the requirement and none of its purpose, so this is checked at\n * publish rather than trusted.\n */\nexport function validateFixtureFile(input: unknown, expectedKind?: string): string[] {\n const problems: string[] = [];\n\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return [\"Fixture file must be a JSON object.\"];\n }\n\n const f = input as Record<string, unknown>;\n\n if (typeof f.kind !== \"string\" || f.kind.trim() === \"\") {\n problems.push(\"`kind` is required — the kind these cases exercise.\");\n } else if (expectedKind && f.kind !== expectedKind) {\n problems.push(`\\`kind\\` is \"${f.kind}\" but the manifest declares \"${expectedKind}\".`);\n }\n\n if (!Array.isArray(f.cases) || f.cases.length === 0) {\n problems.push(\"`cases` must contain at least one case — an empty fixture file proves nothing.\");\n return problems;\n }\n\n f.cases.forEach((c: unknown, i: number) => {\n const label = `cases[${i}]`;\n if (typeof c !== \"object\" || c === null) {\n problems.push(`${label} must be an object.`);\n return;\n }\n const testCase = c as Record<string, unknown>;\n if (typeof testCase.name !== \"string\" || testCase.name.trim() === \"\") {\n problems.push(`${label}.name is required — a failure report names it.`);\n }\n if (typeof testCase.expect !== \"object\" || testCase.expect === null) {\n problems.push(`${label}.expect is required — a case that asserts nothing passes vacuously.`);\n return;\n }\n const expect = testCase.expect as Record<string, unknown>;\n if (\n expect.ports === undefined &&\n expect.value === undefined &&\n expect.pause === undefined &&\n expect.error === undefined\n ) {\n problems.push(`${label}.expect must assert at least one of: ports, value, pause, error.`);\n }\n if (expect.ports !== undefined && (!Array.isArray(expect.ports) || expect.ports.some((p) => typeof p !== \"string\"))) {\n problems.push(`${label}.expect.ports must be an array of port id strings.`);\n }\n });\n\n return problems;\n}\n"]}
package/dist/index.cjs CHANGED
@@ -602,6 +602,7 @@ var KINDS = [
602
602
  {
603
603
  name: "@particle-academy/user_input",
604
604
  aliases: ["user_input", "@fancy/user_input"],
605
+ pausesForHuman: "input",
605
606
  category: "human",
606
607
  label: "User Input",
607
608
  description: "Pause the flow until the user submits the configured form.",
@@ -643,6 +644,7 @@ var KINDS = [
643
644
  {
644
645
  name: "@particle-academy/rich_user_input",
645
646
  aliases: ["rich_user_input", "@fancy/rich_user_input"],
647
+ pausesForHuman: "input",
646
648
  category: "human",
647
649
  label: "Rich User Input",
648
650
  description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
@@ -1171,6 +1173,7 @@ var KINDS = [
1171
1173
  {
1172
1174
  name: "@particle-academy/human_approval",
1173
1175
  aliases: ["human_approval", "@fancy/human_approval"],
1176
+ pausesForHuman: "approval",
1174
1177
  category: "human",
1175
1178
  label: "Human Approval",
1176
1179
  description: "Pause until a human approves or denies.",
@@ -11108,6 +11111,42 @@ function importWorkflow(schema, options = {}) {
11108
11111
  function workflowToBlob(schema) {
11109
11112
  return new Blob([JSON.stringify(schema, null, 2)], { type: "application/json" });
11110
11113
  }
11114
+
11115
+ // src/registry/pause.ts
11116
+ var PAUSE_PREFIX = "fancy-flow:pause:";
11117
+ var LEGACY_PAUSE_PREFIXES = [
11118
+ ["awaiting-approval:", "approval"],
11119
+ ["awaiting-input:", "input"]
11120
+ ];
11121
+ function encodePause(signal) {
11122
+ const { nodeId, awaiting, detail } = signal;
11123
+ return PAUSE_PREFIX + JSON.stringify(detail === void 0 ? { nodeId, awaiting } : { nodeId, awaiting, detail });
11124
+ }
11125
+ function decodePause(reason) {
11126
+ if (typeof reason !== "string") return null;
11127
+ if (reason.startsWith(PAUSE_PREFIX)) {
11128
+ const body = reason.slice(PAUSE_PREFIX.length);
11129
+ try {
11130
+ const parsed = JSON.parse(body);
11131
+ if (typeof parsed?.nodeId !== "string" || typeof parsed?.awaiting !== "string") return null;
11132
+ return "detail" in parsed ? { nodeId: parsed.nodeId, awaiting: parsed.awaiting, detail: parsed.detail } : { nodeId: parsed.nodeId, awaiting: parsed.awaiting };
11133
+ } catch {
11134
+ return null;
11135
+ }
11136
+ }
11137
+ for (const [prefix, awaiting] of LEGACY_PAUSE_PREFIXES) {
11138
+ if (reason.startsWith(prefix)) {
11139
+ return { nodeId: reason.slice(prefix.length), awaiting };
11140
+ }
11141
+ }
11142
+ return null;
11143
+ }
11144
+ function isPause(reason) {
11145
+ return decodePause(reason) !== null;
11146
+ }
11147
+ function pauseForHuman(ctx, awaiting, detail) {
11148
+ return ctx.abort(encodePause({ nodeId: ctx.node.id, awaiting, detail }));
11149
+ }
11111
11150
  function RegistryNodeInner(props) {
11112
11151
  const kindName = props.data.kind ?? props.type;
11113
11152
  const kind = ReactExports.useMemo(() => getNodeKind(kindName), [kindName]);
@@ -11759,12 +11798,14 @@ exports.FlowCanvas = FlowCanvas;
11759
11798
  exports.FlowEditor = FlowEditor;
11760
11799
  exports.FlowRunControls = FlowRunControls;
11761
11800
  exports.FlowRunFeed = FlowRunFeed;
11801
+ exports.LEGACY_PAUSE_PREFIXES = LEGACY_PAUSE_PREFIXES;
11762
11802
  exports.NodeConfigPanel = NodeConfigPanel;
11763
11803
  exports.NodePalette = NodePalette;
11764
11804
  exports.NodePort = NodePort;
11765
11805
  exports.NodeShell = NodeShell;
11766
11806
  exports.NoteNode = NoteNode;
11767
11807
  exports.OutputNode = OutputNode2;
11808
+ exports.PAUSE_PREFIX = PAUSE_PREFIX;
11768
11809
  exports.RegistryNode = RegistryNode;
11769
11810
  exports.RichInputPreview = RichInputPreview;
11770
11811
  exports.SubgraphNode = SubgraphNode;
@@ -11774,18 +11815,22 @@ exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
11774
11815
  exports.applyStatusesToNodes = applyStatusesToNodes;
11775
11816
  exports.buildNodeTypes = buildNodeTypes;
11776
11817
  exports.categoryAccent = categoryAccent;
11818
+ exports.decodePause = decodePause;
11777
11819
  exports.defaultConfigFor = defaultConfigFor;
11778
11820
  exports.defaultNodeTypes = defaultNodeTypes;
11779
11821
  exports.defineNode = defineNode;
11822
+ exports.encodePause = encodePause;
11780
11823
  exports.exportWorkflow = exportWorkflow;
11781
11824
  exports.getNodeKind = getNodeKind;
11782
11825
  exports.getRichInputAdapter = getRichInputAdapter;
11783
11826
  exports.importWorkflow = importWorkflow;
11827
+ exports.isPause = isPause;
11784
11828
  exports.isRichInputEnabled = isRichInputEnabled;
11785
11829
  exports.listNodeKinds = listNodeKinds;
11786
11830
  exports.onNodeKindsChanged = onNodeKindsChanged;
11787
11831
  exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
11788
11832
  exports.paletteDropHandlers = paletteDropHandlers;
11833
+ exports.pauseForHuman = pauseForHuman;
11789
11834
  exports.registerBuiltinKinds = registerBuiltinKinds;
11790
11835
  exports.registerNodeKind = registerNodeKind;
11791
11836
  exports.registerRichInputAdapter = registerRichInputAdapter;