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/AGENTS.md +249 -53
- package/README.md +5 -5
- package/dist/browser/chunks/{chunk-NA6QQAZ2.js → chunk-4JFQX7DL.js} +164 -49
- package/dist/browser/index.js +546 -3
- package/dist/browser/scheduler/index.js +1 -1
- package/dist/node/chunks/{chunk-NA6QQAZ2.js → chunk-4JFQX7DL.js} +164 -49
- package/dist/node/index.js +546 -3
- package/dist/node/scheduler/index.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/definition/assist.d.ts +1 -1
- package/dist/types/src/definition/condition.d.ts +21 -3
- package/dist/types/src/definition/definition-schema.d.ts +23 -0
- package/dist/types/src/definition/types.d.ts +8 -5
- package/dist/types/src/definition/validate.d.ts +50 -6
- package/dist/types/src/definition/wiring.d.ts +15 -5
- package/dist/types/src/driver/driver.d.ts +17 -6
- package/dist/types/src/fold/run-state.d.ts +17 -0
- package/package.json +2 -1
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.
|
|
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,
|
|
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,
|
|
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')); // {
|
|
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 = '{"
|
|
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
|
|
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
|
|
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
|
|
796
|
+
return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
|
|
766
797
|
}
|
|
767
|
-
const
|
|
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
|
|
964
|
-
|
|
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 "${
|
|
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 "${
|
|
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 ["${
|
|
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 =
|
|
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} (
|
|
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
|
|
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
|
|
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/
|
|
2190
|
-
* of a PLAIN object and a missing key, a `null`, an array
|
|
2191
|
-
*
|
|
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
|
|
2296
|
+
const steps = keyPathSteps(key);
|
|
2214
2297
|
let s = schema;
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
|
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[
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
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`** —
|
|
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
|
-
|
|
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,
|