fyflow-orchestration 0.12.0 → 0.14.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.
@@ -402,7 +402,13 @@ function evalLeaf(scope, p, unknownOp) {
402
402
  else if (typeof v === "string") v = v.length;
403
403
  else return false;
404
404
  }
405
- 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
+ }
406
412
  switch (p.op) {
407
413
  // **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
408
414
  // `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
@@ -755,8 +761,9 @@ function gateTypeCollisions(x) {
755
761
  return names.filter((n) => Object.prototype.hasOwnProperty.call(BUILT_IN_GATE_TYPES, n)).sort();
756
762
  }
757
763
  var PREDICATE_NAME_KEYS = ["predicate"];
758
- var PREDICATE_LEAF_KEYS = ["path", "op", "value", "default", "length"];
759
- 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"];
760
767
  var PREDICATE_GROUP_KEYS = ["all", "any"];
761
768
  function predicateShapeOf(o) {
762
769
  const marks = [];
@@ -783,29 +790,53 @@ var GATE_SCOPE = {
783
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',
784
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"
785
792
  };
786
- function leafProblem(o, where, scope = EDGE_SCOPE) {
787
- const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
788
- if (extra.length) {
789
- 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(", ")} }` };
790
- }
791
- for (const k of PREDICATE_LEAF_REQUIRED) {
792
- 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` };
793
- }
794
- const path = o.path;
793
+ function comparisonPathProblem(o, key, where, scope) {
794
+ const path = o[key];
795
795
  if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
796
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
796
+ return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
797
797
  }
798
- const first = path.split(".")[0];
798
+ const segments = path.split(".");
799
+ const first = segments[0];
799
800
  const bracket = first.indexOf("[");
800
801
  const root = bracket === -1 ? first : first.slice(0, bracket);
801
802
  if (!scope.roots.includes(root)) {
802
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
803
+ return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
803
804
  }
804
805
  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
+ 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])` };
806
807
  }
807
- if (path.split(".").length < 2) {
808
- return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
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
+ }
822
+ function leafProblem(o, where, scope = EDGE_SCOPE) {
823
+ const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
824
+ if (extra.length) {
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(", ")} }` };
826
+ }
827
+ for (const k of PREDICATE_LEAF_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 ")}` };
829
+ }
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` };
833
+ }
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;
809
840
  }
810
841
  const op = o.op;
811
842
  if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
@@ -815,6 +846,12 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
815
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` };
816
847
  }
817
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
+ }
818
855
  if (!isScalar(o.value)) {
819
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` };
820
857
  }
@@ -830,6 +867,9 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
830
867
  if (takesLength && STRING_OPS.has(op)) {
831
868
  return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
832
869
  }
870
+ return defaultProblem(o, where, takesLength);
871
+ }
872
+ function defaultProblem(o, where, takesLength) {
833
873
  if ("default" in o) {
834
874
  const d = o.default;
835
875
  const okDefault = Array.isArray(d) ? d.every(isScalar) : isScalar(d);
@@ -845,7 +885,8 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
845
885
  }
846
886
  return null;
847
887
  }
848
- function predicateProblem(v) {
888
+ function predicateProblem(v, edgeNeeds) {
889
+ const scope = edgeNeeds === void 0 ? EDGE_SCOPE : { ...EDGE_SCOPE, needsDeclared: edgeNeeds };
849
890
  if (typeof v !== "object" || v === null || Array.isArray(v)) {
850
891
  return {
851
892
  rule: "FD-V42",
@@ -866,8 +907,8 @@ function predicateProblem(v) {
866
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` };
867
908
  return null;
868
909
  }
869
- if (shape === "leaf") return leafProblem(o, "when");
870
- return groupProblem(o, "when", EDGE_SCOPE);
910
+ if (shape === "leaf") return leafProblem(o, "when", scope);
911
+ return groupProblem(o, "when", scope);
871
912
  }
