fyflow-orchestration 0.10.0 → 0.12.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.
package/README.md CHANGED
@@ -17,7 +17,7 @@ worker returned, routing can **fan out** into parallel branches, and a **join**
17
17
  The scheduler runs tasks; this library decides **which task runs next**. `fyflow-scheduler` has no
18
18
  dependency graph of its own, so sequence, decision, fan-out and join live here.
19
19
 
20
- Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.10.0`.
20
+ Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.12.0`.
21
21
 
22
22
  ## Install
23
23
 
@@ -93,7 +93,7 @@ if (JSON.stringify(shout?.result) !== '{"text":"hello world !"}') throw new Erro
93
93
 
94
94
  A **decision** is an edge with a `when` naming a predicate: code you supply, under a name the
95
95
  registry declares. A **fan-out** opens branches that run concurrently; a **join** waits for them and
96
- hands the next node every branch's result, keyed by branch path. Below, two runs at high and low
96
+ hands the next node every branch's result, each under its own entry id. Below, two runs at high and low
97
97
  confidence take different roads. The worker is
98
98
  [examples/getting-started/tag.ts](examples/getting-started/tag.ts).
99
99
 
@@ -116,7 +116,7 @@ const isLowConfidence: Predicate = (ctx) => {
116
116
 
117
117
  // THE FLOW — a DECISION, a FAN-OUT, a JOIN. `ingest` goes to `review` when the predicate holds,
118
118
  // otherwise to `split`. `split` runs two branches concurrently; `collect` waits for both and hands
119
- // `publish` every branch's result, keyed by branch path.
119
+ // `publish` every branch's result, each under its own entry id.
120
120
  const flow: FlowDefinition = {
121
121
  flowId: 'decide', version: 1, entry: 'ingest',
122
122
  nodes: [
@@ -144,13 +144,13 @@ const ran = (s: RunState) => Object.values(s.nodes).map((n) => n.nodeId).sort();
144
144
  const resultOf = (s: RunState, id: string) => Object.values(s.nodes).find((n) => n.nodeId === id)?.result;
145
145
  console.log(ran(high)); // [ 'classify', 'enrich', 'ingest', 'publish' ]
146
146
  console.log(ran(low)); // [ 'ingest', 'review' ]
147
- console.log(resultOf(high, 'publish')); // { '0': { confidence: 0.9, ingest: true, enrich: true }, '1': { …, classify: true }, publish: true }
147
+ console.log(resultOf(high, 'publish')); // { enrich: { confidence: 0.9, ingest: true, enrich: true }, classify: { …, classify: true }, publish: true }
148
148
 
149
149
  // Verified by the docs gate:
150
150
  if (high.outcome !== 'completed' || low.outcome !== 'completed') throw new Error(`outcomes ${high.outcome} / ${low.outcome}`);
151
151
  if (ran(high).join() !== 'classify,enrich,ingest,publish') throw new Error(`high ran ${ran(high).join()}`);
152
152
  if (ran(low).join() !== 'ingest,review') throw new Error(`low ran ${ran(low).join()}`);
153
- const want = '{"0":{"confidence":0.9,"ingest":true,"enrich":true},"1":{"confidence":0.9,"ingest":true,"classify":true},"publish":true}';
153
+ const want = '{"enrich":{"confidence":0.9,"ingest":true,"enrich":true},"classify":{"confidence":0.9,"ingest":true,"classify":true},"publish":true}';
154
154
  if (JSON.stringify(resultOf(high, 'publish')) !== want) throw new Error(`publish received ${JSON.stringify(resultOf(high, 'publish'))}`);
155
155
  ```
156
156
 
@@ -360,10 +360,32 @@ function describeFindings(fs) {
360
360
  }
361
361
 
362
362
  // src/definition/condition.ts
363
+ var INDEX_SPELLING = "\\[(0|[1-9][0-9]{0,8})\\]";
364
+ var INDEXED_SEGMENT = new RegExp(`^([A-Za-z0-9_-]{1,64})((?:${INDEX_SPELLING})+)$`);
365
+ var INDEX2 = new RegExp(INDEX_SPELLING, "g");
366
+ function keyPathSteps(path) {
367
+ const out = [];
368
+ for (const seg of path.split(".")) {
369
+ const m = INDEXED_SEGMENT.exec(seg);
370
+ if (m === null) {
371
+ out.push({ key: seg });
372
+ continue;
373
+ }
374
+ out.push({ key: m[1] });
375
+ for (const i of m[2].matchAll(INDEX2)) out.push({ index: Number(i[1]) });
376
+ }
377
+ return out;
378
+ }
363
379
  function readKeyPath(value, path) {
364
380
  let v = value;
365
- for (const seg of path.split(".")) {
381
+ for (const step2 of keyPathSteps(path)) {
366
382
  if (typeof v !== "object" || v === null) return void 0;
383
+ if ("index" in step2) {
384
+ if (!Array.isArray(v) || step2.index >= v.length || !Object.prototype.hasOwnProperty.call(v, step2.index)) return void 0;
385
+ v = v[step2.index];
386
+ continue;
387
+ }
388
+ const seg = step2.key;
367
389
  const proto = Object.getPrototypeOf(v);
368
390
  if (proto !== Object.prototype && proto !== null) return void 0;
369
391
  if (!Object.prototype.hasOwnProperty.call(v, seg)) return void 0;
@@ -416,7 +438,10 @@ function evaluateDeclared(when, scope, unknownOp) {
416
438
  // src/definition/validate.ts
417
439
  var ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
418
440
  var KINDS = ["task", "fanout", "map", "join", "end", "gate", "ask"];
441
+ var ID_RE_COPY = new RegExp(ID_RE.source, ID_RE.flags);
442
+ var KINDS_COPY = Object.freeze([...KINDS]);
419
443
  var ROUTABLE = ["done", "failed"];
444
+ var ROUTABLE_STATES = [...ROUTABLE];
420
445
  var DOC_KEYS = ["flowId", "version", "entry", "nodes"];
421
446
  var NODE_KEYS = {
422
447
  // `retryPolicy` and `workerGroups` since v0.16 (W58): what a node says about its OWN task,
@@ -451,6 +476,11 @@ var TASK_KEYS_DECLARED = [...NODE_KEYS.task.req, ...NODE_KEYS.task.opt];
451
476
  var NODE_KEYS_DECLARED = Object.fromEntries(
452
477
  Object.entries(NODE_KEYS).map(([kind, k]) => [kind, [...k.req, ...k.opt]])
453
478
  );
479
+ var NODE_KEYS_REQUIRED = Object.fromEntries(
480
+ Object.entries(NODE_KEYS).map(([kind, k]) => [kind, [...k.req]])
481
+ );
482
+ var EDGE_KEYS_REQUIRED = [...EDGE_KEYS.req];
483
+ var DOC_KEYS_DECLARED = [...DOC_KEYS];
454
484
  var RETRY_POLICY_KEYS = ["maxRetries", "backoffMs"];
455
485
  function retryPolicyProblem(v) {
456
486
  if (typeof v !== "object" || v === null || Array.isArray(v)) {
@@ -506,11 +536,12 @@ function languagesOf(x) {
506
536
  if (x instanceof Map) return x;
507
537
  return new Map(Object.entries(x));
508
538
  }
509
- var KEY_PATH_RE = /^[A-Za-z0-9_-]{1,64}(?:\.[A-Za-z0-9_-]{1,64})*$/;
539
+ var KEY_PATH_RE = /^[A-Za-z0-9_-]{1,64}(?:\[(?:0|[1-9][0-9]{0,8})\])*(?:\.[A-Za-z0-9_-]{1,64}(?:\[(?:0|[1-9][0-9]{0,8})\])*)*$/;
540
+ var KEY_PATH_WORDS = "dot-separated object keys, each [A-Za-z0-9_-]{1,64} and optionally followed by array indexes [n] (n is 0 or a number with no leading zero, at most 999999999 \u2014 files[0].series, m[0][2]); no index leading the path, no quoted key, no wildcard, no expression";
510
541
  function keyPathProblem(v) {
511
542
  if (typeof v !== "string") return `settings.key must be a key path string, got ${JSON.stringify(v)}`;
512
543
  if (KEY_PATH_RE.test(v)) return null;
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)`;
544
+ return `settings.key ${JSON.stringify(v)} is not a key path \u2014 ${KEY_PATH_WORDS} (a key that must be computed is computed in the task before the gate)`;
514
545
  }
515
546
  var GATE_TYPE_KEYS = ["routes", "defaultRoute", "settingsSchema", "keyResolvedRoutes", "routesFor", "settingsProblem", "fn"];
516
547
  var STATIC_ROUTE_KEYS = ["routes", "defaultRoute", "keyResolvedRoutes"];
@@ -762,12 +793,17 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
762
793
  }
763
794
  const path = o.path;
764
795
  if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
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` };
796
+ return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
766
797
  }
767
- const root = path.split(".")[0];
798
+ const first = path.split(".")[0];
799
+ const bracket = first.indexOf("[");
800
+ const root = bracket === -1 ? first : first.slice(0, bracket);
768
801
  if (!scope.roots.includes(root)) {
769
802
  return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
770
803
  }
804
+ if (bracket !== -1) {
805
+ return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} indexes its root ${JSON.stringify(root)} \u2014 a path names a KEY inside its root first, as a gate's key path names a key inside the value that reached it; an index follows a key (${root}.items[0])` };
806
+ }
771
807
  if (path.split(".").length < 2) {
772
808
  return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
773
809
  }
