fyflow-orchestration 0.11.0 → 0.13.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.
@@ -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;
@@ -380,7 +402,13 @@ function evalLeaf(scope, p, unknownOp) {
380
402
  else if (typeof v === "string") v = v.length;
381
403
  else return false;
382
404
  }
383
- const w = p.value;
405
+ let w;
406
+ if (p.valueFrom !== void 0) {
407
+ w = readKeyPath(scope, p.valueFrom);
408
+ if (w === void 0 || typeof w === "object" && w !== null) return false;
409
+ } else {
410
+ w = p.value;
411
+ }
384
412
  switch (p.op) {
385
413
  // **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
386
414
  // `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
@@ -514,11 +542,12 @@ function languagesOf(x) {
514
542
  if (x instanceof Map) return x;
515
543
  return new Map(Object.entries(x));
516
544
  }
517
- var KEY_PATH_RE = /^[A-Za-z0-9_-]{1,64}(?:\.[A-Za-z0-9_-]{1,64})*$/;
545
+ 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})\])*)*$/;
546
+ 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";
518
547
  function keyPathProblem(v) {
519
548
  if (typeof v !== "string") return `settings.key must be a key path string, got ${JSON.stringify(v)}`;
520
549
  if (KEY_PATH_RE.test(v)) return null;
521
- 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)`;
550
+ 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)`;
522
551
  }
523
552
  var GATE_TYPE_KEYS = ["routes", "defaultRoute", "settingsSchema", "keyResolvedRoutes", "routesFor", "settingsProblem", "fn"];
524
553
  var STATIC_ROUTE_KEYS = ["routes", "defaultRoute", "keyResolvedRoutes"];
@@ -732,8 +761,9 @@ function gateTypeCollisions(x) {
732
761
  return names.filter((n) => Object.prototype.hasOwnProperty.call(BUILT_IN_GATE_TYPES, n)).sort();
733
762
  }
734
763
  var PREDICATE_NAME_KEYS = ["predicate"];
735
- var PREDICATE_LEAF_KEYS = ["path", "op", "value", "default", "length"];
736
- var PREDICATE_LEAF_REQUIRED = ["path", "op", "value"];
764
+ var PREDICATE_LEAF_KEYS = ["path", "op", "value", "valueFrom", "default", "length"];
765
+ var PREDICATE_LEAF_REQUIRED = ["path", "op"];
766
+ var RIGHT_SIDE_KEYS = ["value", "valueFrom"];
737
767
  var PREDICATE_GROUP_KEYS = ["all", "any"];
738
768
  function predicateShapeOf(o) {
739
769
  const marks = [];
@@ -760,24 +790,53 @@ var GATE_SCOPE = {
760
790
  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',
761
791
  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"
762
792
  };
793
+ function comparisonPathProblem(o, key, where, scope) {
794
+ const path = o[key];
795
+ if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
796
+ return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
797
+ }
798
+ const segments = path.split(".");
799
+ const first = segments[0];
800
+ const bracket = first.indexOf("[");
801
+ const root = bracket === -1 ? first : first.slice(0, bracket);
802
+ if (!scope.roots.includes(root)) {
803
+ return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
804
+ }
805
+ if (bracket !== -1) {
806
+ return { rule: "FD-V43", message: `${where} ${key} ${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])` };
807
+ }
808
+ if (segments.length < 2) {
809
+ return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
810
+ }
811
+ if (root === "needs" && scope.needsDeclared !== void 0) {
812
+ const seg = segments[1];
813
+ const at = seg.indexOf("[");
814
+ const name = at === -1 ? seg : seg.slice(0, at);
815
+ if (!scope.needsDeclared.includes(name)) {
816
+ const declared = scope.needsDeclared.length === 0 ? "declares no `needs`, so its `ctx.needs` is {}" : `declares needs ${scope.needsDeclared.map((k2) => JSON.stringify(k2)).join(", ")}`;
817
+ return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} reads needs key ${JSON.stringify(name)}, and this edge ${declared} \u2014 the key would be absent for ever and the comparison no match for ever; declare it in the edge's needs` };
818
+ }
819
+ }
820
+ return null;
821
+ }
763
822
  function leafProblem(o, where, scope = EDGE_SCOPE) {
764
823
  const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
765
824
  if (extra.length) {
766
825
  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(", ")} }` };
767
826
  }
768
827
  for (const k of PREDICATE_LEAF_REQUIRED) {
769
- if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required` };
828
+ if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required and exactly one of ${RIGHT_SIDE_KEYS.map((k2) => JSON.stringify(k2)).join(" or ")}` };
770
829
  }
771
- const path = o.path;
772
- if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
773
- 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` };
830
+ const sides = RIGHT_SIDE_KEYS.filter((k) => k in o);
831
+ if (sides.length !== 1) {
832
+ return { rule: "FD-V42", message: `${where} carries ${sides.length === 0 ? "neither" : "both"} "value" and "valueFrom"; a comparison compares against exactly one \u2014 a literal "value", or "valueFrom", a key path over the same context` };
774
833
  }
