fyflow-orchestration 0.7.0 → 0.9.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;
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);
2216
2736
  }
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
- }
2242
- }
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,52 @@ 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"]];
3092
+ var NO_RECORDS = Object.freeze([]);
3093
+ var TaskLog = class {
3094
+ byTask = /* @__PURE__ */ new Map();
3095
+ visits = 0;
3096
+ /** `append`'s half: the record joins its task's sub-log, by reference. */
3097
+ push(record) {
3098
+ const sub = this.byTask.get(record.taskId);
3099
+ if (sub) sub.push(record);
3100
+ else this.byTask.set(record.taskId, [record]);
3101
+ }
3102
+ /** One task's records, in append order — what `effectiveOutcome` and `isSuspended` are handed. Read, never kept. */
3103
+ of(taskId) {
3104
+ const sub = this.byTask.get(taskId) ?? NO_RECORDS;
3105
+ this.visits += sub.length;
3106
+ return sub;
3107
+ }
3108
+ };
3109
+ var Fifo = class {
3110
+ items = [];
3111
+ head = 0;
3112
+ get length() {
3113
+ return this.items.length - this.head;
3114
+ }
3115
+ push(x) {
3116
+ this.items.push(x);
3117
+ }
3118
+ /** The oldest element, removed — `undefined` when empty, as `Array.prototype.shift`. */
3119
+ shift() {
3120
+ if (this.head >= this.items.length) return void 0;
3121
+ const x = this.items[this.head];
3122
+ this.items[this.head] = void 0;
3123
+ this.head += 1;
3124
+ if (this.head === this.items.length) {
3125
+ this.items = [];
3126
+ this.head = 0;
3127
+ }
3128
+ return x;
3129
+ }
3130
+ /** Every element still queued, oldest first. */
3131
+ *[Symbol.iterator]() {
3132
+ for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
3133
+ }
3134
+ };
2493
3135
  var Driver = class {
2494
3136
  /**
2495
3137
  * `OR-R7` — nothing outside the inbound API writes to run state. Private, and
@@ -2507,16 +3149,31 @@ var Driver = class {
2507
3149
  * channels with no shared order, so `RN-R3`'s *"read in append order"* was about
2508
3150
  * a value no one could hold.
2509
3151
  *
2510
- * `log` above is the **record-kind subsequence of this array**, kept as its own
2511
- * array only because `effectiveOutcome` and `isSuspended` scan it on every
2512
- * settlement and rebuilding it by filter each time is quadratic. It holds the
2513
- * same record objects, never copies. `test/driver/` asserts the two agree, so
2514
- * the redundancy is checked rather than trusted.
3152
+ * `log` above is the **record-kind subsequence of this array**, by reference — the
3153
+ * same record objects, never copies; `test/driver/` asserts the two agree, so the
3154
+ * redundancy is checked rather than trusted. *(It was kept "only because
3155
+ * `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
3156
+ * whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
3157
+ * It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
3158
+ * catch-up, and the retention walk's alias check.)*
2515
3159
  */
2516
3160
  inputs = [];
3161
+ /**
3162
+ * **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
3163
+ * `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
3164
+ * `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
3165
+ */
3166
+ taskLog = new TaskLog();
2517
3167
  graph;
2518
3168
  regions;
2519
3169
  dispatched = /* @__PURE__ */ new Set();
3170
+ /**
3171
+ * **Every ASK this run has reached, by task id** — `RN-R37`, v0.22, W180, ADR 0109: the ids of the `asked` elements
3172
+ * appended, so `RS-R1`'s widened *suspended* (an `asked` element and no record after it) is one lookup rather than a
3173
+ * scan of the input sequence at every `resolve()`, and an ask's id is minted once per path (`ID-R1`'s guard). Ids only —
3174
+ * the kept INPUT lives on the element alone. `dispatched`'s twin for the one node that is never dispatched.
3175
+ */
3176
+ asked = /* @__PURE__ */ new Set();
2520
3177
  now;
2521
3178
  opts;
2522
3179
  def;
@@ -2594,6 +3251,29 @@ var Driver = class {
2594
3251
  * quiescent point (`RN-R10`), not here — ADR 0046 rule 5.
2595
3252
  */
2596
3253
  arrivals = /* @__PURE__ */ new Map();
3254
+ /**
3255
+ * **What a map INSTANCE keeps for `"$item"`** — `RN-R13`'s sixth term since v0.28 (`spec/run-state.md`), P38 (e),
3256
+ * [ADR 0118](../../doc/decisions/0118-a-map-instance-keeps-its-item-while-a-node-of-it-may-still-be-entered.md),
3257
+ * [W191](../../doc/work-items/w191-the-map-instances-item-as-a-reference.md). Two halves, one field:
3258
+ *
3259
+ * - **`kept`** — decided from the DOCUMENT at construction: the maps whose region holds a `"$item"` reader (a node, or
3260
+ * an edge of a node, whose innermost containing map is that map — `innermostMapOf`, P38 (c)), and for each whether
3261
+ * its received value may be the CALLER's own object (`true`: reached from the entry through gates alone, with no
3262
+ * `needs` — `start()` hands `enter` the value it was called with, as an entry task and an entry ask receive it). A
3263
+ * map no reader names keeps NOTHING, and a flow declaring no `"$item"` has `kept` empty (P38 decision 2).
3264
+ * - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
3265
+ * 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
3266
+ * a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
3267
+ * `needs` composition's per-key copy), and as its OWN copy only where `kept` says that element is the caller's
3268
+ * (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
3269
+ * **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
3270
+ * `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
3271
+ * (`within`, `nested`, `ended` — `trackItems`): a nested join a policy fires before its last branch is terminal lets
3272
+ * the instance's branch arrive while that branch can still read (W191's conformity reader, C1). A suspended node (an
3273
+ * ask, a parked task) keeps its branch live, and so its item. And at the run-level moments
3274
+ * `arrivals` is cleared at — a halt, a cancel, a stop, a `cancelled` settlement — because no node is entered after.
3275
+ */
3276
+ items;
2597
3277
  /**
2598
3278
  * **The one run this driver serves** — ADR 0022 rule 1, fixed at construction
2599
3279
  * and never `null`.
@@ -2636,16 +3316,212 @@ var Driver = class {
2636
3316
  * elements a consumer does not already hold ({@link Driver.delivers}).
2637
3317
  */
2638
3318
  replay = null;
2639
- /** The run has been stopped by a refusal. Nothing dispatches for it again. */
3319
+ /** The run has been HALTED by a refusal (`OR-R10`). Nothing dispatches for it again. */
2640
3320
  failed = false;
2641
3321
  /**
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,
3322
+ * **A task of this run settled `cancelled`, so the run is HALTED** — `RN-R28`,
3323
+ * [ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md)
3324
+ * rule 3, W158. Every door `failed` shuts, this shuts — nothing is dispatched or routed for the run again,
3325
+ * a later settlement is appended and routed nowhere, and `resolve()` is refused — and it differs from `failed`
3326
+ * in three things only: no `halted` element is appended (the fold reads the run `cancelled` from the
3327
+ * settlement itself), `onRunFailure` is not called (a cancel is its owner's act, not a defect), and
3328
+ * `hasFailed()` stays false. **Separate rather than `failed` with a reason**, because those three are each
3329
+ * a reader of `failed` that must not fire.
3330
+ */
3331
+ cancelled = false;
3332
+ /**
3333
+ * **A resume refused because its replay regenerated a HALT in the lost tail** — `RN-R23`: that driver keeps the
3334
+ * halted run (its `header`, so a store may take its checkpoint) and still hears NOTHING. `onEvent`'s other test,
2644
3335
  * *started with no header*, cannot see it, because this refusal is the one that keeps its header: until W151 such a
2645
3336
  * 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.
3337
+ * reader, finding 6). A driver halted by `failRun` on a run it started is NOT deaf: `OR-R10` halts routing only.
3338
+ *
3339
+ * **And a STOPPED driver, since W174** (`RN-R33`, P34 decision 3): a stop sets it with its hold — before the
3340
+ * `interrupted` element, and so BEFORE it sends the first cancel — so no `task.cancelled` of the stop — nor any later settlement — is appended,
3341
+ * routed, delivered or reported, and the log ends with the in-flight set dispatched and unsettled for a resume to
3342
+ * re-dispatch. A replay feeding a stored `interrupted` element does NOT set it: the replay hears nothing anyway.
2647
3343
  */
2648
3344
  deaf = false;
3345
+ /**
3346
+ * **The run's OWNER cancelled it** — `RN-R31`, v0.18, W173,
3347
+ * [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md). `null`
3348
+ * until `cancel()` is accepted; then what every LATER cancel of a task sends the scheduler — the caller's `reason` and
3349
+ * `graceMs`, each only if given — and, per task id, the promise the scheduler's `cancel` returned for it, which is what
3350
+ * `cancel()`'s own promise waits on. **A halt** (`shut()`), set BEFORE the `cancelRequested` element is appended, so a
3351
+ * consumer's callback on that element already sees the run held. It holds no result: one promise per task STILL
3352
+ * stopping — each dropped as it settles, so none once the run is `cancelled` — and the two option values.
3353
+ */
3354
+ cancelRequest = null;
3355
+ /**
3356
+ * **The run's OWNER stopped it, to resume it elsewhere** — `RN-R33`, v0.19, W174,
3357
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md). `null`
3358
+ * until `stop()` is accepted; then, as `cancelRequest`'s shape, what every cancel the stop sends carries and one promise
3359
+ * per task STILL stopping, each dropped as it settles — and the `actor`, which the element is appended with.
3360
+ *
3361
+ * **`pending` while the stop has been ACCEPTED and not yet TAKEN**: a stop called MID-STEP — from a callback on the
3362
+ * driver's own step — takes effect when that step ends (`stepping` back to `0`), so the step's routing is all in the log
3363
+ * and none of it is lost to a resume (`RN-R33`, W174). While pending it is the resolver of the promise every call made
3364
+ * before then waits on, `resolve()` and `cancel()` are already refused, and the hold is NOT yet shut; `null` once the
3365
+ * stop is taken, from which moment it IS a halt (`shut()`). `took` is the promise a second call made while pending
3366
+ * waits on. A function and a promise held only until the step ends — `null` both once taken, never at rest.
3367
+ *
3368
+ * **And a REPLAY sets it too**, taken, with no options and nothing stopping, where it feeds a stored `interrupted`
3369
+ * element (`RN-R19`): the stored run was held from that position, so the replay holds there as well, until a fed
3370
+ * `resumed` — or the resume itself, once the replay is over — sets it back to `null`. The stop's deafness is `deaf`'s.
3371
+ */
3372
+ stopRequest = null;
3373
+ /**
3374
+ * **Tasks whose `dispatched` element is appended and which the scheduler has NOT been handed yet** — `RN-R29` (i)'s
3375
+ * window, ADR 0103's inherited hazard, W173. A scheduler's `cancel()` of an id it does not hold resolves `false` and
3376
+ * does nothing, so `cancel()` sends nothing for these: the hand-off site cancels each right AFTER its `addTask` —
3377
+ * `dispatch` for the task in progress, `resume()` for its in-flight set. Empty at rest; at most the one task being
3378
+ * dispatched, or a resume's in-flight set while it is handed back.
3379
+ */
3380
+ unhanded = /* @__PURE__ */ new Set();
3381
+ /**
3382
+ * **How deep the driver is in its OWN step** — `start`, `resume`, `resolve` or a scheduler event, each counting itself
3383
+ * in (`RN-R31`, W173's conformity reader, D-1). `0` at rest. A consumer callback the driver calls mid-step sees a
3384
+ * PREFIX that can read finished while the step is about to dispatch more (`RN-R12`'s quiescent point, P34 fact 3), so
3385
+ * `cancel()` refuses `ended` only at rest: mid-step, *nothing in flight and nothing parked* is not *finished*.
3386
+ */
3387
+ stepping = 0;
3388
+ /**
3389
+ * **The run's OWNER has paused it, and the pause is TAKEN** — `RN-R35`, v0.20, W175,
3390
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md):
3391
+ * set when a `paused` element is appended (by `pause()`, or fed to a replay) and cleared when an `unpaused` one is —
3392
+ * so it is always what `RunState.paused` folds to on the driver's own sequence. **Not a halt**: it does not join
3393
+ * `shut()`, because a paused run still routes and still accepts a resolution (ADR 0103's consequence). It is the
3394
+ * dispatch hold's DEFERRED form, at the dispatch door (ADR 0103 rule 6): each dispatch is appended, and held.
3395
+ */
3396
+ paused = false;
3397
+ /**
3398
+ * **Every dispatch the pause HELD, in dispatch order** (`RN-R35`): appended — so the fold reads each `running`, in flight
3399
+ * under `RN-R21` (P34 fact 3) — and NOT handed to the scheduler. An `unpause` hands each on, first held first; a
3400
+ * `cancel` hands each on and cancels it (ADR 0104 rule 7); a `stop` sends nothing for them, so they stay in flight in
3401
+ * the log for a resume to hold again. Empty unless the run is paused — or an unpause's hand-off is under way, while
3402
+ * which a dispatch joins the END of it, so the order holds; or a halt broke that hand-off off, after which nothing is
3403
+ * handed on for the run and what is left here never is. One `createTask` configuration per held task, no result.
3404
+ */
3405
+ pauseHeld = /* @__PURE__ */ new Map();
3406
+ /**
3407
+ * **Pause and unpause calls ACCEPTED mid-step and not yet TAKEN**, in call order (`RN-R35`): a call from a callback on
3408
+ * the driver's own step is taken when that step ends (`stepping` back to `0`, `leaveStep()`), as a stop is (`RN-R33`) —
3409
+ * so its element lands BETWEEN two steps, where a replay can feed it (`RN-R19`). Empty at rest.
3410
+ * **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
3411
+ * brief: *an owner element a resume's replay feeds is TAKEN AT REST*): accepted at the call — every refusal already asked — and its resolution
3412
+ * appended when the step ends, in the same call order as the pauses beside it.
3413
+ */
3414
+ pausePending = new Fifo();
3415
+ /**
3416
+ * **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
3417
+ * number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
3418
+ * repeating a number (a requeue) appends nothing, and what a resume re-hands an in-flight task with as `attemptsSpent`.
3419
+ * One integer per task that ever started an attempt, no result; kept for the life of the run, as `dispatched`'s ids are.
3420
+ */
3421
+ lastAttempt = /* @__PURE__ */ new Map();
3422
+ /**
3423
+ * **`attempted` elements HEARD and not yet appended**, in the order heard (`RN-R40`): a `task.running` arrives on the
3424
+ * driver's OWN step when an inline pool starts the task inside `addTask`, and an element appended there would sit between
3425
+ * two elements of one step — where the replay, which regenerates a step whole, would refuse the checkpoint `diverged`. So
3426
+ * every one is appended when the step ends, BEFORE a pause, an unpause, an answer or a stop pending on that step is taken
3427
+ * (`leaveStep()`): the attempt started before any of them. Empty at rest.
3428
+ */
3429
+ attemptsPending = new Fifo();
3430
+ /**
3431
+ * **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
3432
+ * `RN-R29`,
3433
+ * [ADR 0103](../../doc/decisions/0103-every-halt-shares-one-dispatch-hold.md), P34
3434
+ * decisions 5 and 8, W172. **The ONE predicate every halt shares**, read at these doors and no others — `RN-R29`
3435
+ * states the two things it does not hold: before each of `resume()`'s re-dispatches (the maintainer's answer to
3436
+ * 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
3437
+ * branches), at the head of `dispatch`, after a resolution's append and again before its routing (door (b)),
3438
+ * after a settlement's append (`settle`), after a join's `joinSettled` append (`settleJoin`), and at `resolve()`'s
3439
+ * entry. A halt P34 adds — a cancel or a stop — joins this disjunction, and every door follows.
3440
+ *
3441
+ * **A METHOD over the halt flags, never a field of its own**: a second copy of *the run is halted* is a field that
3442
+ * can disagree with `failed` or `cancelled`, and each of those has readers this predicate must not stand in for
3443
+ * (`hasFailed()`, `onRunFailure`). **The replay's hold is the OTHER form and is not this** — it appends a dispatch
3444
+ * and defers handing it on (`RN-R19`), where a shut hold appends nothing; `dispatch` keeps both.
3445
+ *
3446
+ * **A REQUESTED cancel joins it since W173** (`RN-R31`, ADR 0104) — ADR 0103's *a halt P34 adds joins this
3447
+ * disjunction*: from the moment `cancel()` is accepted, every door above holds for the run. **And a STOP since W174**
3448
+ * (`RN-R33`, ADR 0105): from the moment the stop is TAKEN — at once when `stop()` is called at rest, and when the step
3449
+ * ends when it is called mid-step (`stopRequest.pending`) — and, in a replay, from a fed `interrupted` element on.
3450
+ */
3451
+ shut() {
3452
+ return this.failed || this.cancelled || this.cancelRequest !== null || this.stopRequest !== null && this.stopRequest.pending === null;
3453
+ }
3454
+ /**
3455
+ * **What holds a resume's re-dispatch: the hold, EXCEPT a requested cancel** (ADR 0103 rule 7, ADR 0104 rule 7) — read
3456
+ * through `shut()` and never as a second copy of its disjunction (ADR 0103 rule 1; W173's conformity reader, F7), so a
3457
+ * halt W174 adds to `shut()` holds here too. **W174's stop never reaches this loop taken** (`RN-R33`): a stop is taken
3458
+ * at rest, and a stop called on this stack — on the `resumed` element, or in the replay's lost tail — is PENDING until
3459
+ * `resume()` returns, so each task is handed on here and the stop then cancels it; and the replay's own fed stop is
3460
+ * cleared before the loop. A request is the one halt that hands a task on: its tasks are `dispatched` and unsettled,
3461
+ * and only the scheduler can settle them, so each is handed on and cancelled (`handOn`).
3462
+ */
3463
+ holdsResume() {
3464
+ return this.shut() && this.cancelRequest === null;
3465
+ }
3466
+ /**
3467
+ * **The driver's own step ends** — each of `start`, `resume`, `resolve` and a scheduler event counts itself out here.
3468
+ * At rest again, a stop called on that step is TAKEN (`RN-R33`, W174): the step's routing is whole in the log, and
3469
+ * every task it handed on is in flight for the stop to cancel. **And a pause or an unpause called on it** (`RN-R35`,
3470
+ * W175), each in call order and BEFORE the stop — a pause or an unpause after a stop is refused, so any pending one was
3471
+ * called first. Each take is a step of its own and ends here too, so the next pending call is taken after it.
3472
+ * **And a resolution given on the step** (`RN-R37`, W180), in the same queue and the same call order.
3473
+ * **And FIRST, every `attempted` element heard on the step** (`RN-R40`, W186): the attempts started before anything
3474
+ * taken here, and a stop taken before them would leave them unrecorded — the driver deaf — under P36 (h)'s *a stopped
3475
+ * attempt counts*.
3476
+ */
3477
+ leaveStep() {
3478
+ this.stepping -= 1;
3479
+ if (this.stepping === 0 && this.attemptsPending.length > 0) this.takeAttempts();
3480
+ while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
3481
+ if (this.stepping === 0 && this.stopRequest !== null && this.stopRequest.pending !== null) this.takeStop();
3482
+ }
3483
+ /**
3484
+ * **Every `attempted` element heard and not yet appended, appended — AT REST, as ONE step of their own** (`RN-R40`,
3485
+ * W186): in the order heard, each BETWEEN two of the driver's steps, where a replay feeds it (`RN-R19`). A step, so a
3486
+ * call a consumer makes from a callback on one — a pause, a stop — is taken after the last of them. **Appended on a
3487
+ * halted run too**, as a settlement is: the scheduler started the attempt whatever the run's routing does. A consumer's
3488
+ * `onFoldInput` THROWING on one HALTS the run, reported once (`OR-R10` v0.19, W184), and the throw reaches whoever is on
3489
+ * the stack — a driver call's caller, or the listener boundary of a scheduler event's step; the elements still pending
3490
+ * behind it are appended by this step's own end, SILENTLY (the run is halted, its failure reported), and so is a throw
3491
+ * on any of them once the run is halted.
3492
+ */
3493
+ takeAttempts() {
3494
+ this.stepping += 1;
3495
+ try {
3496
+ while (this.attemptsPending.length > 0) {
3497
+ const a = this.attemptsPending.shift();
3498
+ const silent = this.failed;
3499
+ try {
3500
+ this.append({ kind: "attempted", taskId: a.taskId, attemptNumber: a.attemptNumber });
3501
+ } catch (e) {
3502
+ if (silent) continue;
3503
+ this.haltOnce(e);
3504
+ throw e;
3505
+ }
3506
+ }
3507
+ } finally {
3508
+ this.leaveStep();
3509
+ }
3510
+ }
3511
+ /**
3512
+ * **The ONE rule for a throw that reaches a driver call's boundary once the call is ACCEPTED** — `OR-R10` v0.19, P35 (k),
3513
+ * P36 (b) and (c), W184: reported ONCE through `onRunFailure` under its own class, and the run HALTED — its `halted`
3514
+ * element appended after the element thrown on — unless the run is already halted, whose halt was reported when it was
3515
+ * raised. `start()`'s treatment since W3, brought to every call: `resolve`, `resume` and `cancel` at their own element,
3516
+ * 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
3517
+ * the FIRST throw halts and a call still pending behind it is dropped by its own halted guard (the readers' C1). The
3518
+ * caller rethrows: the consumer's error reaches it.
3519
+ * A refusal the call raises BEFORE it is accepted — `RunControlRefused`, `BoundaryRefused`, `ResumeRefused`,
3520
+ * `MultipleRuns`, a `DataCloneError` — never reaches this: nothing was appended, and a rejected command halts nothing.
3521
+ */
3522
+ haltOnce(e) {
3523
+ if (!this.failed) this.failRun(e);
3524
+ }
2649
3525
  /** 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
3526
  nodeTypes;
2651
3527
  /** The registry's languages, normalised once (W61). Read at construction only; the driver evaluates nothing. */
@@ -2654,6 +3530,11 @@ var Driver = class {
2654
3530
  workers;
2655
3531
  /** 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
3532
  gateTypes;
3533
+ /**
3534
+ * **Each gate's RESOLVED route set, by gate id** — ADR 0092 rule 3: `routesOf` asked once per gate at
3535
+ * construction, after validation passed, and read by every decision. Never the type's members.
3536
+ */
3537
+ gateRoutes;
2657
3538
  /**
2658
3539
  * **Each gate's MEMORY: one per gate per RUN, the driver's, and never written down** — `RN-R26`,
2659
3540
  * [ADR 0067](../../doc/decisions/0067-a-gate-runs-in-the-driver-its-memory-is-the-drivers-and-its-decision-is-recorded.md),
@@ -2726,25 +3607,35 @@ var Driver = class {
2726
3607
  ...opts.gateTypes !== void 0 ? { gateTypes: opts.gateTypes } : {}
2727
3608
  };
2728
3609
  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
- );
3610
+ if (findings2.length) throw InvalidDefinition.fromFindings(definition.flowId, findings2);
3611
+ const gateRoutes = /* @__PURE__ */ new Map();
3612
+ for (const n of definition.nodes) {
3613
+ if (n.kind !== "gate") continue;
3614
+ const resolved = routesOf(this.gateTypes.get(n.type), n.settings ?? {});
3615
+ if ("problem" in resolved) {
3616
+ 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}`);
3617
+ }
3618
+ gateRoutes.set(n.id, resolved);
2734
3619
  }
3620
+ this.gateRoutes = gateRoutes;
2735
3621
  this.def = definition;
2736
3622
  this.opts = opts;
2737
3623
  this.now = opts.now ?? (() => Date.now());
2738
3624
  this.graph = buildGraph(definition);
2739
3625
  this.regions = allRegions(this.graph, definition);
3626
+ const kept = itemMaps(definition, this.regions);
3627
+ this.items = { kept, within: itemWithin(kept, this.regions), held: /* @__PURE__ */ new Map(), nested: /* @__PURE__ */ new Map(), ended: /* @__PURE__ */ new Set() };
2740
3628
  if (this.regions.length > 0 && !opts.joinPolicy) {
2741
3629
  throw new MissingJoinPolicy(
2742
3630
  `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
3631
  );
2744
3632
  }
2745
- for (const name of SETTLEMENT_EVENTS) {
3633
+ for (const name of SUBSCRIBED_EVENTS) {
2746
3634
  opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
2747
3635
  }
3636
+ for (const [name, callback] of PASS_THROUGH) {
3637
+ if (opts[callback] !== void 0) opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
3638
+ }
2748
3639
  }
2749
3640
  /**
2750
3641
  * Dispatch the entry node for the run this driver was constructed for.
@@ -2771,6 +3662,15 @@ var Driver = class {
2771
3662
  * substitution rather than dropping it.
2772
3663
  */
2773
3664
  start(payload) {
3665
+ this.stepping += 1;
3666
+ try {
3667
+ this.startStep(payload);
3668
+ } finally {
3669
+ this.leaveStep();
3670
+ }
3671
+ }
3672
+ /** `start`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3673
+ startStep(payload) {
2774
3674
  const runId = this.runId;
2775
3675
  if (this.started) {
2776
3676
  throw new MultipleRuns(
@@ -2781,11 +3681,12 @@ var Driver = class {
2781
3681
  for (const { node, value } of boundaryTargets(this.def, payload, (g, v) => this.boundaryEdges(g, v))) {
2782
3682
  const n = this.graph.byId.get(node);
2783
3683
  if (!n || n.kind !== "task") continue;
3684
+ if (n.transform !== void 0) continue;
2784
3685
  const type = this.nodeTypes.get(n.type);
2785
3686
  const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
2786
3687
  const schema = worker?.inputSchema;
2787
3688
  if (schema === void 0) continue;
2788
- const received = this.compose(n, value, /* @__PURE__ */ new Map());
3689
+ const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
2789
3690
  const fs = findings(schema, received);
2790
3691
  if (fs.length) {
2791
3692
  throw new BoundaryRefused(
@@ -2799,12 +3700,23 @@ var Driver = class {
2799
3700
  }
2800
3701
  }
2801
3702
  const entryPayload = structuredClone(payload);
2802
- this.header = { format: CHECKPOINT_FORMAT, runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
3703
+ const header = { format: CHECKPOINT_FORMAT, runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
3704
+ const onHeader = this.opts.onHeader;
3705
+ if (onHeader !== void 0) {
3706
+ this.started = true;
3707
+ try {
3708
+ onHeader(Object.freeze({ ...header, entryPayload: structuredClone(entryPayload) }));
3709
+ } catch (e) {
3710
+ this.started = false;
3711
+ throw e;
3712
+ }
3713
+ }
3714
+ this.header = header;
2803
3715
  this.started = true;
2804
3716
  try {
2805
3717
  this.enter(runId, this.def.entry, payload);
2806
3718
  } catch (e) {
2807
- this.failRun(e);
3719
+ this.haltOnce(e);
2808
3720
  throw e;
2809
3721
  }
2810
3722
  }
@@ -2819,28 +3731,43 @@ var Driver = class {
2819
3731
  * **In order, and each step is a rule:**
2820
3732
  * 1. **Refuse what the header says** (`RN-R23`) — {@link resumeRefusal}.
2821
3733
  * 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
3734
+ * `start()` enters it, then FEED every stored settlement, resolution and `resumed` element — and, since
3735
+ * 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
3736
  * other element the replay REGENERATES.
2825
3737
  * 3. **The PREFIX check** (`RN-R20`): each regenerated element at a position the stored events cover
2826
3738
  * must be the stored one; the first that is not is refused as `diverged`, naming its position, and
2827
3739
  * so is a replay that ends short. A replay that runs PAST the stored events is the stored run's
2828
3740
  * lost tail — a checkpoint taken inside `onFoldInput` ends mid-step (ADR 0064) — and it is
2829
3741
  * 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.
3742
+ * 4. **A run the replay HALTS is refused** as `halted` (P23 decision 10), its tail delivered first.
2831
3743
  * 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.
3744
+ * (`RN-R21`, `RN-R22`) — and then hand each to the scheduler at the SAME task id, with `attemptsSpent` the highest
3745
+ * attempt recorded for it where one was (v0.26, W186); or, where the replay
3746
+ * ends PAUSED (a fed `paused`, no `unpaused` after it), HOLD each again for an unpause (`RN-R35`).
2833
3747
  *
2834
3748
  * **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,
3749
+ * `resumed` may have finished in the process that is gone, and runs again here. **Not at a fresh retry budget since
3750
+ * W186** (`RN-R21` v0.26, ADR 0113): it is handed on with `attemptsSpent`, the highest attempt the stored run recorded
3751
+ * 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.
3752
+ * *Until W186 it ran at a FRESH budget ([Q365](../../doc/questions/q365.md) — measured: a budget of two attempts ran
3753
+ * three times across one kill).* A task parked at `user_action` has SETTLED and is not in flight: it stays parked,
2838
3754
  * and `resolve()` works on it.
2839
3755
  *
2840
3756
  * @throws MultipleRuns — the driver has already started or resumed a run.
2841
3757
  * @throws ResumeRefused — see {@link ResumeRefusalReason}; the driver is then spent.
3758
+ * @throws the consumer's own error — a callback that threw on the `resumed` element, or on an element the resume's step
3759
+ * appended after it: the run is HALTED and reported once, and nothing further is handed on (P36 (c), W184).
2842
3760
  */
2843
3761
  resume(checkpoint) {
3762
+ this.stepping += 1;
3763
+ try {
3764
+ this.resumeStep(checkpoint);
3765
+ } finally {
3766
+ this.leaveStep();
3767
+ }
3768
+ }
3769
+ /** `resume`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3770
+ resumeStep(checkpoint) {
2844
3771
  if (this.started) {
2845
3772
  throw new MultipleRuns(
2846
3773
  `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 +3796,49 @@ var Driver = class {
2869
3796
  this.replay = null;
2870
3797
  if (this.failed) {
2871
3798
  this.deaf = true;
2872
- const at = this.inputs.findIndex((e) => e.kind === "stopped");
3799
+ const at = this.inputs.findIndex((e) => e.kind === "halted");
2873
3800
  throw new ResumeRefused(
2874
- "stopped",
3801
+ "halted",
2875
3802
  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.`
3803
+ `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
3804
  );
2878
3805
  }
3806
+ if (this.stopRequest !== null && this.stopRequest.pending === null) this.stopRequest = null;
2879
3807
  const inFlight = this.inFlight();
2880
- this.append({ kind: "resumed", taskIds: inFlight });
2881
- try {
3808
+ const holding = this.paused;
3809
+ for (const id of inFlight) {
3810
+ const config = replay.held.get(id);
3811
+ const spent = this.lastAttempt.get(id) ?? 0;
3812
+ if (config !== void 0 && spent > 0) replay.held.set(id, { ...config, attemptsSpent: spent });
3813
+ }
3814
+ if (holding) {
2882
3815
  for (const id of inFlight) {
2883
3816
  const config = replay.held.get(id);
2884
3817
  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));
3818
+ this.pauseHeld.set(id, config);
3819
+ }
3820
+ } else {
3821
+ for (const id of inFlight) this.unhanded.add(id);
3822
+ }
3823
+ try {
3824
+ this.append({ kind: "resumed", taskIds: inFlight });
3825
+ for (const id of holding ? [] : inFlight) {
3826
+ if (this.holdsResume()) break;
3827
+ const config = replay.held.get(id);
3828
+ if (config === void 0) throw new Error(`resume: in-flight task "${id}" has no held dispatch \u2014 this is a defect in the driver`);
3829
+ this.handOn(id, config);
2886
3830
  }
2887
3831
  } catch (e) {
2888
- this.failRun(e);
3832
+ this.haltOnce(e);
2889
3833
  throw e;
3834
+ } finally {
3835
+ this.unhanded.clear();
2890
3836
  }
2891
3837
  }
2892
3838
  /**
2893
3839
  * **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.
3840
+ * turns a refusal routing raises into a `halted` element exactly as the listener's boundary does
3841
+ * (`OR-R10`), so the replay regenerates a halt where the stored run had one.
2896
3842
  */
2897
3843
  replayStored(stored) {
2898
3844
  const replay = this.replay;
@@ -2906,16 +3852,35 @@ var Driver = class {
2906
3852
  }
2907
3853
  if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
2908
3854
  }
2909
- /** One fed element — a settlement, a resolution, or an earlier resume — at stored position `i`. */
3855
+ /** One fed element — a settlement, a resolution, an earlier resume, a stop, a pause, an unpause or an attempt — at stored position `i`. */
2910
3856
  feedOne(e, i) {
2911
3857
  if (e.kind === "resumed") {
2912
3858
  const now = this.inFlight();
2913
3859
  if (!Array.isArray(e.taskIds) || e.taskIds.length !== now.length || e.taskIds.some((t, k) => t !== now[k])) {
2914
3860
  throw this.diverged(i, `in flight in the replay here: ${JSON.stringify(now)}`);
2915
3861
  }
3862
+ this.stopRequest = null;
2916
3863
  this.append({ kind: "resumed", taskIds: [...e.taskIds] });
2917
3864
  return;
2918
3865
  }
3866
+ if (e.kind === "interrupted") {
3867
+ this.feedInterrupted(e);
3868
+ return;
3869
+ }
3870
+ if (e.kind === "paused" || e.kind === "unpaused") {
3871
+ this.paused = e.kind === "paused";
3872
+ this.append({ kind: e.kind, actor: e.actor });
3873
+ return;
3874
+ }
3875
+ if (e.kind === "attempted") {
3876
+ const n = e.attemptNumber;
3877
+ if (typeof e.taskId !== "string" || !this.dispatched.has(e.taskId)) throw this.diverged(i, "the replay never dispatched that task");
3878
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw this.diverged(i, "its attemptNumber is not a positive integer");
3879
+ 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");
3880
+ this.lastAttempt.set(e.taskId, n);
3881
+ this.append({ kind: "attempted", taskId: e.taskId, attemptNumber: n });
3882
+ return;
3883
+ }
2919
3884
  if (e.kind !== "outcome" && e.kind !== "resolution") return;
2920
3885
  const record = e.record;
2921
3886
  let parts;
@@ -2932,7 +3897,7 @@ var Driver = class {
2932
3897
  this.settle(cloneRecord(record), runId, nodeId, branchPath);
2933
3898
  return;
2934
3899
  }
2935
- if (!isSuspended(this.log, record.taskId)) throw this.diverged(i, "that task is not parked in the replay");
3900
+ if (!this.parked(record.taskId)) throw this.diverged(i, "that task is not parked in the replay");
2936
3901
  this.appendResolution(cloneRecord(record), nodeId, branchPath);
2937
3902
  this.advance(runId, nodeId, branchPath);
2938
3903
  }
@@ -2960,9 +3925,7 @@ var Driver = class {
2960
3925
  * handing it to a worker again would run what a person was asked to decide.
2961
3926
  */
2962
3927
  inFlight() {
2963
- const settled = /* @__PURE__ */ new Set();
2964
- for (const r of this.log) if (r.kind === "outcome") settled.add(r.taskId);
2965
- return [...this.dispatched].filter((id) => !settled.has(id));
3928
+ return [...this.dispatched].filter((id) => !this.taskLog.of(id).some((r) => r.kind === "outcome"));
2966
3929
  }
2967
3930
  /**
2968
3931
  * **Whether a callback fires for the element at `seq`** — `RN-R24`. Always, except during a replay,
@@ -2979,14 +3942,43 @@ var Driver = class {
2979
3942
  * second resolution is refused like any other. A rejected command, not a run
2980
3943
  * failure: it came from outside and a caller can retry.
2981
3944
  *
3945
+ * **And an ASK's answer** — `RS-R1` v0.18, `RN-R37`, ADR 0109: an ask the run reached and nobody has answered is
3946
+ * suspended too, so the person's answer is this call — `'done'` with the answer as `result`, held to the ask's
3947
+ * `answerSchema` (`FD-R4`), or `'failed'` with an `error`, the person DECLINING, held to nothing. The answer IS the
3948
+ * node's result and routes as a task's settlement does.
3949
+ *
2982
3950
  * @returns `false` when refused. Nothing is appended and the run is unaffected.
2983
3951
  */
2984
3952
  resolve(taskId, resolvedAs, value) {
2985
- if (!isSuspended(this.log, taskId)) return false;
3953
+ const midStep = this.stepping > 0;
3954
+ this.stepping += 1;
3955
+ try {
3956
+ return this.resolveStep(taskId, resolvedAs, value, midStep);
3957
+ } finally {
3958
+ this.leaveStep();
3959
+ }
3960
+ }
3961
+ /** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3962
+ resolveStep(taskId, resolvedAs, value, midStep = false) {
3963
+ if (!this.parked(taskId)) return false;
3964
+ for (const c of this.pausePending) if (c.kind === "answer" && c.record.taskId === taskId) return false;
2986
3965
  const { runId, nodeId, branchPath } = parseTaskId(taskId);
2987
- if (this.failed) return false;
3966
+ if (this.shut() || this.stopRequest !== null) return false;
2988
3967
  if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
2989
3968
  const n = this.graph.byId.get(nodeId);
3969
+ if (n?.kind === "ask") {
3970
+ const schema2 = n.answerSchema;
3971
+ const fs = schema2 === void 0 ? [] : findings(schema2, value.result ?? null);
3972
+ if (fs.length) {
3973
+ throw new BoundaryRefused(
3974
+ "resolve",
3975
+ nodeId,
3976
+ taskId,
3977
+ fs,
3978
+ `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.`
3979
+ );
3980
+ }
3981
+ }
2990
3982
  const type = n?.kind === "task" ? this.nodeTypes.get(n.type) : void 0;
2991
3983
  const schema = type?.worker === void 0 ? void 0 : this.workers.get(type.worker)?.outputSchema;
2992
3984
  if (schema !== void 0) {
@@ -3002,7 +3994,7 @@ var Driver = class {
3002
3994
  }
3003
3995
  }
3004
3996
  }
3005
- this.appendResolution({
3997
+ const record = {
3006
3998
  kind: "resolution",
3007
3999
  taskId,
3008
4000
  resolvedAs,
@@ -3010,45 +4002,597 @@ var Driver = class {
3010
4002
  error: resolvedAs === "failed" ? value.error ?? "resolved as failed" : null,
3011
4003
  actor: value.actor,
3012
4004
  observedAt: this.now()
3013
- }, nodeId, branchPath);
4005
+ };
4006
+ if (midStep) {
4007
+ this.pausePending.push({ kind: "answer", record, runId, nodeId, branchPath });
4008
+ return true;
4009
+ }
4010
+ try {
4011
+ this.appendResolution(record, nodeId, branchPath);
4012
+ } catch (e) {
4013
+ this.haltOnce(e);
4014
+ throw e;
4015
+ }
4016
+ if (this.shut()) return true;
4017
+ try {
4018
+ this.advance(runId, nodeId, branchPath);
4019
+ } catch (e) {
4020
+ this.haltOnce(e);
4021
+ throw e;
4022
+ }
4023
+ return true;
4024
+ }
4025
+ /**
4026
+ * **A resolution appended, and the region told** — the half of `resolve()` the REPLAY shares
4027
+ * (`RN-R19`): a stored resolution is appended as it was stored and routes exactly as the live one
4028
+ * did, so a resumed run's `branch.resumed` and everything after it are regenerated rather than
4029
+ * written by a second road.
4030
+ */
4031
+ appendResolution(record, nodeId, branchPath) {
4032
+ this.append({ kind: "resolution", record });
4033
+ if (this.shut()) return;
4034
+ const region = this.regionOf(nodeId);
4035
+ if (region) {
4036
+ this.emit({
4037
+ type: "branch.resumed",
4038
+ regionId: region.fanoutId,
4039
+ regionPath: this.rpathOf(region, this.indicesFrom(nodeId, branchPath)),
4040
+ branchPath,
4041
+ at: nodeId,
4042
+ resolvedAs: record.resolvedAs
4043
+ });
4044
+ }
4045
+ }
4046
+ /**
4047
+ * **Cancel the run: hold it, record the request, and ask the scheduler to cancel each task in flight** — `RN-R31`,
4048
+ * `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),
4049
+ * P34 decision 4 and stop-and-asks (b), (c), (i), (j). **Resolves once every task it cancelled has stopped**; the fold
4050
+ * reads the run `cancelling` until then and `cancelled` after (`RN-R30`), so a runtime's `done` settles after the last
4051
+ * `task.cancelled` from the fold alone.
4052
+ *
4053
+ * **In order, and each step is a rule:**
4054
+ * 1. **Refuse** — {@link RunControlRefused}, before anything is recorded or sent; and a `graceMs` the scheduler
4055
+ * would throw on is thrown here, as it would be (`TypeError`, `RangeError`).
4056
+ * 2. **Hold** (`RN-R29`): `cancelRequest` is set, so `shut()` is true for every door; every region's collection is
4057
+ * released, `failRun`'s run-level moment (ADR 0046 rule 3, `JP-R12`) — no join of this run settles again.
4058
+ * 3. **Record** ONE `cancelRequested` element, BEFORE the scheduler is asked for anything.
4059
+ * 4. **Cancel** each task in flight the scheduler holds — `RN-R21`'s set, so a PARKED task, settled already, is sent
4060
+ * nothing (P34 (i); the scheduler cannot end it, fact 8). A task not yet handed on is cancelled by `handOn` after
4061
+ * its hand-off (`RN-R29` (i)), and so is each task a PAUSE held (`RN-R35`, P34 (c)). Tasks a worker SPAWNED are
4062
+ * not this run's (P34 (j)). **In a `finally` around step 3** (`RN-R31` v0.21, P35 decision 4): a consumer's
4063
+ * `onFoldInput` that throws on the element still has every cancel sent and every held task handed on and
4064
+ * cancelled — and THEN the run is HALTED and reported once, and the throw reaches the caller (`RN-R31` v0.24,
4065
+ * `OR-R10` v0.19, P36 (b), W184).
4066
+ *
4067
+ * **A SECOND call** appends nothing and forwards its `graceMs` to each task still stopping — the scheduler's own
4068
+ * escalation — and resolves once those have stopped. It is never refused for the halt its own first call made.
4069
+ *
4070
+ * @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
4071
+ * @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
4072
+ */
4073
+ cancel(options) {
4074
+ const cancelOf = this.opts.scheduler.cancel;
4075
+ if (typeof cancelOf !== "function") {
4076
+ throw new RunControlRefused(
4077
+ "cancel",
4078
+ "unsupported",
4079
+ `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.`
4080
+ );
4081
+ }
4082
+ const graceMs = options?.graceMs;
4083
+ if (graceMs !== void 0) {
4084
+ if (typeof graceMs !== "number") throw new TypeError(`cancel: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
4085
+ if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`cancel: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
4086
+ }
4087
+ if (this.header === null) {
4088
+ throw new RunControlRefused(
4089
+ "cancel",
4090
+ "not-started",
4091
+ `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.`
4092
+ );
4093
+ }
4094
+ const req = this.cancelRequest;
4095
+ if (req !== null) {
4096
+ if (graceMs !== void 0) req.graceMs = graceMs;
4097
+ for (const id of this.inFlight()) if (this.handedOn(id)) this.cancelTask(id);
4098
+ const stopping2 = req.stopping;
4099
+ return Promise.resolve().then(() => Promise.all([...stopping2.values()])).then(() => void 0);
4100
+ }
4101
+ if (this.failed) {
4102
+ throw new RunControlRefused(
4103
+ "cancel",
4104
+ "halted",
4105
+ `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.`
4106
+ );
4107
+ }
4108
+ if (this.cancelled) {
4109
+ throw new RunControlRefused(
4110
+ "cancel",
4111
+ "cancelled",
4112
+ `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.`
4113
+ );
4114
+ }
4115
+ if (this.stopRequest !== null) {
4116
+ throw new RunControlRefused(
4117
+ "cancel",
4118
+ "interrupted",
4119
+ `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.`
4120
+ );
4121
+ }
4122
+ const inFlight = this.inFlight();
4123
+ if (this.stepping === 0 && inFlight.length === 0 && !this.hasParked()) {
4124
+ throw new RunControlRefused(
4125
+ "cancel",
4126
+ "ended",
4127
+ `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.`
4128
+ );
4129
+ }
4130
+ const reason = options.reason;
4131
+ const stopping = /* @__PURE__ */ new Map();
4132
+ this.cancelRequest = {
4133
+ ...reason !== void 0 ? { reason } : {},
4134
+ ...graceMs !== void 0 ? { graceMs } : {},
4135
+ stopping
4136
+ };
4137
+ this.arrivals.clear();
4138
+ this.releaseAllItems();
4139
+ const released = new Map(this.pauseHeld);
4140
+ this.pauseHeld.clear();
4141
+ for (const id of released.keys()) this.unhanded.add(id);
4142
+ try {
4143
+ try {
4144
+ this.append({ kind: "cancelRequested", actor: options.actor, reason: reason ?? null });
4145
+ } finally {
4146
+ try {
4147
+ for (const id of this.inFlight()) {
4148
+ const config = released.get(id);
4149
+ if (config !== void 0) this.handOn(id, config);
4150
+ else if (this.handedOn(id)) this.cancelTask(id);
4151
+ }
4152
+ } finally {
4153
+ for (const id of released.keys()) this.unhanded.delete(id);
4154
+ }
4155
+ }
4156
+ } catch (e) {
4157
+ this.haltOnce(e);
4158
+ throw e;
4159
+ }
4160
+ return Promise.resolve().then(() => Promise.all([...stopping.values()])).then(() => void 0);
4161
+ }
4162
+ /**
4163
+ * **Stop the run so that a resume ELSEWHERE can continue it: hold it, record the stop, go DEAF, and only then ask the
4164
+ * scheduler to cancel each task in flight** — `RN-R33`, `spec/run-state.md` section 14,
4165
+ * [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md), P34
4166
+ * decisions 3 and 9 and stop-and-ask (d). **Resolves once every task it cancelled has stopped**; the fold reads the run
4167
+ * `interrupted` from the element on (`RN-R32`), and the log ends with the cancelled tasks dispatched and UNSETTLED —
4168
+ * the in-flight set a resume on another runtime re-dispatches at the same ids (`RN-R21`).
4169
+ *
4170
+ * **In order, and the ORDER is the rule** (P34 decision 3):
4171
+ * 1. **Refuse** — {@link RunControlRefused} with `control: 'stop'`, before anything is recorded or sent: `unsupported`,
4172
+ * `not-started`, `halted`, `cancelled` (`RN-R28`'s, or a cancel its owner requested — P34 (c)) and `ended` (at
4173
+ * rest only, as for a cancel); and a `graceMs` the scheduler would throw on is thrown here. From the accepted call
4174
+ * on, `resolve()` and `cancel()` are refused (P34 decision 9, (c)).
4175
+ * 2. **Hold** (`RN-R29`): the stop is TAKEN, so `shut()` is true for every door, and every region's collection is
4176
+ * released.
4177
+ * 3. **Go DEAF**: nothing the scheduler fires for this run is heard again — from BEFORE the element, so not even a
4178
+ * settlement a callback on it provokes — and no `task.cancelled` of the stop is appended.
4179
+ * 4. **Record** ONE `interrupted` element.
4180
+ * 5. **Cancel** each task in flight the scheduler holds — none for a parked task (P34 fact 4), and none for a task a
4181
+ * PAUSE held, which the scheduler was never handed and which stays in flight for the resume (`RN-R35`).
4182
+ *
4183
+ * **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
4184
+ * is made from a callback on that step (`stepping`). A hold taken mid-step would drop the rest of the step's routing
4185
+ * — a fan-out's later branches, a settlement's successor — and a resume re-dispatches only what is IN FLIGHT, so that
4186
+ * work would be lost to the resumed run for good. Taken at rest, the step is whole in the log; the task whose own
4187
+ * `dispatched` element carried the call is handed on in the step and cancelled here (`RN-R29` (i)), and so is a resumed
4188
+ * run's in-flight set, handed back before `resume()` returns.
4189
+ *
4190
+ * **A SECOND call** appends nothing, forwards its `graceMs` to each task still stopping, and resolves as the first does.
4191
+ *
4192
+ * @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
4193
+ * @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
4194
+ */
4195
+ stop(options) {
4196
+ if (typeof this.opts.scheduler.cancel !== "function") {
4197
+ throw new RunControlRefused(
4198
+ "stop",
4199
+ "unsupported",
4200
+ `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.`
4201
+ );
4202
+ }
4203
+ const graceMs = options?.graceMs;
4204
+ if (graceMs !== void 0) {
4205
+ if (typeof graceMs !== "number") throw new TypeError(`stop: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
4206
+ if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`stop: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
4207
+ }
4208
+ if (this.header === null) {
4209
+ throw new RunControlRefused(
4210
+ "stop",
4211
+ "not-started",
4212
+ `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.`
4213
+ );
4214
+ }
4215
+ const req = this.stopRequest;
4216
+ if (req !== null) {
4217
+ if (graceMs !== void 0) req.graceMs = graceMs;
4218
+ if (req.pending === null) {
4219
+ for (const id of this.inFlight()) if (this.handedOn(id) && req.stopping.has(id)) this.cancelTask(id);
4220
+ return this.stopSettled(req);
4221
+ }
4222
+ return req.took.then(() => this.stopSettled(req));
4223
+ }
4224
+ if (this.failed) {
4225
+ throw new RunControlRefused(
4226
+ "stop",
4227
+ "halted",
4228
+ `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.`
4229
+ );
4230
+ }
4231
+ if (this.cancelled || this.cancelRequest !== null) {
4232
+ throw new RunControlRefused(
4233
+ "stop",
4234
+ "cancelled",
4235
+ `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.`
4236
+ );
4237
+ }
4238
+ if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
4239
+ throw new RunControlRefused(
4240
+ "stop",
4241
+ "ended",
4242
+ `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.`
4243
+ );
4244
+ }
4245
+ const reason = options.reason;
4246
+ let taken;
4247
+ const took = new Promise((r) => {
4248
+ taken = r;
4249
+ });
4250
+ const accepted = {
4251
+ actor: options.actor,
4252
+ ...reason !== void 0 ? { reason } : {},
4253
+ ...graceMs !== void 0 ? { graceMs } : {},
4254
+ stopping: /* @__PURE__ */ new Map(),
4255
+ pending: taken,
4256
+ took
4257
+ };
4258
+ this.stopRequest = accepted;
4259
+ if (this.stepping === 0) this.takeStop();
4260
+ return took.then(() => this.stopSettled(accepted));
4261
+ }
4262
+ /**
4263
+ * **The stop, TAKEN** — `RN-R33` steps 2-5, at rest: from `stop()` itself, or from the end of the driver's own step
4264
+ * when `stop()` was called on it (each step's wrapper calls this when `stepping` returns to `0`).
4265
+ */
4266
+ takeStop() {
4267
+ const req = this.stopRequest;
4268
+ const taken = req.pending;
4269
+ req.pending = null;
4270
+ req.took = null;
4271
+ this.arrivals.clear();
4272
+ this.releaseAllItems();
4273
+ this.deaf = true;
4274
+ const inFlight = this.inFlight().filter((id) => this.handedOn(id));
4275
+ const silent = this.failed;
4276
+ try {
4277
+ try {
4278
+ this.append({ kind: "interrupted", actor: req.actor, reason: req.reason ?? null });
4279
+ } finally {
4280
+ try {
4281
+ for (const id of inFlight) this.cancelTask(id);
4282
+ } finally {
4283
+ taken();
4284
+ }
4285
+ }
4286
+ } catch (e) {
4287
+ if (silent) return;
4288
+ this.haltOnce(e);
4289
+ throw e;
4290
+ }
4291
+ }
4292
+ /** What a stop's promise resolves on once it is taken: a microtask on, every task it sent a cancel to has stopped. */
4293
+ stopSettled(req) {
4294
+ return Promise.resolve().then(() => Promise.all([...req.stopping.values()])).then(() => void 0);
4295
+ }
4296
+ /**
4297
+ * **Pause the run: it routes on, and every dispatch it makes from here is appended and HELD until `unpause()`** —
4298
+ * `RN-R35`, `spec/run-state.md` section 15,
4299
+ * [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md),
4300
+ * P34 stop-and-ask (e). Tasks already handed to the scheduler run on and settle, and their settlements route as ever; a
4301
+ * person's `resolve()` is accepted. **Not a halt** (`shut()` is untouched): only the scheduler's `addTask` waits. The
4302
+ * fold reads `RunState.paused` and no new outcome — a held task is `dispatched`, so it reads `running` (P34 fact 3).
4303
+ *
4304
+ * **In order**: (1) **refused by name** before anything is recorded — {@link RunControlRefused} with `control: 'pause'`:
4305
+ * `not-started`, `halted`, `cancelled` (`RN-R28`'s or a requested cancel), `interrupted` (a stop, taken or pending),
4306
+ * `paused` (already paused, or a pause called earlier in this step still to be taken — P34 (e): *a second pause is
4307
+ * refused by name*) and `ended` (nothing in flight and nothing parked, at rest only); (2) **TAKEN AT REST** — at once
4308
+ * 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
4309
+ * `paused` element is appended, BETWEEN two steps, where a resume's replay can feed it (`RN-R19`). A pause taken
4310
+ * mid-step would land between two elements of one step, and the replay — which regenerates a step whole — would refuse
4311
+ * its checkpoint `diverged` there; so the step a pause is called on finishes unpaused, its dispatches handed on.
4312
+ */
4313
+ pause(options) {
4314
+ this.refuseRunHeld("pause");
4315
+ if (this.pauseWanted()) {
4316
+ throw new RunControlRefused(
4317
+ "pause",
4318
+ "paused",
4319
+ `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.`
4320
+ );
4321
+ }
4322
+ if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
4323
+ throw new RunControlRefused(
4324
+ "pause",
4325
+ "ended",
4326
+ `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.`
4327
+ );
4328
+ }
4329
+ this.pausePending.push({ kind: "paused", actor: options.actor });
4330
+ while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
4331
+ }
4332
+ /**
4333
+ * **Unpause the run: record it, then hand every held task to the scheduler in DISPATCH ORDER** — `RN-R35`, ADR 0106,
4334
+ * P34 (e). Refused by name — `control: 'unpause'`: `not-started`, `halted`, `cancelled`, `interrupted` (a stopped
4335
+ * run's held tasks are a resume's: unpause it where it resumes) and `not-paused`. **Never `ended`**: an unpause rewrites
4336
+ * no outcome, so a paused run that finished may still be unpaused. Taken at rest, as a pause is: ONE `unpaused` element,
4337
+ * then the hand-off — each held task's `createTask` and `addTask`, first held first.
4338
+ */
4339
+ unpause(options) {
4340
+ this.refuseRunHeld("unpause");
4341
+ if (!this.pauseWanted()) {
4342
+ throw new RunControlRefused(
4343
+ "unpause",
4344
+ "not-paused",
4345
+ `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.`
4346
+ );
4347
+ }
4348
+ this.pausePending.push({ kind: "unpaused", actor: options.actor });
4349
+ while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
4350
+ }
4351
+ /** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
4352
+ pauseWanted() {
4353
+ const last = [...this.pausePending].filter((c) => c.kind !== "answer").at(-1);
4354
+ return last !== void 0 ? last.kind === "paused" : this.paused;
4355
+ }
4356
+ /**
4357
+ * 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
4358
+ * release: none started, or the run halted, cancelled or stopped. Thrown before anything is recorded.
4359
+ */
4360
+ refuseRunHeld(control) {
4361
+ if (this.header === null) {
4362
+ throw new RunControlRefused(
4363
+ control,
4364
+ "not-started",
4365
+ `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.`
4366
+ );
4367
+ }
4368
+ if (this.failed) {
4369
+ throw new RunControlRefused(
4370
+ control,
4371
+ "halted",
4372
+ `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.`
4373
+ );
4374
+ }
4375
+ if (this.cancelled || this.cancelRequest !== null) {
4376
+ throw new RunControlRefused(
4377
+ control,
4378
+ "cancelled",
4379
+ `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.`
4380
+ );
4381
+ }
4382
+ if (this.stopRequest !== null) {
4383
+ throw new RunControlRefused(
4384
+ control,
4385
+ "interrupted",
4386
+ `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.`
4387
+ );
4388
+ }
4389
+ }
4390
+ /**
4391
+ * **A pause or an unpause, TAKEN** — `RN-R35`, at rest: from the call itself, or from the end of the driver's own step
4392
+ * (`leaveStep()`). **A step of its own** (`stepping`), so a call a consumer makes from a callback on the element — a
4393
+ * second pause, a stop, a `resolve()` that dispatches — is taken, or routes, as on any step. **A pause** sets the flag
4394
+ * BEFORE its element, so a dispatch a callback on the element causes is held. **An unpause** clears it before its
4395
+ * element and hands the held tasks on AFTER it, in a `finally`: a consumer's `onFoldInput` that throws on the element
4396
+ * 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
4397
+ * lesson) — and then the take HALTS the run itself, reported once (`takePause`, `RN-R35` v0.24, W184), so a call pending
4398
+ * behind it is dropped, and the throw reaches whoever is on the stack: the caller of the call at rest or of the driver
4399
+ * 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
4400
+ * of the held queue (`dispatch`), so the order is the dispatch order still.
4401
+ */
4402
+ takePending(c) {
4403
+ if (c.kind === "answer") this.takeAnswer(c);
4404
+ else this.takePause(c);
4405
+ }
4406
+ /**
4407
+ * **A resolution given mid-step, TAKEN at rest** — `RN-R37`, W180 (the conformity reader's R4; S-4 for every
4408
+ * resolution): a step of its own, so the resolution lands BETWEEN two steps — where a resume's replay FEEDS it
4409
+ * (`RN-R19`) — after the step that parked its node has completed (an ask's `asked` or a task's `user_action`
4410
+ * settlement, and in a region `branch.suspended`). Answered on its own `asked` element and appended
4411
+ * there instead, it sat between two elements of one step, and the replay — which regenerates a step whole — refused the
4412
+ * run's checkpoint `diverged` at it. **Dropped, recording nothing, when the step it was given on HALTED or CANCELLED the
4413
+ * run** (or the node is no longer parked) — `takePause`'s rule: it would be refused were it given now. It routes as
4414
+ * `resolve()` at rest routes: a throw — from routing, or a consumer's on the resolution's own element — HALTS the run
4415
+ * here, reported once, so a call pending behind it is dropped; it then reaches whoever is on the stack: a driver call's
4416
+ * caller, or the listener boundary of a scheduler event's step (`onEvent`, `OR-R10` v0.19, W184).
4417
+ */
4418
+ takeAnswer(c) {
4419
+ if (this.failed || this.cancelled || this.cancelRequest !== null || !this.parked(c.record.taskId)) return;
4420
+ this.stepping += 1;
4421
+ try {
4422
+ try {
4423
+ this.appendResolution(c.record, c.nodeId, c.branchPath);
4424
+ if (this.shut()) return;
4425
+ this.advance(c.runId, c.nodeId, c.branchPath);
4426
+ } catch (e) {
4427
+ this.haltOnce(e);
4428
+ throw e;
4429
+ }
4430
+ } finally {
4431
+ this.leaveStep();
4432
+ }
4433
+ }
4434
+ takePause(p) {
4435
+ if (this.failed || this.cancelled || this.cancelRequest !== null) return;
4436
+ this.stepping += 1;
4437
+ try {
4438
+ try {
4439
+ if (p.kind === "paused") {
4440
+ this.paused = true;
4441
+ this.append({ kind: "paused", actor: p.actor });
4442
+ } else {
4443
+ this.paused = false;
4444
+ try {
4445
+ this.append({ kind: "unpaused", actor: p.actor });
4446
+ } finally {
4447
+ this.drainHeld();
4448
+ }
4449
+ }
4450
+ } catch (e) {
4451
+ this.haltOnce(e);
4452
+ throw e;
4453
+ }
4454
+ } finally {
4455
+ this.leaveStep();
4456
+ }
4457
+ }
4458
+ /**
4459
+ * **The unpause's hand-off**: each held task to the scheduler, first held first (`RN-R35`). Taken out of the hold and
4460
+ * marked `unhanded` before its `addTask`, as a resume's re-dispatch is (`RN-R31`), so a cancel raised inside one
4461
+ * hand-off sends nothing for a task not yet handed — and `handOn` then cancels it. **Held for a HALT** — checked before
4462
+ * each, as a resume's re-dispatch is (`RN-R29` (4), `holdsResume()`): a halt raised inside one hand-off hands nothing
4463
+ * further on, and what is left is never handed. A dispatch made on this stack joins the end of the queue and is handed
4464
+ * on in its turn (the `Map` visits what is added while it is walked). Caller-initiated, so a throw from the scheduler
4465
+ * is reported AND rethrown, `start()`'s and `resume()`'s treatment.
4466
+ */
4467
+ drainHeld() {
3014
4468
  try {
3015
- this.advance(runId, nodeId, branchPath);
4469
+ for (const [id, config] of this.pauseHeld) {
4470
+ if (this.holdsResume()) break;
4471
+ this.pauseHeld.delete(id);
4472
+ this.unhanded.add(id);
4473
+ this.handOn(id, config);
4474
+ }
3016
4475
  } catch (e) {
3017
4476
  this.failRun(e);
3018
4477
  throw e;
3019
4478
  }
3020
- return true;
3021
4479
  }
3022
4480
  /**
3023
- * **A resolution appended, and the region told** — the half of `resolve()` the REPLAY shares
3024
- * (`RN-R19`): a stored resolution is appended as it was stored and routes exactly as the live one
3025
- * did, so a resumed run's `branch.resumed` and everything after it are regenerated rather than
3026
- * written by a second road.
4481
+ * Whether the scheduler holds this task — dispatched, not held by a replay, past its `addTask` (`RN-R31`), and not held
4482
+ * by a pause (`RN-R35`, W175): a stop of a paused run cancels only what this says the scheduler holds (ADR 0105's last
4483
+ * consequence), and a cancel hands each held task on before it cancels it.
3027
4484
  */
3028
- appendResolution(record, nodeId, branchPath) {
3029
- this.append({ kind: "resolution", record });
3030
- const region = this.regionOf(nodeId);
3031
- if (region) {
3032
- this.emit({
3033
- type: "branch.resumed",
3034
- regionId: region.fanoutId,
3035
- regionPath: this.rpathOf(region, this.indicesFrom(nodeId, branchPath)),
3036
- branchPath,
3037
- at: nodeId,
3038
- resolvedAs: record.resolvedAs
3039
- });
3040
- }
4485
+ handedOn(taskId) {
4486
+ return this.replay === null && !this.unhanded.has(taskId) && !this.pauseHeld.has(taskId);
3041
4487
  }
3042
4488
  // --- the scheduler's side ----------------------------------------------------
3043
4489
  onEvent(name, detail) {
4490
+ this.stepping += 1;
4491
+ try {
4492
+ this.onEventStep(name, detail);
4493
+ } finally {
4494
+ try {
4495
+ this.leaveStep();
4496
+ } catch (e) {
4497
+ try {
4498
+ if (!this.failed) this.failRun(e);
4499
+ } catch {
4500
+ }
4501
+ }
4502
+ }
4503
+ }
4504
+ /** `onEvent`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
4505
+ onEventStep(name, detail) {
3044
4506
  if (this.deaf || this.started && this.header === null) return;
3045
4507
  try {
3046
4508
  this.onEventOrThrow(name, detail);
3047
4509
  } catch (e) {
3048
- this.failRun(e);
4510
+ try {
4511
+ this.haltOnce(e);
4512
+ } catch {
4513
+ }
4514
+ }
4515
+ }
4516
+ /**
4517
+ * **A `task.running`: an attempt started** — `RN-R40`, W186, P36 (f). Its id and number are COPIED here, while the
4518
+ * listener runs (`OR-R1`). Recorded only for a task THIS run dispatched, and only when the number EXCEEDS the last one
4519
+ * recorded for it — a requeue repeats its number and appends nothing (the scheduler's own recipe's `max`). **Everything
4520
+ * else is dropped in SILENCE, and none of it is a refusal**: an id that does not parse (a plain task on a shared
4521
+ * scheduler — its SETTLEMENT is what `onUnmintedTask` reports, `ID-R5`), another run's (`ID-R4` tier 1), one this run
4522
+ * never dispatched (its settlement is `ID-R4` tier 2's `UndispatchedTask`), and a detail carrying no positive integer
4523
+ * `attemptNumber` (a scheduler older than the pin, or a double). An attempt start never stops a run by existing. **Queued, not
4524
+ * appended**: {@link Driver.takeAttempts} appends it when the step it was heard on ends — at once, on a scheduler
4525
+ * event's own step; at the step's end when an inline pool fires it inside the driver's `addTask`.
4526
+ */
4527
+ heardRunning(detail) {
4528
+ const taskId = detail?.id;
4529
+ const attemptNumber = attemptNumberOf(detail);
4530
+ if (typeof taskId !== "string" || attemptNumber === null) return;
4531
+ let runId;
4532
+ try {
4533
+ runId = parseTaskId(taskId).runId;
4534
+ } catch (e) {
4535
+ if (e instanceof InvalidTaskId) return;
4536
+ throw e;
4537
+ }
4538
+ if (runId !== this.runId || !this.dispatched.has(taskId)) return;
4539
+ if (attemptNumber <= (this.lastAttempt.get(taskId) ?? 0)) return;
4540
+ this.lastAttempt.set(taskId, attemptNumber);
4541
+ this.attemptsPending.push({ taskId, attemptNumber });
4542
+ }
4543
+ /**
4544
+ * **A `task.message` or a `task.progress`: PASSED THROUGH to its callback** — `RN-R42`, W187, P36 (k), ADR 0114. The
4545
+ * task's id is the detail's `taskId` for a message — `task.message` carries no `id` (P36 fact 2) — and its `id` for a
4546
+ * progress report, whose detail is the task spread flat. Handed on only for a task THIS run dispatched (`ID-R4`'s two
4547
+ * tiers, asked SILENTLY, as {@link Driver.heardRunning} asks them): anything else is dropped and is not a refusal. Every
4548
+ * field is COPIED here, while the listener runs (ADR 0007 rule 1, `OR-R1`), and the copy FROZEN (`OR-R7`); a value that
4549
+ * cannot be cloned is handed on `undefined` — the scheduler's own treatment of an uncloneable `emit` (its §6) — so a
4550
+ * pass-through never stops a run by existing. **The consumer's throw is NOT caught here**: it leaves through
4551
+ * `onEventOrThrow` to the listener boundary, which reports it and halts the run (`onUnmintedTask`'s road, `OR-R10`).
4552
+ */
4553
+ heardPassThrough(name, detail) {
4554
+ const isMessage = name === "task.message";
4555
+ const taskId = isMessage ? detail?.taskId : detail?.id;
4556
+ if (typeof taskId !== "string" || detail === void 0) return;
4557
+ let parts;
4558
+ try {
4559
+ parts = parseTaskId(taskId);
4560
+ } catch (e) {
4561
+ if (e instanceof InvalidTaskId) return;
4562
+ throw e;
4563
+ }
4564
+ if (parts.runId !== this.runId || !this.dispatched.has(taskId)) return;
4565
+ const attribution = {
4566
+ runId: parts.runId,
4567
+ nodeId: parts.nodeId,
4568
+ branchPath: parts.branchPath,
4569
+ taskId,
4570
+ flowAttempt: parts.attempt,
4571
+ attemptNumber: attemptNumberOf(detail)
4572
+ };
4573
+ if (isMessage) {
4574
+ const m = {
4575
+ ...attribution,
4576
+ name: detail.name,
4577
+ data: frozenCopy(detail.data),
4578
+ seq: detail.seq,
4579
+ timestamp: detail.timestamp
4580
+ };
4581
+ if (detail.dropped !== void 0) m.dropped = detail.dropped;
4582
+ if (detail.error !== void 0) m.error = detail.error;
4583
+ this.opts.onTaskMessage?.(Object.freeze(m));
4584
+ return;
3049
4585
  }
4586
+ const p = {
4587
+ ...attribution,
4588
+ progress: detail.progress,
4589
+ message: frozenCopy(detail.message),
4590
+ details: frozenCopy(detail.details),
4591
+ timestamp: detail.timestamp
4592
+ };
4593
+ this.opts.onTaskProgress?.(Object.freeze(p));
3050
4594
  }
3051
- /** Whether this run has been stopped by a refusal. */
4595
+ /** Whether this run has been HALTED by a refusal (`OR-R10`; its outcome `halted`). */
3052
4596
  hasFailed() {
3053
4597
  return this.failed;
3054
4598
  }
@@ -3057,19 +4601,36 @@ var Driver = class {
3057
4601
  this.failed = true;
3058
4602
  const err = cause instanceof Error ? cause : new Error(String(cause));
3059
4603
  let seq = null;
4604
+ const report = () => {
4605
+ if (this.replay !== null && (seq === null || !this.delivers(seq))) return;
4606
+ this.opts.onRunFailure?.({
4607
+ runId: this.runId,
4608
+ kind: err.constructor?.name ?? "Error",
4609
+ message: err.message,
4610
+ cause
4611
+ });
4612
+ };
3060
4613
  if (!already) {
3061
- seq = this.append({ kind: "stopped", reason: `${err.constructor?.name ?? "Error"}: ${err.message}` });
3062
- this.arrivals.clear();
4614
+ try {
4615
+ seq = this.append({ kind: "halted", reason: `${err.constructor?.name ?? "Error"}: ${err.message}` });
4616
+ } finally {
4617
+ this.arrivals.clear();
4618
+ this.releaseAllItems();
4619
+ report();
4620
+ }
4621
+ return;
3063
4622
  }
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
- });
4623
+ report();
3071
4624
  }
3072
4625
  onEventOrThrow(name, detail) {
4626
+ if (name === "task.running") {
4627
+ this.heardRunning(detail);
4628
+ return;
4629
+ }
4630
+ if (name === "task.message" || name === "task.progress") {
4631
+ this.heardPassThrough(name, detail);
4632
+ return;
4633
+ }
3073
4634
  const taskId = detail?.id;
3074
4635
  let parts;
3075
4636
  try {
@@ -3102,7 +4663,13 @@ var Driver = class {
3102
4663
  const seq = this.append({ kind: "outcome", record });
3103
4664
  const report = this.opts.onNonconformingResult;
3104
4665
  if (report !== void 0 && this.delivers(seq)) this.reportResultSchema(record, nodeId, report);
3105
- if (this.failed) return;
4666
+ if (this.shut()) return;
4667
+ if (record.terminalState === "cancelled") {
4668
+ this.cancelled = true;
4669
+ this.arrivals.clear();
4670
+ this.releaseAllItems();
4671
+ return;
4672
+ }
3106
4673
  if (record.terminalState === "user_action") {
3107
4674
  const region = this.regionOf(nodeId);
3108
4675
  if (region) {
@@ -3176,7 +4743,7 @@ var Driver = class {
3176
4743
  if (!node) return;
3177
4744
  const itemIndices = this.indicesFrom(nodeId, branchPath);
3178
4745
  const taskId = buildTaskId(runId, nodeId, branchIndices(this.pathOf(nodeId, itemIndices)));
3179
- const outcome = effectiveOutcome(this.log, taskId);
4746
+ const outcome = effectiveOutcome(this.taskLog.of(taskId), taskId);
3180
4747
  if (!outcome) return;
3181
4748
  const result = selectEdge(
3182
4749
  node,
@@ -3222,9 +4789,10 @@ var Driver = class {
3222
4789
  * through within this turn; only a `task` becomes a scheduler task.
3223
4790
  */
3224
4791
  enter(runId, nodeId, payload, fromBranch = "-", fromRegion, itemIndices = EMPTY_INDICES) {
4792
+ if (this.shut()) return;
3225
4793
  const node = this.graph.byId.get(nodeId);
3226
4794
  if (!node) return;
3227
- const received = this.compose(node, payload, itemIndices);
4795
+ const received = this.compose(node, payload, itemIndices, this.runPayload(), () => this.itemOf(node.id, itemIndices));
3228
4796
  switch (node.kind) {
3229
4797
  case "task":
3230
4798
  return this.dispatch(runId, node, received, itemIndices);
@@ -3297,6 +4865,12 @@ var Driver = class {
3297
4865
  if (branch === void 0) return;
3298
4866
  const idx = items.map((_v, i) => withIndex(itemIndices, nodeId, i));
3299
4867
  const paths = idx.map((m) => this.pathOf(branch, m));
4868
+ const kept = this.items.kept.get(nodeId);
4869
+ if (kept !== void 0 && items.length > 0) {
4870
+ const byPath = this.items.held.get(nodeId) ?? /* @__PURE__ */ new Map();
4871
+ this.items.held.set(nodeId, byPath);
4872
+ items.forEach((v, i) => byPath.set(paths[i], kept ? cloneValue(v) : v));
4873
+ }
3300
4874
  for (const p of paths) {
3301
4875
  this.emit({
3302
4876
  type: "branch.opened",
@@ -3372,14 +4946,14 @@ var Driver = class {
3372
4946
  case "gate": {
3373
4947
  const decided = this.decideGate(node, received, this.memoryOf(nodeId));
3374
4948
  if ("threw" in decided) throw decided.threw;
3375
- const type = decided.type;
4949
+ const set = decided.set;
3376
4950
  if (!decided.declared) {
3377
4951
  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.`
4952
+ `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
4953
  );
3380
4954
  }
3381
4955
  const route = decided.route;
3382
- const edge = gateEdge(node, type.defaultRoute, route);
4956
+ const edge = gateEdge(node, set.defaultRoute, route);
3383
4957
  if (edge === null) {
3384
4958
  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
4959
  }
@@ -3389,6 +4963,12 @@ var Driver = class {
3389
4963
  this.enter(runId, edge.to, received, fromBranch, fromRegion, itemIndices);
3390
4964
  return;
3391
4965
  }
4966
+ // **An ASK PARKS, and nothing is handed to the scheduler** — `RN-R37`, v0.22,
4967
+ // [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
4968
+ // [W180](../../doc/work-items/w180-asking-and-answering-in-a-run.md), P35 (b)-(f). It replaces ADR 0108 rule 8's
4969
+ // interim refusal at this arm. See `ask()`.
4970
+ case "ask":
4971
+ return this.ask(runId, node, received, itemIndices);
3392
4972
  // **A node kind this driver cannot execute REFUSES rather than returning.**
3393
4973
  // Added by [W32](../../doc/work-items/w32-what-a-map-fanout-is.md) as a
3394
4974
  // declared late exception, after its conformity reader measured what the
@@ -3487,8 +5067,9 @@ var Driver = class {
3487
5067
  } catch (threw) {
3488
5068
  return { threw };
3489
5069
  }
3490
- const declared = typeof route === "string" && type.routes.includes(route);
3491
- return { type, route, declared, key: recorded };
5070
+ const set = this.gateRoutes.get(node.id);
5071
+ const declared = typeof route === "string" && set.routes.includes(route);
5072
+ return { set, route, declared, key: recorded };
3492
5073
  }
3493
5074
  /**
3494
5075
  * **`FD-R4`'s road through an ENTRY gate: the edge the run will take, and no other** — W93, Outputs (9).
@@ -3501,7 +5082,7 @@ var Driver = class {
3501
5082
  boundaryEdges(gate, value) {
3502
5083
  const decided = this.decideGate(gate, value, /* @__PURE__ */ new Map());
3503
5084
  if ("threw" in decided || !decided.declared) return [];
3504
- const edge = gateEdge(gate, decided.type.defaultRoute, decided.route);
5085
+ const edge = gateEdge(gate, decided.set.defaultRoute, decided.route);
3505
5086
  return edge === null ? [] : [edge];
3506
5087
  }
3507
5088
  /**
@@ -3509,7 +5090,7 @@ var Driver = class {
3509
5090
  * [ADR 0040](../../doc/decisions/0040-what-a-node-can-reach-is-the-runs-own-outputs-and-a-node-id-names-them-relative-to-a-branch-path.md)
3510
5091
  * rules 2 and 3, and `FD-C2`.
3511
5092
  *
3512
- * **It is a PROJECTION of the log and adds no retention** — measured at
5093
+ * **It is a PROJECTION of the log and adds no retention of a result** — measured at
3513
5094
  * [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
3514
5095
  * declaration and again before this was written: this driver holds
3515
5096
  * `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
@@ -3519,14 +5100,11 @@ var Driver = class {
3519
5100
  * described **today** before this method existed, and this adds **no retention**.
3520
5101
  * *(Q19 is RESOLVED since W53 —
3521
5102
  * [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
3522
- * — and `RN-R13` names this memo by id: it holds results **by reference only**,
5103
+ * — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
3523
5104
  * asserted by IDENTITY at ladder L2 rather than by a byte count, because a
3524
5105
  * reachability SET cannot tell an alias from a copy. W53's readers found that
3525
5106
  * from both ends — the rule's stated test returned a third name on a `needs`
3526
5107
  * flow, and cloning here cost ZERO.)*
3527
- * *(It adds ONE field — `outputsCache` below — which is a memo the log invalidates
3528
- * and which holds nothing the log does not. This read *"zero fields"* until W41's
3529
- * conformity reader read it beside the memo the cost measurement had just added.)*
3530
5108
  *
3531
5109
  * **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
3532
5110
  * section 3 rule 4's own reason: a resumed node has two records in the log and
@@ -3534,43 +5112,97 @@ var Driver = class {
3534
5112
  * suspended has no effective outcome and therefore no entry — which is what makes
3535
5113
  * `FD-R3` fire rather than a reference quietly resolving to `undefined`.
3536
5114
  *
3537
- * **Computed on demand rather than maintained**, and the cost is why: it is
3538
- * `O(log)` per call, so maintaining it would be a second source that could
3539
- * disagree with the log — the defect [Q43](../../doc/questions/q43.md) and
3540
- * `arrival-count`'s re-sourcing are each an instance of. **A node that declares no
3541
- * `needs` never reaches this method**, which is what keeps ADR 0040 rule 1's *the
3542
- * default is unchanged* true of the cost as well as of the value.
5115
+ * **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
5116
+ * [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
5117
+ * rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
5118
+ * WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
5119
+ * of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
5120
+ * construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
5121
+ * `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
5122
+ * ONE place, which is ADR 0040 rule 2's own reason, and an entry is always the merge of its task's whole sub-log at
5123
+ * the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
5124
+ * (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
5125
+ * in its tasks' first-record order, each `result` the log record's own object.
5126
+ *
5127
+ * **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
5128
+ * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
3543
5129
  */
3544
5130
  outputs() {
3545
- if (this.outputsCache && this.outputsCache.at === this.log.length) return this.outputsCache.byNode;
3546
- const byNode = /* @__PURE__ */ new Map();
3547
- const groups = /* @__PURE__ */ new Map();
3548
- for (const r of this.log) {
3549
- const g = groups.get(r.taskId);
3550
- if (g) g.push(r);
3551
- else groups.set(r.taskId, [r]);
3552
- }
3553
- for (const [taskId, group] of groups) {
3554
- const eff = effectiveOutcome(group, taskId);
3555
- if (!eff) continue;
3556
- let parts;
3557
- try {
3558
- parts = parseTaskId(taskId);
3559
- } catch {
3560
- continue;
5131
+ const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map() };
5132
+ for (; c.at < this.log.length; c.at += 1) {
5133
+ const r = this.log[c.at];
5134
+ let t = c.tasks.get(r.taskId);
5135
+ if (t === void 0) {
5136
+ let parts;
5137
+ try {
5138
+ parts = parseTaskId(r.taskId);
5139
+ } catch {
5140
+ c.tasks.set(r.taskId, null);
5141
+ continue;
5142
+ }
5143
+ t = { nodeId: parts.nodeId, path: parts.branchPath, order: c.tasks.size, entry: null };
5144
+ c.tasks.set(r.taskId, t);
3561
5145
  }
3562
- const list = byNode.get(parts.nodeId);
3563
- if (list) list.push({ path: parts.branchPath, result: eff.result });
3564
- else byNode.set(parts.nodeId, [{ path: parts.branchPath, result: eff.result }]);
5146
+ if (t === null) continue;
5147
+ this.place(c, t, effectiveOutcome(this.taskLog.of(r.taskId), r.taskId));
3565
5148
  }
3566
- this.outputsCache = { at: this.log.length, byNode };
3567
- return byNode;
5149
+ return c.byNode;
3568
5150
  }
3569
5151
  /**
3570
- * The memo above. **Not run state and not a second copy of it** — it is discarded
3571
- * and rebuilt the moment the log grows, and nothing ever reads it without checking
3572
- * the length first, so there is no state here that could be stale in a way anything
3573
- * could observe. `ADR 0014`'s *the driver computes no run state* is untouched.
5152
+ * **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
5153
+ * was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
5154
+ * in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
5155
+ *
5156
+ * **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
5157
+ * RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
5158
+ * at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
5159
+ * a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
5160
+ * `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
5161
+ * fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
5162
+ * that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
5163
+ * so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
5164
+ * none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
5165
+ */
5166
+ place(c, t, eff) {
5167
+ const list = c.byNode.get(t.nodeId);
5168
+ const orders = c.orders.get(t.nodeId);
5169
+ if (eff === null) {
5170
+ if (t.entry === null || list === void 0 || orders === void 0) return;
5171
+ const at = list.indexOf(t.entry);
5172
+ list.splice(at, 1);
5173
+ orders.splice(at, 1);
5174
+ t.entry = null;
5175
+ if (list.length === 0) {
5176
+ c.byNode.delete(t.nodeId);
5177
+ c.orders.delete(t.nodeId);
5178
+ }
5179
+ return;
5180
+ }
5181
+ const entry = { path: t.path, result: eff.result };
5182
+ if (t.entry !== null && list !== void 0) {
5183
+ list[list.indexOf(t.entry)] = entry;
5184
+ t.entry = entry;
5185
+ return;
5186
+ }
5187
+ t.entry = entry;
5188
+ if (list === void 0 || orders === void 0) {
5189
+ c.byNode.set(t.nodeId, [entry]);
5190
+ c.orders.set(t.nodeId, [t.order]);
5191
+ return;
5192
+ }
5193
+ let i = list.length;
5194
+ while (i > 0 && orders[i - 1] > t.order) i -= 1;
5195
+ list.splice(i, 0, entry);
5196
+ orders.splice(i, 0, t.order);
5197
+ }
5198
+ /**
5199
+ * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
5200
+ * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
5201
+ * never a copy — `tasks` is one slot per task id
5202
+ * read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
5203
+ * node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
5204
+ * read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
5205
+ * *the driver computes no run state* is untouched. `null` until a `needs` first asks.
3574
5206
  */
3575
5207
  outputsCache = null;
3576
5208
  /**
@@ -3585,21 +5217,32 @@ var Driver = class {
3585
5217
  * the edge carried, so the composed object is the whole payload and what the
3586
5218
  * worker gets is what the document says, character for character.
3587
5219
  *
5220
+ * **`item` is a THUNK, read only for a `"$item"` key** (v0.45, P38 (e)): in a run it reads the instance's held item
5221
+ * (`itemOf`), at `FD-R4` it is the walk's own value — and a node declaring no `"$item"` never asks.
5222
+ *
3588
5223
  * @throws UnresolvedReference — `FD-R3`.
3589
5224
  */
3590
- compose(node, edgeValue, itemIndices) {
5225
+ compose(node, edgeValue, itemIndices, runPayload, item) {
3591
5226
  const needs = node.needs;
3592
5227
  if (!needs) return edgeValue;
3593
- const here = this.pathOf(node.id, itemIndices);
3594
- const produced = this.outputs();
5228
+ let lookup = null;
3595
5229
  const out = {};
3596
5230
  for (const [key, ref] of Object.entries(needs)) {
3597
5231
  const t = referenceTarget(ref);
3598
- if (t.node === null) {
5232
+ if (t.form === "self") {
3599
5233
  out[key] = cloneValue(edgeValue);
3600
5234
  continue;
3601
5235
  }
3602
- const v = this.resolveReference(node.id, here, produced, key, t, "needs");
5236
+ if (t.form === "payload") {
5237
+ out[key] = cloneValue(runPayload);
5238
+ continue;
5239
+ }
5240
+ if (t.form === "item") {
5241
+ out[key] = cloneValue(item());
5242
+ continue;
5243
+ }
5244
+ lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
5245
+ const v = this.resolveReference(node.id, lookup.here, lookup.produced, key, t, "needs");
3603
5246
  if (v !== ABSENT) out[key] = v;
3604
5247
  }
3605
5248
  return out;
@@ -3632,6 +5275,35 @@ var Driver = class {
3632
5275
  }
3633
5276
  return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
3634
5277
  }
5278
+ /**
5279
+ * **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
5280
+ * caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
5281
+ * reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
5282
+ * header exists. Read only where a node or an edge composes the token — a run declaring none never clones it
5283
+ * (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
5284
+ */
5285
+ runPayload() {
5286
+ return this.header?.entryPayload;
5287
+ }
5288
+ /**
5289
+ * **The item of the map instance `nodeId` is entered in** — `"$item"`'s source in a run (v0.45, P38 (c), (e)): the
5290
+ * innermost map containing the node (`innermostMapOf`, the walk `FD-V49` checked), and the instance its `itemIndices`
5291
+ * name — the branch path that map's branch entry has in it. **By reference to the held item, uncloned**: the caller
5292
+ * clones, per key. Read only where a node or an edge declares `"$item"`.
5293
+ *
5294
+ * **A miss THROWS a plain `Error` naming the invariant**: a node is entered only while its instance is live, and an
5295
+ * instance's item is released only once nothing of it may still be entered (`trackItems`), so a reader with nothing held is a driver
5296
+ * defect — reported as a halt (`OR-R10`) rather than handed `undefined`, which an item may legitimately be.
5297
+ */
5298
+ itemOf(nodeId, itemIndices) {
5299
+ const m = innermostMapOf(this.regions, nodeId);
5300
+ const path = this.pathOf(m.branches[0], itemIndices);
5301
+ const byPath = this.items.held.get(m.fanoutId);
5302
+ if (byPath === void 0 || !byPath.has(path)) {
5303
+ 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`);
5304
+ }
5305
+ return byPath.get(path);
5306
+ }
3635
5307
  /**
3636
5308
  * **What a decision sees, beyond its node's outcome** — `FD-C3`, and
3637
5309
  * [ADR 0041](../../doc/decisions/0041-a-decision-sees-its-nodes-outcome-and-whatever-its-own-edge-declares.md).
@@ -3642,7 +5314,10 @@ var Driver = class {
3642
5314
  *
3643
5315
  * **No `"."`, and `FD-V26` refuses it on an edge**: *what this predicate would have
3644
5316
  * 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.
5317
+ * `edgeValue` parameter here and none is derivable — see the call site. **A
5318
+ * `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
5319
+ * the NODE received but read from the run's header, `runPayload()` above, its own
5320
+ * copy per key per read.
3646
5321
  *
3647
5322
  * @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
3648
5323
  */
@@ -3654,6 +5329,17 @@ var Driver = class {
3654
5329
  const out = {};
3655
5330
  for (const [key, ref] of Object.entries(needs)) {
3656
5331
  const t = referenceTarget(ref);
5332
+ if (t.form === "payload") {
5333
+ out[key] = cloneValue(this.runPayload());
5334
+ continue;
5335
+ }
5336
+ if (t.form === "item") {
5337
+ out[key] = cloneValue(this.itemOf(node.id, itemIndices));
5338
+ continue;
5339
+ }
5340
+ if (t.form === "self") {
5341
+ throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
5342
+ }
3657
5343
  const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
3658
5344
  if (v !== ABSENT) out[key] = v;
3659
5345
  }
@@ -3668,11 +5354,13 @@ var Driver = class {
3668
5354
  * graph, and there is no longer a carried value that could disagree with it.
3669
5355
  */
3670
5356
  dispatch(runId, node, payload, itemIndices = EMPTY_INDICES) {
5357
+ if (this.shut()) return;
3671
5358
  const taskId = buildTaskId(runId, node.id, branchIndices(this.pathOf(node.id, itemIndices)), 0);
3672
5359
  if (this.dispatched.has(taskId)) {
3673
5360
  throw new DuplicateTaskId(`"${taskId}" was already dispatched in run "${runId}"`);
3674
5361
  }
3675
5362
  this.dispatched.add(taskId);
5363
+ if (this.replay === null) this.unhanded.add(taskId);
3676
5364
  this.append({ kind: "dispatched", taskId });
3677
5365
  const type = this.nodeTypes.get(node.type);
3678
5366
  const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
@@ -3688,7 +5376,108 @@ var Driver = class {
3688
5376
  this.replay.held.set(taskId, config);
3689
5377
  return;
3690
5378
  }
3691
- this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
5379
+ if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
5380
+ this.pauseHeld.set(taskId, config);
5381
+ this.unhanded.delete(taskId);
5382
+ return;
5383
+ }
5384
+ this.handOn(taskId, config);
5385
+ }
5386
+ /**
5387
+ * **A branch reached an ASK: it parks, keeping what reached it, and NOTHING is handed to the scheduler** — `RN-R37`,
5388
+ * v0.22, [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
5389
+ * P35 stop-and-asks (b), (c) and (f), decision 5.
5390
+ *
5391
+ * 1. **The hold** (`RN-R29`), as `dispatch`'s: `enter` checked it at its head and `compose` ran consumer code since.
5392
+ * 2. **Its task id** by `spec/task-id.md`'s scheme, attempt `0` — the address a person's answer names. Minted once
5393
+ * per path: a second `asked` at one id is `ID-R1`'s defect and refused as one (`DuplicateTaskId`), never a
5394
+ * second park.
5395
+ * 3. **ONE `asked` element**, carrying `received` — what reached the ask, composed through its `needs` — as the
5396
+ * driver's OWN deep copy: at the entry `received` is the caller's own `start()` payload, and a kept input must not
5397
+ * move when the caller's object does (P35 (f)). No `dispatched` element, no `createTask`, no `addTask`: the ask is
5398
+ * never in flight (`RN-R21`), so a stop cancels nothing for it and a resume re-dispatches nothing (P35 fact 5).
5399
+ * 4. **In a region, `branch.suspended`** — what a parked task emits (`settle`'s `user_action` arm): the branch is
5400
+ * live and its join waits for the person (Q7), which is how an ask in a fan-out holds its join. AFTER the element,
5401
+ * as the parked task's follows its settlement, and the hold checked between them (`RN-R29`): a consumer's
5402
+ * callback on `asked` may halt the run, and a halted run emits nothing further. It may also ANSWER the ask: the
5403
+ * answer is accepted and PENDING until the step ends (reading R4), so the park completes first.
5404
+ *
5405
+ * **During a replay this is how the park is REGENERATED** (`RN-R19`): the element is compared with the stored one,
5406
+ * input and all (`sameData`, `RN-R20`), so a replay that composes a different input is refused `diverged` here.
5407
+ */
5408
+ ask(runId, node, received, itemIndices) {
5409
+ if (this.shut()) return;
5410
+ const branchPath = this.pathOf(node.id, itemIndices);
5411
+ const taskId = buildTaskId(runId, node.id, branchIndices(branchPath), 0);
5412
+ if (this.asked.has(taskId) || this.dispatched.has(taskId)) {
5413
+ throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
5414
+ }
5415
+ this.asked.add(taskId);
5416
+ this.append({ kind: "asked", taskId, input: cloneValue(received) });
5417
+ if (this.shut()) return;
5418
+ const region = this.regionOf(node.id);
5419
+ if (region) {
5420
+ this.emit({
5421
+ type: "branch.suspended",
5422
+ regionId: region.fanoutId,
5423
+ regionPath: this.rpathOf(region, itemIndices),
5424
+ branchPath,
5425
+ at: node.id
5426
+ });
5427
+ }
5428
+ }
5429
+ /**
5430
+ * **`RS-R1`'s *suspended*, for one task id** — a dispatched task whose last record is a `user_action` settlement, or,
5431
+ * since v0.18 (`RN-R37`, ADR 0109), an ask this run reached that has no record at all. The FIVE readers of the
5432
+ * predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
5433
+ */
5434
+ parked(taskId) {
5435
+ return isSuspended(this.taskLog.of(taskId), taskId, this.asked.has(taskId));
5436
+ }
5437
+ /**
5438
+ * **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
5439
+ * 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
5440
+ * waiting on a person, not finished (P35 fact 5 (iv)). The rest of an ask under run control needs no line of its own:
5441
+ * never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
5442
+ */
5443
+ hasParked() {
5444
+ for (const id of this.dispatched) if (isSuspended(this.taskLog.of(id), id)) return true;
5445
+ for (const id of this.asked) if (this.parked(id)) return true;
5446
+ return false;
5447
+ }
5448
+ /**
5449
+ * **Hand one task to the scheduler — and, if the run's owner cancelled it while the task was on its way, cancel it
5450
+ * at once** (`RN-R29` (i), `RN-R31`, ADR 0103's inherited hazard, W173). The ONE place `addTask` is called: by
5451
+ * `dispatch` for the task in progress, and by `resume()` for each task in flight. A cancel raised on this task's own
5452
+ * `dispatched` element — or on a resume's `resumed` element — ran before the scheduler held the id, and sent nothing
5453
+ * for it; this is where that task is cancelled, so it ends with a genuine `task.cancelled` rather than running.
5454
+ */
5455
+ handOn(taskId, config) {
5456
+ try {
5457
+ this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
5458
+ } finally {
5459
+ this.unhanded.delete(taskId);
5460
+ }
5461
+ if (this.cancelRequest !== null) this.cancelTask(taskId);
5462
+ }
5463
+ /**
5464
+ * **One task cancelled through the scheduler, with the request's options**, its promise kept for `cancel()` — or,
5465
+ * since W174, `stop()` — to wait on (`RN-R31`, `RN-R33`). Each option is passed only when the caller gave it, so an
5466
+ * absent `graceMs` is the pool's `cancelGraceMs` and not `undefined` read as a value. A cancel and a stop never both
5467
+ * hold — each refuses the other — so the one present is the one whose options these are.
5468
+ */
5469
+ cancelTask(taskId) {
5470
+ const req = this.cancelRequest ?? this.stopRequest;
5471
+ const options = {};
5472
+ if (req.reason !== void 0) options.reason = req.reason;
5473
+ if (req.graceMs !== void 0) options.graceMs = req.graceMs;
5474
+ const p = this.opts.scheduler.cancel(taskId, options);
5475
+ if (!req.stopping.has(taskId)) req.stopping.set(taskId, p);
5476
+ const drop = () => {
5477
+ if (req.stopping.get(taskId) === p) req.stopping.delete(taskId);
5478
+ };
5479
+ p.then(drop, drop);
5480
+ return p;
3692
5481
  }
3693
5482
  /**
3694
5483
  * **A node's `branchPath`, derived from the graph** — `spec/task-id.md` section
@@ -3747,6 +5536,49 @@ var Driver = class {
3747
5536
  this.arrivals.get(fanoutId)?.delete(rp);
3748
5537
  if (this.arrivals.get(fanoutId)?.size === 0) this.arrivals.delete(fanoutId);
3749
5538
  }
5539
+ /**
5540
+ * **The release horizon of what instances keep** — ADR 0118 rule 5. A `branch.opened` of a region nested in a kept map's
5541
+ * instance counts one live branch there; its terminal event uncounts it. The instance's OWN terminal event releases its
5542
+ * item when that count is zero, and otherwise marks it ENDED, released by the nested branch that ends last. O(nesting
5543
+ * depth) per event, and nothing at all where no map keeps (`kept` empty). Run in a replay as in a run, so a resume
5544
+ * rebuilds the counts with the items.
5545
+ */
5546
+ trackItems(e) {
5547
+ const it = this.items;
5548
+ if (it.kept.size === 0) return;
5549
+ const opened = e.type === "branch.opened";
5550
+ const terminal = isTerminalEvent(e);
5551
+ if (!opened && !terminal) return;
5552
+ for (const { map, depth } of it.within.get(e.regionId) ?? []) {
5553
+ const path = e.branchPath.split(".").slice(0, depth).join(".");
5554
+ const k = `${map} ${path}`;
5555
+ const n = (it.nested.get(k) ?? 0) + (opened ? 1 : -1);
5556
+ if (n > 0) {
5557
+ it.nested.set(k, n);
5558
+ continue;
5559
+ }
5560
+ it.nested.delete(k);
5561
+ if (it.ended.has(k)) this.releaseItem(map, path);
5562
+ }
5563
+ if (terminal && it.kept.has(e.regionId)) {
5564
+ if (it.nested.has(`${e.regionId} ${e.branchPath}`)) it.ended.add(`${e.regionId} ${e.branchPath}`);
5565
+ else this.releaseItem(e.regionId, e.branchPath);
5566
+ }
5567
+ }
5568
+ /** **One instance's item, dropped** — and the map's own key with its last instance, as `releaseArrivals` drops a region's. */
5569
+ releaseItem(mapId, branchPath) {
5570
+ this.items.ended.delete(`${mapId} ${branchPath}`);
5571
+ const byPath = this.items.held.get(mapId);
5572
+ if (byPath === void 0) return;
5573
+ byPath.delete(branchPath);
5574
+ if (byPath.size === 0) this.items.held.delete(mapId);
5575
+ }
5576
+ /** **Everything instances keep, dropped** — the run-level moments, where no node of the run is entered again. */
5577
+ releaseAllItems() {
5578
+ this.items.held.clear();
5579
+ this.items.nested.clear();
5580
+ this.items.ended.clear();
5581
+ }
3750
5582
  /** The arrival map for one region, created on first arrival. */
3751
5583
  arrivalsOf(fanoutId, rp) {
3752
5584
  const byPath = this.arrivals.get(fanoutId) ?? /* @__PURE__ */ new Map();
@@ -3887,13 +5719,18 @@ var Driver = class {
3887
5719
  *
3888
5720
  * A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
3889
5721
  * the stored object and `OR-R8` derives rather than appends, so a second copy
3890
- * here would be a second truth. What leaves through `onFoldInput` **is** a
5722
+ * here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
5723
+ * by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
5724
+ * element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
3891
5725
  * copy, frozen, for the same reason `snapshot()` is: a listener holding the
3892
5726
  * driver's own array could forge a settlement and bypass `RS-R1`.
3893
5727
  */
3894
5728
  append(input) {
3895
5729
  const seq = this.inputs.push(input) - 1;
3896
- if (input.kind === "outcome" || input.kind === "resolution") this.log.push(input.record);
5730
+ if (input.kind === "outcome" || input.kind === "resolution") {
5731
+ this.log.push(input.record);
5732
+ this.taskLog.push(input.record);
5733
+ }
3897
5734
  const replay = this.replay;
3898
5735
  if (replay !== null && seq < replay.stored.length) {
3899
5736
  const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
@@ -3902,12 +5739,22 @@ var Driver = class {
3902
5739
  if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
3903
5740
  return seq;
3904
5741
  }
5742
+ /**
5743
+ * **A stored `interrupted` element, fed** (`RN-R19`, `RN-R32`): the replay holds as the stopped run was held — the hold
5744
+ * BEFORE the element, as a stop is taken — and appends the element as stored. Neither `deaf` nor a cancel nor the
5745
+ * collections' release: those were the stopped driver's, and a replay hands a scheduler nothing.
5746
+ */
5747
+ feedInterrupted(e) {
5748
+ this.stopRequest = { actor: e.actor, stopping: /* @__PURE__ */ new Map(), pending: null, took: null };
5749
+ this.append({ kind: "interrupted", actor: e.actor, reason: e.reason ?? null });
5750
+ }
3905
5751
  emit(e) {
3906
5752
  const policy = this.opts.joinPolicy;
3907
5753
  if (policy && this.joinStateHas(e.regionId, e.regionPath)) {
3908
5754
  this.joinStateSet(e.regionId, e.regionPath, policy.step(this.joinStateGet(e.regionId, e.regionPath), e));
3909
5755
  }
3910
5756
  const event = Object.freeze({ ...e });
5757
+ this.trackItems(e);
3911
5758
  const seq = this.append({ kind: "region", event });
3912
5759
  if (this.delivers(seq)) this.opts.onRegionEvent?.(event);
3913
5760
  }
@@ -3978,7 +5825,7 @@ var Driver = class {
3978
5825
  joinId: region.joinId,
3979
5826
  fired: true
3980
5827
  });
3981
- if (this.failed) return;
5828
+ if (this.shut()) return;
3982
5829
  const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
3983
5830
  this.releaseArrivals(region.fanoutId, rp);
3984
5831
  const join = this.graph.byId.get(region.joinId);
@@ -4108,7 +5955,7 @@ var Driver = class {
4108
5955
  * the way in.
4109
5956
  */
4110
5957
  effectiveOutcomeOf(taskId) {
4111
- const o = effectiveOutcome(this.log, taskId);
5958
+ const o = effectiveOutcome(this.taskLog.of(taskId), taskId);
4112
5959
  return o === null ? null : cloneRecord(o);
4113
5960
  }
4114
5961
  terminalStateOf(taskId) {
@@ -4116,134 +5963,511 @@ var Driver = class {
4116
5963
  }
4117
5964
  };
4118
5965
 
4119
- // src/fold/run-state.ts
4120
- function emptyState() {
4121
- return { runId: null, outcome: "running", nodes: {}, regions: {}, pathEnds: [], stoppedBy: null, resumes: [], gated: [] };
4122
- }
4123
- function step(prev, input) {
4124
- if (input.kind === "stopped") {
4125
- return { ...prev, stoppedBy: input.reason, outcome: "stopped" };
4126
- }
4127
- if (input.kind === "region") {
4128
- return withOutcome(applyRegion(prev, input.event));
4129
- }
4130
- if (input.kind === "dispatched") {
4131
- const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2 } = parseTaskId(input.taskId);
4132
- if (prev.nodes[input.taskId]) return prev;
4133
- return withOutcome({
4134
- ...prev,
4135
- runId: prev.runId ?? runId2,
4136
- nodes: { ...prev.nodes, [input.taskId]: { nodeId: nodeId2, branchPath: branchPath2, state: "running", result: null, error: null } }
4137
- });
5966
+ // src/fold/accumulator.ts
5967
+ var Suspended = class {
5968
+ items = [];
5969
+ alive = /* @__PURE__ */ new Map();
5970
+ gen = 0;
5971
+ count;
5972
+ constructor(from, count) {
5973
+ this.count = count;
5974
+ const g = ++this.gen;
5975
+ for (const v of from) {
5976
+ this.alive.set(v, g);
5977
+ this.items.push({ v, g });
5978
+ }
5979
+ count(from.length);
4138
5980
  }
4139
- if (input.kind === "pathEnded") {
4140
- return withOutcome({ ...prev, pathEnds: [...prev.pathEnds, { reason: input.reason, at: input.at }] });
5981
+ has(v) {
5982
+ return this.alive.has(v);
4141
5983
  }
4142
- if (input.kind === "regionEntered") {
4143
- if (prev.regions[input.regionId]?.[input.regionPath]) return prev;
4144
- return withOutcome({
4145
- ...prev,
4146
- regions: withRegion(prev.regions, input.regionId, input.regionPath, emptyRegion())
4147
- });
5984
+ add(v) {
5985
+ if (this.alive.has(v)) return;
5986
+ const g = ++this.gen;
5987
+ this.alive.set(v, g);
5988
+ this.items.push({ v, g });
4148
5989
  }
4149
- if (input.kind === "resumed") {
4150
- return { ...prev, resumes: [...prev.resumes, [...input.taskIds]] };
5990
+ remove(v) {
5991
+ this.alive.delete(v);
4151
5992
  }
4152
- if (input.kind === "gated") {
4153
- const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
4154
- if (input.key !== void 0) d.key = structuredClone(input.key);
4155
- return { ...prev, gated: [...prev.gated, d] };
5993
+ toArray() {
5994
+ this.count(this.items.length);
5995
+ const out = [];
5996
+ for (const e of this.items) if (this.alive.get(e.v) === e.g) out.push(e.v);
5997
+ return out;
4156
5998
  }
4157
- if (input.kind === "joinSettled") {
4158
- const cur = prev.regions[input.regionId]?.[input.regionPath] ?? emptyRegion();
4159
- return withOutcome({
4160
- ...prev,
4161
- regions: withRegion(prev.regions, input.regionId, input.regionPath, { ...cur, joined: input.fired })
4162
- });
5999
+ };
6000
+ function regionWeight(opened, joined) {
6001
+ const unjoined = joined !== true ? 1 : 0;
6002
+ return { unjoined, openedNothing: opened.length === 0 && joined !== true ? 1 : 0 };
6003
+ }
6004
+ var hasOwn2 = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
6005
+ function put(o, k, v) {
6006
+ if (k === "__proto__") Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
6007
+ else o[k] = v;
6008
+ }
6009
+ var Accumulator = class _Accumulator {
6010
+ runId;
6011
+ outcome;
6012
+ haltedBy;
6013
+ cancelRequest;
6014
+ interruption;
6015
+ paused;
6016
+ /** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
6017
+ nodes = /* @__PURE__ */ new Map();
6018
+ /** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
6019
+ regions = /* @__PURE__ */ new Map();
6020
+ pathEnds = [];
6021
+ resumes = [];
6022
+ gated = [];
6023
+ counts = null;
6024
+ base;
6025
+ counter;
6026
+ constructor(base, counter) {
6027
+ this.base = base;
6028
+ this.counter = counter;
6029
+ this.runId = base.runId;
6030
+ this.outcome = base.outcome;
6031
+ this.haltedBy = base.haltedBy;
6032
+ this.cancelRequest = base.cancelRequest;
6033
+ this.interruption = base.interruption;
6034
+ this.paused = base.paused;
6035
+ }
6036
+ /** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
6037
+ static load(state, counter) {
6038
+ return new _Accumulator(state, counter);
6039
+ }
6040
+ tick(n = 1) {
6041
+ if (this.counter !== void 0) this.counter.visits += n;
6042
+ }
6043
+ // --- the reads a runtime keeps an accumulator OPEN for — W196, ADR 0122, `RN-R44` -----------------------------
6044
+ /**
6045
+ * **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
6046
+ * O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
6047
+ */
6048
+ outcomeNow() {
6049
+ return this.outcome;
4163
6050
  }
4164
- const r = input.record;
4165
- const { runId, nodeId, branchPath } = parseTaskId(r.taskId);
4166
- const nodes = { ...prev.nodes };
4167
- const existing = nodes[r.taskId];
4168
- if (r.kind === "outcome") {
4169
- nodes[r.taskId] = {
4170
- nodeId,
4171
- branchPath,
4172
- // `user_action` is `suspended` at flow level: a resolution resumes it, and
4173
- // calling it terminal is what makes a design drop the fifth state.
4174
- state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
4175
- result: r.result,
4176
- error: r.error
4177
- };
4178
- } else {
4179
- nodes[r.taskId] = {
4180
- nodeId: existing?.nodeId ?? nodeId,
4181
- branchPath: existing?.branchPath ?? branchPath,
4182
- state: r.resolvedAs,
4183
- result: r.result,
4184
- error: r.error
6051
+ /**
6052
+ * **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
6053
+ * An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
6054
+ * dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
6055
+ */
6056
+ entry(taskId) {
6057
+ if (this.nodes.has(taskId)) return this.nodes.get(taskId);
6058
+ return hasOwn2(this.base.nodes, taskId) ? this.base.nodes[taskId] : void 0;
6059
+ }
6060
+ // --- reads, through to the base ---------------------------------------------------------------------------
6061
+ /** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
6062
+ node(taskId) {
6063
+ return this.nodes.has(taskId) ? this.nodes.get(taskId) : this.base.nodes[taskId];
6064
+ }
6065
+ /** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
6066
+ instance(regionId, regionPath) {
6067
+ const touched = this.regions.get(regionId)?.get(regionPath);
6068
+ return touched ?? this.base.regions[regionId]?.[regionPath];
6069
+ }
6070
+ /** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
6071
+ tally() {
6072
+ if (this.counts !== null) return this.counts;
6073
+ const c = { byState: /* @__PURE__ */ new Map(), nodes: 0, unjoined: 0, openedNothing: 0, noEligibleEdge: 0, ends: 0, pathEnds: 0 };
6074
+ const baseNodes = this.base.nodes;
6075
+ const keys = Object.keys(baseNodes);
6076
+ this.tick(keys.length);
6077
+ for (const k of keys) if (!this.nodes.has(k)) this.countNode(c, baseNodes[k], 1);
6078
+ for (const n of this.nodes.values()) this.countNode(c, n, 1);
6079
+ this.tick(this.nodes.size);
6080
+ const baseRegions = this.base.regions;
6081
+ for (const id of Object.keys(baseRegions)) {
6082
+ const byPath = baseRegions[id];
6083
+ const touched = this.regions.get(id);
6084
+ for (const p of Object.keys(byPath)) {
6085
+ this.tick();
6086
+ if (touched?.has(p)) continue;
6087
+ const r = byPath[p];
6088
+ const w = regionWeight(r.opened, r.joined);
6089
+ c.unjoined += w.unjoined;
6090
+ c.openedNothing += w.openedNothing;
6091
+ }
6092
+ }
6093
+ for (const byPath of this.regions.values()) {
6094
+ for (const acc of byPath.values()) {
6095
+ this.tick();
6096
+ const w = regionWeight(acc.opened, acc.joined);
6097
+ c.unjoined += w.unjoined;
6098
+ c.openedNothing += w.openedNothing;
6099
+ }
6100
+ }
6101
+ for (const list of [this.base.pathEnds, this.pathEnds]) {
6102
+ this.tick(list.length);
6103
+ for (const e of list) {
6104
+ c.pathEnds += 1;
6105
+ if (e.reason === "no-eligible-edge") c.noEligibleEdge += 1;
6106
+ if (e.reason === "end") c.ends += 1;
6107
+ }
6108
+ }
6109
+ this.counts = c;
6110
+ return c;
6111
+ }
6112
+ countNode(c, n, by) {
6113
+ c.nodes += by;
6114
+ c.byState.set(n.state, (c.byState.get(n.state) ?? 0) + by);
6115
+ }
6116
+ stateCount(state) {
6117
+ return this.tally().byState.get(state) ?? 0;
6118
+ }
6119
+ // --- writes ------------------------------------------------------------------------------------------------
6120
+ /** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
6121
+ setNode(taskId, entry) {
6122
+ if (this.counts !== null) {
6123
+ const own3 = this.nodes.has(taskId) || hasOwn2(this.base.nodes, taskId);
6124
+ if (own3) this.countNode(this.counts, this.node(taskId), -1);
6125
+ this.countNode(this.counts, entry, 1);
6126
+ }
6127
+ this.nodes.set(taskId, entry);
6128
+ this.tick();
6129
+ }
6130
+ /**
6131
+ * The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
6132
+ * paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
6133
+ */
6134
+ touchRegion(regionId, regionPath) {
6135
+ let byPath = this.regions.get(regionId);
6136
+ let acc = byPath?.get(regionPath);
6137
+ if (acc === void 0) {
6138
+ const at = this.base.regions[regionId]?.[regionPath];
6139
+ if (at) {
6140
+ this.tick(at.opened.length + Object.keys(at.settled).length);
6141
+ acc = {
6142
+ origin: at,
6143
+ opened: [...at.opened],
6144
+ openedSet: new Set(at.opened),
6145
+ settled: new Map(Object.entries(at.settled)),
6146
+ suspended: new Suspended(at.suspended, (n) => this.tick(n)),
6147
+ joined: at.joined
6148
+ };
6149
+ } else {
6150
+ acc = {
6151
+ origin: { opened: [], settled: {}, suspended: [], joined: null },
6152
+ opened: [],
6153
+ openedSet: /* @__PURE__ */ new Set(),
6154
+ settled: /* @__PURE__ */ new Map(),
6155
+ suspended: new Suspended([], (n) => this.tick(n)),
6156
+ joined: null
6157
+ };
6158
+ }
6159
+ if (byPath === void 0) {
6160
+ byPath = /* @__PURE__ */ new Map();
6161
+ this.regions.set(regionId, byPath);
6162
+ }
6163
+ byPath.set(regionPath, acc);
6164
+ const inner = this.base.regions[regionId];
6165
+ if (this.counts !== null && at && inner !== void 0 && hasOwn2(this.base.regions, regionId) && hasOwn2(inner, regionPath)) {
6166
+ this.unweigh(regionWeight(at.opened, at.joined));
6167
+ }
6168
+ } else if (this.counts !== null) {
6169
+ this.unweigh(regionWeight(acc.opened, acc.joined));
6170
+ }
6171
+ this.tick();
6172
+ return acc;
6173
+ }
6174
+ unweigh(w) {
6175
+ this.counts.unjoined -= w.unjoined;
6176
+ this.counts.openedNothing -= w.openedNothing;
6177
+ }
6178
+ /** The instance's share of the region counts put back after a write. */
6179
+ settleRegion(acc) {
6180
+ if (this.counts === null) return;
6181
+ const w = regionWeight(acc.opened, acc.joined);
6182
+ this.counts.unjoined += w.unjoined;
6183
+ this.counts.openedNothing += w.openedNothing;
6184
+ }
6185
+ addPathEnd(e) {
6186
+ this.pathEnds.push(e);
6187
+ this.tick();
6188
+ if (this.counts !== null) {
6189
+ this.counts.pathEnds += 1;
6190
+ if (e.reason === "no-eligible-edge") this.counts.noEligibleEdge += 1;
6191
+ if (e.reason === "end") this.counts.ends += 1;
6192
+ }
6193
+ }
6194
+ // --- the arms -------------------------------------------------------------------------------------------------
6195
+ /**
6196
+ * **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
6197
+ * `false` where it returned the state it was handed, so `step` can return that same object. Throws where
6198
+ * that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
6199
+ * the module header's stated limit on region paths.
6200
+ */
6201
+ apply(input) {
6202
+ this.tick();
6203
+ if (input.kind === "halted" || input.kind === "stopped") {
6204
+ this.haltedBy = input.reason;
6205
+ this.outcome = "halted";
6206
+ return true;
6207
+ }
6208
+ if (input.kind === "cancelRequested") {
6209
+ if (this.cancelRequest !== null || this.outcome === "cancelled") return false;
6210
+ this.cancelRequest = { actor: input.actor, reason: input.reason };
6211
+ return this.withOutcome();
6212
+ }
6213
+ if (input.kind === "interrupted") {
6214
+ if (this.interruption !== null) return false;
6215
+ this.interruption = { actor: input.actor, reason: input.reason };
6216
+ return this.withOutcome();
6217
+ }
6218
+ if (input.kind === "paused" || input.kind === "unpaused") {
6219
+ const paused = input.kind === "paused";
6220
+ if (this.paused === paused) return false;
6221
+ this.paused = paused;
6222
+ return true;
6223
+ }
6224
+ if (input.kind === "region") {
6225
+ this.applyRegion(input.event);
6226
+ return this.withOutcome();
6227
+ }
6228
+ if (input.kind === "dispatched") {
6229
+ const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
6230
+ if (this.node(input.taskId)) return false;
6231
+ this.runId = this.runId ?? runId2;
6232
+ this.setNode(input.taskId, {
6233
+ runId: runId2,
6234
+ nodeId: nodeId2,
6235
+ branchPath: branchPath2,
6236
+ flowAttempt: attempt2,
6237
+ attemptNumber: null,
6238
+ state: "running",
6239
+ result: null,
6240
+ error: null
6241
+ });
6242
+ return this.withOutcome();
6243
+ }
6244
+ if (input.kind === "asked") {
6245
+ const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
6246
+ if (this.node(input.taskId)) return false;
6247
+ this.runId = this.runId ?? runId2;
6248
+ this.setNode(input.taskId, {
6249
+ runId: runId2,
6250
+ nodeId: nodeId2,
6251
+ branchPath: branchPath2,
6252
+ flowAttempt: attempt2,
6253
+ attemptNumber: null,
6254
+ state: "suspended",
6255
+ result: null,
6256
+ error: null,
6257
+ input: structuredClone(input.input)
6258
+ });
6259
+ return this.withOutcome();
6260
+ }
6261
+ if (input.kind === "pathEnded") {
6262
+ this.addPathEnd({ reason: input.reason, at: input.at });
6263
+ return this.withOutcome();
6264
+ }
6265
+ if (input.kind === "regionEntered") {
6266
+ if (this.instance(input.regionId, input.regionPath)) return false;
6267
+ const acc = this.touchRegion(input.regionId, input.regionPath);
6268
+ this.settleRegion(acc);
6269
+ return this.withOutcome();
6270
+ }
6271
+ if (input.kind === "resumed") {
6272
+ this.resumes.push([...input.taskIds]);
6273
+ this.tick(input.taskIds.length + 1);
6274
+ if (this.interruption !== null) {
6275
+ this.interruption = null;
6276
+ return this.withOutcome();
6277
+ }
6278
+ return true;
6279
+ }
6280
+ if (input.kind === "gated") {
6281
+ const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
6282
+ if (input.key !== void 0) d.key = structuredClone(input.key);
6283
+ this.gated.push(d);
6284
+ this.tick();
6285
+ return true;
6286
+ }
6287
+ if (input.kind === "attempted") {
6288
+ const node = this.node(input.taskId);
6289
+ if (node === void 0) return false;
6290
+ this.setNode(input.taskId, { ...node, attemptNumber: input.attemptNumber });
6291
+ return true;
6292
+ }
6293
+ if (input.kind === "joinSettled") {
6294
+ const acc = this.touchRegion(input.regionId, input.regionPath);
6295
+ acc.joined = input.fired;
6296
+ this.settleRegion(acc);
6297
+ return this.withOutcome();
6298
+ }
6299
+ const r = input.record;
6300
+ const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
6301
+ const existing = this.node(r.taskId);
6302
+ const attemptNumber = existing?.attemptNumber ?? null;
6303
+ if (r.kind === "outcome") {
6304
+ this.setNode(r.taskId, {
6305
+ runId,
6306
+ nodeId,
6307
+ branchPath,
6308
+ flowAttempt: attempt,
6309
+ attemptNumber,
6310
+ // `user_action` is `suspended` at flow level: a resolution resumes it.
6311
+ state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
6312
+ result: r.result,
6313
+ error: r.error
6314
+ });
6315
+ } else {
6316
+ this.setNode(r.taskId, {
6317
+ runId: existing?.runId ?? runId,
6318
+ nodeId: existing?.nodeId ?? nodeId,
6319
+ branchPath: existing?.branchPath ?? branchPath,
6320
+ flowAttempt: existing?.flowAttempt ?? attempt,
6321
+ attemptNumber,
6322
+ state: r.resolvedAs,
6323
+ result: r.result,
6324
+ error: r.error,
6325
+ ...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
6326
+ });
6327
+ }
6328
+ this.runId = this.runId ?? runId;
6329
+ return this.withOutcome();
6330
+ }
6331
+ applyRegion(e) {
6332
+ const acc = this.touchRegion(e.regionId, e.regionPath);
6333
+ switch (e.type) {
6334
+ case "branch.opened":
6335
+ if (!acc.openedSet.has(e.branchPath)) {
6336
+ acc.opened.push(e.branchPath);
6337
+ acc.openedSet.add(e.branchPath);
6338
+ }
6339
+ break;
6340
+ case "branch.arrived":
6341
+ case "branch.exited":
6342
+ case "branch.lost": {
6343
+ const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
6344
+ acc.settled.set(e.branchPath, how);
6345
+ acc.suspended.remove(e.branchPath);
6346
+ break;
6347
+ }
6348
+ case "branch.suspended":
6349
+ acc.suspended.add(e.branchPath);
6350
+ break;
6351
+ case "branch.resumed":
6352
+ acc.suspended.remove(e.branchPath);
6353
+ break;
6354
+ }
6355
+ this.settleRegion(acc);
6356
+ }
6357
+ /**
6358
+ * **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
6359
+ * one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
6360
+ * recomputes has moved the state.
6361
+ */
6362
+ withOutcome() {
6363
+ this.outcome = this.precedence();
6364
+ return true;
6365
+ }
6366
+ precedence() {
6367
+ if (this.haltedBy !== null) return "halted";
6368
+ if (this.cancelRequest === null && this.stateCount("cancelled") > 0) return "cancelled";
6369
+ if (this.cancelRequest !== null) return this.stateCount("running") > 0 ? "cancelling" : "cancelled";
6370
+ if (this.interruption !== null) return "interrupted";
6371
+ if (this.stateCount("suspended") > 0) return "awaiting_action";
6372
+ if (this.stateCount("running") > 0) return "running";
6373
+ const c = this.tally();
6374
+ if (c.nodes === 0 && c.pathEnds === 0 && c.openedNothing === 0) return "running";
6375
+ if (c.unjoined > 0) return "stuck";
6376
+ if (c.noEligibleEdge > 0) return "failed";
6377
+ if (c.ends > 0) return "completed";
6378
+ if (this.stateCount("failed") > 0) return "failed";
6379
+ return "completed";
6380
+ }
6381
+ // --- materialisation ------------------------------------------------------------------------------------------
6382
+ /**
6383
+ * **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
6384
+ * the base's own object, and one that was written is a new one with the base's entries in their places and the new
6385
+ * entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
6386
+ */
6387
+ materialise() {
6388
+ const b = this.base;
6389
+ let nodes = b.nodes;
6390
+ if (this.nodes.size > 0) {
6391
+ nodes = { ...b.nodes };
6392
+ this.tick(Object.keys(b.nodes).length);
6393
+ for (const [k, n] of this.nodes) put(nodes, k, n);
6394
+ this.tick(this.nodes.size);
6395
+ }
6396
+ let regions = b.regions;
6397
+ if (this.regions.size > 0) {
6398
+ regions = { ...b.regions };
6399
+ this.tick(Object.keys(b.regions).length);
6400
+ for (const [id, byPath] of this.regions) {
6401
+ const inner = { ...b.regions[id] };
6402
+ this.tick(Object.keys(inner).length);
6403
+ for (const [p, acc] of byPath) put(inner, p, this.materialiseRegion(acc));
6404
+ put(regions, id, inner);
6405
+ }
6406
+ }
6407
+ return {
6408
+ ...b,
6409
+ runId: this.runId,
6410
+ outcome: this.outcome,
6411
+ nodes,
6412
+ regions,
6413
+ pathEnds: this.list(b.pathEnds, this.pathEnds),
6414
+ haltedBy: this.haltedBy,
6415
+ cancelRequest: this.cancelRequest,
6416
+ interruption: this.interruption,
6417
+ paused: this.paused,
6418
+ resumes: this.list(b.resumes, this.resumes),
6419
+ gated: this.list(b.gated, this.gated)
4185
6420
  };
4186
6421
  }
4187
- return withOutcome({ ...prev, runId: prev.runId ?? runId, nodes });
4188
- }
4189
- function emptyRegion() {
4190
- return { opened: [], settled: {}, suspended: [], joined: null };
4191
- }
4192
- function withRegion(regions, regionId, regionPath, run) {
4193
- return { ...regions, [regionId]: { ...regions[regionId], [regionPath]: run } };
6422
+ list(base, added) {
6423
+ if (added.length === 0) return base;
6424
+ this.tick(base.length + added.length);
6425
+ return [...base, ...added];
6426
+ }
6427
+ materialiseRegion(acc) {
6428
+ const settled = {};
6429
+ for (const [k, v] of acc.settled) settled[k] = v;
6430
+ this.tick(acc.opened.length + acc.settled.size + 1);
6431
+ return { ...acc.origin, opened: [...acc.opened], settled, suspended: acc.suspended.toArray(), joined: acc.joined };
6432
+ }
6433
+ };
6434
+ function foldWith(inputs, counter) {
6435
+ const acc = Accumulator.load(emptyState(), counter);
6436
+ for (const i of inputs) acc.apply(i);
6437
+ return acc.materialise();
4194
6438
  }
4195
- function allRegionRuns(s) {
4196
- return Object.values(s.regions).flatMap((byPath) => Object.values(byPath));
6439
+ function stepWith(prev, input, counter) {
6440
+ const acc = Accumulator.load(prev, counter);
6441
+ return acc.apply(input) ? acc.materialise() : prev;
4197
6442
  }
4198
- function applyRegion(prev, e) {
4199
- const at = prev.regions[e.regionId]?.[e.regionPath];
4200
- const cur = at ? { ...at, opened: [...at.opened], settled: { ...at.settled }, suspended: [...at.suspended] } : emptyRegion();
4201
- switch (e.type) {
4202
- case "branch.opened":
4203
- if (!cur.opened.includes(e.branchPath)) cur.opened.push(e.branchPath);
4204
- break;
4205
- case "branch.arrived":
4206
- case "branch.exited":
4207
- case "branch.lost": {
4208
- const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
4209
- cur.settled[e.branchPath] = how;
4210
- cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
4211
- break;
4212
- }
4213
- case "branch.suspended":
4214
- if (!cur.suspended.includes(e.branchPath)) cur.suspended.push(e.branchPath);
4215
- break;
4216
- case "branch.resumed":
4217
- cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
4218
- break;
4219
- }
4220
- return { ...prev, regions: withRegion(prev.regions, e.regionId, e.regionPath, cur) };
6443
+
6444
+ // src/fold/run-state.ts
6445
+ function emptyState() {
6446
+ return {
6447
+ runId: null,
6448
+ outcome: "running",
6449
+ nodes: {},
6450
+ regions: {},
6451
+ pathEnds: [],
6452
+ haltedBy: null,
6453
+ cancelRequest: null,
6454
+ interruption: null,
6455
+ paused: false,
6456
+ resumes: [],
6457
+ gated: []
6458
+ };
4221
6459
  }
4222
- function withOutcome(s) {
4223
- if (s.stoppedBy !== null) return { ...s, outcome: "stopped" };
4224
- const nodes = Object.values(s.nodes);
4225
- if (nodes.some((n) => n.state === "suspended")) return { ...s, outcome: "awaiting_action" };
4226
- if (nodes.some((n) => n.state === "running")) return { ...s, outcome: "running" };
4227
- const openedNothing = allRegionRuns(s).some((r) => r.opened.length === 0 && r.joined !== true);
4228
- if (nodes.length === 0 && s.pathEnds.length === 0 && !openedNothing) {
4229
- return { ...s, outcome: "running" };
4230
- }
4231
- if (allRegionRuns(s).some((r) => r.joined !== true)) return { ...s, outcome: "stuck" };
4232
- if (s.pathEnds.some((e) => e.reason === "no-eligible-edge")) return { ...s, outcome: "failed" };
4233
- if (s.pathEnds.some((e) => e.reason === "end")) return { ...s, outcome: "completed" };
4234
- if (nodes.some((n) => n.state === "failed")) return { ...s, outcome: "failed" };
4235
- return { ...s, outcome: "completed" };
6460
+ function step(prev, input) {
6461
+ return stepWith(prev, input);
4236
6462
  }
4237
6463
  function fold(inputs) {
4238
- let s = emptyState();
4239
- for (const i of inputs) s = step(s, i);
4240
- return s;
6464
+ return foldWith(inputs);
4241
6465
  }
4242
6466
  function foldLog(log) {
4243
6467
  return fold(log.map((r) => ({ kind: r.kind, record: r })));
4244
6468
  }
4245
6469
  function isTerminal(s) {
4246
- return s.outcome !== "running" && s.outcome !== "awaiting_action";
6470
+ return s.outcome !== "running" && s.outcome !== "awaiting_action" && s.outcome !== "cancelling";
4247
6471
  }
4248
6472
  function nodeOf(s, taskId) {
4249
6473
  return s.nodes[taskId] ?? null;
@@ -4329,6 +6553,7 @@ export {
4329
6553
  innermostRegionOf,
4330
6554
  branchPathOf,
4331
6555
  regionChainOf,
6556
+ innermostMapOf,
4332
6557
  sharedRegionDepth,
4333
6558
  outputIsVisible,
4334
6559
  branchIndexOf,
@@ -4339,6 +6564,8 @@ export {
4339
6564
  canonical,
4340
6565
  findings,
4341
6566
  describeFindings,
6567
+ readKeyPath,
6568
+ ASK_REFUSED_KEYS,
4342
6569
  EDGE_KEYS_DECLARED,
4343
6570
  TASK_KEYS_DECLARED,
4344
6571
  NODE_KEYS_DECLARED,
@@ -4353,9 +6580,9 @@ export {
4353
6580
  languagesOf,
4354
6581
  KEY_PATH_RE,
4355
6582
  keyPathProblem,
4356
- readKeyPath,
4357
6583
  GATE_TYPE_KEYS,
4358
6584
  gateTypeProblem,
6585
+ routesOf,
4359
6586
  distinct,
4360
6587
  BUILT_IN_GATE_TYPES,
4361
6588
  gateTypesOf,
@@ -4393,8 +6620,10 @@ export {
4393
6620
  CHECKED_WORKER_SCHEMAS,
4394
6621
  NODE_TYPE_DECLARATION_KEYS,
4395
6622
  POOL_SPEC_KEYS,
6623
+ MESSAGE_OPTION_KEYS,
4396
6624
  DECLARED_POOL_KEYS,
4397
6625
  INSTANCE_KEYS,
6626
+ POOL_INSTANCE_KEYS,
4398
6627
  isDerivation,
4399
6628
  mergeConfig,
4400
6629
  nodeTypeFromDeclaration,
@@ -4410,6 +6639,7 @@ export {
4410
6639
  fits,
4411
6640
  boundaryTargets,
4412
6641
  checkWiring,
6642
+ givenVerdict,
4413
6643
  describeVerdict,
4414
6644
  VERDICT_KINDS,
4415
6645
  WIRING_SHAPES,
@@ -4429,6 +6659,8 @@ export {
4429
6659
  eligibleEdges,
4430
6660
  gateEdge,
4431
6661
  selectEdge,
6662
+ TERMINAL_EVENTS,
6663
+ isTerminalEvent,
4432
6664
  InvalidDefinition,
4433
6665
  DuplicateTaskId,
4434
6666
  UnresolvedMapArity,
@@ -4437,12 +6669,14 @@ export {
4437
6669
  BoundaryRefused,
4438
6670
  MultipleRuns,
4439
6671
  NotStarted,
6672
+ RunControlRefused,
4440
6673
  CHECKPOINT_FORMAT,
4441
6674
  ResumeRefused,
4442
6675
  resumeRefusal,
4443
6676
  UndispatchedTask,
4444
6677
  MissingJoinPolicy,
4445
6678
  Driver,
6679
+ Accumulator,
4446
6680
  emptyState,
4447
6681
  step,
4448
6682
  fold,