@@ -958,10 +994,35 @@ function referenceTarget(ref) {
958
994
  }
959
995
  return null;
960
996
  }
997
+ var ERROR_PART = "!error";
961
998
  function aimed(s, form) {
962
999
  const conditional = s.endsWith("?");
963
- const node = conditional ? s.slice(0, -1) : s;
964
- return ID_RE.test(node) ? { form, node, conditional } : null;
1000
+ const body = conditional ? s.slice(0, -1) : s;
1001
+ const error = body.endsWith(ERROR_PART);
1002
+ const node = error ? body.slice(0, -ERROR_PART.length) : body;
1003
+ if (!ID_RE.test(node)) return null;
1004
+ return error ? { form, node, conditional, part: "error" } : { form, node, conditional };
1005
+ }
1006
+ var spelled = (t, conditional) => `${t.node}${t.part === "error" ? ERROR_PART : ""}${conditional ? "?" : ""}`;
1007
+ function bangProblem(ref) {
1008
+ const array = Array.isArray(ref) && ref.length === 1;
1009
+ const one = array ? ref[0] : ref;
1010
+ if (typeof one !== "string" || one.startsWith("$") || !one.includes("!")) return null;
1011
+ const wrap = (s) => JSON.stringify(array ? [s] : s);
1012
+ const at = one.indexOf("!");
1013
+ const before = one.slice(0, at);
1014
+ const after = one.slice(at + 1);
1015
+ const afterBare = after.endsWith("?") ? after.slice(0, -1) : after;
1016
+ if (afterBare === "error" && before.endsWith("?") && ID_RE.test(before.slice(0, -1))) {
1017
+ return `${JSON.stringify(ref)} carries "?" before "!error", and the suffix goes last: write ${wrap(`${before.slice(0, -1)}!error?`)}`;
1018
+ }
1019
+ if (before === "") {
1020
+ return `${JSON.stringify(ref)} names no node before "!": write the node whose error you mean first, as ${wrap(`<node>!error${one.endsWith("?") ? "?" : ""}`)}`;
1021
+ }
1022
+ if (afterBare !== "error" && ID_RE.test(before)) {
1023
+ return `${JSON.stringify(ref)} names "${after}" after "!", and the one part of a node's outcome a reference may name is its error: write ${wrap(`${before}!error${after.endsWith("?") ? "?" : ""}`)}, or ${wrap(`${before}${after.endsWith("?") ? "?" : ""}`)} for its result`;
1024
+ }
1025
+ return null;
965
1026
  }