775
- const root = path.split(".")[0];
776
- if (!scope.roots.includes(root)) {
777
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
778
- }
779
- if (path.split(".").length < 2) {
780
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
834
+ const pathProblem = comparisonPathProblem(o, "path", where, scope);
835
+ if (pathProblem !== null) return pathProblem;
836
+ const fromValue = "valueFrom" in o;
837
+ if (fromValue) {
838
+ const fromProblem = comparisonPathProblem(o, "valueFrom", where, scope);
839
+ if (fromProblem !== null) return fromProblem;
781
840
  }
782
841
  const op = o.op;
783
842
  if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
@@ -787,6 +846,12 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
787
846
  return { rule: "FD-V44", message: `${where} length is ${JSON.stringify(o.length)}; it is exactly true or absent \u2014 there is no "compare the value, not its length" to ask for` };
788
847
  }
789
848
  const takesLength = o.length === true;
849
+ if (fromValue) {
850
+ if (takesLength && STRING_OPS.has(op)) {
851
+ return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
852
+ }
853
+ return defaultProblem(o, where, takesLength);
854
+ }
790
855
  if (!isScalar(o.value)) {
791
856
  return { rule: "FD-V44", message: `${where} value is ${scalarName(o.value)}; a comparison compares against a SCALAR \u2014 null, a boolean, a finite number or a string` };
792
857
  }
@@ -802,6 +867,9 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
802
867
  if (takesLength && STRING_OPS.has(op)) {
803
868
  return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
804
869
  }
870
+ return defaultProblem(o, where, takesLength);
871
+ }
872
+ function defaultProblem(o, where, takesLength) {
805
873
  if ("default" in o) {
806
874
  const d = o.default;
807
875
  const okDefault = Array.isArray(d) ? d.every(isScalar) : isScalar(d);
@@ -817,7 +885,8 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
817
885
  }
818
886
  return null;
819
887
  }
