fyflow-orchestration 0.7.0 → 0.8.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.
@@ -119,6 +119,14 @@ function regionChainOf(regions, nodeId) {
119
119
  }
120
120
  return chain.reverse();
121
121
  }
122
+ function innermostMapOf(regions, nodeId) {
123
+ const chain = regionChainOf(regions, nodeId);
124
+ for (let i = chain.length - 1; i >= 0; i--) {
125
+ const r = regions.find((x) => x.fanoutId === chain[i]);
126
+ if (r?.opensWith === "map") return r;
127
+ }
128
+ return void 0;
129
+ }
122
130
  function sharedRegionDepth(regions, a, b) {
123
131
  const ca = regionChainOf(regions, a);
124
132
  const cb = regionChainOf(regions, b);
@@ -223,7 +231,11 @@ var SCHEMA_KEYWORDS = [
223
231
  "maxLength",
224
232
  "items",
225
233
  "minItems",
226
- "examples"
234
+ "examples",
235
+ "title",
236
+ "description",
237
+ "readOnly",
238
+ "writeOnly"
227
239
  ];
228
240
  var SCHEMA_TYPES = ["object", "array", "string", "number", "integer", "boolean", "null"];
229
241
  var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -265,6 +277,12 @@ function schemaProblem(schema, at = "$") {
265
277
  for (const k of ["minLength", "maxLength", "minItems"]) {
266
278
  if (s[k] !== void 0 && !isNonNegInt(s[k])) return `${at}.${k}: must be an integer >= 0, got ${JSON.stringify(s[k])}`;
267
279
  }
280
+ for (const k of ["title", "description"]) {
281
+ if (s[k] !== void 0 && typeof s[k] !== "string") return `${at}.${k}: must be a string \u2014 an annotation that constrains nothing (ADR 0098) \u2014 got ${JSON.stringify(s[k])}`;
282
+ }
283
+ for (const k of ["readOnly", "writeOnly"]) {
284
+ if (s[k] !== void 0 && typeof s[k] !== "boolean") return `${at}.${k}: must be true or false \u2014 an annotation that constrains nothing (ADR 0098) \u2014 got ${JSON.stringify(s[k])}`;
285
+ }
268
286
  if (s.items !== void 0) {
269
287
  if (Array.isArray(s.items)) return `${at}.items: the subset admits one schema for every item, not a tuple`;
270
288
  const p = schemaProblem(s.items, `${at}.items`);
@@ -341,9 +359,63 @@ function describeFindings(fs) {
341
359
  return fs.map((f) => `${f.path} ${f.says}`).join("; ");
342
360
  }
343
361
 
362
+ // src/definition/condition.ts
363
+ function readKeyPath(value, path) {
364
+ let v = value;
365
+ for (const seg of path.split(".")) {
366
+ if (typeof v !== "object" || v === null) return void 0;
367
+ const proto = Object.getPrototypeOf(v);
368
+ if (proto !== Object.prototype && proto !== null) return void 0;
369
+ if (!Object.prototype.hasOwnProperty.call(v, seg)) return void 0;
370
+ v = v[seg];
371
+ }
372
+ return v;
373
+ }
374
+ function evalLeaf(scope, p, unknownOp) {
375
+ let v = readKeyPath(scope, p.path);
376
+ if (v === void 0 && !("default" in p)) return false;
377
+ if ((v === void 0 || v === null) && "default" in p) v = p.default;
378
+ if (p.length === true) {
379
+ if (Array.isArray(v)) v = v.length;
380
+ else if (typeof v === "string") v = v.length;
381
+ else return false;
382
+ }
383
+ const w = p.value;
384
+ switch (p.op) {
385
+ // **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
386
+ // `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
387
+ // measured over 1800 contexts, a type-strict form reproduces those three EXACTLY and differs from
388
+ // `resume.test.ts`'s unguarded `isHigh` on `{score: "9"}`, where the JS body coerces. That is
389
+ // `FD-V30`'s own argument — *a string `maxRetries` retries by coercion* — one rule over.
390
+ case "eq":
391
+ return v === w;
392
+ case "ne":
393
+ return v !== w;
394
+ case "lt":
395
+ return typeof v === "number" && typeof w === "number" && v < w;
396
+ case "lte":
397
+ return typeof v === "number" && typeof w === "number" && v <= w;
398
+ case "gt":
399
+ return typeof v === "number" && typeof w === "number" && v > w;
400
+ case "gte":
401
+ return typeof v === "number" && typeof w === "number" && v >= w;
402
+ case "startsWith":
403
+ return typeof v === "string" && typeof w === "string" && v.startsWith(w);
404
+ default: {
405
+ const never = p.op;
406
+ throw unknownOp(`operator "${String(never)}" is not in the subset; FD-V44 should have caught this at instantiation`);
407
+ }
408
+ }
409
+ }
410
+ function evaluateDeclared(when, scope, unknownOp) {
411
+ if ("all" in when && Array.isArray(when.all)) return when.all.every((m) => evalLeaf(scope, m, unknownOp));
412
+ if ("any" in when && Array.isArray(when.any)) return when.any.some((m) => evalLeaf(scope, m, unknownOp));
413
+ return evalLeaf(scope, when, unknownOp);
414
+ }
415
+
344
416
  // src/definition/validate.ts
345
417
  var ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
346
- var KINDS = ["task", "fanout", "map", "join", "end", "gate"];
418
+ var KINDS = ["task", "fanout", "map", "join", "end", "gate", "ask"];
347
419
  var ROUTABLE = ["done", "failed"];
348
420
  var DOC_KEYS = ["flowId", "version", "entry", "nodes"];
349
421
  var NODE_KEYS = {
@@ -365,8 +437,14 @@ var NODE_KEYS = {
365
437
  end: { req: ["id", "kind", "outcome"], opt: [] },
366
438
  // v0.23 (W92, ADR 0066): a gate names a GATE type and carries settings, as a task names a node type — and
367
439
  // nothing else a task carries. `needs` is absent on purpose: a gate hands on what reached it, unchanged.
368
- gate: { req: ["id", "kind", "type", "edges"], opt: ["settings"] }
440
+ gate: { req: ["id", "kind", "type", "edges"], opt: ["settings"] },
441
+ // v0.42 (W179, ADR 0108, P35 stop-and-ask (a)): an ASK waits for a person. It routes like a task (`edges`,
442
+ // `FD-V7`), receives what its `needs` composes, carries opaque `settings` a UI reads and an `answerSchema` a
443
+ // `done` answer is held to — and NONE of the keys a task's worker reads: those six are `FD-V47`'s
444
+ // (`ASK_REFUSED_KEYS`), refused there by name, so `FD-V17` does not report them a second time.
445
+ ask: { req: ["id", "kind", "edges"], opt: ["settings", "answerSchema", "needs"] }
369
446
  };
447
+ var ASK_REFUSED_KEYS = ["type", "pool", "optional", "retryPolicy", "workerGroups", "transform"];
370
448
  var EDGE_KEYS = { req: ["to"], opt: ["on", "when", "needs", "route"] };
371
449
  var EDGE_KEYS_DECLARED = [...EDGE_KEYS.req, ...EDGE_KEYS.opt];
372
450
  var TASK_KEYS_DECLARED = [...NODE_KEYS.task.req, ...NODE_KEYS.task.opt];
@@ -434,51 +512,110 @@ function keyPathProblem(v) {
434
512
  if (KEY_PATH_RE.test(v)) return null;
435
513
  return `settings.key ${JSON.stringify(v)} is not a key path \u2014 dot-separated object keys, each [A-Za-z0-9_-]{1,64}; no array index, no wildcard, no expression (a key that must be computed is computed in the task before the gate)`;
436
514
  }
437
- function readKeyPath(value, path) {
438
- let v = value;
439
- for (const seg of path.split(".")) {
440
- if (typeof v !== "object" || v === null) return void 0;
441
- const proto = Object.getPrototypeOf(v);
442
- if (proto !== Object.prototype && proto !== null) return void 0;
443
- if (!Object.prototype.hasOwnProperty.call(v, seg)) return void 0;
444
- v = v[seg];
515
+ var GATE_TYPE_KEYS = ["routes", "defaultRoute", "settingsSchema", "keyResolvedRoutes", "routesFor", "settingsProblem", "fn"];
516
+ var STATIC_ROUTE_KEYS = ["routes", "defaultRoute", "keyResolvedRoutes"];
517
+ var ROUTE_SET_KEYS = STATIC_ROUTE_KEYS;
518
+ var GATE_TYPE_OPTIONAL = /* @__PURE__ */ new Set(["settingsSchema", "keyResolvedRoutes", "settingsProblem"]);
519
+ var GATE_TYPE_SHAPE = `{ ${GATE_TYPE_KEYS.map((k) => GATE_TYPE_OPTIONAL.has(k) ? `${k}?` : k).join(", ")} } with its routes declared STATICALLY by routes and defaultRoute (keyResolvedRoutes beside them) or DERIVED by routesFor, exactly one of the two`;
520
+ function describeValue(v) {
521
+ try {
522
+ const j = JSON.stringify(v);
523
+ if (j !== void 0) return j;
524
+ } catch {
525
+ }
526
+ try {
527
+ return typeof v === "bigint" ? `${v}n` : String(v);
528
+ } catch {
529
+ return `an unprintable ${typeof v}`;
445
530
  }
446
- return v;
447
531
  }
448
- var GATE_TYPE_KEYS = ["routes", "defaultRoute", "settingsSchema", "keyResolvedRoutes", "fn"];
449
- var GATE_TYPE_OPTIONAL = /* @__PURE__ */ new Set(["settingsSchema", "keyResolvedRoutes"]);
450
- var GATE_TYPE_SHAPE = `{ ${GATE_TYPE_KEYS.map((k) => GATE_TYPE_OPTIONAL.has(k) ? `${k}?` : k).join(", ")} }`;
451
- function gateTypeProblem(t) {
452
- if (typeof t !== "object" || t === null || Array.isArray(t)) return `a gate type is an object ${GATE_TYPE_SHAPE}, got ${JSON.stringify(t)}`;
453
- const o = t;
454
- const extra = Object.keys(o).filter((k) => !GATE_TYPE_KEYS.includes(k));
455
- if (extra.length) return `it carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a gate type does not declare; it is ${GATE_TYPE_SHAPE}`;
456
- const routes = o.routes;
457
- if (!Array.isArray(routes) || routes.length === 0) return `routes must be a non-empty array of route names, got ${JSON.stringify(routes)}`;
532
+ function describeThrown(e) {
533
+ try {
534
+ if (e instanceof Error) return String(e.message);
535
+ } catch {
536
+ }
537
+ return describeValue(e);
538
+ }
539
+ function routeSetProblem(routes, defaultRoute, keyResolvedRoutes) {
540
+ if (!Array.isArray(routes) || routes.length === 0) return `routes must be a non-empty array of route names, got ${describeValue(routes)}`;
458
541
  for (const r of routes) {
459
- if (typeof r !== "string" || !ID_RE.test(r)) return `route ${JSON.stringify(r)} is not [A-Za-z0-9_-]{1,64}`;
542
+ if (typeof r !== "string" || !ID_RE.test(r)) return `route ${describeValue(r)} is not [A-Za-z0-9_-]{1,64}`;
460
543
  }
461
544
  if (new Set(routes).size !== routes.length) return "routes repeats a route";
462
- if (typeof o.defaultRoute !== "string" || !routes.includes(o.defaultRoute)) {
463
- return `defaultRoute ${JSON.stringify(o.defaultRoute)} is not one of its routes (${routes.join(", ")}) \u2014 an edge with no route would carry a route the type never returns`;
545
+ if (typeof defaultRoute !== "string" || !routes.includes(defaultRoute)) {
546
+ return `defaultRoute ${describeValue(defaultRoute)} is not one of its routes (${routes.join(", ")}) \u2014 an edge with no route would carry a route the type never returns`;
464
547
  }
465
- if (o.settingsSchema !== void 0) {
466
- const p = schemaProblem(o.settingsSchema);
467
- if (p !== null) return `its settingsSchema is not in the built-in subset \u2014 ${p}`;
468
- }
469
- if (o.keyResolvedRoutes !== void 0) {
470
- const kr = o.keyResolvedRoutes;
471
- if (!Array.isArray(kr)) return `keyResolvedRoutes must be an array of route names, got ${JSON.stringify(kr)}`;
548
+ if (keyResolvedRoutes !== void 0) {
549
+ const kr = keyResolvedRoutes;
550
+ if (!Array.isArray(kr)) return `keyResolvedRoutes must be an array of route names, got ${describeValue(kr)}`;
472
551
  for (const r of kr) {
473
552
  if (typeof r !== "string" || !routes.includes(r)) {
474
- return `keyResolvedRoutes names ${JSON.stringify(r)}, which is not one of its routes (${routes.join(", ")}) \u2014 a guarantee on a route the type never returns applies to nothing`;
553
+ return `keyResolvedRoutes names ${describeValue(r)}, which is not one of its routes (${routes.join(", ")}) \u2014 a guarantee on a route the type never returns applies to nothing`;
475
554
  }
476
555
  }
477
556
  if (new Set(kr).size !== kr.length) return "keyResolvedRoutes repeats a route";
478
557
  }
558
+ return null;
559
+ }
560
+ function gateTypeProblem(t) {
561
+ if (typeof t !== "object" || t === null || Array.isArray(t)) return `a gate type is an object ${GATE_TYPE_SHAPE}, got ${describeValue(t)}`;
562
+ const o = t;
563
+ const extra = Object.keys(o).filter((k) => !GATE_TYPE_KEYS.includes(k));
564
+ if (extra.length) return `it carries ${extra.map((k) => describeValue(k)).join(", ")}, which a gate type does not declare; it is ${GATE_TYPE_SHAPE}`;
565
+ const staticKeys = STATIC_ROUTE_KEYS.filter((k) => o[k] !== void 0);
566
+ const derived = o.routesFor !== void 0;
567
+ if (derived && staticKeys.length) {
568
+ return `it carries routesFor AND ${staticKeys.join(", ")} \u2014 a gate type declares its routes statically or derives them from its gate's settings, exactly one of the two`;
569
+ }
570
+ if (!derived && staticKeys.length === 0) {
571
+ return "it declares its routes neither statically (routes, defaultRoute) nor by routesFor \u2014 a gate type declares exactly one of the two";
572
+ }
573
+ if (derived) {
574
+ if (typeof o.routesFor !== "function") return "routesFor must be a function (settings) => { routes, defaultRoute, keyResolvedRoutes? }";
575
+ } else {
576
+ const p = routeSetProblem(o.routes, o.defaultRoute, o.keyResolvedRoutes);
577
+ if (p !== null) return p;
578
+ }
579
+ if (o.settingsSchema !== void 0) {
580
+ const p = schemaProblem(o.settingsSchema);
581
+ if (p !== null) return `its settingsSchema is not in the built-in subset \u2014 ${p}`;
582
+ }
583
+ if (o.settingsProblem !== void 0 && typeof o.settingsProblem !== "function") return "settingsProblem must be a function (settings) => string | null";
479
584
  if (typeof o.fn !== "function") return "fn must be a function (ctx) => route";
480
585
  return null;
481
586
  }
587
+ function routesOf(type, settings) {
588
+ let set;
589
+ if (typeof type.routesFor === "function") {
590
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
591
+ return { problem: `routesFor is asked over settings that are a plain object, got ${describeValue(settings)}` };
592
+ }
593
+ let copy;
594
+ try {
595
+ copy = structuredClone(settings);
596
+ } catch (e) {
597
+ return { problem: `the gate's settings cannot be copied for routesFor \u2014 ${describeThrown(e)}` };
598
+ }
599
+ try {
600
+ set = type.routesFor(copy);
601
+ } catch (e) {
602
+ return { problem: `routesFor threw: ${describeThrown(e)}` };
603
+ }
604
+ if (typeof set !== "object" || set === null || Array.isArray(set)) {
605
+ return { problem: `routesFor returned ${describeValue(set)}, which is not { routes, defaultRoute, keyResolvedRoutes? }` };
606
+ }
607
+ const extra = Object.keys(set).filter((k) => !ROUTE_SET_KEYS.includes(k));
608
+ if (extra.length) return { problem: `routesFor returned ${extra.map((k) => describeValue(k)).join(", ")}, which a route set does not declare; it is { routes, defaultRoute, keyResolvedRoutes? }` };
609
+ } else {
610
+ set = type;
611
+ }
612
+ const s = set;
613
+ const p = routeSetProblem(s.routes, s.defaultRoute, s.keyResolvedRoutes);
614
+ if (p !== null) return { problem: typeof type.routesFor === "function" ? `routesFor returned a malformed route set: ${p}` : p };
615
+ const routes = Object.freeze([...s.routes]);
616
+ const defaultRoute = s.defaultRoute;
617
+ return s.keyResolvedRoutes === void 0 ? Object.freeze({ routes, defaultRoute }) : Object.freeze({ routes, defaultRoute, keyResolvedRoutes: Object.freeze([...s.keyResolvedRoutes]) });
618
+ }
482
619
  var distinct = Object.freeze({
483
620
  routes: Object.freeze(["first", "duplicate", "nokey"]),
484
621
  defaultRoute: "first",
@@ -502,7 +639,79 @@ var distinct = Object.freeze({
502
639
  return "first";
503
640
  }
504
641
  });
505
- var BUILT_IN_GATE_TYPES = Object.freeze({ distinct });
642
+ var UNKNOWN_OP = (message) => new Error(message);
643
+ var ifGate = Object.freeze({
644
+ routes: Object.freeze(["then", "else"]),
645
+ defaultRoute: "else",
646
+ // The KEYS only: the grammar is not the subset's to state (P32 fact 3), and `when`'s TYPE is left to
647
+ // `settingsProblem` so a bare string gets the grammar's own message rather than the schema's.
648
+ settingsSchema: Object.freeze({
649
+ type: "object",
650
+ required: ["when"],
651
+ properties: { when: {} },
652
+ additionalProperties: false
653
+ }),
654
+ settingsProblem(settings) {
655
+ const p = gateConditionProblem(settings.when);
656
+ return p === null ? null : `${p.message} (${p.rule}'s grammar, which an if gate's condition is held to)`;
657
+ },
658
+ fn(ctx) {
659
+ return evaluateDeclared(ctx.settings.when, { input: ctx.input }, UNKNOWN_OP) ? "then" : "else";
660
+ }
661
+ });
662
+ var switchGate = Object.freeze({
663
+ settingsSchema: Object.freeze({
664
+ type: "object",
665
+ required: ["key", "cases"],
666
+ properties: { key: { type: "string", minLength: 1 }, cases: { type: "object" } },
667
+ additionalProperties: false
668
+ }),
669
+ settingsProblem(settings) {
670
+ const cases = settings.cases;
671
+ const seen = [];
672
+ if (Object.keys(cases).length === 0) return 'cases is empty \u2014 a switch with no case routes every value to default, which is "always default" written as a question';
673
+ for (const name of Object.keys(cases)) {
674
+ if (!ID_RE.test(name)) return `cases names the case ${JSON.stringify(name)}, which is not [A-Za-z0-9_-]{1,64} \u2014 a case name is a ROUTE name (FD-V16's charset)`;
675
+ if (name === "default") return 'cases names a case "default", which is the route a key matching no case takes \u2014 the name is reserved';
676
+ const v = cases[name];
677
+ if (Array.isArray(v) && v.length === 0) return `cases.${name} is an empty array; a case names a scalar or a non-empty array of scalars \u2014 a case no value can reach is a route no run can take`;
678
+ for (const x of Array.isArray(v) ? v : [v]) {
679
+ if (!isScalar(x)) {
680
+ return `cases.${name} ${Array.isArray(v) ? "holds" : "is"} ${scalarName(x)}; a case names a SCALAR \u2014 null, a boolean, a finite number or a string \u2014 or a non-empty array of them`;
681
+ }
682
+ const other = seen.find((s) => s.value === x);
683
+ if (other !== void 0 && other.name === name) {
684
+ return `cases.${name} repeats the value ${caseValue(x)} \u2014 a case names each of its values once`;
685
+ }
686
+ if (other !== void 0) {
687
+ if (!Object.is(other.value, x)) {
688
+ return `the values ${caseValue(other.value)} in case "${other.name}" and ${caseValue(x)} in case "${name}" are one value by === \u2014 a value takes one route`;
689
+ }
690
+ return `the value ${caseValue(x)} is named by two cases, "${other.name}" and "${name}" \u2014 a value takes one route (matching is ===, so "1" and 1 are two values)`;
691
+ }
692
+ seen.push({ value: x, name });
693
+ }
694
+ }
695
+ return null;
696
+ },
697
+ routesFor(settings) {
698
+ const cases = settings.cases;
699
+ if (typeof cases !== "object" || cases === null || Array.isArray(cases)) throw new Error(`cases is ${scalarName(cases)}, not an object of case names`);
700
+ const names = Object.keys(cases);
701
+ return { routes: [...names, "default"], defaultRoute: "default", keyResolvedRoutes: names };
702
+ },
703
+ fn(ctx) {
704
+ const key = ctx.key;
705
+ if (key === void 0 || typeof key === "object" && key !== null) return "default";
706
+ const cases = ctx.settings.cases;
707
+ for (const name of Object.keys(cases)) {
708
+ const v = cases[name];
709
+ if (Array.isArray(v) ? v.some((x) => x === key) : v === key) return name;
710
+ }
711
+ return "default";
712
+ }
713
+ });
714
+ var BUILT_IN_GATE_TYPES = Object.freeze({ distinct, if: ifGate, switch: switchGate });
506
715
  function gateTypesOf(x) {
507
716
  const out = new Map(Object.entries(BUILT_IN_GATE_TYPES));
508
717
  const own3 = x === void 0 ? [] : x instanceof Map ? [...x] : Object.entries(x);
@@ -527,11 +736,23 @@ function predicateShapeOf(o) {
527
736
  if (marks.length > 1) return "ambiguous";
528
737
  return marks[0];
529
738
  }
739
+ var caseValue = (v) => Object.is(v, -0) ? "-0" : JSON.stringify(v);
530
740
  var isScalar = (v) => v === null || typeof v === "boolean" || typeof v === "string" || typeof v === "number" && Number.isFinite(v);
531
741
  var scalarName = (v) => v === null ? "null" : Array.isArray(v) ? "an array" : typeof v === "number" && !Number.isFinite(v) ? String(v) : typeof v;
532
742
  var NUMERIC_OPS = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
533
743
  var STRING_OPS = /* @__PURE__ */ new Set(["startsWith"]);
534
- function leafProblem(o, where) {
744
+ var EDGE_SCOPE = {
745
+ roots: PREDICATE_ROOTS,
746
+ foreignRoot: () => `which a decision cannot see \u2014 a predicate's roots are ${PREDICATE_ROOTS.map((r) => JSON.stringify(r)).join(" and ")}, and any other first segment would resolve to nothing in silence`,
747
+ rootAlone: (root) => `and names no value \u2014 ${JSON.stringify(root)} is a container, so a comparison against it could only ever be no match`
748
+ };
749
+ var GATE_CONDITION_ROOTS = ["input"];
750
+ var GATE_SCOPE = {
751
+ roots: GATE_CONDITION_ROOTS,
752
+ foreignRoot: () => 'which an if gate cannot see \u2014 its one root is "input", the value that reached the gate; a gate has no outcome and no edge needs, and any other first segment would resolve to nothing in silence',
753
+ rootAlone: () => "and names no key \u2014 a gate's path names a key INSIDE the value that reached it, as every gate's key path does; a value that must be compared whole is wrapped by the task before the gate"
754
+ };
755
+ function leafProblem(o, where, scope = EDGE_SCOPE) {
535
756
  const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
536
757
  if (extra.length) {
537
758
  return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
@@ -544,11 +765,11 @@ function leafProblem(o, where) {
544
765
  return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is not a key path \u2014 dot-separated object keys, each [A-Za-z0-9_-]{1,64}; no array index, no wildcard, no expression` };
545
766
  }
546
767
  const root = path.split(".")[0];
547
- if (!PREDICATE_ROOTS.includes(root)) {
548
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, which a decision cannot see \u2014 a predicate's roots are ${PREDICATE_ROOTS.map((r) => JSON.stringify(r)).join(" and ")}, and any other first segment would resolve to nothing in silence` };
768
+ if (!scope.roots.includes(root)) {
769
+ return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
549
770
  }
550
771
  if (path.split(".").length < 2) {
551
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone and names no value \u2014 ${JSON.stringify(root)} is a container, so a comparison against it could only ever be no match` };
772
+ return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
552
773
  }
553
774
  const op = o.op;
554
775
  if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
@@ -604,23 +825,26 @@ function predicateProblem(v) {
604
825
  return { rule: "FD-V42", message: `when carries ${Object.keys(o).map((k) => JSON.stringify(k)).join(", ")} and matches more than one shape \u2014 a name, a comparison and a group are told apart by which keys are present, so an object may mark only one` };
605
826
  }
606
827
  if (shape === "name") {
607
- const extra2 = Object.keys(o).filter((k) => !PREDICATE_NAME_KEYS.includes(k));
608
- if (extra2.length) return { rule: "FD-V42", message: `when names a predicate and also carries ${extra2.map((k) => JSON.stringify(k)).join(", ")}; the name form is { predicate } and nothing else` };
828
+ const extra = Object.keys(o).filter((k) => !PREDICATE_NAME_KEYS.includes(k));
829
+ if (extra.length) return { rule: "FD-V42", message: `when names a predicate and also carries ${extra.map((k) => JSON.stringify(k)).join(", ")}; the name form is { predicate } and nothing else` };
609
830
  if (typeof o.predicate !== "string") return { rule: "FD-V42", message: `when predicate is ${scalarName(o.predicate)}; it is the name of a registered predicate` };
610
831
  return null;
611
832
  }
612
833
  if (shape === "leaf") return leafProblem(o, "when");
834
+ return groupProblem(o, "when", EDGE_SCOPE);
835
+ }
836
+ function groupProblem(o, label, scope) {
613
837
  const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
614
- if (extra.length) return { rule: "FD-V42", message: `when is a group and also carries ${extra.map((k) => JSON.stringify(k)).join(", ")}; a group is { all } or { any } and nothing else` };
615
- if ("all" in o && "any" in o) return { rule: "FD-V45", message: 'when carries both "all" and "any"; a group is one or the other, because a document that says both says neither' };
838
+ if (extra.length) return { rule: "FD-V42", message: `${label} is a group and also carries ${extra.map((k) => JSON.stringify(k)).join(", ")}; a group is { all } or { any } and nothing else` };
839
+ if ("all" in o && "any" in o) return { rule: "FD-V45", message: `${label} carries both "all" and "any"; a group is one or the other, because a document that says both says neither` };
616
840
  const which = "all" in o ? "all" : "any";
617
841
  const members = o[which];
618
842
  if (!Array.isArray(members) || members.length === 0) {
619
- return { rule: "FD-V45", message: `when ${which} is ${Array.isArray(members) ? "empty" : scalarName(members)}; it is a non-empty array of comparisons \u2014 an empty group is "always true" or "always false" written as a question` };
843
+ return { rule: "FD-V45", message: `${label} ${which} is ${Array.isArray(members) ? "empty" : scalarName(members)}; it is a non-empty array of comparisons \u2014 an empty group is "always true" or "always false" written as a question` };
620
844
  }
621
845
  for (let i = 0; i < members.length; i++) {
622
846
  const m = members[i];
623
- const at = `when ${which}[${i}]`;
847
+ const at = `${label} ${which}[${i}]`;
624
848
  if (typeof m !== "object" || m === null || Array.isArray(m)) {
625
849
  return { rule: "FD-V45", message: `${at} is ${scalarName(m)}; a group's members are comparisons` };
626
850
  }
@@ -634,11 +858,30 @@ function predicateProblem(v) {
634
858
  if (ms === null || ms === "ambiguous") {
635
859
  return { rule: "FD-V45", message: `${at} carries ${Object.keys(m).map((k) => JSON.stringify(k)).join(", ") || "no keys"} and is not a comparison` };
636
860
  }
637
- const p = leafProblem(m, at);
861
+ const p = leafProblem(m, at, scope);
638
862
  if (p) return p;
639
863
  }
640
864
  return null;
641
865
  }
866
+ function gateConditionProblem(v) {
867
+ const label = "settings.when";
868
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
869
+ return { rule: "FD-V42", message: `${label} is ${scalarName(v)}; it is an object \u2014 a comparison { ${PREDICATE_LEAF_KEYS.join(", ")} } or a flat group { all } or { any }, over the root "input"` };
870
+ }
871
+ const o = v;
872
+ const shape = predicateShapeOf(o);
873
+ if (shape === null) {
874
+ return { rule: "FD-V42", message: `${label} carries ${Object.keys(o).map((k) => JSON.stringify(k)).join(", ") || "no keys"} and is neither a comparison { ${PREDICATE_LEAF_KEYS.join(", ")} } nor a flat group { all } or { any }` };
875
+ }
876
+ if (shape === "ambiguous") {
877
+ return { rule: "FD-V42", message: `${label} carries ${Object.keys(o).map((k) => JSON.stringify(k)).join(", ")} and matches more than one shape \u2014 a comparison and a group are told apart by which keys are present, so an object may mark only one` };
878
+ }
879
+ if (shape === "name") {
880
+ return { rule: "FD-V42", message: `${label} names a registered predicate, which an if gate cannot ask \u2014 a predicate is handed a decision's outcome and its edge's needs, and a gate has neither. Write a comparison over "input", or register a gate type of your own` };
881
+ }
882
+ if (shape === "leaf") return leafProblem(o, label, GATE_SCOPE);
883
+ return groupProblem(o, label, GATE_SCOPE);
884
+ }
642
885
  function predicateNamesIn(v) {
643
886
  if (typeof v !== "object" || v === null || Array.isArray(v)) return [];
644
887
  const o = v;
@@ -707,6 +950,8 @@ function aimedReferences(n) {
707
950
  }
708
951
  function referenceTarget(ref) {
709
952
  if (ref === ".") return { form: "self", node: null, conditional: false };
953
+ if (ref === PAYLOAD_TOKEN) return { form: "payload", node: null, conditional: false };
954
+ if (ref === ITEM_TOKEN) return { form: "item", node: null, conditional: false };
710
955
  if (typeof ref === "string") return aimed(ref, "one");
711
956
  if (Array.isArray(ref) && ref.length === 1 && typeof ref[0] === "string") {
712
957
  return aimed(ref[0], "all");
@@ -718,6 +963,20 @@ function aimed(s, form) {
718
963
  const node = conditional ? s.slice(0, -1) : s;
719
964
  return ID_RE.test(node) ? { form, node, conditional } : null;
720
965
  }
966
+ var PAYLOAD_TOKEN = "$payload";
967
+ var ITEM_TOKEN = "$item";
968
+ function dollarProblem(ref) {
969
+ const one = Array.isArray(ref) && ref.length === 1 ? ref[0] : ref;
970
+ if (typeof one !== "string" || !one.startsWith("$")) return null;
971
+ const bare = one.endsWith("?") ? one.slice(0, -1) : one;
972
+ const known = bare === PAYLOAD_TOKEN || bare === ITEM_TOKEN;
973
+ if (known && Array.isArray(ref)) {
974
+ return `${JSON.stringify(ref)} is an array form, and there is ONE ${bare === PAYLOAD_TOKEN ? "run payload" : "item per map instance"}: write ${JSON.stringify(bare)}`;
975
+ }
976
+ if (known) return `${JSON.stringify(one)} carries "?", and ${JSON.stringify(bare)} is always there: write ${JSON.stringify(bare)} without "?"`;
977
+ return `${JSON.stringify(ref)} is not a reference: the "$" prefix is reserved, and its references are "$payload" (the run's payload) and "$item" (a map instance's item)`;
978
+ }
979
+ var itemOutside = (site, k, nodeId) => `${site}[${JSON.stringify(k)}] is "$item", and "${nodeId}" is not inside a map instance: name the item where a map's branch receives it ("."), or move this node inside the map`;
721
980
  function validateDefinition(doc, registry) {
722
981
  const f = [];
723
982
  const add = (rule, at, detail) => f.push({ rule, at, detail });
@@ -764,7 +1023,7 @@ function validateDefinition(doc, registry) {
764
1023
  }
765
1024
  if (nd?.kind === void 0) continue;
766
1025
  if (typeof nd.kind !== "string" || !KINDS.includes(nd.kind)) {
767
- add("FD-V24", where, `node kind ${JSON.stringify(nd.kind)} is not task|fanout|map|join|end|gate`);
1026
+ add("FD-V24", where, `node kind ${JSON.stringify(nd.kind)} is not task|fanout|map|join|end|gate|ask`);
768
1027
  continue;
769
1028
  }
770
1029
  const spec = NODE_KEYS[nd.kind];
@@ -775,7 +1034,11 @@ function validateDefinition(doc, registry) {
775
1034
  }
776
1035
  for (const k of Object.keys(nd)) {
777
1036
  if (!spec.req.includes(k) && !spec.opt.includes(k)) {
778
- add("FD-V17", where, `${nd.kind} node carries undeclared key "${k}"`);
1037
+ if (nd.kind === "ask" && ASK_REFUSED_KEYS.includes(k)) {
1038
+ add("FD-V47", where, k === "transform" ? 'ask node carries "transform", which only a task declares \u2014 a transform is evaluated in a node type\'s worker wrapper (FD-C6) and an ask runs no worker, so nothing would evaluate it; what reaches an ask is composed by its `needs` (P35 decision 6)' : `ask node carries "${k}", which only a task declares \u2014 it is read by a task's worker or its scheduler task, and an ask runs no worker and is never dispatched, so nothing would read it`);
1039
+ } else {
1040
+ add("FD-V17", where, `${nd.kind} node carries undeclared key "${k}"`);
1041
+ }
779
1042
  }
780
1043
  }
781
1044
  if (nd.kind === "task") {
@@ -865,21 +1128,63 @@ function validateDefinition(doc, registry) {
865
1128
  }
866
1129
  }
867
1130
  const settings = nd.settings;
1131
+ let settingsRefused = false;
868
1132
  if (settings !== void 0 && (typeof settings !== "object" || settings === null || Array.isArray(settings))) {
869
- add("FD-V40", where, `settings must be a plain object, got ${JSON.stringify(settings)}`);
1133
+ add("FD-V40", where, `settings must be a plain object, got ${describeValue(settings)}`);
1134
+ settingsRefused = true;
870
1135
  } else {
871
1136
  const s = settings ?? {};
872
1137
  if (Object.prototype.hasOwnProperty.call(s, "key")) {
873
1138
  const kp = keyPathProblem(s.key);
874
- if (kp !== null) add("FD-V40", where, kp);
1139
+ if (kp !== null) {
1140
+ add("FD-V40", where, kp);
1141
+ settingsRefused = true;
1142
+ }
875
1143
  }
1144
+ let schemaRefused = false;
876
1145
  if (usable?.settingsSchema !== void 0) {
877
1146
  const fs = findings(usable.settingsSchema, s);
878
- if (fs.length) add("FD-V40", where, `settings ${JSON.stringify(s)} fail gate type "${typeName}"'s settingsSchema: ${describeFindings(fs)}`);
1147
+ if (fs.length) {
1148
+ add("FD-V40", where, `settings ${describeValue(s)} fail gate type "${typeName}"'s settingsSchema: ${describeFindings(fs)}`);
1149
+ schemaRefused = true;
1150
+ settingsRefused = true;
1151
+ }
1152
+ }
1153
+ let uncopyable = false;
1154
+ if (usable !== void 0 && (usable.settingsProblem !== void 0 || typeof usable.routesFor === "function")) {
1155
+ try {
1156
+ structuredClone(s);
1157
+ } catch (e) {
1158
+ add("FD-V40", where, `settings cannot be copied for gate type "${typeName}" \u2014 they must survive structuredClone, as a JSON document's do: ${describeThrown(e)}`);
1159
+ uncopyable = true;
1160
+ settingsRefused = true;
1161
+ }
1162
+ }
1163
+ if (usable?.settingsProblem !== void 0 && !schemaRefused && !uncopyable) {
1164
+ let said;
1165
+ let threw = false;
1166
+ const copy = structuredClone(s);
1167
+ try {
1168
+ said = usable.settingsProblem(copy);
1169
+ } catch (e) {
1170
+ threw = true;
1171
+ add("FD-V39", where, `type "${typeName}"'s settingsProblem threw: ${describeThrown(e)}`);
1172
+ }
1173
+ if (!threw && said !== null) {
1174
+ if (typeof said === "string") {
1175
+ add("FD-V40", where, `settings ${describeValue(s)} fail gate type "${typeName}"'s settingsProblem: ` + (said === "" ? "it returned an EMPTY message \u2014 a refusal with no reason given (null is the pass)" : said));
1176
+ } else add("FD-V39", where, `type "${typeName}"'s settingsProblem returned ${describeValue(said)}, which is neither a message nor null`);
1177
+ }
1178
+ if (threw || said !== null) settingsRefused = true;
879
1179
  }
880
1180
  }
881
- if (usable !== void 0 && Array.isArray(nd.edges)) {
882
- const gate = usable;
1181
+ let routeSet;
1182
+ if (usable !== void 0 && !(typeof usable.routesFor === "function" && settingsRefused)) {
1183
+ routeSet = routesOf(usable, settings ?? {});
1184
+ if ("problem" in routeSet) add("FD-V39", where, `type "${typeName}" resolves no route set for this gate: ${routeSet.problem}`);
1185
+ }
1186
+ if (routeSet !== void 0 && !("problem" in routeSet) && Array.isArray(nd.edges)) {
1187
+ const gate = routeSet;
883
1188
  const count = new Map(gate.routes.map((r) => [r, 0]));
884
1189
  nd.edges.forEach((e, i) => {
885
1190
  if (e === null || typeof e !== "object") return;
@@ -887,7 +1192,7 @@ function validateDefinition(doc, registry) {
887
1192
  if (r !== void 0 && typeof r !== "string") return;
888
1193
  const route = r === void 0 ? gate.defaultRoute : r;
889
1194
  if (!count.has(route)) {
890
- add("FD-V41", `${where}#edge${i}`, `route "${route}" is not a route gate type "${typeName}" declares (it declares ${gate.routes.map((x) => `"${x}"`).join(", ")})`);
1195
+ add("FD-V41", `${where}#edge${i}`, `route "${route}" is not one of this gate's routes \u2014 the routes gate type "${typeName}" declares for it are ${gate.routes.map((x) => `"${x}"`).join(", ")}`);
891
1196
  return;
892
1197
  }
893
1198
  count.set(route, count.get(route) + 1);
@@ -895,13 +1200,23 @@ function validateDefinition(doc, registry) {
895
1200
  for (const [route, n2] of count) {
896
1201
  const how = route === gate.defaultRoute ? " (the default route \u2014 an edge with no `route` carries it)" : "";
897
1202
  if (n2 === 0) {
898
- add("FD-V41", where, `route "${route}"${how} of gate type "${typeName}" has no edge; every declared route is wired exactly once \u2014 a drop is an edge to an \`end\` node, because a route left unwired would be dropped in silence the day the type grows one`);
1203
+ add("FD-V41", where, `route "${route}"${how} of this gate (gate type "${typeName}") has no edge; every route of the gate is wired exactly once \u2014 a drop is an edge to an \`end\` node, because a route left unwired would be dropped in silence the day the type grows one`);
899
1204
  } else if (n2 > 1) {
900
- add("FD-V41", where, `route "${route}"${how} of gate type "${typeName}" is wired by ${n2} edges; exactly one`);
1205
+ add("FD-V41", where, `route "${route}"${how} of this gate (gate type "${typeName}") is wired by ${n2} edges; exactly one`);
901
1206
  }
902
1207
  }
903
1208
  }
904
1209
  }
1210
+ if (nd.kind === "ask") {
1211
+ const s = nd.settings;
1212
+ if (s !== void 0 && (typeof s !== "object" || s === null || Array.isArray(s))) {
1213
+ add("FD-V21", where, `ask settings must be a plain object, got ${describeValue(s)}`);
1214
+ }
1215
+ if (nd.answerSchema !== void 0) {
1216
+ const problem = schemaProblem(nd.answerSchema);
1217
+ if (problem !== null) add("FD-V48", where, `answerSchema is not in the built-in schema subset \u2014 ${problem}`);
1218
+ }
1219
+ }
905
1220
  if (nd.kind === "end") {
906
1221
  if (nd.edges !== void 0) add("FD-V8", where, "end node has edges");
907
1222
  if (nd.outcome !== void 0 && typeof nd.outcome !== "string") {
@@ -936,8 +1251,11 @@ function validateDefinition(doc, registry) {
936
1251
  for (const [k, ref] of Object.entries(nv)) {
937
1252
  if (!ID_RE.test(k)) add("FD-V26", where, `needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
938
1253
  const target = referenceTarget(ref);
939
- if (target === null) {
940
- add("FD-V26", where, `needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "." , a node id with an optional "?" suffix, or a one-element array holding one`);
1254
+ const dollar = target === null ? dollarProblem(ref) : null;
1255
+ if (dollar !== null) {
1256
+ add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${dollar}`);
1257
+ } else if (target === null) {
1258
+ add("FD-V26", where, `needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "." , "$payload", "$item", a node id with an optional "?" suffix, or a one-element array holding one`);
941
1259
  }
942
1260
  }
943
1261
  }
@@ -999,9 +1317,12 @@ function validateDefinition(doc, registry) {
999
1317
  for (const [k, ref] of Object.entries(nv)) {
1000
1318
  if (!ID_RE.test(k)) add("FD-V26", at, `edge needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
1001
1319
  const target = referenceTarget(ref);
1002
- if (target === null) {
1003
- add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not a node id with an optional "?" suffix or a one-element array holding one`);
1004
- } else if (target.node === null) {
1320
+ const dollar = target === null ? dollarProblem(ref) : null;
1321
+ if (dollar !== null) {
1322
+ add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${dollar}`);
1323
+ } else if (target === null) {
1324
+ add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "$payload", "$item", a node id with an optional "?" suffix or a one-element array holding one`);
1325
+ } else if (target.form === "self") {
1005
1326
  add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ".", which an EDGE may not use: what a predicate would have received anyway is its node's own outcome, and \`ctx.outcome\` already carries it`);
1006
1327
  }
1007
1328
  }
@@ -1046,18 +1367,18 @@ function validateDefinition(doc, registry) {
1046
1367
  }
1047
1368
  for (const n of nodes) {
1048
1369
  if (typeof n?.id !== "string") continue;
1049
- if (n.kind === "task") {
1370
+ if (n.kind === "task" || n.kind === "ask") {
1050
1371
  const edges = (Array.isArray(n.edges) ? n.edges : []).filter(
1051
1372
  (e) => e !== null && typeof e === "object"
1052
1373
  );
1053
1374
  if (edges.length === 0) {
1054
- add("FD-V7", n.id, "task node has no edges");
1375
+ add("FD-V7", n.id, `${n.kind} node has no edges`);
1055
1376
  continue;
1056
1377
  }
1057
1378
  const onOf2 = (e) => Array.isArray(e?.on) && e.on.length ? e.on : ["done"];
1058
1379
  const states = /* @__PURE__ */ new Set();
1059
1380
  for (const e of edges) for (const s of onOf2(e)) states.add(s);
1060
- if (!states.has("done")) add("FD-V7", n.id, 'no edge of this task node is eligible for "done"');
1381
+ if (!states.has("done")) add("FD-V7", n.id, `no edge of this ${n.kind} node is eligible for "done"`);
1061
1382
  for (const t of states) {
1062
1383
  const eligible = edges.filter((e) => onOf2(e).includes(t));
1063
1384
  const bare = eligible.filter((e) => e.when === void 0);
@@ -1077,8 +1398,8 @@ function validateDefinition(doc, registry) {
1077
1398
  }
1078
1399
  }
1079
1400
  for (const m of maps) {
1080
- if (typeof m.branch === "string" && g.byId.has(m.branch) && kindOf2(m.branch) !== "task") {
1081
- add("FD-V25", m.id, `branch "${m.branch}" is a ${kindOf2(m.branch)}, not a task`);
1401
+ if (typeof m.branch === "string" && g.byId.has(m.branch) && kindOf2(m.branch) !== "task" && kindOf2(m.branch) !== "ask") {
1402
+ add("FD-V25", m.id, `branch "${m.branch}" is a ${kindOf2(m.branch)}, not a task or an ask`);
1082
1403
  }
1083
1404
  }
1084
1405
  for (const fan of fanouts) {
@@ -1086,8 +1407,8 @@ function validateDefinition(doc, registry) {
1086
1407
  if (b.length < 2) add("FD-V9", fan.id, `fanout has ${b.length} branches, needs at least 2`);
1087
1408
  if (new Set(b).size !== b.length) add("FD-V9", fan.id, "fanout repeats a branch entry");
1088
1409
  for (const x of b) {
1089
- if (g.byId.has(x) && kindOf2(x) !== "task") {
1090
- add("FD-V9", fan.id, `branch entry "${x}" is a ${kindOf2(x)}, not a task`);
1410
+ if (g.byId.has(x) && kindOf2(x) !== "task" && kindOf2(x) !== "ask") {
1411
+ add("FD-V9", fan.id, `branch entry "${x}" is a ${kindOf2(x)}, not a task or an ask`);
1091
1412
  }
1092
1413
  }
1093
1414
  }
@@ -1158,14 +1479,19 @@ function validateDefinition(doc, registry) {
1158
1479
  if (typeof n !== "object" || n === null || typeof n.id !== "string") continue;
1159
1480
  for (const [k, ref, site, at] of aimedReferences(n)) {
1160
1481
  const t = referenceTarget(ref);
1161
- if (t === null || t.node === null) continue;
1482
+ if (t === null) continue;
1483
+ if (t.form === "item") {
1484
+ if (innermostMapOf(regions, n.id) === void 0) add("FD-V49", at, itemOutside(site, k, n.id));
1485
+ continue;
1486
+ }
1487
+ if (t.form === "self" || t.form === "payload") continue;
1162
1488
  const target = t.node;
1163
1489
  if (!g.byId.has(target)) {
1164
1490
  add("FD-V27", at, `${site}[${JSON.stringify(k)}] names "${target}", which is not a node`);
1165
1491
  continue;
1166
1492
  }
1167
- if (kindOf2(target) !== "task") {
1168
- add("FD-V27", at, `${site}[${JSON.stringify(k)}] names "${target}", a ${kindOf2(target)}; only a task produces an output`);
1493
+ if (kindOf2(target) !== "task" && kindOf2(target) !== "ask") {
1494
+ add("FD-V27", at, `${site}[${JSON.stringify(k)}] names "${target}", a ${kindOf2(target)}; only a task or an ask produces an output`);
1169
1495
  continue;
1170
1496
  }
1171
1497
  if (!reachFrom(g, target).has(n.id)) {
@@ -1250,9 +1576,12 @@ var DOCUMENT_KEYS = ["workers", "groups", "nodeTypes", "pools", "predicates", "l
1250
1576
  var WORKER_KEYS = ["url", "configSchema", "settingsSchema", "inputSchema", "outputSchema"];
1251
1577
  var CHECKED_WORKER_SCHEMAS = ["configSchema", "settingsSchema"];
1252
1578
  var NODE_TYPE_DECLARATION_KEYS = ["worker", "inline", "config", "pool", "groups", "retryPolicy", "settings"];
1253
- var POOL_SPEC_KEYS = ["config", "maxThreads", "maxConcurrentTasks", "idleTimeout", "groups", "residentGroups"];
1579
+ var POOL_SPEC_KEYS = ["config", "maxThreads", "maxConcurrentTasks", "idleTimeout", "groups", "residentGroups", "cancelGraceMs", "messages"];
1580
+ var MESSAGE_OPTION_KEYS = ["flushIntervalMs", "maxBuffered"];
1254
1581
  var DECLARED_POOL_KEYS = POOL_SPEC_KEYS.filter((k) => k !== "config" && k !== "groups");
1255
1582
  var INSTANCE_KEYS = ["type", "config"];
1583
+ var POOL_INSTANCE_KEYS = [...INSTANCE_KEYS, "pool"];
1584
+ var MAX_TIMER_MS = 2 ** 31 - 1;
1256
1585
  var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1257
1586
  var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
1258
1587
  var isBuiltInNodeType = (name) => hasOwn(BUILT_IN_NODE_TYPES, name);
@@ -1270,8 +1599,23 @@ function mergeConfig(base, over) {
1270
1599
  if (isPlainObject2(base) && isPlainObject2(over)) return { ...base, ...over };
1271
1600
  return over;
1272
1601
  }
1273
- function nodeTypeFromDeclaration(decl) {
1274
- const declaredPool = decl.pool ?? {};
1602
+ var DECLARATIONS = /* @__PURE__ */ new WeakMap();
1603
+ function declarationOf(t) {
1604
+ return isDerivation(t) ? DECLARATIONS.get(t) : t;
1605
+ }
1606
+ function mergePool(base, over) {
1607
+ if (over === void 0) return base;
1608
+ const out = { ...base };
1609
+ for (const [k, v] of Object.entries(over)) if (v !== void 0) out[k] = v;
1610
+ return out;
1611
+ }
1612
+ function copyResident(r) {
1613
+ if (Array.isArray(r)) return [...r];
1614
+ if (isPlainObject2(r)) return { ...r };
1615
+ return r;
1616
+ }
1617
+ function nodeTypeFromDeclaration(decl, over) {
1618
+ const declaredPool = mergePool(decl.pool ?? {}, over);
1275
1619
  const def = {
1276
1620
  worker: decl.worker,
1277
1621
  ...decl.inline !== void 0 ? { inline: decl.inline } : {},
@@ -1283,10 +1627,18 @@ function nodeTypeFromDeclaration(decl) {
1283
1627
  return {
1284
1628
  ...merged !== void 0 ? { config: merged } : {},
1285
1629
  ...declaredPool,
1630
+ // **Every declared pool OBJECT is copied, never aliased** — `messages` and `groups` since W159, and
1631
+ // `residentGroups` since v0.37 (P37 decision 8, ADR 0099 rule 3; it was handed on AS DECLARED until then,
1632
+ // W159's conformity F3), so no two pools' specs share an object and none shares one with the document. A
1633
+ // CLASS derivation's output is handed on as the class returned it — harmless, since the scheduler spreads
1634
+ // `messages` into a fresh object (W159's adequacy F7) and reads `residentGroups` without writing it.
1635
+ ...declaredPool.messages !== void 0 ? { messages: { ...declaredPool.messages } } : {},
1636
+ ...declaredPool.residentGroups !== void 0 ? { residentGroups: copyResident(declaredPool.residentGroups) } : {},
1286
1637
  ...decl.groups !== void 0 ? { groups: [...decl.groups] } : {}
1287
1638
  };
1288
1639
  }
1289
1640
  };
1641
+ DECLARATIONS.set(def, over === void 0 ? decl : { ...decl, pool: declaredPool });
1290
1642
  return def;
1291
1643
  }
1292
1644
  function poolSpecProblem(v) {
@@ -1309,11 +1661,53 @@ function poolSpecProblem(v) {
1309
1661
  for (const [k, c] of Object.entries(r)) if (!(typeof c === "number" && Number.isFinite(c) && c > 0)) return `PoolSpec.residentGroups[${q(k)}] must be a cost > 0, got ${JSON.stringify(c)}`;
1310
1662
  } else return `PoolSpec.residentGroups must be an array of group keys or a { key: cost } record, got ${JSON.stringify(r)}`;
1311
1663
  }
1664
+ if (v.cancelGraceMs !== void 0 && !(typeof v.cancelGraceMs === "number" && v.cancelGraceMs >= 0)) {
1665
+ return `PoolSpec.cancelGraceMs must be a number >= 0 (Infinity: never force), got ${shown(v.cancelGraceMs)}`;
1666
+ }
1667
+ if (v.messages !== void 0) {
1668
+ const m = v.messages;
1669
+ if (!isPlainObject2(m) || !isPlainPrototype(m)) {
1670
+ return `PoolSpec.messages must be a plain object of ${MESSAGE_OPTION_KEYS.join(", ")} \u2014 its prototype Object.prototype or null \u2014 got ${describeNonPlain(m)}`;
1671
+ }
1672
+ for (const k of Object.keys(m)) if (!MESSAGE_OPTION_KEYS.includes(k)) return `PoolSpec.messages carries undeclared key ${q(k)}; it declares ${MESSAGE_OPTION_KEYS.join(", ")}`;
1673
+ if (ownKey(m, "flushIntervalMs") && !(typeof m.flushIntervalMs === "number" && Number.isFinite(m.flushIntervalMs) && m.flushIntervalMs > 0 && m.flushIntervalMs <= MAX_TIMER_MS)) {
1674
+ return `PoolSpec.messages.flushIntervalMs must be a finite number > 0 and at most ${MAX_TIMER_MS} (2^31-1, the longest delay a timer holds \u2014 the scheduler arms setTimeout with it, and a longer one fires at once), got ${shown(m.flushIntervalMs)}`;
1675
+ }
1676
+ if (ownKey(m, "maxBuffered") && !(typeof m.maxBuffered === "number" && Number.isInteger(m.maxBuffered) && m.maxBuffered >= 1)) {
1677
+ return `PoolSpec.messages.maxBuffered must be an integer >= 1, got ${shown(m.maxBuffered)}`;
1678
+ }
1679
+ }
1312
1680
  return null;
1313
1681
  }
1682
+ function isPlainPrototype(o) {
1683
+ const proto = Object.getPrototypeOf(o);
1684
+ return proto === Object.prototype || proto === null;
1685
+ }
1686
+ function ownKey(o, k) {
1687
+ return Object.prototype.propertyIsEnumerable.call(o, k);
1688
+ }
1689
+ function describeNonPlain(v) {
1690
+ if (Array.isArray(v)) return `an array ${shown(v)}`;
1691
+ if (typeof v === "object" && v !== null) {
1692
+ const proto = Object.getPrototypeOf(v);
1693
+ const name = proto?.constructor?.name;
1694
+ return `an object whose prototype is not Object.prototype${name ? ` (a ${name})` : ""}`;
1695
+ }
1696
+ return shown(v);
1697
+ }
1698
+ function shown(v) {
1699
+ if (typeof v === "number" || v === void 0) return String(v);
1700
+ try {
1701
+ return JSON.stringify(v) ?? String(v);
1702
+ } catch {
1703
+ return typeof v;
1704
+ }
1705
+ }
1314
1706
  function groupsNamedBy(spec) {
1315
- const resident = spec.residentGroups === void 0 ? [] : Array.isArray(spec.residentGroups) ? [...spec.residentGroups] : Object.keys(spec.residentGroups);
1316
- return [.../* @__PURE__ */ new Set([...spec.groups ?? [], ...resident])];
1707
+ return [.../* @__PURE__ */ new Set([...spec.groups ?? [], ...residentKeysOf(spec)])];
1708
+ }
1709
+ function residentKeysOf(spec) {
1710
+ return spec.residentGroups === void 0 ? [] : Array.isArray(spec.residentGroups) ? [...spec.residentGroups] : Object.keys(spec.residentGroups);
1317
1711
  }
1318
1712
  function poolOptionsOf(spec, inline, workerUrl, languages) {
1319
1713
  const { config, groups, ...rest } = spec;
@@ -1371,15 +1765,21 @@ function checkNodeTypeShape(name, t, builtIn = false) {
1371
1765
  }
1372
1766
  if (o.settings !== void 0 && !isPlainObject2(o.settings)) throw new DerivationRefused(at, `node type ${q(at)}.settings must be a plain object of defaults, got ${JSON.stringify(o.settings)}`);
1373
1767
  }
1374
- function derivePool(key, typeName, def, worker, config, groupKeys, refuse) {
1768
+ function derivePool(key, typeName, def, worker, config, groupKeys, refuse, pool) {
1375
1769
  const who = key === typeName ? `node type ${q(typeName)}'s default instance` : `pool ${q(key)} (an instance of ${q(typeName)})`;
1376
1770
  if (def.configSchema !== void 0) {
1377
1771
  const fs = findings(def.configSchema, config ?? {});
1378
1772
  if (fs.length) throw new refuse(key, `${who}: config fails the type's configSchema \u2014 ${describeFindings(fs)}`);
1379
1773
  }
1774
+ let derivation = def;
1775
+ if (pool !== void 0) {
1776
+ const declaration = declarationOf(def);
1777
+ if (declaration === void 0) throw new Error(`derivePool: ${who} carries pool options and its type is a class \u2014 the registry's shape check should have refused it`);
1778
+ derivation = nodeTypeFromDeclaration(declaration, pool);
1779
+ }
1380
1780
  let spec;
1381
1781
  try {
1382
- spec = def.pool(config, key);
1782
+ spec = derivation.pool(config, key);
1383
1783
  } catch (e) {
1384
1784
  throw new DerivationRefused(key, `${who}: the derivation pool() THREW: ${e instanceof Error ? e.message : String(e)}`);
1385
1785
  }
@@ -1398,7 +1798,13 @@ function derivePool(key, typeName, def, worker, config, groupKeys, refuse) {
1398
1798
  if (resident > 0 && spec.idleTimeout === 0) {
1399
1799
  throw new DerivationRefused(key, `${who}: residentGroups beside idleTimeout: 0 \u2014 a worker that never idles never releases a resident group, and a second pool contending for it waits for ever (measured on 0.5.2, P18 fact 6); give the pool a positive idleTimeout`);
1400
1800
  }
1401
- return { key, type: typeName, definition: def, worker, config, spec };
1801
+ return { key, type: typeName, definition: def, worker, config, spec, ...pool !== void 0 ? { pool: copyPool(pool) } : {} };
1802
+ }
1803
+ function copyPool(p) {
1804
+ const out = mergePool({}, p);
1805
+ if (isPlainObject2(out.messages)) out.messages = { ...out.messages };
1806
+ if (out.residentGroups !== void 0) out.residentGroups = copyResident(out.residentGroups);
1807
+ return out;
1402
1808
  }
1403
1809
  function poolTypeNames(consumer) {
1404
1810
  const own3 = [...consumer].filter((n) => !Object.prototype.hasOwnProperty.call(BUILT_IN_NODE_TYPES, n)).sort();
@@ -1442,11 +1848,21 @@ function checkNodeTypesShape(rawNodeTypes, workers) {
1442
1848
  function checkPoolsShape(rawPools, types) {
1443
1849
  for (const [key, p] of poolsOf(rawPools)) {
1444
1850
  if (!isPlainObject2(p)) throw new UndeclaredKey(key, `pool ${q(key)} is not an object`);
1445
- noUndeclared("pool", key, p, INSTANCE_KEYS);
1851
+ noUndeclared("pool", key, p, POOL_INSTANCE_KEYS);
1446
1852
  if (types.has(key)) throw new NameCollision(key, `pool ${q(key)} is named as node type ${q(key)} is \u2014 a type's name is its DEFAULT instance's key, so a named instance may not take it` + (isBuiltInNodeType(key) ? ` \u2014 and ${q(key)} is a BUILT-IN node type, so that name is the library's and no registry document declares it` : ""));
1447
1853
  if (typeof p?.type !== "string" || !types.has(p.type)) {
1448
1854
  throw new PoolTypeMissing(key, `pool ${q(key)} names type ${JSON.stringify(p?.type)}, which is not a node type` + poolTypeNames([...types.keys()]));
1449
1855
  }
1856
+ const own3 = p.pool;
1857
+ if (own3 !== void 0) {
1858
+ if (declarationOf(types.get(p.type)) === void 0) {
1859
+ throw new UndeclaredKey(key, `pool ${q(key)} is an instance of CLASS type ${q(p.type)} and carries "pool" \u2014 pool options on an instance are a DOCUMENT type's; a class derives its pool from the instance's config and key, so the knob belongs in its configSchema (ADR 0099)`);
1860
+ }
1861
+ if (!isPlainObject2(own3)) throw new DerivationRefused(key, `pool ${q(key)}: its own pool options must be an object, got ${JSON.stringify(own3)}`);
1862
+ noUndeclared("pool instance pool", key, own3, DECLARED_POOL_KEYS);
1863
+ const problem = poolSpecProblem(own3);
1864
+ if (problem !== null) throw new DerivationRefused(key, `pool ${q(key)}.pool: ${problem}`);
1865
+ }
1450
1866
  }
1451
1867
  }
1452
1868
  function checkGateTypes(x) {
@@ -1526,7 +1942,14 @@ function resolveRegistry(doc, code = {}) {
1526
1942
  const builtIn = builtInEntry === void 0 ? void 0 : isDerivation(builtInEntry) ? builtInEntry : nodeTypeFromDeclaration(builtInEntry);
1527
1943
  const def = (typeof inst.type === "string" ? nodeTypes.get(inst.type) : void 0) ?? builtIn;
1528
1944
  if (def === void 0) throw new Error(`resolveRegistry: pool ${q(key)} reached derivation with no type \u2014 checkRegistryShape should have refused it`);
1529
- pools.set(key, derivePool(key, inst.type, def, def.worker === void 0 ? void 0 : workers.get(def.worker), inst.config, groupKeys, ConfigRefused));
1945
+ pools.set(key, derivePool(key, inst.type, def, def.worker === void 0 ? void 0 : workers.get(def.worker), inst.config, groupKeys, ConfigRefused, inst.pool));
1946
+ }
1947
+ for (const p of pools.values()) {
1948
+ for (const g of residentKeysOf(p.spec)) {
1949
+ if (groups.get(g)?.type !== "KeyedRateLimitGroup") continue;
1950
+ const who = p.key === p.type ? `node type ${q(p.type)}'s default instance` : `pool ${q(p.key)} (an instance of ${q(p.type)})`;
1951
+ throw new DerivationRefused(p.key, `${who}: the derived pool holds group ${q(g)}, of the built-in type "KeyedRateLimitGroup", in residentGroups \u2014 the scheduler resolves no key for a resident group, so every worker holding it is counted in ONE "" bucket, in silence (Q619). Name ${q(g)} in groups, where each task is counted under its limitKey (FD-R6), or hold a ConcurrentLimitGroup or RateLimitGroup resident`);
1952
+ }
1530
1953
  }
1531
1954
  const predicateNames = doc.predicates ?? [];
1532
1955
  if (!Array.isArray(predicateNames) || predicateNames.some((p) => typeof p !== "string")) throw new UndeclaredKey("predicates", `registry document's predicates must be an array of names, got ${JSON.stringify(predicateNames)}`);
@@ -1672,6 +2095,7 @@ function entryWalk(flow, payload, sampled, choose) {
1672
2095
  out.push({ node: id, value });
1673
2096
  return;
1674
2097
  }
2098
+ if (n.kind === "ask") return;
1675
2099
  seen.add(id);
1676
2100
  if (n.kind === "fanout") for (const b of n.branches) walk(b, value);
1677
2101
  if (n.kind === "gate") for (const e of choose ? choose(n, value) : n.edges ?? []) walk(e.to, value);
@@ -1680,7 +2104,10 @@ function entryWalk(flow, payload, sampled, choose) {
1680
2104
  walk(n.branch, void 0);
1681
2105
  return;
1682
2106
  }
1683
- const composed = n.needs === void 0 ? value : Object.fromEntries(Object.entries(n.needs).filter(([, r]) => r === ".").map(([k]) => [k, value]));
2107
+ const composed = n.needs === void 0 ? value : Object.fromEntries(Object.entries(n.needs).flatMap(([k, r]) => {
2108
+ const form = referenceTarget(r)?.form;
2109
+ return form === "self" ? [[k, value]] : form === "payload" ? [[k, payload]] : [];
2110
+ }));
1684
2111
  const arr = composed?.[n.over];
1685
2112
  if (Array.isArray(arr)) for (const v of arr) walk(n.branch, v);
1686
2113
  }
@@ -1697,6 +2124,13 @@ var Wiring = class {
1697
2124
  workers;
1698
2125
  types;
1699
2126
  gateTypes;
2127
+ /**
2128
+ * **Each gate's RESOLVED route set, by gate id — or nothing** (ADR 0092 rule 3): `routesOf` over the gate's
2129
+ * type and settings, asked once here. This check does not run the validator first, so an unknown type and a
2130
+ * `routesOf` problem (it is total: a malformed set, a throwing `routesFor`) both leave the gate with NO set — no default
2131
+ * route and no guarantee, the arcs reported as they are, which is what an unknown type always got.
2132
+ */
2133
+ gateRoutes = /* @__PURE__ */ new Map();
1700
2134
  // Explicit fields, never parameter properties: Node's type stripping refuses those (`RT-R9`, `S4`),
1701
2135
  // which is how P18's own sample failed to load (W67's declaration step).
1702
2136
  constructor(flow, workers, types, gateTypes) {
@@ -1708,9 +2142,12 @@ var Wiring = class {
1708
2142
  this.regions = allRegions(this.graph, flow);
1709
2143
  const add = (to, from, kind, route) => this.inbound.set(to, [...this.inbound.get(to) ?? [], { from, kind, route }]);
1710
2144
  for (const n of flow.nodes) {
1711
- if (n.kind === "task" || n.kind === "join") for (const e of n.edges ?? []) add(e.to, n.id, n.kind === "task" ? "edge" : "join");
2145
+ if (n.kind === "task" || n.kind === "ask" || n.kind === "join") for (const e of n.edges ?? []) add(e.to, n.id, n.kind === "join" ? "join" : "edge");
1712
2146
  if (n.kind === "gate") {
1713
- const dflt = this.gateTypes.get(n.type)?.defaultRoute;
2147
+ const type = this.gateTypes.get(n.type);
2148
+ const resolved = type === void 0 ? void 0 : routesOf(type, n.settings ?? {});
2149
+ if (resolved !== void 0 && !("problem" in resolved)) this.gateRoutes.set(n.id, resolved);
2150
+ const dflt = this.gateRoutes.get(n.id)?.defaultRoute;
1714
2151
  for (const e of n.edges ?? []) add(e.to, n.id, "gate", e.route ?? dflt);
1715
2152
  }
1716
2153
  if (n.kind === "fanout") for (const b of n.branches) add(b, n.id, "branch");
@@ -1813,8 +2250,16 @@ var Wiring = class {
1813
2250
  if (s === void 0) return { why: `"${nodeId}" (type "${n.type}", worker "${t.worker}") declares no ${kind}` };
1814
2251
  return { schema: s };
1815
2252
  }
1816
- /** What a task PRODUCES, as an arrival on the arc it sends. */
2253
+ /** What a task — or an ask — PRODUCES, as an arrival on the arc it sends. */
1817
2254
  produced(taskId) {
2255
+ const ask = this.byId.get(taskId);
2256
+ if (ask?.kind === "ask") {
2257
+ if (ask.answerSchema === void 0) return { unchecked: `"${taskId}" is an ask and declares no answerSchema, which is what would hold the person's answer` };
2258
+ const malformed = schemaProblem(ask.answerSchema);
2259
+ if (malformed !== null) return { unchecked: `"${taskId}" is an ask whose answerSchema is not in the built-in subset (FD-V48) \u2014 ${malformed}` };
2260
+ const a = sampleOf(ask.answerSchema);
2261
+ return { schema: ask.answerSchema, sample: { value: a.value, source: a.source } };
2262
+ }
1818
2263
  const { schema, why } = this.schemaOf(taskId, "outputSchema");
1819
2264
  if (schema === void 0) return { unchecked: why };
1820
2265
  const s = sampleOf(schema);
@@ -1833,7 +2278,7 @@ var Wiring = class {
1833
2278
  else if (kind === "gate") {
1834
2279
  const gate = this.byId.get(from);
1835
2280
  const key = this.keyOf(gate);
1836
- const resolved = key !== "" && route !== void 0 && (this.gateTypes.get(gate.type)?.keyResolvedRoutes ?? []).includes(route);
2281
+ const resolved = key !== "" && route !== void 0 && (this.gateRoutes.get(gate.id)?.keyResolvedRoutes ?? []).includes(route);
1837
2282
  for (const a of this.arrivals(from)) {
1838
2283
  if (!resolved || a.schema === void 0) {
1839
2284
  raw.push({ ...a });
@@ -1849,7 +2294,7 @@ var Wiring = class {
1849
2294
  } else raw.push(this.record(this.byId.get(from)));
1850
2295
  }
1851
2296
  const needs = node?.needs;
1852
- const out = needs ? raw.map((a) => this.composed(a, needs)) : raw;
2297
+ const out = needs ? raw.map((a) => this.composed(a, needs, nodeId)) : raw;
1853
2298
  this.memo.set(nodeId, out);
1854
2299
  return out;
1855
2300
  }
@@ -1874,7 +2319,7 @@ var Wiring = class {
1874
2319
  /** The node(s) of one branch whose edge reaches the region's join — the value the branch carries in. */
1875
2320
  arrivingAt(region, branch) {
1876
2321
  const members = region.branchOf.get(branch) ?? /* @__PURE__ */ new Set();
1877
- return [...members].map((id) => this.byId.get(id)).filter((n) => (n.kind === "task" || n.kind === "join" || n.kind === "gate") && (n.edges ?? []).some((e) => e.to === region.joinId));
2322
+ return [...members].map((id) => this.byId.get(id)).filter((n) => (n.kind === "task" || n.kind === "ask" || n.kind === "join" || n.kind === "gate") && (n.edges ?? []).some((e) => e.to === region.joinId));
1878
2323
  }
1879
2324
  /** `JP-R12`'s record for the region a join closes — exact for a fan-out, keyed by minted paths for a map. */
1880
2325
  record(j) {
@@ -1922,8 +2367,24 @@ var Wiring = class {
1922
2367
  sample: { value: sample, source, arity }
1923
2368
  };
1924
2369
  }
2370
+ /**
2371
+ * **What a `"$item"` key composes from: the enclosing map's ITEM arrival** — P38 (i), v0.45. The map is the innermost
2372
+ * one containing `nodeId` (`innermostMapOf`, P38 (c)), and its item is `item()` over the map's OWN arrival — the
2373
+ * `items` sub-schema of what it receives under `over`, with the per-element sample. **One arrival or none at all**: a
2374
+ * map entered from two producers has no single item shape, and an ENTRY map has no arrival (the run's payload, which no
2375
+ * schema declares) — both `unchecked`, never a guessed `fits`.
2376
+ */
2377
+ itemArrival(nodeId) {
2378
+ const m = innermostMapOf(this.regions, nodeId);
2379
+ if (m === void 0) return { unchecked: `"${nodeId}" is inside no map instance (FD-V49)` };
2380
+ const at = this.arrivals(m.fanoutId);
2381
+ if (at.length !== 1) {
2382
+ return { unchecked: `map "${m.fanoutId}" has ${at.length === 0 ? "no arrival \u2014 the run's payload, which no schema declares" : `${at.length} arrivals`}, so no single item shape` };
2383
+ }
2384
+ return this.item(at[0], this.byId.get(m.fanoutId));
2385
+ }
1925
2386
  /** `FD-C1`'s table over one arrival: the object `needs` describes, schema and sample alike. */
1926
- composed(a, needs) {
2387
+ composed(a, needs, nodeId) {
1927
2388
  const base = { shape: "needs", producer: a.producer, ...a.through ? { through: a.through } : {} };
1928
2389
  if (a.refused !== void 0) return { ...base, refused: a.refused };
1929
2390
  const props = {};
@@ -1932,11 +2393,27 @@ var Wiring = class {
1932
2393
  let source = "example";
1933
2394
  const arity = a.sample?.arity;
1934
2395
  const dot = [];
2396
+ const itemKeys = [];
2397
+ let itemElements;
1935
2398
  for (const [key, ref] of Object.entries(needs)) {
1936
2399
  const t = referenceTarget(ref);
1937
2400
  if (t === null) return { ...base, unchecked: `needs.${key} is not a reference` };
1938
2401
  let schema2, value, src = "example";
1939
- if (t.node === null) {
2402
+ if (t.form === "payload") return { ...base, unchecked: `needs.${key} is "$payload" \u2014 the run's payload, which no schema declares` };
2403
+ if (t.form === "item") {
2404
+ const it = this.itemArrival(nodeId);
2405
+ if (it.schema === void 0) return { ...base, unchecked: `needs.${key} is "$item", and ${it.unchecked ?? it.refused}` };
2406
+ schema2 = it.schema;
2407
+ src = it.sample?.source ?? "witness";
2408
+ if (it.perItem && it.sample !== void 0) {
2409
+ itemElements = it.sample.value;
2410
+ itemKeys.push(key);
2411
+ } else {
2412
+ const w = sampleOf(it.schema);
2413
+ value = w?.value;
2414
+ src = w?.source ?? "witness";
2415
+ }
2416
+ } else if (t.form === "self") {
1940
2417
  if (a.schema === void 0) return { ...base, unchecked: a.unchecked ?? a.refused ?? `needs.${key} is "." and the edge's value has no schema` };
1941
2418
  schema2 = a.schema;
1942
2419
  value = a.sample?.value;
@@ -1959,7 +2436,16 @@ var Wiring = class {
1959
2436
  const elements = a.sample.value;
1960
2437
  const value = elements.map((el) => {
1961
2438
  const s = { ...sample };
1962
- for (const k of dot) s[k] = el;
2439
+ for (const k of [...dot, ...itemKeys]) s[k] = el;
2440
+ return s;
2441
+ });
2442
+ return { ...base, perItem: true, schema, sample: { value, source, arity: elements.length } };
2443
+ }
2444
+ if (itemKeys.length > 0 && itemElements !== void 0) {
2445
+ const elements = itemElements;
2446
+ const value = elements.map((el) => {
2447
+ const s = { ...sample };
2448
+ for (const k of itemKeys) s[k] = el;
1963
2449
  return s;
1964
2450
  });
1965
2451
  return { ...base, perItem: true, schema, sample: { value, source, arity: elements.length } };
@@ -1976,8 +2462,7 @@ async function checkWiring(registry, flow, options = {}) {
1976
2462
  for (const node of flow.nodes) {
1977
2463
  if (node.kind !== "task") continue;
1978
2464
  const type = types.get(node.type);
1979
- const consumer = type?.worker === void 0 ? void 0 : workers.get(type.worker)?.inputSchema;
1980
- const consumerWhy = !type ? `type "${node.type}" of "${node.id}" is not in the registry` : type.worker === void 0 ? `"${node.id}" is of type "${node.type}", which only TRANSFORMS \u2014 it names no worker, so it declares no inputSchema` : !workers.get(type.worker) ? `worker "${type.worker}" of type "${node.type}" is not in the registry` : consumer === void 0 ? `"${node.id}" (type "${node.type}", worker "${type.worker}") declares no inputSchema` : void 0;
2465
+ const { consumer, consumerWhy } = consumerOf(node, type, workers);
1981
2466
  for (const a of w.arrivals(node.id)) {
1982
2467
  const v = { consumer: node.id, shape: a.shape, producer: a.producer, ...a.through ? { through: a.through } : {}, kind: "unchecked", why: "" };
1983
2468
  if (a.refused !== void 0) {
@@ -2009,57 +2494,8 @@ async function checkWiring(registry, flow, options = {}) {
2009
2494
  continue;
2010
2495
  }
2011
2496
  const sample = { source: a.sample.source, ...a.sample.arity !== void 0 ? { arity: a.sample.arity } : {}, value: a.sample.value };
2012
- const adapter = languages.get(node.transform.lang);
2013
- if (adapter === void 0) {
2014
- v.why = `language "${node.transform.lang}" is not in the registry`;
2015
- v.sample = sample;
2016
- verdicts.push(v);
2017
- continue;
2018
- }
2019
2497
  const inputs = a.perItem ? a.sample.value : [a.sample.value];
2020
- if (inputs.length === 0) {
2021
- v.why = `the sample holds no item to run "${node.id}"'s transform on (arity 0)`;
2022
- v.sample = sample;
2023
- verdicts.push(v);
2024
- continue;
2025
- }
2026
- const settings = effectiveSettings(node, type);
2027
- let results;
2028
- try {
2029
- let compile;
2030
- try {
2031
- const mod = await import(adapter.worker);
2032
- if (typeof mod.default === "function") compile = mod.default;
2033
- } catch (e) {
2034
- if (options.workerHalf?.[node.transform.lang] === void 0) throw e;
2035
- }
2036
- compile ??= options.workerHalf?.[node.transform.lang];
2037
- if (typeof compile !== "function") throw new Error(`the worker half of language "${node.transform.lang}" has no default export, and none was handed in`);
2038
- const c = compile(node.transform.expr);
2039
- results = [];
2040
- for (const input of inputs) results.push(await c.evaluate(input, settings));
2041
- } catch (e) {
2042
- v.kind = "transform-fails";
2043
- v.why = `the transform threw on the ${sample.source}: ${e instanceof Error ? e.message : String(e)}`;
2044
- v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
2045
- verdicts.push(v);
2046
- continue;
2047
- }
2048
- sample.result = a.perItem ? results : results[0];
2049
- v.sample = sample;
2050
- if (consumer === void 0) {
2051
- v.why = consumerWhy + " \u2014 the transform ran and nothing holds its result";
2052
- verdicts.push(v);
2053
- continue;
2054
- }
2055
- const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
2056
- if (bad) {
2057
- v.kind = "transform-fails";
2058
- v.why = `the transform's result on the ${sample.source}${a.perItem ? ` (item ${bad[0]})` : ""} fails the consumer's inputSchema \u2014 ${describeFindings(bad[1])}`;
2059
- } else {
2060
- v.kind = "transform-passes";
2061
- v.why = `the sample passed \u2014 the ${sample.source}${sample.arity !== void 0 ? ` at arity ${sample.arity}` : ""}, ${inputs.length} evaluation${inputs.length === 1 ? "" : "s"}, every result accepted by the consumer's inputSchema`;
2062
- }
2498
+ await judgeTransform(v, node, node.transform, type, consumer, consumerWhy, languages, sample, inputs, a.perItem === true, options);
2063
2499
  verdicts.push(v);
2064
2500
  }
2065
2501
  }
@@ -2067,13 +2503,84 @@ async function checkWiring(registry, flow, options = {}) {
2067
2503
  for (const v of verdicts) counts[v.kind]++;
2068
2504
  return { flowId: flow.flowId, verdicts, counts, entry: entryTasks(flow) };
2069
2505
  }
2506
+ function consumerOf(node, type, workers) {
2507
+ const consumer = type?.worker === void 0 ? void 0 : workers.get(type.worker)?.inputSchema;
2508
+ const consumerWhy = !type ? `type "${node.type}" of "${node.id}" is not in the registry` : type.worker === void 0 ? `"${node.id}" is of type "${node.type}", which only TRANSFORMS \u2014 it names no worker, so it declares no inputSchema` : !workers.get(type.worker) ? `worker "${type.worker}" of type "${node.type}" is not in the registry` : consumer === void 0 ? `"${node.id}" (type "${node.type}", worker "${type.worker}") declares no inputSchema` : void 0;
2509
+ return { consumer, consumerWhy };
2510
+ }
2511
+ var sourceWord = (s) => s === "given" ? "given input" : s;
2512
+ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, languages, sample, inputs, perItem, options) {
2513
+ const adapter = languages.get(transform.lang);
2514
+ if (adapter === void 0) {
2515
+ v.why = `language "${transform.lang}" is not in the registry`;
2516
+ v.sample = sample;
2517
+ return;
2518
+ }
2519
+ if (inputs.length === 0) {
2520
+ v.why = `the sample holds no item to run "${node.id}"'s transform on (arity 0)`;
2521
+ v.sample = sample;
2522
+ return;
2523
+ }
2524
+ const settings = effectiveSettings(node, type);
2525
+ let results;
2526
+ try {
2527
+ let compile;
2528
+ try {
2529
+ const mod = await import(adapter.worker);
2530
+ if (typeof mod.default === "function") compile = mod.default;
2531
+ } catch (e) {
2532
+ if (options.workerHalf?.[transform.lang] === void 0) throw e;
2533
+ }
2534
+ compile ??= options.workerHalf?.[transform.lang];
2535
+ if (typeof compile !== "function") throw new Error(`the worker half of language "${transform.lang}" has no default export, and none was handed in`);
2536
+ const c = compile(transform.expr);
2537
+ results = [];
2538
+ for (const input of inputs) results.push(await c.evaluate(input, settings));
2539
+ } catch (e) {
2540
+ v.kind = "transform-fails";
2541
+ v.why = `the transform threw on the ${sourceWord(sample.source)}: ${e instanceof Error ? e.message : String(e)}`;
2542
+ v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
2543
+ return;
2544
+ }
2545
+ sample.result = perItem ? results : results[0];
2546
+ v.sample = sample;
2547
+ if (consumer === void 0) {
2548
+ v.why = consumerWhy + " \u2014 the transform ran and nothing holds its result";
2549
+ return;
2550
+ }
2551
+ const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
2552
+ if (bad) {
2553
+ v.kind = "transform-fails";
2554
+ v.why = `the transform's result on the ${sourceWord(sample.source)}${perItem ? ` (item ${bad[0]})` : ""} fails the consumer's inputSchema \u2014 ${describeFindings(bad[1])}`;
2555
+ } else {
2556
+ v.kind = "transform-passes";
2557
+ v.why = `the sample passed \u2014 the ${sourceWord(sample.source)}${sample.arity !== void 0 ? ` at arity ${sample.arity}` : ""}, ${inputs.length} evaluation${inputs.length === 1 ? "" : "s"}, every result accepted by the consumer's inputSchema`;
2558
+ }
2559
+ }
2560
+ async function givenVerdict(registry, flow, nodeId, input, options = {}) {
2561
+ if (input === void 0) return (await checkWiring(registry, flow, options)).verdicts.filter((v2) => v2.consumer === nodeId);
2562
+ const node = flow.nodes.find((n) => n.id === nodeId);
2563
+ if (node === void 0 || node.kind !== "task") return [];
2564
+ const workers = workersOf(registry.workers);
2565
+ const type = nodeTypesOf(registry.nodeTypes).get(node.type);
2566
+ const { consumer, consumerWhy } = consumerOf(node, type, workers);
2567
+ const v = { consumer: node.id, shape: "given", kind: "unchecked", why: "" };
2568
+ const sample = { source: "given", value: input };
2569
+ if (node.transform === void 0) {
2570
+ v.why = `"${node.id}" has no transform to run the given input through, so nothing is evaluated \u2014 holding one value to the inputSchema is \`findings\`'s question${consumerWhy !== void 0 ? `; and ${consumerWhy}` : ""}`;
2571
+ v.sample = sample;
2572
+ return [v];
2573
+ }
2574
+ await judgeTransform(v, node, node.transform, type, consumer, consumerWhy, languagesOf(registry.languages), sample, [input], false, options);
2575
+ return [v];
2576
+ }
2070
2577
  function describeVerdict(v) {
2071
- const road = v.shape === "edge" ? `${v.producer} -> ${v.consumer}` : v.shape === "branch" ? `${v.producer} -> (${v.through}) -> ${v.consumer}` : v.shape === "item" ? `${v.producer} -> (${v.through}, one item) -> ${v.consumer}` : v.shape === "record" ? `${v.producer} (the record) -> ${v.consumer}` : `${v.producer} -> needs -> ${v.consumer}`;
2072
- const s = v.sample ? ` [on the ${v.sample.source}${v.sample.arity !== void 0 ? `, arity ${v.sample.arity}` : ""}]` : "";
2578
+ const road = v.shape === "given" ? `(given) -> ${v.consumer}` : v.shape === "edge" ? `${v.producer} -> ${v.consumer}` : v.shape === "branch" ? `${v.producer} -> (${v.through}) -> ${v.consumer}` : v.shape === "item" ? `${v.producer} -> (${v.through}, one item) -> ${v.consumer}` : v.shape === "record" ? `${v.producer} (the record) -> ${v.consumer}` : `${v.producer} -> needs -> ${v.consumer}`;
2579
+ const s = v.sample ? ` [on the ${sourceWord(v.sample.source)}${v.sample.arity !== void 0 ? `, arity ${v.sample.arity}` : ""}]` : "";
2073
2580
  return `${v.kind.padEnd(16)} ${road}: ${v.why}${s}`;
2074
2581
  }
2075
2582
  var VERDICT_KINDS = ["fits", "no", "transform-passes", "transform-fails", "unchecked"];
2076
- var WIRING_SHAPES = ["edge", "branch", "item", "record", "needs"];
2583
+ var WIRING_SHAPES = ["edge", "branch", "item", "record", "needs", "given"];
2077
2584
 
2078
2585
  // src/driver/ids.ts
2079
2586
  var SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
@@ -2132,9 +2639,10 @@ function lastRecordFor(log, taskId) {
2132
2639
  }
2133
2640
  return void 0;
2134
2641
  }
2135
- function isSuspended(log, taskId) {
2642
+ function isSuspended(log, taskId, asked = false) {
2136
2643
  const last = lastRecordFor(log, taskId);
2137
- return last?.kind === "outcome" && last.terminalState === "user_action";
2644
+ if (last === void 0) return asked;
2645
+ return last.kind === "outcome" && last.terminalState === "user_action";
2138
2646
  }
2139
2647
  function effectiveOutcome(log, taskId) {
2140
2648
  let settlement = null;
@@ -2144,7 +2652,22 @@ function effectiveOutcome(log, taskId) {
2144
2652
  if (r.kind === "outcome") settlement = r;
2145
2653
  else resolution = r;
2146
2654
  }
2147
- if (!settlement) return null;
2655
+ if (!settlement) {
2656
+ if (!resolution) return null;
2657
+ return {
2658
+ kind: "outcome",
2659
+ taskId,
2660
+ terminalState: resolution.resolvedAs,
2661
+ result: resolution.result,
2662
+ error: resolution.error,
2663
+ attempts: 0,
2664
+ schedulerTimestamp: null,
2665
+ observedAt: resolution.observedAt,
2666
+ startTime: null,
2667
+ endTime: null,
2668
+ executionTime: null
2669
+ };
2670
+ }
2148
2671
  if (!resolution) return settlement.terminalState === "user_action" ? null : settlement;
2149
2672
  return {
2150
2673
  ...settlement,
@@ -2165,16 +2688,15 @@ var UncloneableResult = class extends Error {
2165
2688
  this.name = "UncloneableResult";
2166
2689
  }
2167
2690
  };
2168
- var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "user_action"]);
2691
+ var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "user_action", "cancelled"]);
2692
+ var SETTLEMENT = /* @__PURE__ */ new Set(["task.completed", "task.failed", "task.user_action", "task.cancelled"]);
2169
2693
  var num = (x) => typeof x === "number" ? x : null;
2170
2694
  function normalizeEvent(eventName, detail, now) {
2171
- if (eventName === "task.running" || eventName === "task.progress") return null;
2695
+ if (eventName === "task.running" || eventName === "task.progress" || eventName === "task.cancelling") return null;
2172
2696
  const state = detail?.state;
2173
2697
  if (typeof state !== "string" || !TERMINAL.has(state)) return null;
2174
2698
  if (eventName === "task.failed" && state === "user_action") return null;
2175
- if (eventName !== "task.completed" && eventName !== "task.failed" && eventName !== "task.user_action") {
2176
- return null;
2177
- }
2699
+ if (!SETTLEMENT.has(eventName)) return null;
2178
2700
  const terminalState = state;
2179
2701
  let result = null;
2180
2702
  if (terminalState === "done") {
@@ -2205,55 +2727,18 @@ function normalizeEvent(eventName, detail, now) {
2205
2727
  // src/driver/routing.ts
2206
2728
  var NoMatchingEdge = class extends Error {
2207
2729
  };
2208
- function evalLeaf(ctx, p) {
2209
- let v = readKeyPath(ctx, p.path);
2210
- if (v === void 0 && !("default" in p)) return false;
2211
- if ((v === void 0 || v === null) && "default" in p) v = p.default;
2212
- if (p.length === true) {
2213
- if (Array.isArray(v)) v = v.length;
2214
- else if (typeof v === "string") v = v.length;
2215
- else return false;
2216
- }
2217
- const w = p.value;
2218
- switch (p.op) {
2219
- // **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
2220
- // `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
2221
- // measured over 1800 contexts, a type-strict form reproduces those three EXACTLY and differs from
2222
- // `resume.test.ts`'s unguarded `isHigh` on `{score: "9"}`, where the JS body coerces. That is
2223
- // `FD-V30`'s own argument — *a string `maxRetries` retries by coercion* — one rule over.
2224
- case "eq":
2225
- return v === w;
2226
- case "ne":
2227
- return v !== w;
2228
- case "lt":
2229
- return typeof v === "number" && typeof w === "number" && v < w;
2230
- case "lte":
2231
- return typeof v === "number" && typeof w === "number" && v <= w;
2232
- case "gt":
2233
- return typeof v === "number" && typeof w === "number" && v > w;
2234
- case "gte":
2235
- return typeof v === "number" && typeof w === "number" && v >= w;
2236
- case "startsWith":
2237
- return typeof v === "string" && typeof w === "string" && v.startsWith(w);
2238
- default: {
2239
- const never = p.op;
2240
- throw new NoMatchingEdge(`operator "${String(never)}" is not in the subset; FD-V44 should have caught this at instantiation`);
2241
- }
2730
+ var UNKNOWN_OP2 = (message) => new NoMatchingEdge(message);
2731
+ function evaluateWhen(when, ctx, predicates) {
2732
+ if ("predicate" in when && typeof when.predicate === "string") {
2733
+ const p = predicates.get(when.predicate);
2734
+ if (!p) throw new NoMatchingEdge(`predicate "${when.predicate}" is not registered; FD-V4 should have caught this at instantiation`);
2735
+ return p(ctx);
2242
2736
  }
2243
- }
2244
- function evaluateWhen(when, ctx, predicates) {
2245
- if ("predicate" in when && typeof when.predicate === "string") {
2246
- const p = predicates.get(when.predicate);
2247
- if (!p) throw new NoMatchingEdge(`predicate "${when.predicate}" is not registered; FD-V4 should have caught this at instantiation`);
2248
- return p(ctx);
2249
- }
2250
- if ("all" in when && Array.isArray(when.all)) return when.all.every((m) => evalLeaf(ctx, m));
2251
- if ("any" in when && Array.isArray(when.any)) return when.any.some((m) => evalLeaf(ctx, m));
2252
- return evalLeaf(ctx, when);
2737
+ return evaluateDeclared(when, ctx, UNKNOWN_OP2);
2253
2738
  }
2254
2739
  var onOf = (e) => Array.isArray(e.on) && e.on.length ? e.on : ["done"];
2255
2740
  function eligibleEdges(node, terminalState) {
2256
- if (node.kind !== "task" && node.kind !== "join") return [];
2741
+ if (node.kind !== "task" && node.kind !== "join" && node.kind !== "ask") return [];
2257
2742
  const edges = Array.isArray(node.edges) ? node.edges : [];
2258
2743
  return edges.filter((e) => onOf(e).includes(terminalState));
2259
2744
  }
@@ -2273,9 +2758,15 @@ function selectEdge(node, runId, terminalState, contextFor, predicates) {
2273
2758
  );
2274
2759
  }
2275
2760
 
2761
+ // src/driver/events.ts
2762
+ var TERMINAL_EVENTS = ["branch.arrived", "branch.exited", "branch.lost"];
2763
+ function isTerminalEvent(e) {
2764
+ return TERMINAL_EVENTS.includes(e.type);
2765
+ }
2766
+
2276
2767
  // src/driver/driver.ts
2277
2768
  import { FyflowTask } from "fyflow-scheduler";
2278
- var InvalidDefinition = class extends Error {
2769
+ var InvalidDefinition = class _InvalidDefinition extends Error {
2279
2770
  /**
2280
2771
  * The rule ids, on the error rather than only in its text, so a caller can act
2281
2772
  * on them.
@@ -2291,6 +2782,25 @@ var InvalidDefinition = class extends Error {
2291
2782
  super(message);
2292
2783
  this.rules = rules;
2293
2784
  }
2785
+ /**
2786
+ * **A definition's findings as the error, built in ONE place** — P34 fact 12, W169. `new Driver` throws it for what
2787
+ * `validateDefinition` found, and `createRuntime`'s `start` and `resume` throw it for what `runtime.check` found (ADR 0096
2788
+ * rule 5), so the two roads cannot drift apart: `rules` is `violatedRules(findings)` — sorted, each once — and the
2789
+ * message is `definition "<flowId>" is invalid: ` then every finding as `<rule> at <at>: <detail>`, `at` reading
2790
+ * `<document>` where a finding has none, joined by `; `. *(Until W169 the two roads each built it inline, the same text
2791
+ * twice.)* `at` is printed as given — a node id, an edge site (`id#edgeN`), or `<document>` for `null`.
2792
+ *
2793
+ * **Its precondition is at least one finding**, and an EMPTY list is refused with a `RangeError` (the conformity reader's
2794
+ * C3): an `InvalidDefinition` naming no defect would read *"is invalid: "* and name nothing. Both callers throw only
2795
+ * when they found something.
2796
+ */
2797
+ static fromFindings(flowId, findings2) {
2798
+ if (findings2.length === 0) throw new RangeError(`InvalidDefinition.fromFindings: definition "${flowId}" has no finding \u2014 an invalid definition names at least one`);
2799
+ return new _InvalidDefinition(
2800
+ violatedRules(findings2),
2801
+ `definition "${flowId}" is invalid: ` + findings2.map((f) => `${f.rule} at ${f.at ?? "<document>"}: ${f.detail}`).join("; ")
2802
+ );
2803
+ }
2294
2804
  };
2295
2805
  var DuplicateTaskId = class extends Error {
2296
2806
  constructor(message, options) {
@@ -2319,9 +2829,9 @@ var UndeclaredRoute = class extends Error {
2319
2829
  var BoundaryRefused = class extends Error {
2320
2830
  /** Which boundary refused: the run's payload at `start`, or a person's value at `resolve`. */
2321
2831
  boundary;
2322
- /** The task node whose worker's schema refused the value. */
2832
+ /** The task node whose worker's schema refused the value — or, at `resolve`, the ASK whose `answerSchema` did (W180). */
2323
2833
  nodeId;
2324
- /** The suspended task, at `resolve`; `start` has dispatched nothing yet. */
2834
+ /** The suspended task — or parked ask — at `resolve`; `start` has dispatched nothing yet. */
2325
2835
  taskId;
2326
2836
  /** Every way the value failed the schema — `findings`' own list, one line each in `message`. */
2327
2837
  findings;
@@ -2346,10 +2856,20 @@ var NotStarted = class extends Error {
2346
2856
  this.name = "NotStarted";
2347
2857
  }
2348
2858
  };
2859
+ var RunControlRefused = class extends Error {
2860
+ control;
2861
+ reason;
2862
+ constructor(control, reason, message) {
2863
+ super(message);
2864
+ this.name = "RunControlRefused";
2865
+ this.control = control;
2866
+ this.reason = reason;
2867
+ }
2868
+ };
2349
2869
  var CHECKPOINT_FORMAT = 1;
2350
2870
  var ResumeRefused = class extends Error {
2351
2871
  reason;
2352
- /** The position `stopped` and `diverged` name; `null` for the three a header decides. */
2872
+ /** The position `halted`, `cancelled` and `diverged` name; `null` for the three a header decides. */
2353
2873
  index;
2354
2874
  constructor(reason, index, message) {
2355
2875
  super(message);
@@ -2396,12 +2916,29 @@ function resumeRefusal(definition, checkpoint, runId) {
2396
2916
  `ResumeRefused (flow): the checkpoint is of flow ${JSON.stringify(h.flowId)} version ${JSON.stringify(h.version)}, and this driver runs "${definition.flowId}" version ${definition.version}. There is no migration (P23 decision 7): a checkpoint resumes against the definition that produced it, and a changed flow is a new version.`
2397
2917
  );
2398
2918
  }
2399
- const at = cp.events.findIndex((e) => e?.kind === "stopped");
2919
+ const at = cp.events.findIndex((e) => {
2920
+ const k = e?.kind;
2921
+ return k === "halted" || k === "stopped";
2922
+ });
2400
2923
  if (at >= 0) {
2924
+ const e = cp.events[at];
2401
2925
  return new ResumeRefused(
2402
- "stopped",
2926
+ "halted",
2403
2927
  at,
2404
- `ResumeRefused (stopped): the stored run was STOPPED \u2014 its element at position ${at} is \`stopped\` (${JSON.stringify(cp.events[at]?.reason)}). A stopped run is not resumable (P23 decision 10): the refusal that stopped it would stop it again, and the one class whose cause never reached the log \u2014 a result OR-R6 refused \u2014 cannot be regenerated at all.`
2928
+ `ResumeRefused (halted): the stored run was HALTED by a refusal \u2014 its element at position ${at} is \`${String(e.kind)}\`${e.kind === "stopped" ? " (the name 0.7.0 wrote for `halted`, ADR 0102)" : ""} (${JSON.stringify(e.reason)}). A halted run is not resumable (P23 decision 10): the refusal that halted it would halt it again, and the one class whose cause never reached the log \u2014 a result OR-R6 refused \u2014 cannot be regenerated at all.`
2929
+ );
2930
+ }
2931
+ const c = cp.events.findIndex((e) => {
2932
+ const x = e;
2933
+ return x?.kind === "cancelRequested" || x?.kind === "outcome" && x.record?.terminalState === "cancelled";
2934
+ });
2935
+ if (c >= 0) {
2936
+ const e = cp.events[c];
2937
+ const what = e?.kind === "cancelRequested" ? `the request of ${JSON.stringify(e.actor)} to cancel it (\`cancelRequested\`)` : `the settlement of ${JSON.stringify(e?.record?.taskId)} as \`cancelled\``;
2938
+ return new ResumeRefused(
2939
+ "cancelled",
2940
+ c,
2941
+ `ResumeRefused (cancelled): the stored run was CANCELLED \u2014 its element at position ${c} is ${what}. A cancelled run is not resumable (ADR 0094 rule 5, ADR 0104): a cancel is its owner\`s act, and a resume would hand the scheduler again what the owner stopped.`
2405
2942
  );
2406
2943
  }
2407
2944
  return null;
@@ -2409,14 +2946,42 @@ function resumeRefusal(definition, checkpoint, runId) {
2409
2946
  function sameData(a, b) {
2410
2947
  if (Object.is(a, b)) return true;
2411
2948
  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
2412
- if (Array.isArray(a) !== Array.isArray(b)) return false;
2949
+ const tag = Object.prototype.toString.call(a);
2950
+ if (tag !== Object.prototype.toString.call(b)) return false;
2951
+ if (Array.isArray(a)) {
2952
+ const y = b;
2953
+ return a.length === y.length && a.every((v, i) => sameData(v, y[i]));
2954
+ }
2955
+ if (a instanceof Map || a instanceof Set) {
2956
+ const x = [...a.entries()], y = [...b.entries()];
2957
+ return x.length === y.length && x.every((e, i) => sameData(e[0], y[i][0]) && sameData(e[1], y[i][1]));
2958
+ }
2959
+ if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
2960
+ if (a instanceof RegExp) return String(a) === String(b);
2961
+ if (a instanceof Error) return a.name === b.name && a.message === b.message;
2962
+ if (ArrayBuffer.isView(a) || a instanceof ArrayBuffer) {
2963
+ const bytes = (v) => v instanceof ArrayBuffer ? new Uint8Array(v) : new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
2964
+ const x = bytes(a), y = bytes(b);
2965
+ return x.length === y.length && x.every((v, i) => v === y[i]);
2966
+ }
2413
2967
  const ra = a, rb = b;
2414
2968
  const ka = Object.keys(ra).filter((k) => ra[k] !== void 0);
2415
2969
  const kb = Object.keys(rb).filter((k) => rb[k] !== void 0);
2416
2970
  return ka.length === kb.length && ka.every((k) => Object.hasOwn(rb, k) && sameData(ra[k], rb[k]));
2417
2971
  }
2418
2972
  function isFed(e) {
2419
- return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed";
2973
+ return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed" || e.kind === "interrupted" || e.kind === "paused" || e.kind === "unpaused" || e.kind === "attempted";
2974
+ }
2975
+ function frozenCopy(v) {
2976
+ try {
2977
+ return Object.freeze(cloneValue(v));
2978
+ } catch {
2979
+ return void 0;
2980
+ }
2981
+ }
2982
+ function attemptNumberOf(detail) {
2983
+ const n = detail?.attemptNumber;
2984
+ return typeof n === "number" && Number.isInteger(n) && n >= 1 ? n : null;
2420
2985
  }
2421
2986
  function describeElement(e) {
2422
2987
  if (e === void 0) return "nothing";
@@ -2460,6 +3025,38 @@ function withIndex(base, mapId, index) {
2460
3025
  next.set(mapId, index);
2461
3026
  return next;
2462
3027
  }
3028
+ function itemMaps(def, regions) {
3029
+ const kept = /* @__PURE__ */ new Map();
3030
+ for (const n of def.nodes) {
3031
+ const own3 = Object.values(n.needs ?? {});
3032
+ const onEdges = (n.edges ?? []).flatMap((e) => Object.values(e.needs ?? {}));
3033
+ if (![...own3, ...onEdges].some((r) => referenceTarget(r)?.form === "item")) continue;
3034
+ const m = innermostMapOf(regions, n.id);
3035
+ if (m !== void 0) kept.set(m.fanoutId, false);
3036
+ }
3037
+ const byId = new Map(def.nodes.map((n) => [n.id, n]));
3038
+ const seen = /* @__PURE__ */ new Set();
3039
+ const walk = (id) => {
3040
+ const n = byId.get(id);
3041
+ if (n === void 0 || seen.has(id)) return;
3042
+ seen.add(id);
3043
+ if (n.kind === "map" && n.needs === void 0 && kept.has(id)) kept.set(id, true);
3044
+ if (n.kind === "gate") for (const e of n.edges ?? []) walk(e.to);
3045
+ };
3046
+ walk(def.entry);
3047
+ return kept;
3048
+ }
3049
+ function itemWithin(kept, regions) {
3050
+ const out = /* @__PURE__ */ new Map();
3051
+ for (const id of kept.keys()) {
3052
+ const m = regions.find((r) => r.fanoutId === id);
3053
+ const branch = m?.branches[0];
3054
+ if (m === void 0 || branch === void 0) continue;
3055
+ const depth = regionChainOf(regions, branch).length;
3056
+ for (const r of regions) if (r !== m && m.members.has(r.fanoutId)) out.set(r.fanoutId, [...out.get(r.fanoutId) ?? [], { map: id, depth }]);
3057
+ }
3058
+ return out;
3059
+ }
2463
3060
  function describeRoute(v) {
2464
3061
  if (typeof v === "string") return JSON.stringify(v);
2465
3062
  return `${describeShape(v)} (${String(v)}) \u2014 not a route name`;
@@ -2476,7 +3073,7 @@ function cloneRecord(r) {
2476
3073
  }
2477
3074
  function freezeInput(i) {
2478
3075
  return Object.freeze(
2479
- i.kind === "outcome" || i.kind === "resolution" ? { ...i, record: Object.freeze(cloneRecord(i.record)) } : i.kind === "region" ? { ...i, event: Object.freeze({ ...i.event }) } : i.kind === "resumed" ? { ...i, taskIds: Object.freeze([...i.taskIds]) } : i.kind === "gated" && i.key !== void 0 ? { ...i, key: Object.freeze(cloneValue(i.key)) } : { ...i }
3076
+ i.kind === "outcome" || i.kind === "resolution" ? { ...i, record: Object.freeze(cloneRecord(i.record)) } : i.kind === "region" ? { ...i, event: Object.freeze({ ...i.event }) } : i.kind === "resumed" ? { ...i, taskIds: Object.freeze([...i.taskIds]) } : i.kind === "gated" && i.key !== void 0 ? { ...i, key: Object.freeze(cloneValue(i.key)) } : i.kind === "asked" ? { ...i, input: Object.freeze(cloneValue(i.input)) } : { ...i }
2480
3077
  );
2481
3078
  }
2482
3079
  var MissingJoinPolicy = class extends Error {
@@ -2489,7 +3086,9 @@ function detailOf(e) {
2489
3086
  return "detail" in e ? e.detail : void 0;
2490
3087
  }
2491
3088
  var defaultCreateTask = (config) => new FyflowTask(config);
2492
- var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action"];
3089
+ var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action", "task.cancelled"];
3090
+ var SUBSCRIBED_EVENTS = [...SETTLEMENT_EVENTS, "task.running"];
3091
+ var PASS_THROUGH = [["task.message", "onTaskMessage"], ["task.progress", "onTaskProgress"]];
2493
3092
  var Driver = class {
2494
3093
  /**
2495
3094
  * `OR-R7` — nothing outside the inbound API writes to run state. Private, and
@@ -2517,6 +3116,13 @@ var Driver = class {
2517
3116
  graph;
2518
3117
  regions;
2519
3118
  dispatched = /* @__PURE__ */ new Set();
3119
+ /**
3120
+ * **Every ASK this run has reached, by task id** — `RN-R37`, v0.22, W180, ADR 0109: the ids of the `asked` elements
3121
+ * appended, so `RS-R1`'s widened *suspended* (an `asked` element and no record after it) is one lookup rather than a
3122
+ * scan of the input sequence at every `resolve()`, and an ask's id is minted once per path (`ID-R1`'s guard). Ids only —
3123
+ * the kept INPUT lives on the element alone. `dispatched`'s twin for the one node that is never dispatched.
3124
+ */
3125
+ asked = /* @__PURE__ */ new Set();
2520
3126
  now;
2521
3127
  opts;
2522
3128
  def;
@@ -2594,6 +3200,29 @@ var Driver = class {
2594
3200
  * quiescent point (`RN-R10`), not here — ADR 0046 rule 5.
2595
3201
  */
2596
3202
  arrivals = /* @__PURE__ */ new Map();
3203
+ /**
3204
+ * **What a map INSTANCE keeps for `"$item"`** — `RN-R13`'s sixth term since v0.28 (`spec/run-state.md`), P38 (e),
3205
+ * [ADR 0118](../../doc/decisions/0118-a-map-instance-keeps-its-item-while-a-node-of-it-may-still-be-entered.md),
3206
+ * [W191](../../doc/work-items/w191-the-map-instances-item-as-a-reference.md). Two halves, one field:
3207
+ *
3208
+ * - **`kept`** — decided from the DOCUMENT at construction: the maps whose region holds a `"$item"` reader (a node, or
3209
+ * an edge of a node, whose innermost containing map is that map — `innermostMapOf`, P38 (c)), and for each whether
3210
+ * its received value may be the CALLER's own object (`true`: reached from the entry through gates alone, with no
3211
+ * `needs` — `start()` hands `enter` the value it was called with, as an entry task and an entry ask receive it). A
3212
+ * map no reader names keeps NOTHING, and a flow declaring no `"$item"` has `kept` empty (P38 decision 2).
3213
+ * - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
3214
+ * by a run and by a resume's replay alike, so a replay REBUILDS it (`RN-R19`; P38's brief: *taken at rest*, never by
3215
+ * a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
3216
+ * `needs` composition's per-key copy), and as its OWN copy only where `kept` says that element is the caller's
3217
+ * (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
3218
+ * **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
3219
+ * `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
3220
+ * (`within`, `nested`, `ended` — `trackItems`): a nested join a policy fires before its last branch is terminal lets
3221
+ * the instance's branch arrive while that branch can still read (W191's conformity reader, C1). A suspended node (an
3222
+ * ask, a parked task) keeps its branch live, and so its item. And at the run-level moments
3223
+ * `arrivals` is cleared at — a halt, a cancel, a stop, a `cancelled` settlement — because no node is entered after.
3224
+ */
3225
+ items;
2597
3226
  /**
2598
3227
  * **The one run this driver serves** — ADR 0022 rule 1, fixed at construction
2599
3228
  * and never `null`.
@@ -2636,16 +3265,212 @@ var Driver = class {
2636
3265
  * elements a consumer does not already hold ({@link Driver.delivers}).
2637
3266
  */
2638
3267
  replay = null;
2639
- /** The run has been stopped by a refusal. Nothing dispatches for it again. */
3268
+ /** The run has been HALTED by a refusal (`OR-R10`). Nothing dispatches for it again. */
2640
3269
  failed = false;
2641
3270
  /**
2642
- * **A resume refused because its replay regenerated a STOP in the lost tail** — `RN-R23`: that driver keeps the
2643
- * stopped run (its `header`, so a store may take its checkpoint) and still hears NOTHING. `onEvent`'s other test,
3271
+ * **A task of this run settled `cancelled`, so the run is HALTED** — `RN-R28`,
3272
+ * [ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md)
3273
+ * rule 3, W158. Every door `failed` shuts, this shuts — nothing is dispatched or routed for the run again,
3274
+ * a later settlement is appended and routed nowhere, and `resolve()` is refused — and it differs from `failed`
3275
+ * in three things only: no `halted` element is appended (the fold reads the run `cancelled` from the
3276
+ * settlement itself), `onRunFailure` is not called (a cancel is its owner's act, not a defect), and
3277
+ * `hasFailed()` stays false. **Separate rather than `failed` with a reason**, because those three are each
3278
+ * a reader of `failed` that must not fire.
3279
+ */
3280
+ cancelled = false;
3281
+ /**
3282
+ * **A resume refused because its replay regenerated a HALT in the lost tail** — `RN-R23`: that driver keeps the
3283
+ * halted run (its `header`, so a store may take its checkpoint) and still hears NOTHING. `onEvent`'s other test,
2644
3284
  * *started with no header*, cannot see it, because this refusal is the one that keeps its header: until W151 such a
2645
3285
  * driver built by hand appended and delivered a later settlement and reported an unminted id (W151's conformity
2646
- * reader, finding 6). A driver stopped by `failRun` on a run it started is NOT deaf: `OR-R10` stops routing only.
3286
+ * reader, finding 6). A driver halted by `failRun` on a run it started is NOT deaf: `OR-R10` halts routing only.
3287
+ *
3288
+ * **And a STOPPED driver, since W174** (`RN-R33`, P34 decision 3): a stop sets it with its hold — before the
3289
+ * `interrupted` element, and so BEFORE it sends the first cancel — so no `task.cancelled` of the stop — nor any later settlement — is appended,
3290
+ * routed, delivered or reported, and the log ends with the in-flight set dispatched and unsettled for a resume to
3291
+ * re-dispatch. A replay feeding a stored `interrupted` element does NOT set it: the replay hears nothing anyway.
2647
3292
  */
2648
3293
  deaf = false;
3294
+ /**
3295
+ * **The run's OWNER cancelled it** — `RN-R31`, v0.18, W173,
3296
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md). `null`
3297
+ * until `cancel()` is accepted; then what every LATER cancel of a task sends the scheduler — the caller's `reason` and
3298
+ * `graceMs`, each only if given — and, per task id, the promise the scheduler's `cancel` returned for it, which is what
3299
+ * `cancel()`'s own promise waits on. **A halt** (`shut()`), set BEFORE the `cancelRequested` element is appended, so a
3300
+ * consumer's callback on that element already sees the run held. It holds no result: one promise per task STILL
3301
+ * stopping — each dropped as it settles, so none once the run is `cancelled` — and the two option values.
3302
+ */
3303
+ cancelRequest = null;
3304
+ /**
3305
+ * **The run's OWNER stopped it, to resume it elsewhere** — `RN-R33`, v0.19, W174,
3306
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md). `null`
3307
+ * until `stop()` is accepted; then, as `cancelRequest`'s shape, what every cancel the stop sends carries and one promise
3308
+ * per task STILL stopping, each dropped as it settles — and the `actor`, which the element is appended with.
3309
+ *
3310
+ * **`pending` while the stop has been ACCEPTED and not yet TAKEN**: a stop called MID-STEP — from a callback on the
3311
+ * driver's own step — takes effect when that step ends (`stepping` back to `0`), so the step's routing is all in the log
3312
+ * and none of it is lost to a resume (`RN-R33`, W174). While pending it is the resolver of the promise every call made
3313
+ * before then waits on, `resolve()` and `cancel()` are already refused, and the hold is NOT yet shut; `null` once the
3314
+ * stop is taken, from which moment it IS a halt (`shut()`). `took` is the promise a second call made while pending
3315
+ * waits on. A function and a promise held only until the step ends — `null` both once taken, never at rest.
3316
+ *
3317
+ * **And a REPLAY sets it too**, taken, with no options and nothing stopping, where it feeds a stored `interrupted`
3318
+ * element (`RN-R19`): the stored run was held from that position, so the replay holds there as well, until a fed
3319
+ * `resumed` — or the resume itself, once the replay is over — sets it back to `null`. The stop's deafness is `deaf`'s.
3320
+ */
3321
+ stopRequest = null;
3322
+ /**
3323
+ * **Tasks whose `dispatched` element is appended and which the scheduler has NOT been handed yet** — `RN-R29` (i)'s
3324
+ * window, ADR 0103's inherited hazard, W173. A scheduler's `cancel()` of an id it does not hold resolves `false` and
3325
+ * does nothing, so `cancel()` sends nothing for these: the hand-off site cancels each right AFTER its `addTask` —
3326
+ * `dispatch` for the task in progress, `resume()` for its in-flight set. Empty at rest; at most the one task being
3327
+ * dispatched, or a resume's in-flight set while it is handed back.
3328
+ */
3329
+ unhanded = /* @__PURE__ */ new Set();
3330
+ /**
3331
+ * **How deep the driver is in its OWN step** — `start`, `resume`, `resolve` or a scheduler event, each counting itself
3332
+ * in (`RN-R31`, W173's conformity reader, D-1). `0` at rest. A consumer callback the driver calls mid-step sees a
3333
+ * PREFIX that can read finished while the step is about to dispatch more (`RN-R12`'s quiescent point, P34 fact 3), so
3334
+ * `cancel()` refuses `ended` only at rest: mid-step, *nothing in flight and nothing parked* is not *finished*.
3335
+ */
3336
+ stepping = 0;
3337
+ /**
3338
+ * **The run's OWNER has paused it, and the pause is TAKEN** — `RN-R35`, v0.20, W175,
3339
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md):
3340
+ * set when a `paused` element is appended (by `pause()`, or fed to a replay) and cleared when an `unpaused` one is —
3341
+ * so it is always what `RunState.paused` folds to on the driver's own sequence. **Not a halt**: it does not join
3342
+ * `shut()`, because a paused run still routes and still accepts a resolution (ADR 0103's consequence). It is the
3343
+ * dispatch hold's DEFERRED form, at the dispatch door (ADR 0103 rule 6): each dispatch is appended, and held.
3344
+ */
3345
+ paused = false;
3346
+ /**
3347
+ * **Every dispatch the pause HELD, in dispatch order** (`RN-R35`): appended — so the fold reads each `running`, in flight
3348
+ * under `RN-R21` (P34 fact 3) — and NOT handed to the scheduler. An `unpause` hands each on, first held first; a
3349
+ * `cancel` hands each on and cancels it (ADR 0104 rule 7); a `stop` sends nothing for them, so they stay in flight in
3350
+ * the log for a resume to hold again. Empty unless the run is paused — or an unpause's hand-off is under way, while
3351
+ * which a dispatch joins the END of it, so the order holds; or a halt broke that hand-off off, after which nothing is
3352
+ * handed on for the run and what is left here never is. One `createTask` configuration per held task, no result.
3353
+ */
3354
+ pauseHeld = /* @__PURE__ */ new Map();
3355
+ /**
3356
+ * **Pause and unpause calls ACCEPTED mid-step and not yet TAKEN**, in call order (`RN-R35`): a call from a callback on
3357
+ * the driver's own step is taken when that step ends (`stepping` back to `0`, `leaveStep()`), as a stop is (`RN-R33`) —
3358
+ * so its element lands BETWEEN two steps, where a replay can feed it (`RN-R19`). Empty at rest.
3359
+ * **And, since W180, a RESOLUTION given mid-step** — an ask's answer or a parked task's (`RN-R37`, ADR 0109 rule 5, P35's
3360
+ * brief: *an owner element a resume's replay feeds is TAKEN AT REST*): accepted at the call — every refusal already asked — and its resolution
3361
+ * appended when the step ends, in the same call order as the pauses beside it.
3362
+ */
3363
+ pausePending = [];
3364
+ /**
3365
+ * **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
3366
+ * number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
3367
+ * repeating a number (a requeue) appends nothing, and what a resume re-hands an in-flight task with as `attemptsSpent`.
3368
+ * One integer per task that ever started an attempt, no result; kept for the life of the run, as `dispatched`'s ids are.
3369
+ */
3370
+ lastAttempt = /* @__PURE__ */ new Map();
3371
+ /**
3372
+ * **`attempted` elements HEARD and not yet appended**, in the order heard (`RN-R40`): a `task.running` arrives on the
3373
+ * driver's OWN step when an inline pool starts the task inside `addTask`, and an element appended there would sit between
3374
+ * two elements of one step — where the replay, which regenerates a step whole, would refuse the checkpoint `diverged`. So
3375
+ * every one is appended when the step ends, BEFORE a pause, an unpause, an answer or a stop pending on that step is taken
3376
+ * (`leaveStep()`): the attempt started before any of them. Empty at rest.
3377
+ */
3378
+ attemptsPending = [];
3379
+ /**
3380
+ * **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
3381
+ * `RN-R29`,
3382
+ * [ADR 0103](../../doc/decisions/0103-every-halt-shares-one-dispatch-hold.md), P34
3383
+ * decisions 5 and 8, W172. **The ONE predicate every halt shares**, read at these doors and no others — `RN-R29`
3384
+ * states the two things it does not hold: before each of `resume()`'s re-dispatches (the maintainer's answer to
3385
+ * W172's S-1), at the head of `enter` (door (a) of [Q605](../../doc/questions/q605.md) — a fan-out's or a map's remaining
3386
+ * branches), at the head of `dispatch`, after a resolution's append and again before its routing (door (b)),
3387
+ * after a settlement's append (`settle`), after a join's `joinSettled` append (`settleJoin`), and at `resolve()`'s
3388
+ * entry. A halt P34 adds — a cancel or a stop — joins this disjunction, and every door follows.
3389
+ *
3390
+ * **A METHOD over the halt flags, never a field of its own**: a second copy of *the run is halted* is a field that
3391
+ * can disagree with `failed` or `cancelled`, and each of those has readers this predicate must not stand in for
3392
+ * (`hasFailed()`, `onRunFailure`). **The replay's hold is the OTHER form and is not this** — it appends a dispatch
3393
+ * and defers handing it on (`RN-R19`), where a shut hold appends nothing; `dispatch` keeps both.
3394
+ *
3395
+ * **A REQUESTED cancel joins it since W173** (`RN-R31`, ADR 0104) — ADR 0103's *a halt P34 adds joins this
3396
+ * disjunction*: from the moment `cancel()` is accepted, every door above holds for the run. **And a STOP since W174**
3397
+ * (`RN-R33`, ADR 0105): from the moment the stop is TAKEN — at once when `stop()` is called at rest, and when the step
3398
+ * ends when it is called mid-step (`stopRequest.pending`) — and, in a replay, from a fed `interrupted` element on.
3399
+ */
3400
+ shut() {
3401
+ return this.failed || this.cancelled || this.cancelRequest !== null || this.stopRequest !== null && this.stopRequest.pending === null;
3402
+ }
3403
+ /**
3404
+ * **What holds a resume's re-dispatch: the hold, EXCEPT a requested cancel** (ADR 0103 rule 7, ADR 0104 rule 7) — read
3405
+ * through `shut()` and never as a second copy of its disjunction (ADR 0103 rule 1; W173's conformity reader, F7), so a
3406
+ * halt W174 adds to `shut()` holds here too. **W174's stop never reaches this loop taken** (`RN-R33`): a stop is taken
3407
+ * at rest, and a stop called on this stack — on the `resumed` element, or in the replay's lost tail — is PENDING until
3408
+ * `resume()` returns, so each task is handed on here and the stop then cancels it; and the replay's own fed stop is
3409
+ * cleared before the loop. A request is the one halt that hands a task on: its tasks are `dispatched` and unsettled,
3410
+ * and only the scheduler can settle them, so each is handed on and cancelled (`handOn`).
3411
+ */
3412
+ holdsResume() {
3413
+ return this.shut() && this.cancelRequest === null;
3414
+ }
3415
+ /**
3416
+ * **The driver's own step ends** — each of `start`, `resume`, `resolve` and a scheduler event counts itself out here.
3417
+ * At rest again, a stop called on that step is TAKEN (`RN-R33`, W174): the step's routing is whole in the log, and
3418
+ * every task it handed on is in flight for the stop to cancel. **And a pause or an unpause called on it** (`RN-R35`,
3419
+ * W175), each in call order and BEFORE the stop — a pause or an unpause after a stop is refused, so any pending one was
3420
+ * called first. Each take is a step of its own and ends here too, so the next pending call is taken after it.
3421
+ * **And a resolution given on the step** (`RN-R37`, W180), in the same queue and the same call order.
3422
+ * **And FIRST, every `attempted` element heard on the step** (`RN-R40`, W186): the attempts started before anything
3423
+ * taken here, and a stop taken before them would leave them unrecorded — the driver deaf — under P36 (h)'s *a stopped
3424
+ * attempt counts*.
3425
+ */
3426
+ leaveStep() {
3427
+ this.stepping -= 1;
3428
+ if (this.stepping === 0 && this.attemptsPending.length > 0) this.takeAttempts();
3429
+ while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
3430
+ if (this.stepping === 0 && this.stopRequest !== null && this.stopRequest.pending !== null) this.takeStop();
3431
+ }
3432
+ /**
3433
+ * **Every `attempted` element heard and not yet appended, appended — AT REST, as ONE step of their own** (`RN-R40`,
3434
+ * W186): in the order heard, each BETWEEN two of the driver's steps, where a replay feeds it (`RN-R19`). A step, so a
3435
+ * call a consumer makes from a callback on one — a pause, a stop — is taken after the last of them. **Appended on a
3436
+ * halted run too**, as a settlement is: the scheduler started the attempt whatever the run's routing does. A consumer's
3437
+ * `onFoldInput` THROWING on one HALTS the run, reported once (`OR-R10` v0.19, W184), and the throw reaches whoever is on
3438
+ * the stack — a driver call's caller, or the listener boundary of a scheduler event's step; the elements still pending
3439
+ * behind it are appended by this step's own end, SILENTLY (the run is halted, its failure reported), and so is a throw
3440
+ * on any of them once the run is halted.
3441
+ */
3442
+ takeAttempts() {
3443
+ this.stepping += 1;
3444
+ try {
3445
+ while (this.attemptsPending.length > 0) {
3446
+ const a = this.attemptsPending.shift();
3447
+ const silent = this.failed;
3448
+ try {
3449
+ this.append({ kind: "attempted", taskId: a.taskId, attemptNumber: a.attemptNumber });
3450
+ } catch (e) {
3451
+ if (silent) continue;
3452
+ this.haltOnce(e);
3453
+ throw e;
3454
+ }
3455
+ }
3456
+ } finally {
3457
+ this.leaveStep();
3458
+ }
3459
+ }
3460
+ /**
3461
+ * **The ONE rule for a throw that reaches a driver call's boundary once the call is ACCEPTED** — `OR-R10` v0.19, P35 (k),
3462
+ * P36 (b) and (c), W184: reported ONCE through `onRunFailure` under its own class, and the run HALTED — its `halted`
3463
+ * element appended after the element thrown on — unless the run is already halted, whose halt was reported when it was
3464
+ * raised. `start()`'s treatment since W3, brought to every call: `resolve`, `resume` and `cancel` at their own element,
3465
+ * and EVERY take — a stop's, a pause's, an unpause's, an answer's — at its own element, at rest or at a step's end, so
3466
+ * the FIRST throw halts and a call still pending behind it is dropped by its own halted guard (the readers' C1). The
3467
+ * caller rethrows: the consumer's error reaches it.
3468
+ * A refusal the call raises BEFORE it is accepted — `RunControlRefused`, `BoundaryRefused`, `ResumeRefused`,
3469
+ * `MultipleRuns`, a `DataCloneError` — never reaches this: nothing was appended, and a rejected command halts nothing.
3470
+ */
3471
+ haltOnce(e) {
3472
+ if (!this.failed) this.failRun(e);
3473
+ }
2649
3474
  /** The registry's node types, normalised once (W59; entries the document or the class form since W65). Read at dispatch for the type's retry default and its settings defaults. */
2650
3475
  nodeTypes;
2651
3476
  /** The registry's languages, normalised once (W61). Read at construction only; the driver evaluates nothing. */
@@ -2654,6 +3479,11 @@ var Driver = class {
2654
3479
  workers;
2655
3480
  /** The gate types — the built-ins and the registry's — normalised once (W93). Read where a branch enters a gate, and at `start()` for an ENTRY gate's `FD-R4` target. */
2656
3481
  gateTypes;
3482
+ /**
3483
+ * **Each gate's RESOLVED route set, by gate id** — ADR 0092 rule 3: `routesOf` asked once per gate at
3484
+ * construction, after validation passed, and read by every decision. Never the type's members.
3485
+ */
3486
+ gateRoutes;
2657
3487
  /**
2658
3488
  * **Each gate's MEMORY: one per gate per RUN, the driver's, and never written down** — `RN-R26`,
2659
3489
  * [ADR 0067](../../doc/decisions/0067-a-gate-runs-in-the-driver-its-memory-is-the-drivers-and-its-decision-is-recorded.md),
@@ -2726,25 +3556,35 @@ var Driver = class {
2726
3556
  ...opts.gateTypes !== void 0 ? { gateTypes: opts.gateTypes } : {}
2727
3557
  };
2728
3558
  const findings2 = validateDefinition(definition, registry);
2729
- if (findings2.length) {
2730
- throw new InvalidDefinition(
2731
- violatedRules(findings2),
2732
- `definition "${definition.flowId}" is invalid: ` + findings2.map((f) => `${f.rule} at ${f.at ?? "<document>"}: ${f.detail}`).join("; ")
2733
- );
3559
+ if (findings2.length) throw InvalidDefinition.fromFindings(definition.flowId, findings2);
3560
+ const gateRoutes = /* @__PURE__ */ new Map();
3561
+ for (const n of definition.nodes) {
3562
+ if (n.kind !== "gate") continue;
3563
+ const resolved = routesOf(this.gateTypes.get(n.type), n.settings ?? {});
3564
+ if ("problem" in resolved) {
3565
+ throw new InvalidDefinition(["FD-V39"], `gate "${n.id}"'s type "${n.type}" resolved no route set at construction, after validation accepted one \u2014 FD-V39: ${resolved.problem}`);
3566
+ }
3567
+ gateRoutes.set(n.id, resolved);
2734
3568
  }
3569
+ this.gateRoutes = gateRoutes;
2735
3570
  this.def = definition;
2736
3571
  this.opts = opts;
2737
3572
  this.now = opts.now ?? (() => Date.now());
2738
3573
  this.graph = buildGraph(definition);
2739
3574
  this.regions = allRegions(this.graph, definition);
3575
+ const kept = itemMaps(definition, this.regions);
3576
+ this.items = { kept, within: itemWithin(kept, this.regions), held: /* @__PURE__ */ new Map(), nested: /* @__PURE__ */ new Map(), ended: /* @__PURE__ */ new Set() };
2740
3577
  if (this.regions.length > 0 && !opts.joinPolicy) {
2741
3578
  throw new MissingJoinPolicy(
2742
3579
  `definition "${definition.flowId}" has ${this.regions.length} fan-out region(s) and no joinPolicy. Without one every branch would arrive and the join would never fire, which is indistinguishable from the case P1 exists to measure.`
2743
3580
  );
2744
3581
  }
2745
- for (const name of SETTLEMENT_EVENTS) {
3582
+ for (const name of SUBSCRIBED_EVENTS) {
2746
3583
  opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
2747
3584
  }
3585
+ for (const [name, callback] of PASS_THROUGH) {
3586
+ if (opts[callback] !== void 0) opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
3587
+ }
2748
3588
  }
2749
3589
  /**
2750
3590
  * Dispatch the entry node for the run this driver was constructed for.
@@ -2771,6 +3611,15 @@ var Driver = class {
2771
3611
  * substitution rather than dropping it.
2772
3612
  */
2773
3613
  start(payload) {
3614
+ this.stepping += 1;
3615
+ try {
3616
+ this.startStep(payload);
3617
+ } finally {
3618
+ this.leaveStep();
3619
+ }
3620
+ }
3621
+ /** `start`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3622
+ startStep(payload) {
2774
3623
  const runId = this.runId;
2775
3624
  if (this.started) {
2776
3625
  throw new MultipleRuns(
@@ -2781,11 +3630,12 @@ var Driver = class {
2781
3630
  for (const { node, value } of boundaryTargets(this.def, payload, (g, v) => this.boundaryEdges(g, v))) {
2782
3631
  const n = this.graph.byId.get(node);
2783
3632
  if (!n || n.kind !== "task") continue;
3633
+ if (n.transform !== void 0) continue;
2784
3634
  const type = this.nodeTypes.get(n.type);
2785
3635
  const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
2786
3636
  const schema = worker?.inputSchema;
2787
3637
  if (schema === void 0) continue;
2788
- const received = this.compose(n, value, /* @__PURE__ */ new Map());
3638
+ const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
2789
3639
  const fs = findings(schema, received);
2790
3640
  if (fs.length) {
2791
3641
  throw new BoundaryRefused(
@@ -2799,12 +3649,23 @@ var Driver = class {
2799
3649
  }
2800
3650
  }
2801
3651
  const entryPayload = structuredClone(payload);
2802
- this.header = { format: CHECKPOINT_FORMAT, runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
3652
+ const header = { format: CHECKPOINT_FORMAT, runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
3653
+ const onHeader = this.opts.onHeader;
3654
+ if (onHeader !== void 0) {
3655
+ this.started = true;
3656
+ try {
3657
+ onHeader(Object.freeze({ ...header, entryPayload: structuredClone(entryPayload) }));
3658
+ } catch (e) {
3659
+ this.started = false;
3660
+ throw e;
3661
+ }
3662
+ }
3663
+ this.header = header;
2803
3664
  this.started = true;
2804
3665
  try {
2805
3666
  this.enter(runId, this.def.entry, payload);
2806
3667
  } catch (e) {
2807
- this.failRun(e);
3668
+ this.haltOnce(e);
2808
3669
  throw e;
2809
3670
  }
2810
3671
  }
@@ -2819,28 +3680,43 @@ var Driver = class {
2819
3680
  * **In order, and each step is a rule:**
2820
3681
  * 1. **Refuse what the header says** (`RN-R23`) — {@link resumeRefusal}.
2821
3682
  * 2. **REPLAY** (`RN-R19`): enter the entry node with a copy of `header.entryPayload` exactly as
2822
- * `start()` enters it, then FEED every stored settlement, resolution and `resumed` element at its
2823
- * own position through the routing the listener and `resolve()` use — every dispatch HELD. Every
3683
+ * `start()` enters it, then FEED every stored settlement, resolution and `resumed` element — and, since
3684
+ * v0.19 and v0.20, every `interrupted`, `paused` and `unpaused` one, and since v0.26 every `attempted` one — at its own position through the routing the listener and `resolve()` use — every dispatch HELD. Every
2824
3685
  * other element the replay REGENERATES.
2825
3686
  * 3. **The PREFIX check** (`RN-R20`): each regenerated element at a position the stored events cover
2826
3687
  * must be the stored one; the first that is not is refused as `diverged`, naming its position, and
2827
3688
  * so is a replay that ends short. A replay that runs PAST the stored events is the stored run's
2828
3689
  * lost tail — a checkpoint taken inside `onFoldInput` ends mid-step (ADR 0064) — and it is
2829
3690
  * DELIVERED, because the consumer does not hold it (`RN-R24`).
2830
- * 4. **A run the replay STOPS is refused** as `stopped` (P23 decision 10), its tail delivered first.
3691
+ * 4. **A run the replay HALTS is refused** as `halted` (P23 decision 10), its tail delivered first.
2831
3692
  * 5. **Append `resumed`** naming the IN-FLIGHT set — dispatched and never settled, in dispatch order
2832
- * (`RN-R21`, `RN-R22`) — and then hand each to the scheduler at the SAME task id.
3693
+ * (`RN-R21`, `RN-R22`) — and then hand each to the scheduler at the SAME task id, with `attemptsSpent` the highest
3694
+ * attempt recorded for it where one was (v0.26, W186); or, where the replay
3695
+ * ends PAUSED (a fed `paused`, no `unpaused` after it), HOLD each again for an unpause (`RN-R35`).
2833
3696
  *
2834
3697
  * **At-least-once, and that is the contract rather than a defect** (P23 decision 6): a task named in
2835
- * `resumed` may have finished in the process that is gone, and runs again here with a FRESH retry
2836
- * budget ([Q365](../../doc/questions/q365.md) — measured: a budget of two attempts ran three times
2837
- * across one kill). A task parked at `user_action` has SETTLED and is not in flight: it stays parked,
3698
+ * `resumed` may have finished in the process that is gone, and runs again here. **Not at a fresh retry budget since
3699
+ * W186** (`RN-R21` v0.26, ADR 0113): it is handed on with `attemptsSpent`, the highest attempt the stored run recorded
3700
+ * for it — the attempt in flight at the kill or the stop counted as spent — so a spent budget runs ONCE more and no more.
3701
+ * *Until W186 it ran at a FRESH budget ([Q365](../../doc/questions/q365.md) — measured: a budget of two attempts ran
3702
+ * three times across one kill).* A task parked at `user_action` has SETTLED and is not in flight: it stays parked,
2838
3703
  * and `resolve()` works on it.
2839
3704
  *
2840
3705
  * @throws MultipleRuns — the driver has already started or resumed a run.
2841
3706
  * @throws ResumeRefused — see {@link ResumeRefusalReason}; the driver is then spent.
3707
+ * @throws the consumer's own error — a callback that threw on the `resumed` element, or on an element the resume's step
3708
+ * appended after it: the run is HALTED and reported once, and nothing further is handed on (P36 (c), W184).
2842
3709
  */
2843
3710
  resume(checkpoint) {
3711
+ this.stepping += 1;
3712
+ try {
3713
+ this.resumeStep(checkpoint);
3714
+ } finally {
3715
+ this.leaveStep();
3716
+ }
3717
+ }
3718
+ /** `resume`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3719
+ resumeStep(checkpoint) {
2844
3720
  if (this.started) {
2845
3721
  throw new MultipleRuns(
2846
3722
  `this driver already started or resumed run "${this.runId}", and resume() is the other door into the SAME run. ID-R3: a Driver serves exactly one run, entered once \u2014 construct a fresh Driver for the checkpoint (runtime.resume does).`
@@ -2869,30 +3745,49 @@ var Driver = class {
2869
3745
  this.replay = null;
2870
3746
  if (this.failed) {
2871
3747
  this.deaf = true;
2872
- const at = this.inputs.findIndex((e) => e.kind === "stopped");
3748
+ const at = this.inputs.findIndex((e) => e.kind === "halted");
2873
3749
  throw new ResumeRefused(
2874
- "stopped",
3750
+ "halted",
2875
3751
  at,
2876
- `ResumeRefused (stopped): replaying run "${this.runId}" regenerated its STOP at position ${at} \u2014 past the ${stored.length} stored element(s), in the tail the store had lost: ${describeElement(this.inputs[at])}. A stopped run is not resumable (P23 decision 10); the tail, \`stopped\` included, has been delivered to onFoldInput so a store can hold it.`
3752
+ `ResumeRefused (halted): replaying run "${this.runId}" regenerated its HALT at position ${at} \u2014 past the ${stored.length} stored element(s), in the tail the store had lost: ${describeElement(this.inputs[at])}. A halted run is not resumable (P23 decision 10); the tail, \`halted\` included, has been delivered to onFoldInput so a store can hold it.`
2877
3753
  );
2878
3754
  }
3755
+ if (this.stopRequest !== null && this.stopRequest.pending === null) this.stopRequest = null;
2879
3756
  const inFlight = this.inFlight();
2880
- this.append({ kind: "resumed", taskIds: inFlight });
2881
- try {
3757
+ const holding = this.paused;
3758
+ for (const id of inFlight) {
3759
+ const config = replay.held.get(id);
3760
+ const spent = this.lastAttempt.get(id) ?? 0;
3761
+ if (config !== void 0 && spent > 0) replay.held.set(id, { ...config, attemptsSpent: spent });
3762
+ }
3763
+ if (holding) {
2882
3764
  for (const id of inFlight) {
2883
3765
  const config = replay.held.get(id);
2884
3766
  if (config === void 0) throw new Error(`resume: in-flight task "${id}" has no held dispatch \u2014 this is a defect in the driver`);
2885
- this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
3767
+ this.pauseHeld.set(id, config);
3768
+ }
3769
+ } else {
3770
+ for (const id of inFlight) this.unhanded.add(id);
3771
+ }
3772
+ try {
3773
+ this.append({ kind: "resumed", taskIds: inFlight });
3774
+ for (const id of holding ? [] : inFlight) {
3775
+ if (this.holdsResume()) break;
3776
+ const config = replay.held.get(id);
3777
+ if (config === void 0) throw new Error(`resume: in-flight task "${id}" has no held dispatch \u2014 this is a defect in the driver`);
3778
+ this.handOn(id, config);
2886
3779
  }
2887
3780
  } catch (e) {
2888
- this.failRun(e);
3781
+ this.haltOnce(e);
2889
3782
  throw e;
3783
+ } finally {
3784
+ this.unhanded.clear();
2890
3785
  }
2891
3786
  }
2892
3787
  /**
2893
3788
  * **The replay** — `RN-R19` and `RN-R20`. Every fed element goes through {@link Driver.feed}, which
2894
- * turns a refusal routing raises into a `stopped` element exactly as the listener's boundary does
2895
- * (`OR-R10`), so the replay regenerates a stop where the stored run had one.
3789
+ * turns a refusal routing raises into a `halted` element exactly as the listener's boundary does
3790
+ * (`OR-R10`), so the replay regenerates a halt where the stored run had one.
2896
3791
  */
2897
3792
  replayStored(stored) {
2898
3793
  const replay = this.replay;
@@ -2906,16 +3801,35 @@ var Driver = class {
2906
3801
  }
2907
3802
  if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
2908
3803
  }
2909
- /** One fed element — a settlement, a resolution, or an earlier resume — at stored position `i`. */
3804
+ /** One fed element — a settlement, a resolution, an earlier resume, a stop, a pause, an unpause or an attempt — at stored position `i`. */
2910
3805
  feedOne(e, i) {
2911
3806
  if (e.kind === "resumed") {
2912
3807
  const now = this.inFlight();
2913
3808
  if (!Array.isArray(e.taskIds) || e.taskIds.length !== now.length || e.taskIds.some((t, k) => t !== now[k])) {
2914
3809
  throw this.diverged(i, `in flight in the replay here: ${JSON.stringify(now)}`);
2915
3810
  }
3811
+ this.stopRequest = null;
2916
3812
  this.append({ kind: "resumed", taskIds: [...e.taskIds] });
2917
3813
  return;
2918
3814
  }
3815
+ if (e.kind === "interrupted") {
3816
+ this.feedInterrupted(e);
3817
+ return;
3818
+ }
3819
+ if (e.kind === "paused" || e.kind === "unpaused") {
3820
+ this.paused = e.kind === "paused";
3821
+ this.append({ kind: e.kind, actor: e.actor });
3822
+ return;
3823
+ }
3824
+ if (e.kind === "attempted") {
3825
+ const n = e.attemptNumber;
3826
+ if (typeof e.taskId !== "string" || !this.dispatched.has(e.taskId)) throw this.diverged(i, "the replay never dispatched that task");
3827
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw this.diverged(i, "its attemptNumber is not a positive integer");
3828
+ if (n <= (this.lastAttempt.get(e.taskId) ?? 0)) throw this.diverged(i, "its attemptNumber does not exceed the one stored before it for that task");
3829
+ this.lastAttempt.set(e.taskId, n);
3830
+ this.append({ kind: "attempted", taskId: e.taskId, attemptNumber: n });
3831
+ return;
3832
+ }
2919
3833
  if (e.kind !== "outcome" && e.kind !== "resolution") return;
2920
3834
  const record = e.record;
2921
3835
  let parts;
@@ -2932,7 +3846,7 @@ var Driver = class {
2932
3846
  this.settle(cloneRecord(record), runId, nodeId, branchPath);
2933
3847
  return;
2934
3848
  }
2935
- if (!isSuspended(this.log, record.taskId)) throw this.diverged(i, "that task is not parked in the replay");
3849
+ if (!this.parked(record.taskId)) throw this.diverged(i, "that task is not parked in the replay");
2936
3850
  this.appendResolution(cloneRecord(record), nodeId, branchPath);
2937
3851
  this.advance(runId, nodeId, branchPath);
2938
3852
  }
@@ -2979,14 +3893,43 @@ var Driver = class {
2979
3893
  * second resolution is refused like any other. A rejected command, not a run
2980
3894
  * failure: it came from outside and a caller can retry.
2981
3895
  *
3896
+ * **And an ASK's answer** — `RS-R1` v0.18, `RN-R37`, ADR 0109: an ask the run reached and nobody has answered is
3897
+ * suspended too, so the person's answer is this call — `'done'` with the answer as `result`, held to the ask's
3898
+ * `answerSchema` (`FD-R4`), or `'failed'` with an `error`, the person DECLINING, held to nothing. The answer IS the
3899
+ * node's result and routes as a task's settlement does.
3900
+ *
2982
3901
  * @returns `false` when refused. Nothing is appended and the run is unaffected.
2983
3902
  */
2984
3903
  resolve(taskId, resolvedAs, value) {
2985
- if (!isSuspended(this.log, taskId)) return false;
3904
+ const midStep = this.stepping > 0;
3905
+ this.stepping += 1;
3906
+ try {
3907
+ return this.resolveStep(taskId, resolvedAs, value, midStep);
3908
+ } finally {
3909
+ this.leaveStep();
3910
+ }
3911
+ }
3912
+ /** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3913
+ resolveStep(taskId, resolvedAs, value, midStep = false) {
3914
+ if (!this.parked(taskId)) return false;
3915
+ if (this.pausePending.some((c) => c.kind === "answer" && c.record.taskId === taskId)) return false;
2986
3916
  const { runId, nodeId, branchPath } = parseTaskId(taskId);
2987
- if (this.failed) return false;
3917
+ if (this.shut() || this.stopRequest !== null) return false;
2988
3918
  if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
2989
3919
  const n = this.graph.byId.get(nodeId);
3920
+ if (n?.kind === "ask") {
3921
+ const schema2 = n.answerSchema;
3922
+ const fs = schema2 === void 0 ? [] : findings(schema2, value.result ?? null);
3923
+ if (fs.length) {
3924
+ throw new BoundaryRefused(
3925
+ "resolve",
3926
+ nodeId,
3927
+ taskId,
3928
+ fs,
3929
+ `the answer to "${taskId}" (ask "${nodeId}") does not satisfy the ask's answerSchema \u2014 ${describeFindings(fs)}. FD-R4: a person\`s answer to an ask is held to the schema the ask declares; nothing was appended and the ask is still parked, so answer it again with a conforming value. \`validateBoundaries: false\` turns the check off.`
3930
+ );
3931
+ }
3932
+ }
2990
3933
  const type = n?.kind === "task" ? this.nodeTypes.get(n.type) : void 0;
2991
3934
  const schema = type?.worker === void 0 ? void 0 : this.workers.get(type.worker)?.outputSchema;
2992
3935
  if (schema !== void 0) {
@@ -3002,7 +3945,7 @@ var Driver = class {
3002
3945
  }
3003
3946
  }
3004
3947
  }
3005
- this.appendResolution({
3948
+ const record = {
3006
3949
  kind: "resolution",
3007
3950
  taskId,
3008
3951
  resolvedAs,
@@ -3010,11 +3953,22 @@ var Driver = class {
3010
3953
  error: resolvedAs === "failed" ? value.error ?? "resolved as failed" : null,
3011
3954
  actor: value.actor,
3012
3955
  observedAt: this.now()
3013
- }, nodeId, branchPath);
3956
+ };
3957
+ if (midStep) {
3958
+ this.pausePending.push({ kind: "answer", record, runId, nodeId, branchPath });
3959
+ return true;
3960
+ }
3961
+ try {
3962
+ this.appendResolution(record, nodeId, branchPath);
3963
+ } catch (e) {
3964
+ this.haltOnce(e);
3965
+ throw e;
3966
+ }
3967
+ if (this.shut()) return true;
3014
3968
  try {
3015
3969
  this.advance(runId, nodeId, branchPath);
3016
3970
  } catch (e) {
3017
- this.failRun(e);
3971
+ this.haltOnce(e);
3018
3972
  throw e;
3019
3973
  }
3020
3974
  return true;
@@ -3027,6 +3981,7 @@ var Driver = class {
3027
3981
  */
3028
3982
  appendResolution(record, nodeId, branchPath) {
3029
3983
  this.append({ kind: "resolution", record });
3984
+ if (this.shut()) return;
3030
3985
  const region = this.regionOf(nodeId);
3031
3986
  if (region) {
3032
3987
  this.emit({
@@ -3039,16 +3994,556 @@ var Driver = class {
3039
3994
  });
3040
3995
  }
3041
3996
  }
3997
+ /**
3998
+ * **Cancel the run: hold it, record the request, and ask the scheduler to cancel each task in flight** — `RN-R31`,
3999
+ * `spec/run-state.md` section 13, [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md),
4000
+ * P34 decision 4 and stop-and-asks (b), (c), (i), (j). **Resolves once every task it cancelled has stopped**; the fold
4001
+ * reads the run `cancelling` until then and `cancelled` after (`RN-R30`), so a runtime's `done` settles after the last
4002
+ * `task.cancelled` from the fold alone.
4003
+ *
4004
+ * **In order, and each step is a rule:**
4005
+ * 1. **Refuse** — {@link RunControlRefused}, before anything is recorded or sent; and a `graceMs` the scheduler
4006
+ * would throw on is thrown here, as it would be (`TypeError`, `RangeError`).
4007
+ * 2. **Hold** (`RN-R29`): `cancelRequest` is set, so `shut()` is true for every door; every region's collection is
4008
+ * released, `failRun`'s run-level moment (ADR 0046 rule 3, `JP-R12`) — no join of this run settles again.
4009
+ * 3. **Record** ONE `cancelRequested` element, BEFORE the scheduler is asked for anything.
4010
+ * 4. **Cancel** each task in flight the scheduler holds — `RN-R21`'s set, so a PARKED task, settled already, is sent
4011
+ * nothing (P34 (i); the scheduler cannot end it, fact 8). A task not yet handed on is cancelled by `handOn` after
4012
+ * its hand-off (`RN-R29` (i)), and so is each task a PAUSE held (`RN-R35`, P34 (c)). Tasks a worker SPAWNED are
4013
+ * not this run's (P34 (j)). **In a `finally` around step 3** (`RN-R31` v0.21, P35 decision 4): a consumer's
4014
+ * `onFoldInput` that throws on the element still has every cancel sent and every held task handed on and
4015
+ * cancelled — and THEN the run is HALTED and reported once, and the throw reaches the caller (`RN-R31` v0.24,
4016
+ * `OR-R10` v0.19, P36 (b), W184).
4017
+ *
4018
+ * **A SECOND call** appends nothing and forwards its `graceMs` to each task still stopping — the scheduler's own
4019
+ * escalation — and resolves once those have stopped. It is never refused for the halt its own first call made.
4020
+ *
4021
+ * @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
4022
+ * @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
4023
+ */
4024
+ cancel(options) {
4025
+ const cancelOf = this.opts.scheduler.cancel;
4026
+ if (typeof cancelOf !== "function") {
4027
+ throw new RunControlRefused(
4028
+ "cancel",
4029
+ "unsupported",
4030
+ `RunControlRefused (unsupported): run "${this.runId}"'s scheduler has no cancel(taskId, options) \u2014 SchedulerLike's optional third member (P34 (b), ADR 0104). A FyflowScheduler has one, and a runtime\`s per-run view forwards it; a scheduler double must declare it to cancel a run. Nothing was recorded and nothing was sent.`
4031
+ );
4032
+ }
4033
+ const graceMs = options?.graceMs;
4034
+ if (graceMs !== void 0) {
4035
+ if (typeof graceMs !== "number") throw new TypeError(`cancel: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
4036
+ if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`cancel: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
4037
+ }
4038
+ if (this.header === null) {
4039
+ throw new RunControlRefused(
4040
+ "cancel",
4041
+ "not-started",
4042
+ `RunControlRefused (not-started): driver for run "${this.runId}" holds no run to cancel \u2014 it has not started, or its resume was refused and it is spent (RN-R23). Nothing was recorded and nothing was sent.`
4043
+ );
4044
+ }
4045
+ const req = this.cancelRequest;
4046
+ if (req !== null) {
4047
+ if (graceMs !== void 0) req.graceMs = graceMs;
4048
+ for (const id of this.inFlight()) if (this.handedOn(id)) this.cancelTask(id);
4049
+ const stopping2 = req.stopping;
4050
+ return Promise.resolve().then(() => Promise.all([...stopping2.values()])).then(() => void 0);
4051
+ }
4052
+ if (this.failed) {
4053
+ throw new RunControlRefused(
4054
+ "cancel",
4055
+ "halted",
4056
+ `RunControlRefused (halted): run "${this.runId}" was HALTED by a refusal (OR-R10) \u2014 it routes nothing again, and its outcome is halted. Nothing was recorded and nothing was sent.`
4057
+ );
4058
+ }
4059
+ if (this.cancelled) {
4060
+ throw new RunControlRefused(
4061
+ "cancel",
4062
+ "cancelled",
4063
+ `RunControlRefused (cancelled): run "${this.runId}" was already ended CANCELLED by a task the scheduler cancelled on its own (RN-R28). Cancel a task it still runs by its id. Nothing was recorded and nothing was sent.`
4064
+ );
4065
+ }
4066
+ if (this.stopRequest !== null) {
4067
+ throw new RunControlRefused(
4068
+ "cancel",
4069
+ "interrupted",
4070
+ `RunControlRefused (interrupted): run "${this.runId}" was STOPPED by its owner (RN-R33) \u2014 this driver is deaf to it, and its in-flight tasks are a resume\`s to run again. Resume its checkpoint on another runtime and cancel it there. Nothing was recorded and nothing was sent.`
4071
+ );
4072
+ }
4073
+ const inFlight = this.inFlight();
4074
+ if (this.stepping === 0 && inFlight.length === 0 && !this.hasParked()) {
4075
+ throw new RunControlRefused(
4076
+ "cancel",
4077
+ "ended",
4078
+ `RunControlRefused (ended): run "${this.runId}" has nothing in flight and nothing parked on a person \u2014 it has finished, and a cancel would rewrite its outcome. Nothing was recorded and nothing was sent.`
4079
+ );
4080
+ }
4081
+ const reason = options.reason;
4082
+ const stopping = /* @__PURE__ */ new Map();
4083
+ this.cancelRequest = {
4084
+ ...reason !== void 0 ? { reason } : {},
4085
+ ...graceMs !== void 0 ? { graceMs } : {},
4086
+ stopping
4087
+ };
4088
+ this.arrivals.clear();
4089
+ this.releaseAllItems();
4090
+ const released = new Map(this.pauseHeld);
4091
+ this.pauseHeld.clear();
4092
+ for (const id of released.keys()) this.unhanded.add(id);
4093
+ try {
4094
+ try {
4095
+ this.append({ kind: "cancelRequested", actor: options.actor, reason: reason ?? null });
4096
+ } finally {
4097
+ try {
4098
+ for (const id of this.inFlight()) {
4099
+ const config = released.get(id);
4100
+ if (config !== void 0) this.handOn(id, config);
4101
+ else if (this.handedOn(id)) this.cancelTask(id);
4102
+ }
4103
+ } finally {
4104
+ for (const id of released.keys()) this.unhanded.delete(id);
4105
+ }
4106
+ }
4107
+ } catch (e) {
4108
+ this.haltOnce(e);
4109
+ throw e;
4110
+ }
4111
+ return Promise.resolve().then(() => Promise.all([...stopping.values()])).then(() => void 0);
4112
+ }
4113
+ /**
4114
+ * **Stop the run so that a resume ELSEWHERE can continue it: hold it, record the stop, go DEAF, and only then ask the
4115
+ * scheduler to cancel each task in flight** — `RN-R33`, `spec/run-state.md` section 14,
4116
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md), P34
4117
+ * decisions 3 and 9 and stop-and-ask (d). **Resolves once every task it cancelled has stopped**; the fold reads the run
4118
+ * `interrupted` from the element on (`RN-R32`), and the log ends with the cancelled tasks dispatched and UNSETTLED —
4119
+ * the in-flight set a resume on another runtime re-dispatches at the same ids (`RN-R21`).
4120
+ *
4121
+ * **In order, and the ORDER is the rule** (P34 decision 3):
4122
+ * 1. **Refuse** — {@link RunControlRefused} with `control: 'stop'`, before anything is recorded or sent: `unsupported`,
4123
+ * `not-started`, `halted`, `cancelled` (`RN-R28`'s, or a cancel its owner requested — P34 (c)) and `ended` (at
4124
+ * rest only, as for a cancel); and a `graceMs` the scheduler would throw on is thrown here. From the accepted call
4125
+ * on, `resolve()` and `cancel()` are refused (P34 decision 9, (c)).
4126
+ * 2. **Hold** (`RN-R29`): the stop is TAKEN, so `shut()` is true for every door, and every region's collection is
4127
+ * released.
4128
+ * 3. **Go DEAF**: nothing the scheduler fires for this run is heard again — from BEFORE the element, so not even a
4129
+ * settlement a callback on it provokes — and no `task.cancelled` of the stop is appended.
4130
+ * 4. **Record** ONE `interrupted` element.
4131
+ * 5. **Cancel** each task in flight the scheduler holds — none for a parked task (P34 fact 4), and none for a task a
4132
+ * PAUSE held, which the scheduler was never handed and which stays in flight for the resume (`RN-R35`).
4133
+ *
4134
+ * **Steps 2-5 are taken at REST** — at once when the call is made at rest, and when the driver's own step ends when it
4135
+ * is made from a callback on that step (`stepping`). A hold taken mid-step would drop the rest of the step's routing
4136
+ * — a fan-out's later branches, a settlement's successor — and a resume re-dispatches only what is IN FLIGHT, so that
4137
+ * work would be lost to the resumed run for good. Taken at rest, the step is whole in the log; the task whose own
4138
+ * `dispatched` element carried the call is handed on in the step and cancelled here (`RN-R29` (i)), and so is a resumed
4139
+ * run's in-flight set, handed back before `resume()` returns.
4140
+ *
4141
+ * **A SECOND call** appends nothing, forwards its `graceMs` to each task still stopping, and resolves as the first does.
4142
+ *
4143
+ * @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
4144
+ * @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
4145
+ */
4146
+ stop(options) {
4147
+ if (typeof this.opts.scheduler.cancel !== "function") {
4148
+ throw new RunControlRefused(
4149
+ "stop",
4150
+ "unsupported",
4151
+ `RunControlRefused (unsupported): run "${this.runId}"'s scheduler has no cancel(taskId, options) \u2014 SchedulerLike's optional third member (P34 (b), ADR 0105). A stop cancels each task in flight, so a scheduler double must declare it to stop a run. Nothing was recorded and nothing was sent.`
4152
+ );
4153
+ }
4154
+ const graceMs = options?.graceMs;
4155
+ if (graceMs !== void 0) {
4156
+ if (typeof graceMs !== "number") throw new TypeError(`stop: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
4157
+ if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`stop: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
4158
+ }
4159
+ if (this.header === null) {
4160
+ throw new RunControlRefused(
4161
+ "stop",
4162
+ "not-started",
4163
+ `RunControlRefused (not-started): driver for run "${this.runId}" holds no run to stop \u2014 it has not started, or its resume was refused and it is spent (RN-R23). Nothing was recorded and nothing was sent.`
4164
+ );
4165
+ }
4166
+ const req = this.stopRequest;
4167
+ if (req !== null) {
4168
+ if (graceMs !== void 0) req.graceMs = graceMs;
4169
+ if (req.pending === null) {
4170
+ for (const id of this.inFlight()) if (this.handedOn(id) && req.stopping.has(id)) this.cancelTask(id);
4171
+ return this.stopSettled(req);
4172
+ }
4173
+ return req.took.then(() => this.stopSettled(req));
4174
+ }
4175
+ if (this.failed) {
4176
+ throw new RunControlRefused(
4177
+ "stop",
4178
+ "halted",
4179
+ `RunControlRefused (halted): run "${this.runId}" was HALTED by a refusal (OR-R10) \u2014 a halted run is not resumable (RN-R23), so there is nothing to stop for a resume elsewhere. Nothing was recorded and nothing was sent.`
4180
+ );
4181
+ }
4182
+ if (this.cancelled || this.cancelRequest !== null) {
4183
+ throw new RunControlRefused(
4184
+ "stop",
4185
+ "cancelled",
4186
+ `RunControlRefused (cancelled): run "${this.runId}" was CANCELLED${this.cancelRequest !== null ? " by its owner (RN-R31)" : " by a task the scheduler cancelled on its own (RN-R28)"} \u2014 a cancelled run is not resumable, so it cannot be stopped to resume it elsewhere (P34 (c)). Nothing was recorded and nothing was sent.`
4187
+ );
4188
+ }
4189
+ if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
4190
+ throw new RunControlRefused(
4191
+ "stop",
4192
+ "ended",
4193
+ `RunControlRefused (ended): run "${this.runId}" has nothing in flight and nothing parked on a person \u2014 it has finished, and there is nothing to resume elsewhere. Nothing was recorded and nothing was sent.`
4194
+ );
4195
+ }
4196
+ const reason = options.reason;
4197
+ let taken;
4198
+ const took = new Promise((r) => {
4199
+ taken = r;
4200
+ });
4201
+ const accepted = {
4202
+ actor: options.actor,
4203
+ ...reason !== void 0 ? { reason } : {},
4204
+ ...graceMs !== void 0 ? { graceMs } : {},
4205
+ stopping: /* @__PURE__ */ new Map(),
4206
+ pending: taken,
4207
+ took
4208
+ };
4209
+ this.stopRequest = accepted;
4210
+ if (this.stepping === 0) this.takeStop();
4211
+ return took.then(() => this.stopSettled(accepted));
4212
+ }
4213
+ /**
4214
+ * **The stop, TAKEN** — `RN-R33` steps 2-5, at rest: from `stop()` itself, or from the end of the driver's own step
4215
+ * when `stop()` was called on it (each step's wrapper calls this when `stepping` returns to `0`).
4216
+ */
4217
+ takeStop() {
4218
+ const req = this.stopRequest;
4219
+ const taken = req.pending;
4220
+ req.pending = null;
4221
+ req.took = null;
4222
+ this.arrivals.clear();
4223
+ this.releaseAllItems();
4224
+ this.deaf = true;
4225
+ const inFlight = this.inFlight().filter((id) => this.handedOn(id));
4226
+ const silent = this.failed;
4227
+ try {
4228
+ try {
4229
+ this.append({ kind: "interrupted", actor: req.actor, reason: req.reason ?? null });
4230
+ } finally {
4231
+ try {
4232
+ for (const id of inFlight) this.cancelTask(id);
4233
+ } finally {
4234
+ taken();
4235
+ }
4236
+ }
4237
+ } catch (e) {
4238
+ if (silent) return;
4239
+ this.haltOnce(e);
4240
+ throw e;
4241
+ }
4242
+ }
4243
+ /** What a stop's promise resolves on once it is taken: a microtask on, every task it sent a cancel to has stopped. */
4244
+ stopSettled(req) {
4245
+ return Promise.resolve().then(() => Promise.all([...req.stopping.values()])).then(() => void 0);
4246
+ }
4247
+ /**
4248
+ * **Pause the run: it routes on, and every dispatch it makes from here is appended and HELD until `unpause()`** —
4249
+ * `RN-R35`, `spec/run-state.md` section 15,
4250
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md),
4251
+ * P34 stop-and-ask (e). Tasks already handed to the scheduler run on and settle, and their settlements route as ever; a
4252
+ * person's `resolve()` is accepted. **Not a halt** (`shut()` is untouched): only the scheduler's `addTask` waits. The
4253
+ * fold reads `RunState.paused` and no new outcome — a held task is `dispatched`, so it reads `running` (P34 fact 3).
4254
+ *
4255
+ * **In order**: (1) **refused by name** before anything is recorded — {@link RunControlRefused} with `control: 'pause'`:
4256
+ * `not-started`, `halted`, `cancelled` (`RN-R28`'s or a requested cancel), `interrupted` (a stop, taken or pending),
4257
+ * `paused` (already paused, or a pause called earlier in this step still to be taken — P34 (e): *a second pause is
4258
+ * refused by name*) and `ended` (nothing in flight and nothing parked, at rest only); (2) **TAKEN AT REST** — at once
4259
+ * when called at rest, and when the driver's own step ends when called from a callback on it: the run is paused and ONE
4260
+ * `paused` element is appended, BETWEEN two steps, where a resume's replay can feed it (`RN-R19`). A pause taken
4261
+ * mid-step would land between two elements of one step, and the replay — which regenerates a step whole — would refuse
4262
+ * its checkpoint `diverged` there; so the step a pause is called on finishes unpaused, its dispatches handed on.
4263
+ */
4264
+ pause(options) {
4265
+ this.refuseRunHeld("pause");
4266
+ if (this.pauseWanted()) {
4267
+ throw new RunControlRefused(
4268
+ "pause",
4269
+ "paused",
4270
+ `RunControlRefused (paused): run "${this.runId}" is already paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 by a call earlier in this step, taken when it ends" : ""} (RN-R35, P34 (e)). Unpause it first. Nothing was recorded.`
4271
+ );
4272
+ }
4273
+ if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
4274
+ throw new RunControlRefused(
4275
+ "pause",
4276
+ "ended",
4277
+ `RunControlRefused (ended): run "${this.runId}" has nothing in flight and nothing parked on a person \u2014 it has finished, and there is nothing left to hold. Nothing was recorded.`
4278
+ );
4279
+ }
4280
+ this.pausePending.push({ kind: "paused", actor: options.actor });
4281
+ while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
4282
+ }
4283
+ /**
4284
+ * **Unpause the run: record it, then hand every held task to the scheduler in DISPATCH ORDER** — `RN-R35`, ADR 0106,
4285
+ * P34 (e). Refused by name — `control: 'unpause'`: `not-started`, `halted`, `cancelled`, `interrupted` (a stopped
4286
+ * run's held tasks are a resume's: unpause it where it resumes) and `not-paused`. **Never `ended`**: an unpause rewrites
4287
+ * no outcome, so a paused run that finished may still be unpaused. Taken at rest, as a pause is: ONE `unpaused` element,
4288
+ * then the hand-off — each held task's `createTask` and `addTask`, first held first.
4289
+ */
4290
+ unpause(options) {
4291
+ this.refuseRunHeld("unpause");
4292
+ if (!this.pauseWanted()) {
4293
+ throw new RunControlRefused(
4294
+ "unpause",
4295
+ "not-paused",
4296
+ `RunControlRefused (not-paused): run "${this.runId}" is not paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 an unpause earlier in this step already lifts it" : ""} (RN-R35, P34 (e)). Nothing was recorded and nothing was handed on.`
4297
+ );
4298
+ }
4299
+ this.pausePending.push({ kind: "unpaused", actor: options.actor });
4300
+ while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
4301
+ }
4302
+ /** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
4303
+ pauseWanted() {
4304
+ const last = this.pausePending.filter((c) => c.kind !== "answer").at(-1);
4305
+ return last !== void 0 ? last.kind === "paused" : this.paused;
4306
+ }
4307
+ /**
4308
+ * The refusals a pause and an unpause share, in `RN-R35`'s order — each a run that is not this driver's to hold or
4309
+ * release: none started, or the run halted, cancelled or stopped. Thrown before anything is recorded.
4310
+ */
4311
+ refuseRunHeld(control) {
4312
+ if (this.header === null) {
4313
+ throw new RunControlRefused(
4314
+ control,
4315
+ "not-started",
4316
+ `RunControlRefused (not-started): driver for run "${this.runId}" holds no run to ${control} \u2014 it has not started, or its resume was refused and it is spent (RN-R23). Nothing was recorded.`
4317
+ );
4318
+ }
4319
+ if (this.failed) {
4320
+ throw new RunControlRefused(
4321
+ control,
4322
+ "halted",
4323
+ `RunControlRefused (halted): run "${this.runId}" was HALTED by a refusal (OR-R10) \u2014 it dispatches nothing again, so there is nothing to ${control === "pause" ? "hold" : "hand on"}. Nothing was recorded.`
4324
+ );
4325
+ }
4326
+ if (this.cancelled || this.cancelRequest !== null) {
4327
+ throw new RunControlRefused(
4328
+ control,
4329
+ "cancelled",
4330
+ `RunControlRefused (cancelled): run "${this.runId}" was CANCELLED${this.cancelRequest !== null ? " by its owner (RN-R31)" : " by a task the scheduler cancelled on its own (RN-R28)"} \u2014 it dispatches nothing again, so there is nothing to ${control === "pause" ? "hold" : "hand on"}. Nothing was recorded.`
4331
+ );
4332
+ }
4333
+ if (this.stopRequest !== null) {
4334
+ throw new RunControlRefused(
4335
+ control,
4336
+ "interrupted",
4337
+ `RunControlRefused (interrupted): run "${this.runId}" was STOPPED by its owner (RN-R33) \u2014 its in-flight tasks, held ones included, are a resume\`s to run. Resume its checkpoint on another runtime and ${control} it there. Nothing was recorded.`
4338
+ );
4339
+ }
4340
+ }
4341
+ /**
4342
+ * **A pause or an unpause, TAKEN** — `RN-R35`, at rest: from the call itself, or from the end of the driver's own step
4343
+ * (`leaveStep()`). **A step of its own** (`stepping`), so a call a consumer makes from a callback on the element — a
4344
+ * second pause, a stop, a `resolve()` that dispatches — is taken, or routes, as on any step. **A pause** sets the flag
4345
+ * BEFORE its element, so a dispatch a callback on the element causes is held. **An unpause** clears it before its
4346
+ * element and hands the held tasks on AFTER it, in a `finally`: a consumer's `onFoldInput` that throws on the element
4347
+ * still has the element in the log, and the run it says is unpaused is (W174's conformity reader's finding 5, the stop's
4348
+ * lesson) — and then the take HALTS the run itself, reported once (`takePause`, `RN-R35` v0.24, W184), so a call pending
4349
+ * behind it is dropped, and the throw reaches whoever is on the stack: the caller of the call at rest or of the driver
4350
+ * call mid-step, or, on a SCHEDULER EVENT's step, the listener boundary (`onEvent`, P35 (j)). A dispatch a callback on the element causes joins the end
4351
+ * of the held queue (`dispatch`), so the order is the dispatch order still.
4352
+ */
4353
+ takePending(c) {
4354
+ if (c.kind === "answer") this.takeAnswer(c);
4355
+ else this.takePause(c);
4356
+ }
4357
+ /**
4358
+ * **A resolution given mid-step, TAKEN at rest** — `RN-R37`, W180 (the conformity reader's R4; S-4 for every
4359
+ * resolution): a step of its own, so the resolution lands BETWEEN two steps — where a resume's replay FEEDS it
4360
+ * (`RN-R19`) — after the step that parked its node has completed (an ask's `asked` or a task's `user_action`
4361
+ * settlement, and in a region `branch.suspended`). Answered on its own `asked` element and appended
4362
+ * there instead, it sat between two elements of one step, and the replay — which regenerates a step whole — refused the
4363
+ * run's checkpoint `diverged` at it. **Dropped, recording nothing, when the step it was given on HALTED or CANCELLED the
4364
+ * run** (or the node is no longer parked) — `takePause`'s rule: it would be refused were it given now. It routes as
4365
+ * `resolve()` at rest routes: a throw — from routing, or a consumer's on the resolution's own element — HALTS the run
4366
+ * here, reported once, so a call pending behind it is dropped; it then reaches whoever is on the stack: a driver call's
4367
+ * caller, or the listener boundary of a scheduler event's step (`onEvent`, `OR-R10` v0.19, W184).
4368
+ */
4369
+ takeAnswer(c) {
4370
+ if (this.failed || this.cancelled || this.cancelRequest !== null || !this.parked(c.record.taskId)) return;
4371
+ this.stepping += 1;
4372
+ try {
4373
+ try {
4374
+ this.appendResolution(c.record, c.nodeId, c.branchPath);
4375
+ if (this.shut()) return;
4376
+ this.advance(c.runId, c.nodeId, c.branchPath);
4377
+ } catch (e) {
4378
+ this.haltOnce(e);
4379
+ throw e;
4380
+ }
4381
+ } finally {
4382
+ this.leaveStep();
4383
+ }
4384
+ }
4385
+ takePause(p) {
4386
+ if (this.failed || this.cancelled || this.cancelRequest !== null) return;
4387
+ this.stepping += 1;
4388
+ try {
4389
+ try {
4390
+ if (p.kind === "paused") {
4391
+ this.paused = true;
4392
+ this.append({ kind: "paused", actor: p.actor });
4393
+ } else {
4394
+ this.paused = false;
4395
+ try {
4396
+ this.append({ kind: "unpaused", actor: p.actor });
4397
+ } finally {
4398
+ this.drainHeld();
4399
+ }
4400
+ }
4401
+ } catch (e) {
4402
+ this.haltOnce(e);
4403
+ throw e;
4404
+ }
4405
+ } finally {
4406
+ this.leaveStep();
4407
+ }
4408
+ }
4409
+ /**
4410
+ * **The unpause's hand-off**: each held task to the scheduler, first held first (`RN-R35`). Taken out of the hold and
4411
+ * marked `unhanded` before its `addTask`, as a resume's re-dispatch is (`RN-R31`), so a cancel raised inside one
4412
+ * hand-off sends nothing for a task not yet handed — and `handOn` then cancels it. **Held for a HALT** — checked before
4413
+ * each, as a resume's re-dispatch is (`RN-R29` (4), `holdsResume()`): a halt raised inside one hand-off hands nothing
4414
+ * further on, and what is left is never handed. A dispatch made on this stack joins the end of the queue and is handed
4415
+ * on in its turn (the `Map` visits what is added while it is walked). Caller-initiated, so a throw from the scheduler
4416
+ * is reported AND rethrown, `start()`'s and `resume()`'s treatment.
4417
+ */
4418
+ drainHeld() {
4419
+ try {
4420
+ for (const [id, config] of this.pauseHeld) {
4421
+ if (this.holdsResume()) break;
4422
+ this.pauseHeld.delete(id);
4423
+ this.unhanded.add(id);
4424
+ this.handOn(id, config);
4425
+ }
4426
+ } catch (e) {
4427
+ this.failRun(e);
4428
+ throw e;
4429
+ }
4430
+ }
4431
+ /**
4432
+ * Whether the scheduler holds this task — dispatched, not held by a replay, past its `addTask` (`RN-R31`), and not held
4433
+ * by a pause (`RN-R35`, W175): a stop of a paused run cancels only what this says the scheduler holds (ADR 0105's last
4434
+ * consequence), and a cancel hands each held task on before it cancels it.
4435
+ */
4436
+ handedOn(taskId) {
4437
+ return this.replay === null && !this.unhanded.has(taskId) && !this.pauseHeld.has(taskId);
4438
+ }
3042
4439
  // --- the scheduler's side ----------------------------------------------------
3043
4440
  onEvent(name, detail) {
4441
+ this.stepping += 1;
4442
+ try {
4443
+ this.onEventStep(name, detail);
4444
+ } finally {
4445
+ try {
4446
+ this.leaveStep();
4447
+ } catch (e) {
4448
+ try {
4449
+ if (!this.failed) this.failRun(e);
4450
+ } catch {
4451
+ }
4452
+ }
4453
+ }
4454
+ }
4455
+ /** `onEvent`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
4456
+ onEventStep(name, detail) {
3044
4457
  if (this.deaf || this.started && this.header === null) return;
3045
4458
  try {
3046
4459
  this.onEventOrThrow(name, detail);
3047
4460
  } catch (e) {
3048
- this.failRun(e);
4461
+ try {
4462
+ this.haltOnce(e);
4463
+ } catch {
4464
+ }
4465
+ }
4466
+ }
4467
+ /**
4468
+ * **A `task.running`: an attempt started** — `RN-R40`, W186, P36 (f). Its id and number are COPIED here, while the
4469
+ * listener runs (`OR-R1`). Recorded only for a task THIS run dispatched, and only when the number EXCEEDS the last one
4470
+ * recorded for it — a requeue repeats its number and appends nothing (the scheduler's own recipe's `max`). **Everything
4471
+ * else is dropped in SILENCE, and none of it is a refusal**: an id that does not parse (a plain task on a shared
4472
+ * scheduler — its SETTLEMENT is what `onUnmintedTask` reports, `ID-R5`), another run's (`ID-R4` tier 1), one this run
4473
+ * never dispatched (its settlement is `ID-R4` tier 2's `UndispatchedTask`), and a detail carrying no positive integer
4474
+ * `attemptNumber` (a scheduler older than the pin, or a double). An attempt start never stops a run by existing. **Queued, not
4475
+ * appended**: {@link Driver.takeAttempts} appends it when the step it was heard on ends — at once, on a scheduler
4476
+ * event's own step; at the step's end when an inline pool fires it inside the driver's `addTask`.
4477
+ */
4478
+ heardRunning(detail) {
4479
+ const taskId = detail?.id;
4480
+ const attemptNumber = attemptNumberOf(detail);
4481
+ if (typeof taskId !== "string" || attemptNumber === null) return;
4482
+ let runId;
4483
+ try {
4484
+ runId = parseTaskId(taskId).runId;
4485
+ } catch (e) {
4486
+ if (e instanceof InvalidTaskId) return;
4487
+ throw e;
3049
4488
  }
4489
+ if (runId !== this.runId || !this.dispatched.has(taskId)) return;
4490
+ if (attemptNumber <= (this.lastAttempt.get(taskId) ?? 0)) return;
4491
+ this.lastAttempt.set(taskId, attemptNumber);
4492
+ this.attemptsPending.push({ taskId, attemptNumber });
3050
4493
  }
3051
- /** Whether this run has been stopped by a refusal. */
4494
+ /**
4495
+ * **A `task.message` or a `task.progress`: PASSED THROUGH to its callback** — `RN-R42`, W187, P36 (k), ADR 0114. The
4496
+ * task's id is the detail's `taskId` for a message — `task.message` carries no `id` (P36 fact 2) — and its `id` for a
4497
+ * progress report, whose detail is the task spread flat. Handed on only for a task THIS run dispatched (`ID-R4`'s two
4498
+ * tiers, asked SILENTLY, as {@link Driver.heardRunning} asks them): anything else is dropped and is not a refusal. Every
4499
+ * field is COPIED here, while the listener runs (ADR 0007 rule 1, `OR-R1`), and the copy FROZEN (`OR-R7`); a value that
4500
+ * cannot be cloned is handed on `undefined` — the scheduler's own treatment of an uncloneable `emit` (its §6) — so a
4501
+ * pass-through never stops a run by existing. **The consumer's throw is NOT caught here**: it leaves through
4502
+ * `onEventOrThrow` to the listener boundary, which reports it and halts the run (`onUnmintedTask`'s road, `OR-R10`).
4503
+ */
4504
+ heardPassThrough(name, detail) {
4505
+ const isMessage = name === "task.message";
4506
+ const taskId = isMessage ? detail?.taskId : detail?.id;
4507
+ if (typeof taskId !== "string" || detail === void 0) return;
4508
+ let parts;
4509
+ try {
4510
+ parts = parseTaskId(taskId);
4511
+ } catch (e) {
4512
+ if (e instanceof InvalidTaskId) return;
4513
+ throw e;
4514
+ }
4515
+ if (parts.runId !== this.runId || !this.dispatched.has(taskId)) return;
4516
+ const attribution = {
4517
+ runId: parts.runId,
4518
+ nodeId: parts.nodeId,
4519
+ branchPath: parts.branchPath,
4520
+ taskId,
4521
+ flowAttempt: parts.attempt,
4522
+ attemptNumber: attemptNumberOf(detail)
4523
+ };
4524
+ if (isMessage) {
4525
+ const m = {
4526
+ ...attribution,
4527
+ name: detail.name,
4528
+ data: frozenCopy(detail.data),
4529
+ seq: detail.seq,
4530
+ timestamp: detail.timestamp
4531
+ };
4532
+ if (detail.dropped !== void 0) m.dropped = detail.dropped;
4533
+ if (detail.error !== void 0) m.error = detail.error;
4534
+ this.opts.onTaskMessage?.(Object.freeze(m));
4535
+ return;
4536
+ }
4537
+ const p = {
4538
+ ...attribution,
4539
+ progress: detail.progress,
4540
+ message: frozenCopy(detail.message),
4541
+ details: frozenCopy(detail.details),
4542
+ timestamp: detail.timestamp
4543
+ };
4544
+ this.opts.onTaskProgress?.(Object.freeze(p));
4545
+ }
4546
+ /** Whether this run has been HALTED by a refusal (`OR-R10`; its outcome `halted`). */
3052
4547
  hasFailed() {
3053
4548
  return this.failed;
3054
4549
  }
@@ -3057,19 +4552,36 @@ var Driver = class {
3057
4552
  this.failed = true;
3058
4553
  const err = cause instanceof Error ? cause : new Error(String(cause));
3059
4554
  let seq = null;
4555
+ const report = () => {
4556
+ if (this.replay !== null && (seq === null || !this.delivers(seq))) return;
4557
+ this.opts.onRunFailure?.({
4558
+ runId: this.runId,
4559
+ kind: err.constructor?.name ?? "Error",
4560
+ message: err.message,
4561
+ cause
4562
+ });
4563
+ };
3060
4564
  if (!already) {
3061
- seq = this.append({ kind: "stopped", reason: `${err.constructor?.name ?? "Error"}: ${err.message}` });
3062
- this.arrivals.clear();
4565
+ try {
4566
+ seq = this.append({ kind: "halted", reason: `${err.constructor?.name ?? "Error"}: ${err.message}` });
4567
+ } finally {
4568
+ this.arrivals.clear();
4569
+ this.releaseAllItems();
4570
+ report();
4571
+ }
4572
+ return;
3063
4573
  }
3064
- if (this.replay !== null && (seq === null || !this.delivers(seq))) return;
3065
- this.opts.onRunFailure?.({
3066
- runId: this.runId,
3067
- kind: err.constructor?.name ?? "Error",
3068
- message: err.message,
3069
- cause
3070
- });
4574
+ report();
3071
4575
  }
3072
4576
  onEventOrThrow(name, detail) {
4577
+ if (name === "task.running") {
4578
+ this.heardRunning(detail);
4579
+ return;
4580
+ }
4581
+ if (name === "task.message" || name === "task.progress") {
4582
+ this.heardPassThrough(name, detail);
4583
+ return;
4584
+ }
3073
4585
  const taskId = detail?.id;
3074
4586
  let parts;
3075
4587
  try {
@@ -3102,7 +4614,13 @@ var Driver = class {
3102
4614
  const seq = this.append({ kind: "outcome", record });
3103
4615
  const report = this.opts.onNonconformingResult;
3104
4616
  if (report !== void 0 && this.delivers(seq)) this.reportResultSchema(record, nodeId, report);
3105
- if (this.failed) return;
4617
+ if (this.shut()) return;
4618
+ if (record.terminalState === "cancelled") {
4619
+ this.cancelled = true;
4620
+ this.arrivals.clear();
4621
+ this.releaseAllItems();
4622
+ return;
4623
+ }
3106
4624
  if (record.terminalState === "user_action") {
3107
4625
  const region = this.regionOf(nodeId);
3108
4626
  if (region) {
@@ -3222,9 +4740,10 @@ var Driver = class {
3222
4740
  * through within this turn; only a `task` becomes a scheduler task.
3223
4741
  */
3224
4742
  enter(runId, nodeId, payload, fromBranch = "-", fromRegion, itemIndices = EMPTY_INDICES) {
4743
+ if (this.shut()) return;
3225
4744
  const node = this.graph.byId.get(nodeId);
3226
4745
  if (!node) return;
3227
- const received = this.compose(node, payload, itemIndices);
4746
+ const received = this.compose(node, payload, itemIndices, this.runPayload(), () => this.itemOf(node.id, itemIndices));
3228
4747
  switch (node.kind) {
3229
4748
  case "task":
3230
4749
  return this.dispatch(runId, node, received, itemIndices);
@@ -3297,6 +4816,12 @@ var Driver = class {
3297
4816
  if (branch === void 0) return;
3298
4817
  const idx = items.map((_v, i) => withIndex(itemIndices, nodeId, i));
3299
4818
  const paths = idx.map((m) => this.pathOf(branch, m));
4819
+ const kept = this.items.kept.get(nodeId);
4820
+ if (kept !== void 0 && items.length > 0) {
4821
+ const byPath = this.items.held.get(nodeId) ?? /* @__PURE__ */ new Map();
4822
+ this.items.held.set(nodeId, byPath);
4823
+ items.forEach((v, i) => byPath.set(paths[i], kept ? cloneValue(v) : v));
4824
+ }
3300
4825
  for (const p of paths) {
3301
4826
  this.emit({
3302
4827
  type: "branch.opened",
@@ -3372,14 +4897,14 @@ var Driver = class {
3372
4897
  case "gate": {
3373
4898
  const decided = this.decideGate(node, received, this.memoryOf(nodeId));
3374
4899
  if ("threw" in decided) throw decided.threw;
3375
- const type = decided.type;
4900
+ const set = decided.set;
3376
4901
  if (!decided.declared) {
3377
4902
  throw new UndeclaredRoute(
3378
- `FD-R5 \u2014 run "${runId}" reached gate "${nodeId}" and its type "${node.type}" returned ${describeRoute(decided.route)}, which it does not declare; its routes are ${type.routes.map((r) => JSON.stringify(r)).join(", ")}. A route only running the type reveals cannot be refused at load (FD-V41 wires every DECLARED route), so the run stops here rather than routing nowhere. The type is the CODE registered under that name, not the document.`
4903
+ `FD-R5 \u2014 run "${runId}" reached gate "${nodeId}" and its type "${node.type}" returned ${describeRoute(decided.route)}, which it does not declare; its routes are ${set.routes.map((r) => JSON.stringify(r)).join(", ")}. A route only running the type reveals cannot be refused at load (FD-V41 wires every DECLARED route), so the run halts here rather than routing nowhere. The type is the CODE registered under that name, not the document.`
3379
4904
  );
3380
4905
  }
3381
4906
  const route = decided.route;
3382
- const edge = gateEdge(node, type.defaultRoute, route);
4907
+ const edge = gateEdge(node, set.defaultRoute, route);
3383
4908
  if (edge === null) {
3384
4909
  throw new InvalidDefinition(["FD-V41"], `gate "${nodeId}" has no edge for route ${JSON.stringify(route)} \u2014 FD-V41 should have refused this definition at construction`);
3385
4910
  }
@@ -3389,6 +4914,12 @@ var Driver = class {
3389
4914
  this.enter(runId, edge.to, received, fromBranch, fromRegion, itemIndices);
3390
4915
  return;
3391
4916
  }
4917
+ // **An ASK PARKS, and nothing is handed to the scheduler** — `RN-R37`, v0.22,
4918
+ // [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
4919
+ // [W180](../../doc/work-items/w180-asking-and-answering-in-a-run.md), P35 (b)-(f). It replaces ADR 0108 rule 8's
4920
+ // interim refusal at this arm. See `ask()`.
4921
+ case "ask":
4922
+ return this.ask(runId, node, received, itemIndices);
3392
4923
  // **A node kind this driver cannot execute REFUSES rather than returning.**
3393
4924
  // Added by [W32](../../doc/work-items/w32-what-a-map-fanout-is.md) as a
3394
4925
  // declared late exception, after its conformity reader measured what the
@@ -3487,8 +5018,9 @@ var Driver = class {
3487
5018
  } catch (threw) {
3488
5019
  return { threw };
3489
5020
  }
3490
- const declared = typeof route === "string" && type.routes.includes(route);
3491
- return { type, route, declared, key: recorded };
5021
+ const set = this.gateRoutes.get(node.id);
5022
+ const declared = typeof route === "string" && set.routes.includes(route);
5023
+ return { set, route, declared, key: recorded };
3492
5024
  }
3493
5025
  /**
3494
5026
  * **`FD-R4`'s road through an ENTRY gate: the edge the run will take, and no other** — W93, Outputs (9).
@@ -3501,7 +5033,7 @@ var Driver = class {
3501
5033
  boundaryEdges(gate, value) {
3502
5034
  const decided = this.decideGate(gate, value, /* @__PURE__ */ new Map());
3503
5035
  if ("threw" in decided || !decided.declared) return [];
3504
- const edge = gateEdge(gate, decided.type.defaultRoute, decided.route);
5036
+ const edge = gateEdge(gate, decided.set.defaultRoute, decided.route);
3505
5037
  return edge === null ? [] : [edge];
3506
5038
  }
3507
5039
  /**
@@ -3585,21 +5117,32 @@ var Driver = class {
3585
5117
  * the edge carried, so the composed object is the whole payload and what the
3586
5118
  * worker gets is what the document says, character for character.
3587
5119
  *
5120
+ * **`item` is a THUNK, read only for a `"$item"` key** (v0.45, P38 (e)): in a run it reads the instance's held item
5121
+ * (`itemOf`), at `FD-R4` it is the walk's own value — and a node declaring no `"$item"` never asks.
5122
+ *
3588
5123
  * @throws UnresolvedReference — `FD-R3`.
3589
5124
  */
3590
- compose(node, edgeValue, itemIndices) {
5125
+ compose(node, edgeValue, itemIndices, runPayload, item) {
3591
5126
  const needs = node.needs;
3592
5127
  if (!needs) return edgeValue;
3593
- const here = this.pathOf(node.id, itemIndices);
3594
- const produced = this.outputs();
5128
+ let lookup = null;
3595
5129
  const out = {};
3596
5130
  for (const [key, ref] of Object.entries(needs)) {
3597
5131
  const t = referenceTarget(ref);
3598
- if (t.node === null) {
5132
+ if (t.form === "self") {
3599
5133
  out[key] = cloneValue(edgeValue);
3600
5134
  continue;
3601
5135
  }
3602
- const v = this.resolveReference(node.id, here, produced, key, t, "needs");
5136
+ if (t.form === "payload") {
5137
+ out[key] = cloneValue(runPayload);
5138
+ continue;
5139
+ }
5140
+ if (t.form === "item") {
5141
+ out[key] = cloneValue(item());
5142
+ continue;
5143
+ }
5144
+ lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
5145
+ const v = this.resolveReference(node.id, lookup.here, lookup.produced, key, t, "needs");
3603
5146
  if (v !== ABSENT) out[key] = v;
3604
5147
  }
3605
5148
  return out;
@@ -3632,6 +5175,35 @@ var Driver = class {
3632
5175
  }
3633
5176
  return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
3634
5177
  }
5178
+ /**
5179
+ * **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
5180
+ * caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
5181
+ * reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
5182
+ * header exists. Read only where a node or an edge composes the token — a run declaring none never clones it
5183
+ * (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
5184
+ */
5185
+ runPayload() {
5186
+ return this.header?.entryPayload;
5187
+ }
5188
+ /**
5189
+ * **The item of the map instance `nodeId` is entered in** — `"$item"`'s source in a run (v0.45, P38 (c), (e)): the
5190
+ * innermost map containing the node (`innermostMapOf`, the walk `FD-V49` checked), and the instance its `itemIndices`
5191
+ * name — the branch path that map's branch entry has in it. **By reference to the held item, uncloned**: the caller
5192
+ * clones, per key. Read only where a node or an edge declares `"$item"`.
5193
+ *
5194
+ * **A miss THROWS a plain `Error` naming the invariant**: a node is entered only while its instance is live, and an
5195
+ * instance's item is released only once nothing of it may still be entered (`trackItems`), so a reader with nothing held is a driver
5196
+ * defect — reported as a halt (`OR-R10`) rather than handed `undefined`, which an item may legitimately be.
5197
+ */
5198
+ itemOf(nodeId, itemIndices) {
5199
+ const m = innermostMapOf(this.regions, nodeId);
5200
+ const path = this.pathOf(m.branches[0], itemIndices);
5201
+ const byPath = this.items.held.get(m.fanoutId);
5202
+ if (byPath === void 0 || !byPath.has(path)) {
5203
+ throw new Error(`node "${nodeId}" reads "$item" of map "${m.fanoutId}" instance "${path}", and that instance holds no item \u2014 an instance keeps its item until nothing of it may still be entered (RN-R13, ADR 0118); this is a defect in the driver`);
5204
+ }
5205
+ return byPath.get(path);
5206
+ }
3635
5207
  /**
3636
5208
  * **What a decision sees, beyond its node's outcome** — `FD-C3`, and
3637
5209
  * [ADR 0041](../../doc/decisions/0041-a-decision-sees-its-nodes-outcome-and-whatever-its-own-edge-declares.md).
@@ -3642,7 +5214,10 @@ var Driver = class {
3642
5214
  *
3643
5215
  * **No `"."`, and `FD-V26` refuses it on an edge**: *what this predicate would have
3644
5216
  * received anyway* is `ctx.outcome`, which it already has. So there is no
3645
- * `edgeValue` parameter here and none is derivable — see the call site.
5217
+ * `edgeValue` parameter here and none is derivable — see the call site. **A
5218
+ * `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
5219
+ * the NODE received but read from the run's header, `runPayload()` above, its own
5220
+ * copy per key per read.
3646
5221
  *
3647
5222
  * @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
3648
5223
  */
@@ -3654,6 +5229,17 @@ var Driver = class {
3654
5229
  const out = {};
3655
5230
  for (const [key, ref] of Object.entries(needs)) {
3656
5231
  const t = referenceTarget(ref);
5232
+ if (t.form === "payload") {
5233
+ out[key] = cloneValue(this.runPayload());
5234
+ continue;
5235
+ }
5236
+ if (t.form === "item") {
5237
+ out[key] = cloneValue(this.itemOf(node.id, itemIndices));
5238
+ continue;
5239
+ }
5240
+ if (t.form === "self") {
5241
+ throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
5242
+ }
3657
5243
  const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
3658
5244
  if (v !== ABSENT) out[key] = v;
3659
5245
  }
@@ -3668,11 +5254,13 @@ var Driver = class {
3668
5254
  * graph, and there is no longer a carried value that could disagree with it.
3669
5255
  */
3670
5256
  dispatch(runId, node, payload, itemIndices = EMPTY_INDICES) {
5257
+ if (this.shut()) return;
3671
5258
  const taskId = buildTaskId(runId, node.id, branchIndices(this.pathOf(node.id, itemIndices)), 0);
3672
5259
  if (this.dispatched.has(taskId)) {
3673
5260
  throw new DuplicateTaskId(`"${taskId}" was already dispatched in run "${runId}"`);
3674
5261
  }
3675
5262
  this.dispatched.add(taskId);
5263
+ if (this.replay === null) this.unhanded.add(taskId);
3676
5264
  this.append({ kind: "dispatched", taskId });
3677
5265
  const type = this.nodeTypes.get(node.type);
3678
5266
  const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
@@ -3688,7 +5276,108 @@ var Driver = class {
3688
5276
  this.replay.held.set(taskId, config);
3689
5277
  return;
3690
5278
  }
3691
- this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
5279
+ if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
5280
+ this.pauseHeld.set(taskId, config);
5281
+ this.unhanded.delete(taskId);
5282
+ return;
5283
+ }
5284
+ this.handOn(taskId, config);
5285
+ }
5286
+ /**
5287
+ * **A branch reached an ASK: it parks, keeping what reached it, and NOTHING is handed to the scheduler** — `RN-R37`,
5288
+ * v0.22, [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
5289
+ * P35 stop-and-asks (b), (c) and (f), decision 5.
5290
+ *
5291
+ * 1. **The hold** (`RN-R29`), as `dispatch`'s: `enter` checked it at its head and `compose` ran consumer code since.
5292
+ * 2. **Its task id** by `spec/task-id.md`'s scheme, attempt `0` — the address a person's answer names. Minted once
5293
+ * per path: a second `asked` at one id is `ID-R1`'s defect and refused as one (`DuplicateTaskId`), never a
5294
+ * second park.
5295
+ * 3. **ONE `asked` element**, carrying `received` — what reached the ask, composed through its `needs` — as the
5296
+ * driver's OWN deep copy: at the entry `received` is the caller's own `start()` payload, and a kept input must not
5297
+ * move when the caller's object does (P35 (f)). No `dispatched` element, no `createTask`, no `addTask`: the ask is
5298
+ * never in flight (`RN-R21`), so a stop cancels nothing for it and a resume re-dispatches nothing (P35 fact 5).
5299
+ * 4. **In a region, `branch.suspended`** — what a parked task emits (`settle`'s `user_action` arm): the branch is
5300
+ * live and its join waits for the person (Q7), which is how an ask in a fan-out holds its join. AFTER the element,
5301
+ * as the parked task's follows its settlement, and the hold checked between them (`RN-R29`): a consumer's
5302
+ * callback on `asked` may halt the run, and a halted run emits nothing further. It may also ANSWER the ask: the
5303
+ * answer is accepted and PENDING until the step ends (reading R4), so the park completes first.
5304
+ *
5305
+ * **During a replay this is how the park is REGENERATED** (`RN-R19`): the element is compared with the stored one,
5306
+ * input and all (`sameData`, `RN-R20`), so a replay that composes a different input is refused `diverged` here.
5307
+ */
5308
+ ask(runId, node, received, itemIndices) {
5309
+ if (this.shut()) return;
5310
+ const branchPath = this.pathOf(node.id, itemIndices);
5311
+ const taskId = buildTaskId(runId, node.id, branchIndices(branchPath), 0);
5312
+ if (this.asked.has(taskId) || this.dispatched.has(taskId)) {
5313
+ throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
5314
+ }
5315
+ this.asked.add(taskId);
5316
+ this.append({ kind: "asked", taskId, input: cloneValue(received) });
5317
+ if (this.shut()) return;
5318
+ const region = this.regionOf(node.id);
5319
+ if (region) {
5320
+ this.emit({
5321
+ type: "branch.suspended",
5322
+ regionId: region.fanoutId,
5323
+ regionPath: this.rpathOf(region, itemIndices),
5324
+ branchPath,
5325
+ at: node.id
5326
+ });
5327
+ }
5328
+ }
5329
+ /**
5330
+ * **`RS-R1`'s *suspended*, for one task id** — a dispatched task whose last record is a `user_action` settlement, or,
5331
+ * since v0.18 (`RN-R37`, ADR 0109), an ask this run reached that has no record at all. The FIVE readers of the
5332
+ * predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
5333
+ */
5334
+ parked(taskId) {
5335
+ return isSuspended(this.log, taskId, this.asked.has(taskId));
5336
+ }
5337
+ /**
5338
+ * **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
5339
+ * refuse a run `ended` only when nothing is in flight and THIS is false: a run whose only live node is a parked ask is
5340
+ * waiting on a person, not finished (P35 fact 5 (iv)). The rest of an ask under run control needs no line of its own:
5341
+ * never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
5342
+ */
5343
+ hasParked() {
5344
+ for (const id of this.dispatched) if (isSuspended(this.log, id)) return true;
5345
+ for (const id of this.asked) if (this.parked(id)) return true;
5346
+ return false;
5347
+ }
5348
+ /**
5349
+ * **Hand one task to the scheduler — and, if the run's owner cancelled it while the task was on its way, cancel it
5350
+ * at once** (`RN-R29` (i), `RN-R31`, ADR 0103's inherited hazard, W173). The ONE place `addTask` is called: by
5351
+ * `dispatch` for the task in progress, and by `resume()` for each task in flight. A cancel raised on this task's own
5352
+ * `dispatched` element — or on a resume's `resumed` element — ran before the scheduler held the id, and sent nothing
5353
+ * for it; this is where that task is cancelled, so it ends with a genuine `task.cancelled` rather than running.
5354
+ */
5355
+ handOn(taskId, config) {
5356
+ try {
5357
+ this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
5358
+ } finally {
5359
+ this.unhanded.delete(taskId);
5360
+ }
5361
+ if (this.cancelRequest !== null) this.cancelTask(taskId);
5362
+ }
5363
+ /**
5364
+ * **One task cancelled through the scheduler, with the request's options**, its promise kept for `cancel()` — or,
5365
+ * since W174, `stop()` — to wait on (`RN-R31`, `RN-R33`). Each option is passed only when the caller gave it, so an
5366
+ * absent `graceMs` is the pool's `cancelGraceMs` and not `undefined` read as a value. A cancel and a stop never both
5367
+ * hold — each refuses the other — so the one present is the one whose options these are.
5368
+ */
5369
+ cancelTask(taskId) {
5370
+ const req = this.cancelRequest ?? this.stopRequest;
5371
+ const options = {};
5372
+ if (req.reason !== void 0) options.reason = req.reason;
5373
+ if (req.graceMs !== void 0) options.graceMs = req.graceMs;
5374
+ const p = this.opts.scheduler.cancel(taskId, options);
5375
+ if (!req.stopping.has(taskId)) req.stopping.set(taskId, p);
5376
+ const drop = () => {
5377
+ if (req.stopping.get(taskId) === p) req.stopping.delete(taskId);
5378
+ };
5379
+ p.then(drop, drop);
5380
+ return p;
3692
5381
  }
3693
5382
  /**
3694
5383
  * **A node's `branchPath`, derived from the graph** — `spec/task-id.md` section
@@ -3747,6 +5436,49 @@ var Driver = class {
3747
5436
  this.arrivals.get(fanoutId)?.delete(rp);
3748
5437
  if (this.arrivals.get(fanoutId)?.size === 0) this.arrivals.delete(fanoutId);
3749
5438
  }
5439
+ /**
5440
+ * **The release horizon of what instances keep** — ADR 0118 rule 5. A `branch.opened` of a region nested in a kept map's
5441
+ * instance counts one live branch there; its terminal event uncounts it. The instance's OWN terminal event releases its
5442
+ * item when that count is zero, and otherwise marks it ENDED, released by the nested branch that ends last. O(nesting
5443
+ * depth) per event, and nothing at all where no map keeps (`kept` empty). Run in a replay as in a run, so a resume
5444
+ * rebuilds the counts with the items.
5445
+ */
5446
+ trackItems(e) {
5447
+ const it = this.items;
5448
+ if (it.kept.size === 0) return;
5449
+ const opened = e.type === "branch.opened";
5450
+ const terminal = isTerminalEvent(e);
5451
+ if (!opened && !terminal) return;
5452
+ for (const { map, depth } of it.within.get(e.regionId) ?? []) {
5453
+ const path = e.branchPath.split(".").slice(0, depth).join(".");
5454
+ const k = `${map} ${path}`;
5455
+ const n = (it.nested.get(k) ?? 0) + (opened ? 1 : -1);
5456
+ if (n > 0) {
5457
+ it.nested.set(k, n);
5458
+ continue;
5459
+ }
5460
+ it.nested.delete(k);
5461
+ if (it.ended.has(k)) this.releaseItem(map, path);
5462
+ }
5463
+ if (terminal && it.kept.has(e.regionId)) {
5464
+ if (it.nested.has(`${e.regionId} ${e.branchPath}`)) it.ended.add(`${e.regionId} ${e.branchPath}`);
5465
+ else this.releaseItem(e.regionId, e.branchPath);
5466
+ }
5467
+ }
5468
+ /** **One instance's item, dropped** — and the map's own key with its last instance, as `releaseArrivals` drops a region's. */
5469
+ releaseItem(mapId, branchPath) {
5470
+ this.items.ended.delete(`${mapId} ${branchPath}`);
5471
+ const byPath = this.items.held.get(mapId);
5472
+ if (byPath === void 0) return;
5473
+ byPath.delete(branchPath);
5474
+ if (byPath.size === 0) this.items.held.delete(mapId);
5475
+ }
5476
+ /** **Everything instances keep, dropped** — the run-level moments, where no node of the run is entered again. */
5477
+ releaseAllItems() {
5478
+ this.items.held.clear();
5479
+ this.items.nested.clear();
5480
+ this.items.ended.clear();
5481
+ }
3750
5482
  /** The arrival map for one region, created on first arrival. */
3751
5483
  arrivalsOf(fanoutId, rp) {
3752
5484
  const byPath = this.arrivals.get(fanoutId) ?? /* @__PURE__ */ new Map();
@@ -3902,12 +5634,22 @@ var Driver = class {
3902
5634
  if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
3903
5635
  return seq;
3904
5636
  }
5637
+ /**
5638
+ * **A stored `interrupted` element, fed** (`RN-R19`, `RN-R32`): the replay holds as the stopped run was held — the hold
5639
+ * BEFORE the element, as a stop is taken — and appends the element as stored. Neither `deaf` nor a cancel nor the
5640
+ * collections' release: those were the stopped driver's, and a replay hands a scheduler nothing.
5641
+ */
5642
+ feedInterrupted(e) {
5643
+ this.stopRequest = { actor: e.actor, stopping: /* @__PURE__ */ new Map(), pending: null, took: null };
5644
+ this.append({ kind: "interrupted", actor: e.actor, reason: e.reason ?? null });
5645
+ }
3905
5646
  emit(e) {
3906
5647
  const policy = this.opts.joinPolicy;
3907
5648
  if (policy && this.joinStateHas(e.regionId, e.regionPath)) {
3908
5649
  this.joinStateSet(e.regionId, e.regionPath, policy.step(this.joinStateGet(e.regionId, e.regionPath), e));
3909
5650
  }
3910
5651
  const event = Object.freeze({ ...e });
5652
+ this.trackItems(e);
3911
5653
  const seq = this.append({ kind: "region", event });
3912
5654
  if (this.delivers(seq)) this.opts.onRegionEvent?.(event);
3913
5655
  }
@@ -3978,7 +5720,7 @@ var Driver = class {
3978
5720
  joinId: region.joinId,
3979
5721
  fired: true
3980
5722
  });
3981
- if (this.failed) return;
5723
+ if (this.shut()) return;
3982
5724
  const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
3983
5725
  this.releaseArrivals(region.fanoutId, rp);
3984
5726
  const join = this.graph.byId.get(region.joinId);
@@ -4118,22 +5860,72 @@ var Driver = class {
4118
5860
 
4119
5861
  // src/fold/run-state.ts
4120
5862
  function emptyState() {
4121
- return { runId: null, outcome: "running", nodes: {}, regions: {}, pathEnds: [], stoppedBy: null, resumes: [], gated: [] };
5863
+ return {
5864
+ runId: null,
5865
+ outcome: "running",
5866
+ nodes: {},
5867
+ regions: {},
5868
+ pathEnds: [],
5869
+ haltedBy: null,
5870
+ cancelRequest: null,
5871
+ interruption: null,
5872
+ paused: false,
5873
+ resumes: [],
5874
+ gated: []
5875
+ };
4122
5876
  }
4123
5877
  function step(prev, input) {
4124
- if (input.kind === "stopped") {
4125
- return { ...prev, stoppedBy: input.reason, outcome: "stopped" };
5878
+ if (input.kind === "halted" || input.kind === "stopped") {
5879
+ return { ...prev, haltedBy: input.reason, outcome: "halted" };
5880
+ }
5881
+ if (input.kind === "cancelRequested") {
5882
+ if (prev.cancelRequest !== null || prev.outcome === "cancelled") return prev;
5883
+ return withOutcome({ ...prev, cancelRequest: { actor: input.actor, reason: input.reason } });
5884
+ }
5885
+ if (input.kind === "interrupted") {
5886
+ if (prev.interruption !== null) return prev;
5887
+ return withOutcome({ ...prev, interruption: { actor: input.actor, reason: input.reason } });
5888
+ }
5889
+ if (input.kind === "paused" || input.kind === "unpaused") {
5890
+ const paused = input.kind === "paused";
5891
+ if (prev.paused === paused) return prev;
5892
+ return { ...prev, paused };
4126
5893
  }
4127
5894
  if (input.kind === "region") {
4128
5895
  return withOutcome(applyRegion(prev, input.event));
4129
5896
  }
4130
5897
  if (input.kind === "dispatched") {
4131
- const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2 } = parseTaskId(input.taskId);
5898
+ const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
4132
5899
  if (prev.nodes[input.taskId]) return prev;
4133
5900
  return withOutcome({
4134
5901
  ...prev,
4135
5902
  runId: prev.runId ?? runId2,
4136
- nodes: { ...prev.nodes, [input.taskId]: { nodeId: nodeId2, branchPath: branchPath2, state: "running", result: null, error: null } }
5903
+ nodes: {
5904
+ ...prev.nodes,
5905
+ [input.taskId]: { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, flowAttempt: attempt2, attemptNumber: null, state: "running", result: null, error: null }
5906
+ }
5907
+ });
5908
+ }
5909
+ if (input.kind === "asked") {
5910
+ const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
5911
+ if (prev.nodes[input.taskId]) return prev;
5912
+ return withOutcome({
5913
+ ...prev,
5914
+ runId: prev.runId ?? runId2,
5915
+ nodes: {
5916
+ ...prev.nodes,
5917
+ [input.taskId]: {
5918
+ runId: runId2,
5919
+ nodeId: nodeId2,
5920
+ branchPath: branchPath2,
5921
+ flowAttempt: attempt2,
5922
+ attemptNumber: null,
5923
+ state: "suspended",
5924
+ result: null,
5925
+ error: null,
5926
+ input: structuredClone(input.input)
5927
+ }
5928
+ }
4137
5929
  });
4138
5930
  }
4139
5931
  if (input.kind === "pathEnded") {
@@ -4147,13 +5939,20 @@ function step(prev, input) {
4147
5939
  });
4148
5940
  }
4149
5941
  if (input.kind === "resumed") {
4150
- return { ...prev, resumes: [...prev.resumes, [...input.taskIds]] };
5942
+ const resumes = [...prev.resumes, [...input.taskIds]];
5943
+ if (prev.interruption !== null) return withOutcome({ ...prev, interruption: null, resumes });
5944
+ return { ...prev, resumes };
4151
5945
  }
4152
5946
  if (input.kind === "gated") {
4153
5947
  const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
4154
5948
  if (input.key !== void 0) d.key = structuredClone(input.key);
4155
5949
  return { ...prev, gated: [...prev.gated, d] };
4156
5950
  }
5951
+ if (input.kind === "attempted") {
5952
+ const node = prev.nodes[input.taskId];
5953
+ if (node === void 0) return prev;
5954
+ return { ...prev, nodes: { ...prev.nodes, [input.taskId]: { ...node, attemptNumber: input.attemptNumber } } };
5955
+ }
4157
5956
  if (input.kind === "joinSettled") {
4158
5957
  const cur = prev.regions[input.regionId]?.[input.regionPath] ?? emptyRegion();
4159
5958
  return withOutcome({
@@ -4162,13 +5961,17 @@ function step(prev, input) {
4162
5961
  });
4163
5962
  }
4164
5963
  const r = input.record;
4165
- const { runId, nodeId, branchPath } = parseTaskId(r.taskId);
5964
+ const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
4166
5965
  const nodes = { ...prev.nodes };
4167
5966
  const existing = nodes[r.taskId];
5967
+ const attemptNumber = existing?.attemptNumber ?? null;
4168
5968
  if (r.kind === "outcome") {
4169
5969
  nodes[r.taskId] = {
5970
+ runId,
4170
5971
  nodeId,
4171
5972
  branchPath,
5973
+ flowAttempt: attempt,
5974
+ attemptNumber,
4172
5975
  // `user_action` is `suspended` at flow level: a resolution resumes it, and
4173
5976
  // calling it terminal is what makes a design drop the fifth state.
4174
5977
  state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
@@ -4177,11 +5980,15 @@ function step(prev, input) {
4177
5980
  };
4178
5981
  } else {
4179
5982
  nodes[r.taskId] = {
5983
+ runId: existing?.runId ?? runId,
4180
5984
  nodeId: existing?.nodeId ?? nodeId,
4181
5985
  branchPath: existing?.branchPath ?? branchPath,
5986
+ flowAttempt: existing?.flowAttempt ?? attempt,
5987
+ attemptNumber,
4182
5988
  state: r.resolvedAs,
4183
5989
  result: r.result,
4184
- error: r.error
5990
+ error: r.error,
5991
+ ...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
4185
5992
  };
4186
5993
  }
4187
5994
  return withOutcome({ ...prev, runId: prev.runId ?? runId, nodes });
@@ -4220,8 +6027,13 @@ function applyRegion(prev, e) {
4220
6027
  return { ...prev, regions: withRegion(prev.regions, e.regionId, e.regionPath, cur) };
4221
6028
  }
4222
6029
  function withOutcome(s) {
4223
- if (s.stoppedBy !== null) return { ...s, outcome: "stopped" };
6030
+ if (s.haltedBy !== null) return { ...s, outcome: "halted" };
4224
6031
  const nodes = Object.values(s.nodes);
6032
+ if (s.cancelRequest === null && nodes.some((n) => n.state === "cancelled")) return { ...s, outcome: "cancelled" };
6033
+ if (s.cancelRequest !== null) {
6034
+ return { ...s, outcome: nodes.some((n) => n.state === "running") ? "cancelling" : "cancelled" };
6035
+ }
6036
+ if (s.interruption !== null) return { ...s, outcome: "interrupted" };
4225
6037
  if (nodes.some((n) => n.state === "suspended")) return { ...s, outcome: "awaiting_action" };
4226
6038
  if (nodes.some((n) => n.state === "running")) return { ...s, outcome: "running" };
4227
6039
  const openedNothing = allRegionRuns(s).some((r) => r.opened.length === 0 && r.joined !== true);
@@ -4243,7 +6055,7 @@ function foldLog(log) {
4243
6055
  return fold(log.map((r) => ({ kind: r.kind, record: r })));
4244
6056
  }
4245
6057
  function isTerminal(s) {
4246
- return s.outcome !== "running" && s.outcome !== "awaiting_action";
6058
+ return s.outcome !== "running" && s.outcome !== "awaiting_action" && s.outcome !== "cancelling";
4247
6059
  }
4248
6060
  function nodeOf(s, taskId) {
4249
6061
  return s.nodes[taskId] ?? null;
@@ -4329,6 +6141,7 @@ export {
4329
6141
  innermostRegionOf,
4330
6142
  branchPathOf,
4331
6143
  regionChainOf,
6144
+ innermostMapOf,
4332
6145
  sharedRegionDepth,
4333
6146
  outputIsVisible,
4334
6147
  branchIndexOf,
@@ -4339,6 +6152,8 @@ export {
4339
6152
  canonical,
4340
6153
  findings,
4341
6154
  describeFindings,
6155
+ readKeyPath,
6156
+ ASK_REFUSED_KEYS,
4342
6157
  EDGE_KEYS_DECLARED,
4343
6158
  TASK_KEYS_DECLARED,
4344
6159
  NODE_KEYS_DECLARED,
@@ -4353,9 +6168,9 @@ export {
4353
6168
  languagesOf,
4354
6169
  KEY_PATH_RE,
4355
6170
  keyPathProblem,
4356
- readKeyPath,
4357
6171
  GATE_TYPE_KEYS,
4358
6172
  gateTypeProblem,
6173
+ routesOf,
4359
6174
  distinct,
4360
6175
  BUILT_IN_GATE_TYPES,
4361
6176
  gateTypesOf,
@@ -4393,8 +6208,10 @@ export {
4393
6208
  CHECKED_WORKER_SCHEMAS,
4394
6209
  NODE_TYPE_DECLARATION_KEYS,
4395
6210
  POOL_SPEC_KEYS,
6211
+ MESSAGE_OPTION_KEYS,
4396
6212
  DECLARED_POOL_KEYS,
4397
6213
  INSTANCE_KEYS,
6214
+ POOL_INSTANCE_KEYS,
4398
6215
  isDerivation,
4399
6216
  mergeConfig,
4400
6217
  nodeTypeFromDeclaration,
@@ -4410,6 +6227,7 @@ export {
4410
6227
  fits,
4411
6228
  boundaryTargets,
4412
6229
  checkWiring,
6230
+ givenVerdict,
4413
6231
  describeVerdict,
4414
6232
  VERDICT_KINDS,
4415
6233
  WIRING_SHAPES,
@@ -4429,6 +6247,8 @@ export {
4429
6247
  eligibleEdges,
4430
6248
  gateEdge,
4431
6249
  selectEdge,
6250
+ TERMINAL_EVENTS,
6251
+ isTerminalEvent,
4432
6252
  InvalidDefinition,
4433
6253
  DuplicateTaskId,
4434
6254
  UnresolvedMapArity,
@@ -4437,6 +6257,7 @@ export {
4437
6257
  BoundaryRefused,
4438
6258
  MultipleRuns,
4439
6259
  NotStarted,
6260
+ RunControlRefused,
4440
6261
  CHECKPOINT_FORMAT,
4441
6262
  ResumeRefused,
4442
6263
  resumeRefusal,