966
1027
  var PAYLOAD_TOKEN = "$payload";
967
1028
  var ITEM_TOKEN = "$item";
@@ -1230,8 +1291,15 @@ function validateDefinition(doc, registry) {
1230
1291
  if (nd.edges !== void 0) add("FD-V9", where, "fanout has edges");
1231
1292
  if (nd.branches !== void 0 && !Array.isArray(nd.branches)) {
1232
1293
  add("FD-V21", where, "fanout branches must be an array");
1294
+ } else if (Array.isArray(nd.branches)) {
1295
+ nd.branches.forEach((b, i) => {
1296
+ if (typeof b !== "string") add("FD-V21", where, `fanout branches[${i}] must be a string, got ${describeValue(b)}`);
1297
+ });
1233
1298
  }
1234
1299
  }
1300
+ if (nd.kind === "join" && nd.of !== void 0 && typeof nd.of !== "string") {
1301
+ add("FD-V21", where, `join of must be a string, got ${describeValue(nd.of)}`);
1302
+ }
1235
1303
  if (nd.kind === "map") {
1236
1304
  if (nd.edges !== void 0) add("FD-V25", where, "map has edges");
1237
1305
  if (nd.branch !== void 0 && typeof nd.branch !== "string") {
@@ -1252,10 +1320,13 @@ function validateDefinition(doc, registry) {
1252
1320
  if (!ID_RE.test(k)) add("FD-V26", where, `needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
1253
1321
  const target = referenceTarget(ref);
1254
1322
  const dollar = target === null ? dollarProblem(ref) : null;
1323
+ const bang = target === null && dollar === null ? bangProblem(ref) : null;
1255
1324
  if (dollar !== null) {
1256
1325
  add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${dollar}`);
1326
+ } else if (bang !== null) {
1327
+ add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${bang}`);
1257
1328
  } 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`);
1329
+ add("FD-V26", where, `needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "." , "$payload", "$item", a node id with an optional "!error" and an optional "?" suffix, or a one-element array holding one`);
1259
1330
  }
1260
1331
  }