820
- function predicateProblem(v) {
888
+ function predicateProblem(v, edgeNeeds) {
889
+ const scope = edgeNeeds === void 0 ? EDGE_SCOPE : { ...EDGE_SCOPE, needsDeclared: edgeNeeds };
821
890
  if (typeof v !== "object" || v === null || Array.isArray(v)) {
822
891
  return {
823
892
  rule: "FD-V42",
@@ -838,8 +907,8 @@ function predicateProblem(v) {
838
907
  if (typeof o.predicate !== "string") return { rule: "FD-V42", message: `when predicate is ${scalarName(o.predicate)}; it is the name of a registered predicate` };
839
908
  return null;
840
909
  }
841
- if (shape === "leaf") return leafProblem(o, "when");
842
- return groupProblem(o, "when", EDGE_SCOPE);
910
+ if (shape === "leaf") return leafProblem(o, "when", scope);
911
+ return groupProblem(o, "when", scope);
843
912
  }
844
913
  function groupProblem(o, label, scope) {
845
914
  const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
@@ -1263,8 +1332,15 @@ function validateDefinition(doc, registry) {
1263
1332
  if (nd.edges !== void 0) add("FD-V9", where, "fanout has edges");
1264
1333
  if (nd.branches !== void 0 && !Array.isArray(nd.branches)) {
1265
1334
  add("FD-V21", where, "fanout branches must be an array");
1335
+ } else if (Array.isArray(nd.branches)) {
1336
+ nd.branches.forEach((b, i) => {
1337
+ if (typeof b !== "string") add("FD-V21", where, `fanout branches[${i}] must be a string, got ${describeValue(b)}`);
1338
+ });
1266
1339
  }
1267
1340
  }
1341
+ if (nd.kind === "join" && nd.of !== void 0 && typeof nd.of !== "string") {
1342
+ add("FD-V21", where, `join of must be a string, got ${describeValue(nd.of)}`);
1343
+ }
1268
1344
  if (nd.kind === "map") {
1269
1345
  if (nd.edges !== void 0) add("FD-V25", where, "map has edges");
1270
1346
  if (nd.branch !== void 0 && typeof nd.branch !== "string") {
@@ -1325,7 +1401,9 @@ function validateDefinition(doc, registry) {
1325
1401
  }
1326
1402
  }
1327
1403
  if (ed?.when !== void 0) {
1328
- const problem = predicateProblem(ed.when);
1404
+ const nv = ed.needs;
1405
+ const edgeNeeds = nv === void 0 ? [] : typeof nv === "object" && nv !== null && !Array.isArray(nv) && Object.keys(nv).length > 0 ? Object.keys(nv) : void 0;
1406
+ const problem = predicateProblem(ed.when, edgeNeeds);
1329
1407
  if (problem) add(problem.rule, at, problem.message);
1330
1408
  else for (const name of predicateNamesIn(ed.when)) {
1331
1409
  if (!preds.has(name)) add("FD-V4", at, `when names "${name}", which is not a registered predicate`);
@@ -2063,7 +2141,7 @@ var typesOf = (s) => {
2063
2141
  };
2064
2142
  var widen = (ts) => ts.flatMap((t) => t === "integer" ? ["integer"] : t === "number" ? ["number", "integer"] : [t]);
2065
2143
  var canon = (v) => JSON.stringify(v);
2066
- var BRANCH_KEY = /^\d+(\.\d+)*$/;
2144
+ var BRANCH_KEY = /^(?:0|[1-9][0-9]*)$/;
2067
2145
  var ERROR_SCHEMA = Object.freeze({ type: ["string", "null"] });
2068
2146
  function fits(P, C, path = "$") {
2069
2147
  const p = P;
@@ -2095,7 +2173,7 @@ function fits(P, C, path = "$") {
2095
2173
  if (!own2(pp, k)) {
2096
2174
  if (record !== void 0 && BRANCH_KEY.test(k)) {
2097
2175
  const r2 = fits(record, cp[k], `${path}.${k}`);
2098
- if (r2.verdict !== "fits") return { verdict: "no", why: `${r2.why} (a branch path the map's record may mint)` };
2176
+ if (r2.verdict !== "fits") return { verdict: "no", why: `${r2.why} (an item index the map's record may mint)` };
2099
2177
  continue;
2100
2178
  }
2101
2179
  if (p.additionalProperties === false) continue;
@@ -2199,14 +2277,21 @@ var Wiring = class {
2199
2277
  * key-resolved route licenses. A step the producer's schema does not describe is left alone rather than
2200
2278
  * invented: `required` on a key no `properties` entry mentions is still a promise the value carries it,
2201
2279
  * which is what the consumer asked about, and describing its SHAPE is not this function's to guess.
2280
+ * **An index step `[n]` raises the array's `minItems` to `n + 1` and ends the walk** (v0.53, ADR 0137) — below.
2202
2281
  */
2203
2282
  requireAlong(schema, key) {
2204
2283
  if (schema === void 0 || key === "") return schema;
2205
- const steps = key.split(".");
2284
+ const steps = keyPathSteps(key);
2206
2285
  const go = (s, i) => {
2207
2286
  if (i >= steps.length || typeof s !== "object" || s === null || Array.isArray(s)) return s;
2208
2287
  const o = s;
2209
- const here = steps[i];
2288
+ const step2 = steps[i];
2289
+ if ("index" in step2) {
2290
+ const floor = step2.index + 1;
2291
+ const had = typeof o.minItems === "number" ? o.minItems : 0;
2292
+ return had >= floor ? o : { ...o, minItems: floor };
2293
+ }
2294
+ const here = step2.key;
2210
2295
  const req = Array.isArray(o.required) ? o.required : [];
2211
2296
  const props = o.properties ?? {};
2212
2297
  const described = own2(props, here);
@@ -2226,9 +2311,10 @@ var Wiring = class {
2226
2311
  * the producer can emit.
2227
2312
  *
2228
2313
  * **The question is per STEP and about the CONTAINER, because that is what `readKeyPath` is** —
2229
- * the DRIVER's own resolution (`src/definition/validate.ts`, W93), where each segment is an OWN key
2230
- * of a PLAIN object and a missing key, a `null`, an array mid-path or a primitive is `undefined`.
2231
- * So two shapes forbid a step and the third is a silence:
2314
+ * the DRIVER's own resolution (`src/definition/condition.ts`, W93), where each KEY step is an OWN key
2315
+ * of a PLAIN object and a missing key, a `null`, an array under a key step or a primitive is `undefined`
2316
+ * — and, since v0.53, each INDEX step `[n]` an element of a REAL array (ADR 0137), asked below in the loop.
2317
+ * So, for a key step, two shapes forbid it and the third is a silence:
2232
2318
  *
2233
2319
  * - **its declared types cannot include `object`** — `name.id` where the producer calls `name` a
2234
2320
  * string. No value the schema admits carries the path.
@@ -2250,13 +2336,21 @@ var Wiring = class {
2250
2336
  * Every rung this repository has is green on a flow that did none of the work it exists for.
2251
2337
  */
2252
2338
  keyPathRefusal(schema, key, gateId, route) {
2253
- const steps = key.split(".");
2339
+ const steps = keyPathSteps(key);
2254
2340
  let s = schema;
2255
- 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`;
2256
- for (let i = 0; i < steps.length; i++) {
2257
- const here = steps[i];
2258
- const at = i === 0 ? "$" : `$.${steps.slice(0, i).join(".")}`;
2341
+ let at = "$";
2342
+ 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`;
2343
+ for (const step2 of steps) {
2259
2344
  const ts = typesOf(s);
2345
+ if ("index" in step2) {
2346
+ if (!ts.includes("array")) return because(at, `the schema calls it ${ts.join("|")}, and an index [${step2.index}] reads an element of an array`);
2347
+ if (s.items === void 0) return null;
2348
+ if (!isPlainObject3(s.items)) return null;
2349
+ s = s.items;
2350
+ at = `${at}[${step2.index}]`;
2351
+ continue;
2352
+ }
2353
+ const here = step2.key;
2260
2354
  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`);
2261
2355
  const props = s.properties ?? {};
2262
2356
  if (!own2(props, here)) {
@@ -2269,6 +2363,7 @@ var Wiring = class {
2269
2363
  const next = props[here];
2270
2364
  if (!isPlainObject3(next)) return null;
2271
2365
  s = next;
2366
+ at = at === "$" ? `$.${here}` : `${at}.${here}`;
2272
2367
  }
2273
2368
  return null;
2274
2369
  }
@@ -2361,7 +2456,7 @@ var Wiring = class {
2361
2456
  const members = region.branchOf.get(branch) ?? /* @__PURE__ */ new Set();
2362
2457
  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));
2363
2458
  }
2364
- /** `JP-R12`'s record for the region a join closes — exact for a fan-out, keyed by minted paths for a map. */
2459
+ /** `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. */
2365
2460
  record(j) {
2366
2461
  const region = this.regions.find((r) => r.joinId === j.id);
2367
2462
  const base = { shape: "record", producer: j.id };
@@ -2385,22 +2480,16 @@ var Wiring = class {
2385
2480
  values.push({ branch: b, schema: firstSchema, sample: first.sample ?? { value: null, source: "witness" } });
2386
2481
  }
2387
2482
  const source = values.every((v2) => v2.sample.source === "example") ? "example" : "witness";
2388
- const enclosing = new Map(this.regions.filter((r) => r.opensWith === "map" && r.members.has(region.fanoutId)).map((r) => [r.fanoutId, 0]));
2389
2483
  if (opener.kind === "fanout") {
2390
- const props = {};
2391
- const sample2 = {};
2392
- for (const v2 of values) {
2393
- const path = branchPathOf(this.regions, v2.branch, enclosing);
2394
- props[path] = v2.schema;
2395
- sample2[path] = v2.sample.value;
2396
- }
2484
+ const props = Object.fromEntries(values.map((v2) => [v2.branch, v2.schema]));
2485
+ const sample2 = Object.fromEntries(values.map((v2) => [v2.branch, v2.sample.value]));
2397
2486
  return { ...base, schema: { type: "object", required: [], properties: props, additionalProperties: false }, sample: { value: sample2, source, arity: values.length } };
2398
2487
  }
2399
2488
  const v = values[0];
2400
2489
  if (!v) return { ...base, unchecked: `map "${opener.id}"'s branch never arrives at "${j.id}"` };
2401
2490
  const arity = this.arrivals(opener.branch)[0]?.sample?.arity ?? 1;
2402
2491
  const sample = {};
2403
- for (let i = 0; i < arity; i++) sample[branchPathOf(this.regions, opener.branch, new Map([...enclosing, [opener.id, i]]))] = v.sample.value;
2492
+ for (let i = 0; i < arity; i++) sample[String(i)] = v.sample.value;
2404
2493
  return {
2405
2494
  ...base,
2406
2495
  schema: { type: "object", additionalProperties: v.schema, branchKeys: true },
@@ -2633,7 +2722,7 @@ var WIRING_SHAPES = ["edge", "branch", "item", "record", "needs", "given"];
2633
2722
  var SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
2634
2723
  var BRANCH_PATH = /^(?:-|(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*))*)$/;
2635
2724
  var ATTEMPT = /^(?:0|[1-9][0-9]*)$/;
2636
- var INDEX2 = /^(?:0|[1-9][0-9]*)$/;
2725
+ var INDEX3 = /^(?:0|[1-9][0-9]*)$/;
2637
2726
  var InvalidRunId = class extends Error {
2638
2727
  };
2639
2728
  var InvalidTaskId = class extends Error {
@@ -2657,7 +2746,7 @@ function branchPathSegment(branchIndex) {
2657
2746
  throw new InvalidTaskId("branchPath must be null for a node in no region, never an empty path");
2658
2747
  }
2659
2748
  for (const n of parts) {
2660
- if (!INDEX2.test(String(n))) {
2749
+ if (!INDEX3.test(String(n))) {
2661
2750
  throw new InvalidTaskId(`branchPath segment ${JSON.stringify(n)} is not a non-negative integer`);
2662
2751
  }
2663
2752
  }
@@ -2981,7 +3070,7 @@ var RunControlRefused = class extends Error {
2981
3070
  this.reason = reason;
2982
3071
  }
2983
3072
  };
2984
- var CHECKPOINT_FORMAT = 1;
3073
+ var CHECKPOINT_FORMAT = 2;
2985
3074
  var ResumeRefused = class extends Error {
2986
3075
  reason;
2987
3076
  /** The position `halted`, `cancelled` and `diverged` name; `null` for the three a header decides. */
@@ -3180,11 +3269,12 @@ function compareBranchPaths(a, b) {
3180
3269
  }
3181
3270
  return 0;
3182
3271
  }
3183
- function collectArrivals(arrived) {
3184
- const out = {};
3185
- if (!arrived) return out;
3186
- for (const path of [...arrived.keys()].sort(compareBranchPaths)) out[path] = arrived.get(path);
3187
- return out;
3272
+ function collectArrivals(arrived, region) {
3273
+ if (!arrived) return {};
3274
+ return Object.fromEntries([...arrived.keys()].sort(compareBranchPaths).map((path) => {
3275
+ const segment = path.slice(path.lastIndexOf(".") + 1);
3276
+ return [region.opensWith === "fanout" ? region.branches[Number(segment)] : segment, arrived.get(path)];
3277
+ }));
3188
3278
  }
3189
3279
  var EMPTY_INDICES = /* @__PURE__ */ new Map();
3190
3280
  var EMPTY_NEEDS = Object.freeze({});
@@ -3367,7 +3457,8 @@ var Driver = class _Driver {
3367
3457
  firedJoins = /* @__PURE__ */ new Map();
3368
3458
  /**
3369
3459
  * **What each branch carried into its join, keyed by region and then by
3370
- * `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)
3460
+ * `branchPath`** — the FULL path, here where it is recorded; the collection a join hands on is keyed by each
3461
+ * 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)
3371
3462
  * rule 4, and `JP-R11` in [spec/join-policy.md](../../spec/join-policy.md)
3372
3463
  * section 4.
3373
3464
  *
@@ -6262,7 +6353,7 @@ var Driver = class _Driver {
6262
6353
  fired: true
6263
6354
  });
6264
6355
  if (this.shut()) return;
6265
- const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
6356
+ const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp), region);
6266
6357
  this.releaseArrivals(region.fanoutId, rp);
6267
6358
  const reading = this.replay?.reading ?? null;
6268
6359
  if (reading !== null && region.joinId === reading.nodeId) {
@@ -6980,6 +7071,7 @@ export {
6980
7071
  canonical,
6981
7072
  findings,
6982
7073
  describeFindings,
7074
+ keyPathSteps,
6983
7075
  readKeyPath,
6984
7076
  ID_RE_COPY,
6985
7077
  KINDS_COPY,
@@ -111,6 +111,7 @@ import {
111
111
  isTerminal,
112
112
  isTerminalEvent,
113
113
  keyPathProblem,
114
+ keyPathSteps,
114
115
  languagesOf,
115
116
  lastRecordFor,
116
117
  liveToken,
@@ -151,11 +152,10 @@ import {
151
152
  visibilityKey,
152
153
  witness,
153
154
  workersOf
154
- } from "./chunks/chunk-5CVSWDCB.js";
155
+ } from "./chunks/chunk-53EDI674.js";
155
156
 
156
157
  // src/definition/definition-schema.ts
157
158
  var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
158
- var idOrNonString = () => ({ anyOf: [ref("id"), { not: { type: "string" } }] });
159
159
  var ref = (name) => ({ $ref: `#/$defs/${name}` });
160
160
  function body(re, name) {
161
161
  const s = re.source;
@@ -164,6 +164,14 @@ function body(re, name) {
164
164
  }
165
165
  return s.slice(1, -1);
166
166
  }
167
+ function keyPathSegment(id) {
168
+ const seg = `${id}(?:\\[(?:0|[1-9][0-9]{0,8})\\])*`;
169
+ const whole = body(KEY_PATH_RE, "KEY_PATH_RE");
170
+ if (whole !== `${seg}(?:\\.${seg})*`) {
171
+ throw new Error(`definitionSchema: KEY_PATH_RE (${String(KEY_PATH_RE)}) is no longer one segment pattern repeated \u2014 a comparison's path pattern cannot be derived from it`);
172
+ }
173
+ return seg;
174
+ }
167
175
  function described(table, key, where) {
168
176
  const d = Object.prototype.hasOwnProperty.call(table, key) ? table[key] : void 0;
169
177
  if (d === void 0) throw new Error(`definitionSchema: ${where} declares the key "${key}" and no description is written for it`);
@@ -197,7 +205,7 @@ var KIND_DESCRIPTIONS = {
197
205
  },
198
206
  fanout: {
199
207
  kind: "A `fanout`: opens a region of N branches declared HERE, closed by the one `join` naming it. Dispatches nothing.",
200
- branches: "The region's branch entries: at least two distinct node ids (FD-V9), each a task or an ask, none of them its own join (FD-V9, FD-V12 \u2014 validateDefinition's)."
208
+ branches: "The region's branch entries: at least two distinct node ids (FD-V9), each a string (FD-V21), each a task or an ask, none of them its own join (FD-V9, FD-V12 \u2014 validateDefinition's)."
201
209
  },
202
210
  map: {
203
211
  kind: "A `map`: opens a region of ONE branch, opened once per item of an array at run time, closed by the one `join` naming it. Dispatches nothing.",
@@ -207,7 +215,7 @@ var KIND_DESCRIPTIONS = {
207
215
  },
208
216
  join: {
209
217
  kind: "A `join`: closes the region its `of` opened, by the join policy the driver is given. Dispatches nothing.",
210
- of: "The id of the `fanout` or `map` whose region this closes (FD-V6, validateDefinition's).",
218
+ of: "The id of the `fanout` or `map` whose region this closes: a string (FD-V21); that it names an existing node (FD-V2) and that the node is one (FD-V6) are validateDefinition's.",
211
219
  edges: "Exactly one edge, carrying neither `when` nor `on` (FD-V7): a join has no outcome for either to read."
212
220
  },
213
221
  end: {
@@ -230,7 +238,7 @@ var KIND_DESCRIPTIONS = {
230
238
  var EDGE_DESCRIPTIONS = {
231
239
  to: "The id of the node this edge leads to (FD-V2, validateDefinition's).",
232
240
  on: 'The terminal states this edge is eligible for: a non-empty list of distinct "done" / "failed" (FD-V18). Absent: ["done"]. "user_action" suspends rather than routes.',
233
- when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
241
+ when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value or valueFrom, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
234
242
  needs: "What this edge's predicate sees beyond its node's outcome \u2014 `needs`' grammar minus \".\" (FD-V26).",
235
243
  route: "On a gate's edge, which of the gate type's routes it carries; absent: the type's DEFAULT route (FD-V41). Refused on every other kind's edge."
236
244
  };
@@ -244,9 +252,10 @@ var TRANSFORM_DESCRIPTIONS = {
244
252
  };
245
253
  var PREDICATE_DESCRIPTIONS = {
246
254
  predicate: "A registered predicate's name (FD-V4, validateDefinition's).",
247
- path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it (FD-V43).`,
255
+ path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never; a "needs" key the edge declares (FD-V43).`,
248
256
  op: `One of ${PREDICATE_OPS.join(", ")} (FD-V44). The ordering operators compare numbers and "startsWith" a string \u2014 a comparison between kinds is no match, never a coercion.`,
249
- value: "The scalar compared against: null, a boolean, a number or a string (FD-V44).",
257
+ value: "The scalar compared against: null, a boolean, a number or a string (FD-V44). Exactly one of value and valueFrom (FD-V42).",
258
+ valueFrom: `A key path over the same context, compared against as a literal would be \u2014 the same roots as path, ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")}, and a "needs" key the edge declares (FD-V43). Exactly one of value and valueFrom (FD-V42). Where it does not resolve, or resolves to an object or an array, the comparison is no match for every operator.`,
250
259
  default: "What is compared where the path does not resolve or resolves to null: a scalar, or \u2014 with `length` \u2014 an array or a string (FD-V44).",
251
260
  length: "Exactly true: compare the LENGTH of the array or string, against a number (FD-V44).",
252
261
  all: "A flat group: a non-empty list of comparisons that must all hold (FD-V45).",
@@ -284,7 +293,7 @@ function nodeKeySchema(kind, key) {
284
293
  case "pool":
285
294
  return { type: "string" };
286
295
  case "settings":
287
- return kind === "gate" ? { type: "object", properties: { key: { type: "string", pattern: KEY_PATH_RE.source, description: "A key path: dot-separated object keys, each [A-Za-z0-9_-]{1,64} \u2014 no index, no wildcard, no expression (FD-V40)." } } } : { type: "object" };
296
+ return kind === "gate" ? { type: "object", properties: { key: { type: "string", pattern: KEY_PATH_RE.source, description: "A key path: dot-separated object keys, each [A-Za-z0-9_-]{1,64} and optionally followed by array indexes [n] (n is 0 or has no leading zero, at most 999999999; never leading the path) \u2014 files[0].series; no quoted key, no wildcard, no expression (FD-V40)." } } } : { type: "object" };
288
297
  case "transform":
289
298
  return ref("transform");
290
299
  case "optional":
@@ -295,14 +304,14 @@ function nodeKeySchema(kind, key) {
295
304
  return ref("retryPolicy");
296
305
  case "workerGroups":
297
306
  return { type: "array", minItems: 1, uniqueItems: true, items: { type: "string" } };
298
- // **`branches`' entries and `of` admit a NON-string** — W210's conformity reader, B1: the validator holds both only
299
- // through the graph (`FD-V2`, `FD-V6` and `FD-V9` read a string and pass anything else over), so `of: null` on a join
300
- // of no region and `["b", "c", 5]` are ACCEPTED there, and refusing them here would refuse what the validator accepts
301
- // (ADR 0132 rule 2). A string is held to the id charset. That `FD-V21` should refuse the rest is Q774.
307
+ // **`branches`' entries and `of` are ids, unconditionally** — v0.52, W215, Q774 resolved (P44 (d1)): `FD-V21` refuses
308
+ // a non-string `of` and a non-string branch entry, so the schema may too (ADR 0132 rule 2). Until then both admitted
309
+ // a non-string here, because the validator accepted one (W210's conformity reader, B1). A string outside the charset
310
+ // names no node — every node id is `FD-V16`'s — so `FD-V2` refuses it.
302
311
  case "branches":
303
- return { type: "array", minItems: 2, uniqueItems: true, items: idOrNonString() };
312
+ return { type: "array", minItems: 2, uniqueItems: true, items: ref("id") };
304
313
  case "of":
305
- return idOrNonString();
314
+ return ref("id");
306
315
  case "over":
307
316
  case "branch":
308
317
  return ref("id");
@@ -425,12 +434,15 @@ function referenceSchema(withSelf, id) {
425
434
  };
426
435
  }
427
436
  function leafSchema(id) {
428
- const path = `^(?:${PREDICATE_ROOTS.join("|")})(?:\\.${id})+$`;
437
+ const path = `^(?:${PREDICATE_ROOTS.join("|")})(?:\\.${keyPathSegment(id)})+$`;
429
438
  const scalar = () => ({ type: ["null", "boolean", "number", "string"] });
430
439
  const keySchemas = {
431
440
  path: { type: "string", pattern: path },
432
441
  op: { enum: [...PREDICATE_OPS] },
433
442
  value: scalar(),
443
+ // v0.55 (ADR 0140): the RIGHT side as a key path — `path`'s own pattern, so the two keys cannot be held to different
444
+ // grammars; (g1)'s *a `needs` key the edge declares* needs the edge's own `needs` and is the validator's alone.
445
+ valueFrom: { type: "string", pattern: path },
434
446
  default: { anyOf: [scalar(), { type: "array", items: scalar() }] },
435
447
  length: { const: true }
436
448
  };
@@ -438,6 +450,8 @@ function leafSchema(id) {
438
450
  description: "A declared comparison over the decision's context.",
439
451
  type: "object",
440
452
  required: [...PREDICATE_LEAF_REQUIRED],
453
+ // **Exactly one of `value` / `valueFrom`** (`FD-V42`, v0.55): both satisfy both branches and neither satisfies none.
454
+ oneOf: [{ required: ["value"] }, { required: ["valueFrom"] }],
441
455
  properties: Object.fromEntries(PREDICATE_LEAF_KEYS.map((k) => {
442
456
  const s = keySchemas[k];
443
457
  if (s === void 0) throw new Error(`definitionSchema: a comparison declares the key "${k}" and no schema is written for it`);
@@ -935,6 +949,7 @@ export {
935
949
  isTerminal,
936
950
  isTerminalEvent,
937
951
  keyPathProblem,
952
+ keyPathSteps,
938
953
  languagesOf,
939
954
  lastRecordFor,
940
955
  liveToken,
@@ -26,7 +26,7 @@ import {
26
26
  retryPolicyProblem,
27
27
  validateDefinition,
28
28
  workersOf
29
- } from "../chunks/chunk-5CVSWDCB.js";
29
+ } from "../chunks/chunk-53EDI674.js";
30
30
 
31
31
  // src/scheduler/bootstrap.ts
32
32
  import { WorkerManager } from "fyflow-scheduler";