872
913
  function groupProblem(o, label, scope) {
873
914
  const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
@@ -1360,7 +1401,9 @@ function validateDefinition(doc, registry) {
1360
1401
  }
1361
1402
  }
1362
1403
  if (ed?.when !== void 0) {
1363
- 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);
1364
1407
  if (problem) add(problem.rule, at, problem.message);
1365
1408
  else for (const name of predicateNamesIn(ed.when)) {
1366
1409
  if (!preds.has(name)) add("FD-V4", at, `when names "${name}", which is not a registered predicate`);
@@ -1641,11 +1684,38 @@ var GateTypeRefused = class extends RegistryError {
1641
1684
  var BUILT_IN_GROUP_TYPES = {
1642
1685
  ConcurrentLimitGroup: { configSchema: { type: "object", required: ["limit"], properties: { limit: { type: "integer", minimum: 1 } }, additionalProperties: false } },
1643
1686
  RateLimitGroup: { configSchema: { type: "object", required: ["windows"], properties: { windows: RATE_WINDOWS() }, additionalProperties: false } },
1644
- KeyedRateLimitGroup: { configSchema: { type: "object", required: ["windows"], properties: { windows: RATE_WINDOWS() }, additionalProperties: false } }
1687
+ KeyedRateLimitGroup: { configSchema: { type: "object", required: ["windows"], properties: { windows: RATE_WINDOWS() }, additionalProperties: false } },
1688
+ // v0.56: `units` a count (the units `0`…`n-1`) or a list of names — `type` may be a list in the subset (P46 fact 7), and
1689
+ // `minimum` reads a number and `minItems` / `items` an array, so each keyword holds its own form. Distinct: beside it.
1690
+ UnitGroup: { configSchema: { type: "object", required: ["units"], properties: { units: { type: ["integer", "array"], minimum: 1, minItems: 1, items: { type: "string", minLength: 1 } } }, additionalProperties: false } },
1691
+ // v0.56: `limits` an object of per-key limits; each value a positive integer is beside it (no schema-valued
1692
+ // `additionalProperties` in the subset).
1693
+ KeyedConcurrentLimitGroup: { configSchema: { type: "object", required: ["limit"], properties: { limit: { type: "integer", minimum: 1 }, limits: { type: "object" } }, additionalProperties: false } }
1645
1694
  };
1646
1695
  function RATE_WINDOWS() {
1647
1696
  return { type: "array", minItems: 1, items: { type: "object", required: ["limit", "windowMs"], properties: { limit: { type: "integer", minimum: 1 }, windowMs: { type: "integer", minimum: 1 } }, additionalProperties: false } };
1648
1697
  }
1698
+ var isPositiveInteger = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1;
1699
+ function builtInConfigProblem(type, config) {
1700
+ if (type === "UnitGroup") {
1701
+ const units = config.units;
1702
+ if (Array.isArray(units)) {
1703
+ const seen = /* @__PURE__ */ new Set();
1704
+ for (const u of units) {
1705
+ if (seen.has(u)) return `$.units lists unit ${JSON.stringify(u)} twice \u2014 a UnitGroup's units are distinct`;
1706
+ seen.add(u);
1707
+ }
1708
+ }
1709
+ } else if (type === "KeyedConcurrentLimitGroup") {
1710
+ const limits = config.limits;
1711
+ if (limits !== void 0) {
1712
+ for (const [k, v] of Object.entries(limits)) {
1713
+ if (!isPositiveInteger(v)) return `$.limits[${q(k)}] must be a positive integer \u2014 a per-key limit, as \`limit\` is \u2014 got ${shown(v)}`;
1714
+ }
1715
+ }
1716
+ }
1717
+ return null;
1718
+ }
1649
1719
  var DOCUMENT_KEYS = ["workers", "groups", "nodeTypes", "pools", "predicates", "languages"];
1650
1720
  var WORKER_KEYS = ["url", "configSchema", "settingsSchema", "inputSchema", "outputSchema"];
1651
1721
  var CHECKED_WORKER_SCHEMAS = ["configSchema", "settingsSchema"];
@@ -1685,9 +1755,12 @@ function mergePool(base, over) {
1685
1755
  }
1686
1756
  function copyResident(r) {
1687
1757
  if (Array.isArray(r)) return [...r];
1688
- if (isPlainObject2(r)) return { ...r };
1758
+ if (isPlainObject2(r)) return Object.fromEntries(Object.entries(r).map(([k, v]) => [k, isPlainObject2(v) ? copyRequest(v) : v]));
1689
1759
  return r;
1690
1760
  }
1761
+ function copyRequest(v) {
1762
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, Array.isArray(x) ? [...x] : x]));
1763
+ }
1691
1764
  function nodeTypeFromDeclaration(decl, over) {
1692
1765
  const declaredPool = mergePool(decl.pool ?? {}, over);
1693
1766
  const def = {
@@ -1732,7 +1805,10 @@ function poolSpecProblem(v) {
1732
1805
  if (r.some((g) => typeof g !== "string" || g.length === 0)) return `PoolSpec.residentGroups must be an array of group keys or a { key: cost } record, got ${JSON.stringify(r)}`;
1733
1806
  if (new Set(r).size !== r.length) return "PoolSpec.residentGroups repeats a key";
1734
1807
  } else if (isPlainObject2(r)) {
1735
- 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)}`;
1808
+ for (const [k, c] of Object.entries(r)) {
1809
+ const p = residentValueProblem(c);
1810
+ if (p !== null) return `PoolSpec.residentGroups[${q(k)}] ${p}`;
1811
+ }
1736
1812
  } else return `PoolSpec.residentGroups must be an array of group keys or a { key: cost } record, got ${JSON.stringify(r)}`;
1737
1813
  }
1738
1814
  if (v.cancelGraceMs !== void 0 && !(typeof v.cancelGraceMs === "number" && v.cancelGraceMs >= 0)) {
@@ -1753,6 +1829,27 @@ function poolSpecProblem(v) {
1753
1829
  }
1754
1830
  return null;
1755
1831
  }
1832
+ var RESIDENT_REQUEST_KEYS = ["cost", "from", "prefer"];
1833
+ function residentValueProblem(c) {
1834
+ if (typeof c === "number") return isPositiveInteger(c) ? null : `must be a cost > 0, an INTEGER (the scheduler's rule), or a request { ${RESIDENT_REQUEST_KEYS.map((k) => `${k}?`).join(", ")} }, got ${shown(c)}`;
1835
+ if (!isPlainObject2(c) || !isPlainPrototype(c)) {
1836
+ return `must be a cost (a positive integer) or a request { ${RESIDENT_REQUEST_KEYS.map((k) => `${k}?`).join(", ")} } \u2014 a plain object \u2014 got ${describeNonPlain(c)}`;
1837
+ }
1838
+ for (const k of Object.keys(c)) if (!RESIDENT_REQUEST_KEYS.includes(k)) return `is a request carrying undeclared key ${q(k)}; it declares ${RESIDENT_REQUEST_KEYS.join(", ")}`;
1839
+ if (ownKey(c, "cost") && !isPositiveInteger(c.cost)) return `.cost must be a positive integer, got ${shown(c.cost)}`;
1840
+ for (const f of ["from", "prefer"]) {
1841
+ if (!ownKey(c, f)) continue;
1842
+ const list = c[f];
1843
+ if (!Array.isArray(list) || list.length === 0) return `.${f} must be a non-empty list of unit ids, got ${shown(list)}`;
1844
+ for (const u of list) {
1845
+ if (!(typeof u === "string" && u.length > 0 || typeof u === "number" && Number.isInteger(u) && u >= 0)) {
1846
+ return `.${f} holds ${shown(u)} \u2014 a unit id is a non-empty string, or an integer >= 0 for a UnitGroup declared by count`;
1847
+ }
1848
+ }
1849
+ if (new Set(list).size !== list.length) return `.${f} repeats a unit id`;
1850
+ }
1851
+ return null;
1852
+ }
1756
1853
  function isPlainPrototype(o) {
1757
1854
  const proto = Object.getPrototypeOf(o);
1758
1855
  return proto === Object.prototype || proto === null;
@@ -1783,6 +1880,45 @@ function groupsNamedBy(spec) {
1783
1880
  function residentKeysOf(spec) {
1784
1881
  return spec.residentGroups === void 0 ? [] : Array.isArray(spec.residentGroups) ? [...spec.residentGroups] : Object.keys(spec.residentGroups);
1785
1882
  }
1883
+ function residentEntriesOf(spec) {
1884
+ const r = spec.residentGroups;
1885
+ return r === void 0 ? [] : Array.isArray(r) ? r.map((g) => [g, 1]) : Object.entries(r);
1886
+ }
1887
+ var KEYED_BUILT_INS = {
1888
+ KeyedRateLimitGroup: 'the scheduler resolves no key for a resident group, so every worker holding it is counted in ONE "" bucket, in silence',
1889
+ KeyedConcurrentLimitGroup: "the scheduler REJECTS a KeyedConcurrentLimitGroup in residentGroups when it is constructed: a worker holds its units across tasks, and so across keys, with no key to hold them under"
1890
+ };
1891
+ function residentAgainstGroupProblem(group, value) {
1892
+ if (!hasOwn(BUILT_IN_GROUP_TYPES, group.type)) return null;
1893
+ const request = isPlainObject2(value) ? value : void 0;
1894
+ const cost = typeof value === "number" ? value : request?.cost ?? 1;
1895
+ const named = `group ${q(group.key)}, of the built-in type ${q(group.type)}`;
1896
+ if (group.type === "UnitGroup") {
1897
+ const declared = group.config.units;
1898
+ const units = typeof declared === "number" ? Array.from({ length: declared }, (_, i) => i) : [...declared];
1899
+ const has = new Set(units);
1900
+ for (const f of ["from", "prefer"]) {
1901
+ for (const u of request?.[f] ?? []) {
1902
+ if (!has.has(u)) {
1903
+ return `${f} names unit ${shown(u)}, which ${named} does not have \u2014 its units are ${units.map((x) => shown(x)).join(", ")}` + (typeof declared === "number" ? " (declared by count: integers, never strings)" : "");
1904
+ }
1905
+ }
1906
+ }
1907
+ if (request?.from !== void 0 && cost > request.from.length) {
1908
+ return `a cost of ${cost} from ${request.from.length} candidate unit(s) in from can never be granted by ${named}`;
1909
+ }
1910
+ if (cost > units.length) return `a cost of ${cost} is above the ${units.length} unit(s) of ${named} \u2014 a worker of this pool could never start`;
1911
+ return null;
1912
+ }
1913
+ if (request !== void 0 && (ownKey(request, "from") || ownKey(request, "prefer"))) {
1914
+ return `a request naming ${["from", "prefer"].filter((f) => ownKey(request, f)).join(" and ")} on ${named}, which takes no request \u2014 from and prefer are a UnitGroup's (or a code group type's with a checkRequest); { cost } alone is a cost, for any group`;
1915
+ }
1916
+ const limit = group.type === "ConcurrentLimitGroup" ? group.config.limit : group.type === "RateLimitGroup" ? Math.min(...group.config.windows.map((w) => w.limit)) : void 0;
1917
+ if (limit !== void 0 && cost > limit) {
1918
+ return `a cost of ${cost} is above the limit of ${limit} of ${named}${group.type === "RateLimitGroup" ? " (its smallest window's)" : ""} \u2014 a worker of this pool could never start`;
1919
+ }
1920
+ return null;
1921
+ }
1786
1922
  function poolOptionsOf(spec, inline, workerUrl, languages) {
1787
1923
  const { config, groups, ...rest } = spec;
1788
1924
  return {
@@ -1870,7 +2006,7 @@ function derivePool(key, typeName, def, worker, config, groupKeys, refuse, pool)
1870
2006
  }
1871
2007
  const resident = spec.residentGroups === void 0 ? 0 : Array.isArray(spec.residentGroups) ? spec.residentGroups.length : Object.keys(spec.residentGroups).length;
1872
2008
  if (resident > 0 && spec.idleTimeout === 0) {
1873
- 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`);
2009
+ 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 at P18 fact 6 and re-measured at P46 fact 6); give the pool a positive idleTimeout`);
1874
2010
  }
1875
2011
  return { key, type: typeName, definition: def, worker, config, spec, ...pool !== void 0 ? { pool: copyPool(pool) } : {} };
1876
2012
  }
@@ -1981,6 +2117,10 @@ function resolveRegistry(doc, code = {}) {
1981
2117
  const fs = findings(gt.configSchema, inst.config ?? {});
1982
2118
  if (fs.length) throw new GroupConfigRefused(key, `group ${q(key)}: config fails ${q(inst.type)}'s configSchema \u2014 ${describeFindings(fs)}`);
1983
2119
  }
2120
+ if (hasOwn(BUILT_IN_GROUP_TYPES, inst.type)) {
2121
+ const p = builtInConfigProblem(inst.type, inst.config);
2122
+ if (p !== null) throw new GroupConfigRefused(key, `group ${q(key)}: config fails ${q(inst.type)}'s rule beside its configSchema \u2014 ${p}`);
2123
+ }
1984
2124
  groups.set(key, { key, type: inst.type, groupType: gt, config: inst.config });
1985
2125
  }
1986
2126
  const groupKeys = new Set(groups.keys());
@@ -2019,10 +2159,16 @@ function resolveRegistry(doc, code = {}) {
2019
2159
  pools.set(key, derivePool(key, inst.type, def, def.worker === void 0 ? void 0 : workers.get(def.worker), inst.config, groupKeys, ConfigRefused, inst.pool));
2020
2160
  }
2021
2161
  for (const p of pools.values()) {
2022
- for (const g of residentKeysOf(p.spec)) {
2023
- if (groups.get(g)?.type !== "KeyedRateLimitGroup") continue;
2024
- const who = p.key === p.type ? `node type ${q(p.type)}'s default instance` : `pool ${q(p.key)} (an instance of ${q(p.type)})`;
2025
- 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`);
2162
+ const who = p.key === p.type ? `node type ${q(p.type)}'s default instance` : `pool ${q(p.key)} (an instance of ${q(p.type)})`;
2163
+ for (const [g, value] of residentEntriesOf(p.spec)) {
2164
+ const group = groups.get(g);
2165
+ if (group === void 0) continue;
2166
+ const keyed = KEYED_BUILT_INS[group.type];
2167
+ if (hasOwn(KEYED_BUILT_INS, group.type) && keyed !== void 0) {
2168
+ throw new DerivationRefused(p.key, `${who}: the derived pool holds group ${q(g)}, of the built-in type ${q(group.type)}, in residentGroups \u2014 ${keyed} (Q619). Name ${q(g)} in groups, where each task is counted under its limitKey (FD-R6), or hold a ConcurrentLimitGroup or RateLimitGroup resident (or a UnitGroup)`);
2169
+ }
2170
+ const problem = residentAgainstGroupProblem(group, value);
2171
+ if (problem !== null) throw new DerivationRefused(p.key, `${who}: residentGroups[${q(g)}] \u2014 ${problem}`);
2026
2172
  }
2027
2173
  }
2028
2174
  const predicateNames = doc.predicates ?? [];
@@ -7028,6 +7174,7 @@ export {
7028
7174
  canonical,
7029
7175
  findings,
7030
7176
  describeFindings,
7177
+ keyPathSteps,
7031
7178
  readKeyPath,
7032
7179
  ID_RE_COPY,
7033
7180
  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,7 +152,7 @@ import {
151
152
  visibilityKey,
152
153
  witness,
153
154
  workersOf
154
- } from "./chunks/chunk-4JFQX7DL.js";
155
+ } from "./chunks/chunk-5KUYTCXI.js";
155
156
 
156
157
  // src/definition/definition-schema.ts
157
158
  var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
@@ -237,7 +238,7 @@ var KIND_DESCRIPTIONS = {
237
238
  var EDGE_DESCRIPTIONS = {
238
239
  to: "The id of the node this edge leads to (FD-V2, validateDefinition's).",
239
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.',
240
- 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.",
241
242
  needs: "What this edge's predicate sees beyond its node's outcome \u2014 `needs`' grammar minus \".\" (FD-V26).",
242
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."
243
244
  };
@@ -251,9 +252,10 @@ var TRANSFORM_DESCRIPTIONS = {
251
252
  };
252
253
  var PREDICATE_DESCRIPTIONS = {
253
254
  predicate: "A registered predicate's name (FD-V4, validateDefinition's).",
254
- 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 (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).`,
255
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.`,
256
- 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.`,
257
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).",
258
260
  length: "Exactly true: compare the LENGTH of the array or string, against a number (FD-V44).",
259
261
  all: "A flat group: a non-empty list of comparisons that must all hold (FD-V45).",
@@ -438,6 +440,9 @@ function leafSchema(id) {
438
440
  path: { type: "string", pattern: path },
439
441
  op: { enum: [...PREDICATE_OPS] },
440
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 },
441
446
  default: { anyOf: [scalar(), { type: "array", items: scalar() }] },
442
447
  length: { const: true }
443
448
  };
@@ -445,6 +450,8 @@ function leafSchema(id) {
445
450
  description: "A declared comparison over the decision's context.",
446
451
  type: "object",
447
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"] }],
448
455
  properties: Object.fromEntries(PREDICATE_LEAF_KEYS.map((k) => {
449
456
  const s = keySchemas[k];
450
457
  if (s === void 0) throw new Error(`definitionSchema: a comparison declares the key "${k}" and no schema is written for it`);
@@ -942,6 +949,7 @@ export {
942
949
  isTerminal,
943
950
  isTerminalEvent,
944
951
  keyPathProblem,
952
+ keyPathSteps,
945
953
  languagesOf,
946
954
  lastRecordFor,
947
955
  liveToken,
@@ -26,7 +26,7 @@ import {
26
26
  retryPolicyProblem,
27
27
  validateDefinition,
28
28
  workersOf
29
- } from "../chunks/chunk-4JFQX7DL.js";
29
+ } from "../chunks/chunk-5KUYTCXI.js";
30
30
 
31
31
  // src/scheduler/bootstrap.ts
32
32
  import { WorkerManager } from "fyflow-scheduler";
@@ -83,7 +83,7 @@ function bootstrap(registry, groups = {}, options = {}) {
83
83
  }
84
84
 
85
85
  // src/scheduler/runtime.ts
86
- import { ConcurrentLimitGroup, FyflowScheduler, KeyedRateLimitGroup, RateLimitGroup } from "fyflow-scheduler";
86
+ import { ConcurrentLimitGroup, FyflowScheduler, KeyedConcurrentLimitGroup, KeyedRateLimitGroup, RateLimitGroup, UnitGroup } from "fyflow-scheduler";
87
87
  var RuntimeError = class extends Error {
88
88
  /** The entry this refusal is about — a worker's key, a group's key, a type's name. `RegistryError`'s field, one layer out. */
89
89
  at;
@@ -107,8 +107,18 @@ var KEYED_RULE = "FD-R6";
107
107
  var BUILT_IN_GROUP_FACTORIES = {
108
108
  ConcurrentLimitGroup: (config, key) => new ConcurrentLimitGroup(config.limit, key),
109
109
  RateLimitGroup: (config, key) => new RateLimitGroup(config.windows, key),
110
- KeyedRateLimitGroup: (config, key) => new KeyedRateLimitGroup(config.windows, { id: key })
110
+ KeyedRateLimitGroup: (config, key) => new KeyedRateLimitGroup(config.windows, { id: key }),
111
+ UnitGroup: (config, key) => {
112
+ const units = config.units;
113
+ return new UnitGroup(typeof units === "number" ? units : [...units], { id: key });
114
+ },
115
+ KeyedConcurrentLimitGroup: (config, key) => {
116
+ const { limit, limits } = config;
117
+ const own = new Map(Object.entries(limits ?? {}));
118
+ return new KeyedConcurrentLimitGroup(limit, { id: key, limitFor: (k) => own.get(k) ?? limit });
119
+ }
111
120
  };
121
+ var KEYED_BUILT_IN_GROUP_TYPES = ["KeyedRateLimitGroup", "KeyedConcurrentLimitGroup"];
112
122
  var GROUP_MEMBERS = [
113
123
  { name: "canRun", kind: "function", why: "the scheduler calls it before every admission; a group without it throws on the first task" },
114
124
  { name: "onStart", kind: "function", why: "the scheduler calls it unguarded at admission, though the interface marks it optional; a group without it throws on the first task (measured on 0.5.2)" },
@@ -210,8 +220,8 @@ function createRuntime(document, code = {}, options = {}) {
210
220
  pools: poolsOf(registry.pools),
211
221
  ...registry.gateTypes !== void 0 ? { gateTypes: registry.gateTypes } : {}
212
222
  };
213
- const keyedByBuiltIn = /* @__PURE__ */ new Set();
214
- for (const [key, g] of resolved.groups) if (g.type === "KeyedRateLimitGroup") keyedByBuiltIn.add(key);
223
+ const keyedByBuiltIn = /* @__PURE__ */ new Map();
224
+ for (const [key, g] of resolved.groups) if (KEYED_BUILT_IN_GROUP_TYPES.includes(g.type)) keyedByBuiltIn.set(key, g.type);
215
225
  const keyedFindings = (flow) => {
216
226
  const nodes = flow?.nodes;
217
227
  if (keyedByBuiltIn.size === 0 || typeof nodes?.[Symbol.iterator] !== "function") return [];
@@ -226,10 +236,11 @@ function createRuntime(document, code = {}, options = {}) {
226
236
  const reached = [.../* @__PURE__ */ new Set([...fromPool, ...fromNode])].filter((g) => keyedByBuiltIn.has(g)).sort();
227
237
  if (reached.length === 0) continue;
228
238
  const at = typeof n.id === "string" ? n.id : null;
239
+ const types = [...new Set(reached.map((g) => keyedByBuiltIn.get(g)))].sort().join(" and ");
229
240
  out.push({
230
241
  rule: KEYED_RULE,
231
242
  at,
232
- detail: `node ${at === null ? "<unnamed>" : q(at)} reaches keyed group${reached.length > 1 ? "s" : ""} ${reached.map(q).join(", ")}, built by the built-in KeyedRateLimitGroup factory with no keyFrom \u2014 so every task of it needs a limitKey, which nothing sets unless start is given a createTask, and without one the scheduler's addTask throws "Missing limit key". Two roads work: pass start a createTask that sets limitKey on the task it builds, or declare the group with a code group type (code.groupTypes) whose create() builds a KeyedRateLimitGroup with a keyFrom that derives the key. A key derived from the flow itself is Q248, deferred`
243
+ detail: `node ${at === null ? "<unnamed>" : q(at)} reaches keyed group${reached.length > 1 ? "s" : ""} ${reached.map(q).join(", ")}, built by the built-in ${types} factory with no keyFrom \u2014 so every task of it needs a limitKey, which nothing sets unless start is given a createTask, and without one the scheduler's addTask throws "Missing limit key". Two roads work: pass start a createTask that sets limitKey on the task it builds, or declare the group with a code group type (code.groupTypes) whose create() builds a ${types} with a keyFrom that derives the key. A key derived from the flow itself is Q248, deferred`
233
244
  });
234
245
  }
235
246
  return out;