1261
1332
  }
@@ -1318,10 +1389,13 @@ function validateDefinition(doc, registry) {
1318
1389
  if (!ID_RE.test(k)) add("FD-V26", at, `edge needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
1319
1390
  const target = referenceTarget(ref);
1320
1391
  const dollar = target === null ? dollarProblem(ref) : null;
1392
+ const bang = target === null && dollar === null ? bangProblem(ref) : null;
1321
1393
  if (dollar !== null) {
1322
1394
  add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${dollar}`);
1395
+ } else if (bang !== null) {
1396
+ add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${bang}`);
1323
1397
  } 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`);
1398
+ add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "$payload", "$item", a node id with an optional "!error" and an optional "?" suffix or a one-element array holding one`);
1325
1399
  } else if (target.form === "self") {
1326
1400
  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`);
1327
1401
  }
@@ -1502,14 +1576,14 @@ function validateDefinition(doc, registry) {
1502
1576
  add(
1503
1577
  "FD-V29",
1504
1578
  at,
1505
- `${site}[${JSON.stringify(k)}] names "${target}" unconditionally, and a run can reach this node without "${target}" having run \u2014 a decision routes around it, or it is inside a region whose branch may be lost before it is entered. \`FD-V27\` checked that this node is reachable FROM it, which is weaker. Write "${target}?"${t.form === "all" ? " inside the array" : ""} if you mean *if it ran*, and read the key as ${t.form === "all" ? "a possibly-empty array" : "possibly absent"}; otherwise move the reference to a node every path reaches`
1579
+ `${site}[${JSON.stringify(k)}] names "${spelled(t, false)}" unconditionally, and a run can reach this node without "${target}" having run \u2014 a decision routes around it, or it is inside a region whose branch may be lost before it is entered. \`FD-V27\` checked that this node is reachable FROM it, which is weaker. Write "${spelled(t, true)}"${t.form === "all" ? " inside the array" : ""} if you mean *if it ran*, and read the key as ${t.form === "all" ? "a possibly-empty array" : "possibly absent"}; otherwise move the reference to a node every path reaches`
1506
1580
  );
1507
1581
  }
1508
1582
  const many = regions.some((r) => r.opensWith === "map" && r.members.has(target) && !r.members.has(n.id));
1509
1583
  if (many && t.form === "one") {
1510
- add("FD-V28", at, `${site}[${JSON.stringify(k)}] names "${target}" as one value, but it is inside a map region this node is outside of, so it produces one output per ITEM; write ["${target}"]`);
1584
+ add("FD-V28", at, `${site}[${JSON.stringify(k)}] names "${spelled(t, false)}" as one value, but it is inside a map region this node is outside of, so it produces one output per ITEM; write ["${spelled(t, false)}"]`);
1511
1585
  } else if (!many && t.form === "all") {
1512
- add("FD-V28", at, `${site}[${JSON.stringify(k)}] names ["${target}"] as a list, but it produces exactly one output on this node's branch path; write "${target}"`);
1586
+ add("FD-V28", at, `${site}[${JSON.stringify(k)}] names ["${spelled(t, false)}"] as a list, but it produces exactly one output on this node's branch path; write "${spelled(t, false)}"`);
1513
1587
  }
1514
1588
  }
1515
1589
  }
@@ -2024,7 +2098,8 @@ var typesOf = (s) => {
2024
2098
  };
2025
2099
  var widen = (ts) => ts.flatMap((t) => t === "integer" ? ["integer"] : t === "number" ? ["number", "integer"] : [t]);
2026
2100
  var canon = (v) => JSON.stringify(v);
2027
- var BRANCH_KEY = /^\d+(\.\d+)*$/;
2101
+ var BRANCH_KEY = /^(?:0|[1-9][0-9]*)$/;
2102
+ var ERROR_SCHEMA = Object.freeze({ type: ["string", "null"] });
2028
2103
  function fits(P, C, path = "$") {
2029
2104
  const p = P;
2030
2105
  const c = C;
@@ -2055,7 +2130,7 @@ function fits(P, C, path = "$") {
2055
2130
  if (!own2(pp, k)) {
2056
2131
  if (record !== void 0 && BRANCH_KEY.test(k)) {
2057
2132
  const r2 = fits(record, cp[k], `${path}.${k}`);
2058
- if (r2.verdict !== "fits") return { verdict: "no", why: `${r2.why} (a branch path the map's record may mint)` };
2133
+ if (r2.verdict !== "fits") return { verdict: "no", why: `${r2.why} (an item index the map's record may mint)` };
2059
2134
  continue;
2060
2135
  }
2061
2136
  if (p.additionalProperties === false) continue;
@@ -2159,14 +2234,21 @@ var Wiring = class {
2159
2234
  * key-resolved route licenses. A step the producer's schema does not describe is left alone rather than
2160
2235
  * invented: `required` on a key no `properties` entry mentions is still a promise the value carries it,
2161
2236
  * which is what the consumer asked about, and describing its SHAPE is not this function's to guess.
2237
+ * **An index step `[n]` raises the array's `minItems` to `n + 1` and ends the walk** (v0.53, ADR 0137) — below.
2162
2238
  */
2163
2239
  requireAlong(schema, key) {
2164
2240
  if (schema === void 0 || key === "") return schema;
2165
- const steps = key.split(".");
2241
+ const steps = keyPathSteps(key);
2166
2242
  const go = (s, i) => {
2167
2243
  if (i >= steps.length || typeof s !== "object" || s === null || Array.isArray(s)) return s;
2168
2244
  const o = s;
2169
- const here = steps[i];
2245
+ const step2 = steps[i];
2246
+ if ("index" in step2) {
2247
+ const floor = step2.index + 1;
2248
+ const had = typeof o.minItems === "number" ? o.minItems : 0;
2249
+ return had >= floor ? o : { ...o, minItems: floor };
2250
+ }
2251
+ const here = step2.key;
2170
2252
  const req = Array.isArray(o.required) ? o.required : [];
2171
2253
  const props = o.properties ?? {};
2172
2254
  const described = own2(props, here);
@@ -2186,9 +2268,10 @@ var Wiring = class {
2186
2268
  * the producer can emit.
2187
2269
  *
2188
2270
  * **The question is per STEP and about the CONTAINER, because that is what `readKeyPath` is** —
2189
- * the DRIVER's own resolution (`src/definition/validate.ts`, W93), where each segment is an OWN key
2190
- * of a PLAIN object and a missing key, a `null`, an array mid-path or a primitive is `undefined`.
2191
- * So two shapes forbid a step and the third is a silence:
2271
+ * the DRIVER's own resolution (`src/definition/condition.ts`, W93), where each KEY step is an OWN key
2272
+ * of a PLAIN object and a missing key, a `null`, an array under a key step or a primitive is `undefined`
2273
+ * — and, since v0.53, each INDEX step `[n]` an element of a REAL array (ADR 0137), asked below in the loop.
2274
+ * So, for a key step, two shapes forbid it and the third is a silence:
2192
2275
  *
2193
2276
  * - **its declared types cannot include `object`** — `name.id` where the producer calls `name` a
2194
2277
  * string. No value the schema admits carries the path.
@@ -2210,13 +2293,21 @@ var Wiring = class {
2210
2293
  * Every rung this repository has is green on a flow that did none of the work it exists for.
2211
2294
  */
2212
2295
  keyPathRefusal(schema, key, gateId, route) {
2213
- const steps = key.split(".");
2296
+ const steps = keyPathSteps(key);
2214
2297
  let s = schema;
2215
- const because = (at, detail) => `gate "${gateId}" is keyed on "${key}", which the producer's schema FORBIDS at ${at}: ${detail} \u2014 so route "${route}" can never be taken, and a run may route every branch by another route, doing none of the work this gate guards`;
2216
- for (let i = 0; i < steps.length; i++) {
2217
- const here = steps[i];
2218
- const at = i === 0 ? "$" : `$.${steps.slice(0, i).join(".")}`;
2298
+ let at = "$";
2299
+ const because = (at2, detail) => `gate "${gateId}" is keyed on "${key}", which the producer's schema FORBIDS at ${at2}: ${detail} \u2014 so route "${route}" can never be taken, and a run may route every branch by another route, doing none of the work this gate guards`;
2300
+ for (const step2 of steps) {
2219
2301
  const ts = typesOf(s);
2302
+ if ("index" in step2) {
2303
+ if (!ts.includes("array")) return because(at, `the schema calls it ${ts.join("|")}, and an index [${step2.index}] reads an element of an array`);
2304
+ if (s.items === void 0) return null;
2305
+ if (!isPlainObject3(s.items)) return null;
2306
+ s = s.items;
2307
+ at = `${at}[${step2.index}]`;
2308
+ continue;
2309
+ }
2310
+ const here = step2.key;
2220
2311
  if (!ts.includes("object")) return because(at, `the schema calls it ${ts.join("|")}, and a key path reads an own key of a plain object`);
2221
2312
  const props = s.properties ?? {};
2222
2313
  if (!own2(props, here)) {
@@ -2229,6 +2320,7 @@ var Wiring = class {
2229
2320
  const next = props[here];
2230
2321
  if (!isPlainObject3(next)) return null;
2231
2322
  s = next;
2323
+ at = at === "$" ? `$.${here}` : `${at}.${here}`;
2232
2324
  }
2233
2325
  return null;
2234
2326
  }
@@ -2321,7 +2413,7 @@ var Wiring = class {
2321
2413
  const members = region.branchOf.get(branch) ?? /* @__PURE__ */ new Set();
2322
2414
  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));
2323
2415
  }
2324
- /** `JP-R12`'s record for the region a join closes — exact for a fan-out, keyed by minted paths for a map. */
2416
+ /** `JP-R12`'s record for the region a join closes — exact for a fan-out, keyed by entry id; keyed by item index for a map. */
2325
2417
  record(j) {
2326
2418
  const region = this.regions.find((r) => r.joinId === j.id);
2327
2419
  const base = { shape: "record", producer: j.id };
@@ -2345,22 +2437,16 @@ var Wiring = class {
2345
2437
  values.push({ branch: b, schema: firstSchema, sample: first.sample ?? { value: null, source: "witness" } });
2346
2438
  }
2347
2439
  const source = values.every((v2) => v2.sample.source === "example") ? "example" : "witness";
2348
- const enclosing = new Map(this.regions.filter((r) => r.opensWith === "map" && r.members.has(region.fanoutId)).map((r) => [r.fanoutId, 0]));
2349
2440
  if (opener.kind === "fanout") {
2350
- const props = {};
2351
- const sample2 = {};
2352
- for (const v2 of values) {
2353
- const path = branchPathOf(this.regions, v2.branch, enclosing);
2354
- props[path] = v2.schema;
2355
- sample2[path] = v2.sample.value;
2356
- }
2441
+ const props = Object.fromEntries(values.map((v2) => [v2.branch, v2.schema]));
2442
+ const sample2 = Object.fromEntries(values.map((v2) => [v2.branch, v2.sample.value]));
2357
2443
  return { ...base, schema: { type: "object", required: [], properties: props, additionalProperties: false }, sample: { value: sample2, source, arity: values.length } };
2358
2444
  }
2359
2445
  const v = values[0];
2360
2446
  if (!v) return { ...base, unchecked: `map "${opener.id}"'s branch never arrives at "${j.id}"` };
2361
2447
  const arity = this.arrivals(opener.branch)[0]?.sample?.arity ?? 1;
2362
2448
  const sample = {};
2363
- for (let i = 0; i < arity; i++) sample[branchPathOf(this.regions, opener.branch, new Map([...enclosing, [opener.id, i]]))] = v.sample.value;
2449
+ for (let i = 0; i < arity; i++) sample[String(i)] = v.sample.value;
2364
2450
  return {
2365
2451
  ...base,
2366
2452
  schema: { type: "object", additionalProperties: v.schema, branchKeys: true },
@@ -2419,6 +2505,11 @@ var Wiring = class {
2419
2505
  value = a.sample?.value;
2420
2506
  src = a.sample?.source ?? "witness";
2421
2507
  dot.push(key);
2508
+ } else if (t.part === "error") {
2509
+ schema2 = ERROR_SCHEMA;
2510
+ const w = sampleOf(ERROR_SCHEMA);
2511
+ value = w.value;
2512
+ src = w.source;
2422
2513
  } else {
2423
2514
  const p = this.produced(t.node);
2424
2515
  if (p.schema === void 0) return { ...base, unchecked: `needs.${key} names "${t.node}", which ${p.unchecked}` };
@@ -2538,6 +2629,7 @@ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, l
2538
2629
  for (const input of inputs) results.push(await c.evaluate(input, settings));
2539
2630
  } catch (e) {
2540
2631
  v.kind = "transform-fails";
2632
+ v.cause = "threw";
2541
2633
  v.why = `the transform threw on the ${sourceWord(sample.source)}: ${e instanceof Error ? e.message : String(e)}`;
2542
2634
  v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
2543
2635
  return;
@@ -2551,6 +2643,7 @@ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, l
2551
2643
  const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
2552
2644
  if (bad) {
2553
2645
  v.kind = "transform-fails";
2646
+ v.cause = "schema";
2554
2647
  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
2648
  } else {
2556
2649
  v.kind = "transform-passes";
@@ -2586,7 +2679,7 @@ var WIRING_SHAPES = ["edge", "branch", "item", "record", "needs", "given"];
2586
2679
  var SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
2587
2680
  var BRANCH_PATH = /^(?:-|(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*))*)$/;
2588
2681
  var ATTEMPT = /^(?:0|[1-9][0-9]*)$/;
2589
- var INDEX2 = /^(?:0|[1-9][0-9]*)$/;
2682
+ var INDEX3 = /^(?:0|[1-9][0-9]*)$/;
2590
2683
  var InvalidRunId = class extends Error {
2591
2684
  };
2592
2685
  var InvalidTaskId = class extends Error {
@@ -2610,7 +2703,7 @@ function branchPathSegment(branchIndex) {
2610
2703
  throw new InvalidTaskId("branchPath must be null for a node in no region, never an empty path");
2611
2704
  }
2612
2705
  for (const n of parts) {
2613
- if (!INDEX2.test(String(n))) {
2706
+ if (!INDEX3.test(String(n))) {
2614
2707
  throw new InvalidTaskId(`branchPath segment ${JSON.stringify(n)} is not a non-negative integer`);
2615
2708
  }
2616
2709
  }
@@ -2934,7 +3027,7 @@ var RunControlRefused = class extends Error {
2934
3027
  this.reason = reason;
2935
3028
  }
2936
3029
  };
2937
- var CHECKPOINT_FORMAT = 1;
3030
+ var CHECKPOINT_FORMAT = 2;
2938
3031
  var ResumeRefused = class extends Error {
2939
3032
  reason;
2940
3033
  /** The position `halted`, `cancelled` and `diverged` name; `null` for the three a header decides. */
@@ -3046,6 +3139,11 @@ function sameData(a, b) {
3046
3139
  const kb = Object.keys(rb).filter((k) => rb[k] !== void 0);
3047
3140
  return ka.length === kb.length && ka.every((k) => Object.hasOwn(rb, k) && sameData(ra[k], rb[k]));
3048
3141
  }
3142
+ function storedAskedAt(e) {
3143
+ if (e === void 0 || e === null || typeof e !== "object" || e.kind !== "asked") return void 0;
3144
+ const t = e.observedAt;
3145
+ return typeof t === "number" ? t : void 0;
3146
+ }
3049
3147
  function isFed(e) {
3050
3148
  return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed" || e.kind === "interrupted" || e.kind === "paused" || e.kind === "unpaused" || e.kind === "attempted";
3051
3149
  }
@@ -3128,11 +3226,12 @@ function compareBranchPaths(a, b) {
3128
3226
  }
3129
3227
  return 0;
3130
3228
  }
3131
- function collectArrivals(arrived) {
3132
- const out = {};
3133
- if (!arrived) return out;
3134
- for (const path of [...arrived.keys()].sort(compareBranchPaths)) out[path] = arrived.get(path);
3135
- return out;
3229
+ function collectArrivals(arrived, region) {
3230
+ if (!arrived) return {};
3231
+ return Object.fromEntries([...arrived.keys()].sort(compareBranchPaths).map((path) => {
3232
+ const segment = path.slice(path.lastIndexOf(".") + 1);
3233
+ return [region.opensWith === "fanout" ? region.branches[Number(segment)] : segment, arrived.get(path)];
3234
+ }));
3136
3235
  }
3137
3236
  var EMPTY_INDICES = /* @__PURE__ */ new Map();
3138
3237
  var EMPTY_NEEDS = Object.freeze({});
@@ -3315,7 +3414,8 @@ var Driver = class _Driver {
3315
3414
  firedJoins = /* @__PURE__ */ new Map();
3316
3415
  /**
3317
3416
  * **What each branch carried into its join, keyed by region and then by
3318
- * `branchPath`** — [ADR 0037](../../doc/decisions/0037-what-a-branch-receives-is-a-property-of-the-fan-out-and-what-a-join-carries-is-a-property-of-the-region.md)
3417
+ * `branchPath`** — the FULL path, here where it is recorded; the collection a join hands on is keyed by each
3418
+ * branch's OWN key, derived from these paths by `collectArrivals` at the exit (ADR 0138) —[ADR 0037](../../doc/decisions/0037-what-a-branch-receives-is-a-property-of-the-fan-out-and-what-a-join-carries-is-a-property-of-the-region.md)
3319
3419
  * rule 4, and `JP-R11` in [spec/join-policy.md](../../spec/join-policy.md)
3320
3420
  * section 4.
3321
3421
  *
@@ -5408,7 +5508,7 @@ var Driver = class _Driver {
5408
5508
  }
5409
5509
  /**
5410
5510
  * **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
5411
- * was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
5511
+ * was just read. An entry is `{ path, result, error }` with `result` the record's own object (`RN-R13`) and `error` its string or `null` (v0.49, ADR 0131); a node's list is kept
5412
5512
  * in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
5413
5513
  *
5414
5514
  * **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
@@ -5441,7 +5541,7 @@ var Driver = class _Driver {
5441
5541
  }
5442
5542
  return;
5443
5543
  }
5444
- const entry = { path: t.path, result: eff.result };
5544
+ const entry = { path: t.path, result: eff.result, error: eff.error };
5445
5545
  if (t.entry !== null && list !== void 0) {
5446
5546
  list[list.indexOf(t.entry)] = entry;
5447
5547
  this.reindex(c, t.nodeId, t.entry, entry);
@@ -5616,6 +5716,9 @@ var Driver = class _Driver {
5616
5716
  }
5617
5717
  const c = this.outputsCache;
5618
5718
  c.examined += t.form === "all" ? visible.length : 1;
5719
+ if (t.part === "error") {
5720
+ return t.form === "all" ? visible.map((e) => e.error) : visible[0].error;
5721
+ }
5619
5722
  return t.form === "all" ? visible.map((e) => this.sharedOf(c, e)) : this.sharedOf(c, visible[0]);
5620
5723
  }
5621
5724
  /** **An output entry's shared copy**, made at its first read and kept on the entry (`OutputEntry.shared`) — counted. */
@@ -5782,7 +5885,10 @@ var Driver = class _Driver {
5782
5885
  throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
5783
5886
  }
5784
5887
  this.asked.add(taskId);
5785
- this.append({ kind: "asked", taskId, input: cloneValue(received) });
5888
+ const replay = this.replay;
5889
+ const at = this.inputs.length;
5890
+ const observedAt = replay !== null && at < replay.stored.length ? storedAskedAt(replay.stored[at]) : this.now();
5891
+ this.append(observedAt === void 0 ? { kind: "asked", taskId, input: cloneValue(received) } : { kind: "asked", taskId, input: cloneValue(received), observedAt });
5786
5892
  if (this.shut()) return;
5787
5893
  const region = this.regionOf(node.id);
5788
5894
  if (region) {
@@ -6204,7 +6310,7 @@ var Driver = class _Driver {
6204
6310
  fired: true
6205
6311
  });
6206
6312
  if (this.shut()) return;
6207
- const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
6313
+ const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp), region);
6208
6314
  this.releaseArrivals(region.fanoutId, rp);
6209
6315
  const reading = this.replay?.reading ?? null;
6210
6316
  if (reading !== null && region.joinId === reading.nodeId) {
@@ -6676,7 +6782,9 @@ var Accumulator = class _Accumulator {
6676
6782
  state: "suspended",
6677
6783
  result: null,
6678
6784
  error: null,
6679
- input: structuredClone(input.input)
6785
+ input: structuredClone(input.input),
6786
+ // `RN-R36` v0.35 (W208, ADR 0130): WHEN it was asked, where the element says — and only there.
6787
+ ...input.observedAt !== void 0 ? { askedAt: input.observedAt } : {}
6680
6788
  });
6681
6789
  return this.withOutcome();
6682
6790
  }
@@ -6744,7 +6852,8 @@ var Accumulator = class _Accumulator {
6744
6852
  state: r.resolvedAs,
6745
6853
  result: r.result,
6746
6854
  error: r.error,
6747
- ...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
6855
+ ...existing !== void 0 && "input" in existing ? { input: existing.input } : {},
6856
+ ...existing?.askedAt !== void 0 ? { askedAt: existing.askedAt } : {}
6748
6857
  });
6749
6858
  }
6750
6859
  this.runId = this.runId ?? runId;
@@ -6920,10 +7029,16 @@ export {
6920
7029
  findings,
6921
7030
  describeFindings,
6922
7031
  readKeyPath,
7032
+ ID_RE_COPY,
7033
+ KINDS_COPY,
7034
+ ROUTABLE_STATES,
6923
7035
  ASK_REFUSED_KEYS,
6924
7036
  EDGE_KEYS_DECLARED,
6925
7037
  TASK_KEYS_DECLARED,
6926
7038
  NODE_KEYS_DECLARED,
7039
+ NODE_KEYS_REQUIRED,
7040
+ EDGE_KEYS_REQUIRED,
7041
+ DOC_KEYS_DECLARED,
6927
7042
  RETRY_POLICY_KEYS,
6928
7043
  retryPolicyProblem,
6929
7044
  BUILT_IN_NODE_TYPES,