fyflow-orchestration 0.9.1 → 0.11.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 +322 -29
- package/README.md +1 -1
- package/dist/browser/chunks/{chunk-I3KDEOLB.js → chunk-5CVSWDCB.js} +442 -111
- package/dist/browser/index.js +541 -3
- package/dist/browser/scheduler/index.js +13 -2
- package/dist/node/chunks/{chunk-I3KDEOLB.js → chunk-5CVSWDCB.js} +442 -111
- package/dist/node/index.js +541 -3
- package/dist/node/scheduler/index.js +13 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/definition/definition-schema.d.ts +23 -0
- package/dist/types/src/definition/types.d.ts +2 -1
- package/dist/types/src/definition/validate.d.ts +41 -2
- package/dist/types/src/definition/wiring.d.ts +10 -0
- package/dist/types/src/driver/driver.d.ts +161 -11
- package/dist/types/src/fold/run-state.d.ts +17 -0
- package/dist/types/src/scheduler/runtime.d.ts +17 -1
- package/package.json +2 -1
|
@@ -416,7 +416,10 @@ function evaluateDeclared(when, scope, unknownOp) {
|
|
|
416
416
|
// src/definition/validate.ts
|
|
417
417
|
var ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
418
418
|
var KINDS = ["task", "fanout", "map", "join", "end", "gate", "ask"];
|
|
419
|
+
var ID_RE_COPY = new RegExp(ID_RE.source, ID_RE.flags);
|
|
420
|
+
var KINDS_COPY = Object.freeze([...KINDS]);
|
|
419
421
|
var ROUTABLE = ["done", "failed"];
|
|
422
|
+
var ROUTABLE_STATES = [...ROUTABLE];
|
|
420
423
|
var DOC_KEYS = ["flowId", "version", "entry", "nodes"];
|
|
421
424
|
var NODE_KEYS = {
|
|
422
425
|
// `retryPolicy` and `workerGroups` since v0.16 (W58): what a node says about its OWN task,
|
|
@@ -451,6 +454,11 @@ var TASK_KEYS_DECLARED = [...NODE_KEYS.task.req, ...NODE_KEYS.task.opt];
|
|
|
451
454
|
var NODE_KEYS_DECLARED = Object.fromEntries(
|
|
452
455
|
Object.entries(NODE_KEYS).map(([kind, k]) => [kind, [...k.req, ...k.opt]])
|
|
453
456
|
);
|
|
457
|
+
var NODE_KEYS_REQUIRED = Object.fromEntries(
|
|
458
|
+
Object.entries(NODE_KEYS).map(([kind, k]) => [kind, [...k.req]])
|
|
459
|
+
);
|
|
460
|
+
var EDGE_KEYS_REQUIRED = [...EDGE_KEYS.req];
|
|
461
|
+
var DOC_KEYS_DECLARED = [...DOC_KEYS];
|
|
454
462
|
var RETRY_POLICY_KEYS = ["maxRetries", "backoffMs"];
|
|
455
463
|
function retryPolicyProblem(v) {
|
|
456
464
|
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
@@ -958,10 +966,35 @@ function referenceTarget(ref) {
|
|
|
958
966
|
}
|
|
959
967
|
return null;
|
|
960
968
|
}
|
|
969
|
+
var ERROR_PART = "!error";
|
|
961
970
|
function aimed(s, form) {
|
|
962
971
|
const conditional = s.endsWith("?");
|
|
963
|
-
const
|
|
964
|
-
|
|
972
|
+
const body = conditional ? s.slice(0, -1) : s;
|
|
973
|
+
const error = body.endsWith(ERROR_PART);
|
|
974
|
+
const node = error ? body.slice(0, -ERROR_PART.length) : body;
|
|
975
|
+
if (!ID_RE.test(node)) return null;
|
|
976
|
+
return error ? { form, node, conditional, part: "error" } : { form, node, conditional };
|
|
977
|
+
}
|
|
978
|
+
var spelled = (t, conditional) => `${t.node}${t.part === "error" ? ERROR_PART : ""}${conditional ? "?" : ""}`;
|
|
979
|
+
function bangProblem(ref) {
|
|
980
|
+
const array = Array.isArray(ref) && ref.length === 1;
|
|
981
|
+
const one = array ? ref[0] : ref;
|
|
982
|
+
if (typeof one !== "string" || one.startsWith("$") || !one.includes("!")) return null;
|
|
983
|
+
const wrap = (s) => JSON.stringify(array ? [s] : s);
|
|
984
|
+
const at = one.indexOf("!");
|
|
985
|
+
const before = one.slice(0, at);
|
|
986
|
+
const after = one.slice(at + 1);
|
|
987
|
+
const afterBare = after.endsWith("?") ? after.slice(0, -1) : after;
|
|
988
|
+
if (afterBare === "error" && before.endsWith("?") && ID_RE.test(before.slice(0, -1))) {
|
|
989
|
+
return `${JSON.stringify(ref)} carries "?" before "!error", and the suffix goes last: write ${wrap(`${before.slice(0, -1)}!error?`)}`;
|
|
990
|
+
}
|
|
991
|
+
if (before === "") {
|
|
992
|
+
return `${JSON.stringify(ref)} names no node before "!": write the node whose error you mean first, as ${wrap(`<node>!error${one.endsWith("?") ? "?" : ""}`)}`;
|
|
993
|
+
}
|
|
994
|
+
if (afterBare !== "error" && ID_RE.test(before)) {
|
|
995
|
+
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`;
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
965
998
|
}
|
|
966
999
|
var PAYLOAD_TOKEN = "$payload";
|
|
967
1000
|
var ITEM_TOKEN = "$item";
|
|
@@ -1252,10 +1285,13 @@ function validateDefinition(doc, registry) {
|
|
|
1252
1285
|
if (!ID_RE.test(k)) add("FD-V26", where, `needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
|
|
1253
1286
|
const target = referenceTarget(ref);
|
|
1254
1287
|
const dollar = target === null ? dollarProblem(ref) : null;
|
|
1288
|
+
const bang = target === null && dollar === null ? bangProblem(ref) : null;
|
|
1255
1289
|
if (dollar !== null) {
|
|
1256
1290
|
add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${dollar}`);
|
|
1291
|
+
} else if (bang !== null) {
|
|
1292
|
+
add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${bang}`);
|
|
1257
1293
|
} 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`);
|
|
1294
|
+
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
1295
|
}
|
|
1260
1296
|
}
|
|
1261
1297
|
}
|
|
@@ -1318,10 +1354,13 @@ function validateDefinition(doc, registry) {
|
|
|
1318
1354
|
if (!ID_RE.test(k)) add("FD-V26", at, `edge needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
|
|
1319
1355
|
const target = referenceTarget(ref);
|
|
1320
1356
|
const dollar = target === null ? dollarProblem(ref) : null;
|
|
1357
|
+
const bang = target === null && dollar === null ? bangProblem(ref) : null;
|
|
1321
1358
|
if (dollar !== null) {
|
|
1322
1359
|
add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${dollar}`);
|
|
1360
|
+
} else if (bang !== null) {
|
|
1361
|
+
add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${bang}`);
|
|
1323
1362
|
} 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`);
|
|
1363
|
+
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
1364
|
} else if (target.form === "self") {
|
|
1326
1365
|
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
1366
|
}
|
|
@@ -1502,14 +1541,14 @@ function validateDefinition(doc, registry) {
|
|
|
1502
1541
|
add(
|
|
1503
1542
|
"FD-V29",
|
|
1504
1543
|
at,
|
|
1505
|
-
`${site}[${JSON.stringify(k)}] names "${
|
|
1544
|
+
`${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
1545
|
);
|
|
1507
1546
|
}
|
|
1508
1547
|
const many = regions.some((r) => r.opensWith === "map" && r.members.has(target) && !r.members.has(n.id));
|
|
1509
1548
|
if (many && t.form === "one") {
|
|
1510
|
-
add("FD-V28", at, `${site}[${JSON.stringify(k)}] names "${
|
|
1549
|
+
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
1550
|
} else if (!many && t.form === "all") {
|
|
1512
|
-
add("FD-V28", at, `${site}[${JSON.stringify(k)}] names ["${
|
|
1551
|
+
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
1552
|
}
|
|
1514
1553
|
}
|
|
1515
1554
|
}
|
|
@@ -2025,6 +2064,7 @@ var typesOf = (s) => {
|
|
|
2025
2064
|
var widen = (ts) => ts.flatMap((t) => t === "integer" ? ["integer"] : t === "number" ? ["number", "integer"] : [t]);
|
|
2026
2065
|
var canon = (v) => JSON.stringify(v);
|
|
2027
2066
|
var BRANCH_KEY = /^\d+(\.\d+)*$/;
|
|
2067
|
+
var ERROR_SCHEMA = Object.freeze({ type: ["string", "null"] });
|
|
2028
2068
|
function fits(P, C, path = "$") {
|
|
2029
2069
|
const p = P;
|
|
2030
2070
|
const c = C;
|
|
@@ -2419,6 +2459,11 @@ var Wiring = class {
|
|
|
2419
2459
|
value = a.sample?.value;
|
|
2420
2460
|
src = a.sample?.source ?? "witness";
|
|
2421
2461
|
dot.push(key);
|
|
2462
|
+
} else if (t.part === "error") {
|
|
2463
|
+
schema2 = ERROR_SCHEMA;
|
|
2464
|
+
const w = sampleOf(ERROR_SCHEMA);
|
|
2465
|
+
value = w.value;
|
|
2466
|
+
src = w.source;
|
|
2422
2467
|
} else {
|
|
2423
2468
|
const p = this.produced(t.node);
|
|
2424
2469
|
if (p.schema === void 0) return { ...base, unchecked: `needs.${key} names "${t.node}", which ${p.unchecked}` };
|
|
@@ -2538,6 +2583,7 @@ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, l
|
|
|
2538
2583
|
for (const input of inputs) results.push(await c.evaluate(input, settings));
|
|
2539
2584
|
} catch (e) {
|
|
2540
2585
|
v.kind = "transform-fails";
|
|
2586
|
+
v.cause = "threw";
|
|
2541
2587
|
v.why = `the transform threw on the ${sourceWord(sample.source)}: ${e instanceof Error ? e.message : String(e)}`;
|
|
2542
2588
|
v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
|
|
2543
2589
|
return;
|
|
@@ -2551,6 +2597,7 @@ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, l
|
|
|
2551
2597
|
const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
|
|
2552
2598
|
if (bad) {
|
|
2553
2599
|
v.kind = "transform-fails";
|
|
2600
|
+
v.cause = "schema";
|
|
2554
2601
|
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
2602
|
} else {
|
|
2556
2603
|
v.kind = "transform-passes";
|
|
@@ -2764,6 +2811,74 @@ function isTerminalEvent(e) {
|
|
|
2764
2811
|
return TERMINAL_EVENTS.includes(e.type);
|
|
2765
2812
|
}
|
|
2766
2813
|
|
|
2814
|
+
// src/fold/policies.ts
|
|
2815
|
+
var arrivalCount = {
|
|
2816
|
+
id: "arrival-count",
|
|
2817
|
+
// **`region` is deliberately unread.** It was the source of `need` and is now
|
|
2818
|
+
// the source of nothing: `branches` cannot answer *how many branches did this
|
|
2819
|
+
// region open* for a map, and a policy that read it for a fanout and counted for
|
|
2820
|
+
// a map would be branching on the node kind — which it cannot see
|
|
2821
|
+
// ([Q159](../../doc/questions/q159.md)) and, per that row, should not.
|
|
2822
|
+
open: () => ({ need: 0, arrived: [] }),
|
|
2823
|
+
step: (s, e) => {
|
|
2824
|
+
if (e.type === "branch.opened") return { ...s, need: s.need + 1 };
|
|
2825
|
+
if (e.type !== "branch.arrived" || s.arrived.includes(e.branchPath)) return s;
|
|
2826
|
+
return { ...s, arrived: [...s.arrived, e.branchPath] };
|
|
2827
|
+
},
|
|
2828
|
+
// **`s.need > 0` is a PIN, not a refinement**, and it is exactly `JP-R8`'s own
|
|
2829
|
+
// *"having been positive"* argument in the policy that argument was denied to.
|
|
2830
|
+
// A bare `arrived.length >= need` is true at `0 >= 0`, so without it `open`
|
|
2831
|
+
// itself reports `satisfied` — and at a map of zero items this policy would fire
|
|
2832
|
+
// a join **no branch ever reached**, which is character for character what
|
|
2833
|
+
// [ADR 0025](../../doc/decisions/0025-the-zero-arrival-join-is-reportable-and-arrival-counting-is-not-repaired.md)
|
|
2834
|
+
// REFUSED `JP-R7`'s variant for. It is vacuous on every `fanout` region, because
|
|
2835
|
+
// `FD-V9`'s floor puts `need` at 2 or more.
|
|
2836
|
+
status: (s) => s.need > 0 && s.arrived.length >= s.need ? "satisfied" : "waiting",
|
|
2837
|
+
// Never `unsatisfiable`: this policy has no way to know a join can never fire,
|
|
2838
|
+
// which is precisely the argument Q4 states as a testable claim. **At a map of
|
|
2839
|
+
// zero items it says `waiting`, exactly as the other two do** — the region is
|
|
2840
|
+
// reported dead by the FOLD (`RN-R10`, via the `regionEntered` input) rather
|
|
2841
|
+
// than by any policy, which is `spec/join-policy.md` section 5's own case.
|
|
2842
|
+
describe: (s) => ({ need: s.need, arrived: [...s.arrived] })
|
|
2843
|
+
};
|
|
2844
|
+
var liveToken = {
|
|
2845
|
+
id: "live-token",
|
|
2846
|
+
open: () => ({ live: 0, positive: false }),
|
|
2847
|
+
step: (s, e) => {
|
|
2848
|
+
if (e.type === "branch.opened") return { live: s.live + 1, positive: true };
|
|
2849
|
+
if (e.type === "branch.arrived" || e.type === "branch.exited" || e.type === "branch.lost") {
|
|
2850
|
+
return { ...s, live: s.live - 1 };
|
|
2851
|
+
}
|
|
2852
|
+
return s;
|
|
2853
|
+
},
|
|
2854
|
+
status: (s) => s.positive && s.live === 0 ? "satisfied" : "waiting",
|
|
2855
|
+
// NOT requiring an arrival before reporting `satisfied`, and NOT returning
|
|
2856
|
+
// `unsatisfiable` on zero-with-no-arrivals. That is JP-R8's third policy, and
|
|
2857
|
+
// it is the one that would hand live-token a graceful answer arrival-count is
|
|
2858
|
+
// structurally denied — which is the asymmetry JP-R8 exists to forbid.
|
|
2859
|
+
describe: (s) => ({ live: s.live, positive: s.positive })
|
|
2860
|
+
};
|
|
2861
|
+
var liveTokenRequiringArrival = {
|
|
2862
|
+
id: "live-token-requiring-arrival",
|
|
2863
|
+
open: () => ({ live: 0, positive: false, arrived: 0 }),
|
|
2864
|
+
step: (s, e) => {
|
|
2865
|
+
if (e.type === "branch.opened") return { ...s, live: s.live + 1, positive: true };
|
|
2866
|
+
if (e.type === "branch.arrived") return { ...s, live: s.live - 1, arrived: s.arrived + 1 };
|
|
2867
|
+
if (e.type === "branch.exited" || e.type === "branch.lost") return { ...s, live: s.live - 1 };
|
|
2868
|
+
return s;
|
|
2869
|
+
},
|
|
2870
|
+
status: (s) => {
|
|
2871
|
+
if (!s.positive || s.live !== 0) return "waiting";
|
|
2872
|
+
return s.arrived > 0 ? "satisfied" : "unsatisfiable";
|
|
2873
|
+
},
|
|
2874
|
+
describe: (s) => ({ live: s.live, positive: s.positive, arrived: s.arrived })
|
|
2875
|
+
};
|
|
2876
|
+
var POLICIES = {
|
|
2877
|
+
"arrival-count": arrivalCount,
|
|
2878
|
+
"live-token": liveToken,
|
|
2879
|
+
"live-token-requiring-arrival": liveTokenRequiringArrival
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2767
2882
|
// src/driver/driver.ts
|
|
2768
2883
|
import { FyflowTask } from "fyflow-scheduler";
|
|
2769
2884
|
var InvalidDefinition = class _InvalidDefinition extends Error {
|
|
@@ -2879,6 +2994,12 @@ var ResumeRefused = class extends Error {
|
|
|
2879
2994
|
}
|
|
2880
2995
|
};
|
|
2881
2996
|
function resumeRefusal(definition, checkpoint, runId) {
|
|
2997
|
+
const header = headerRefusal(definition, checkpoint, runId);
|
|
2998
|
+
if (header !== null) return header;
|
|
2999
|
+
const cp = checkpoint;
|
|
3000
|
+
return haltRefusal(cp.events);
|
|
3001
|
+
}
|
|
3002
|
+
function headerRefusal(definition, checkpoint, runId) {
|
|
2882
3003
|
const cp = checkpoint;
|
|
2883
3004
|
const h = typeof cp === "object" && cp !== null ? cp.header : void 0;
|
|
2884
3005
|
if (typeof h !== "object" || h === null || !Array.isArray(cp.events)) {
|
|
@@ -2916,24 +3037,27 @@ function resumeRefusal(definition, checkpoint, runId) {
|
|
|
2916
3037
|
`ResumeRefused (flow): the checkpoint is of flow ${JSON.stringify(h.flowId)} version ${JSON.stringify(h.version)}, and this driver runs "${definition.flowId}" version ${definition.version}. There is no migration (P23 decision 7): a checkpoint resumes against the definition that produced it, and a changed flow is a new version.`
|
|
2917
3038
|
);
|
|
2918
3039
|
}
|
|
2919
|
-
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
function haltRefusal(events) {
|
|
3043
|
+
const at = events.findIndex((e) => {
|
|
2920
3044
|
const k = e?.kind;
|
|
2921
3045
|
return k === "halted" || k === "stopped";
|
|
2922
3046
|
});
|
|
2923
3047
|
if (at >= 0) {
|
|
2924
|
-
const e =
|
|
3048
|
+
const e = events[at];
|
|
2925
3049
|
return new ResumeRefused(
|
|
2926
3050
|
"halted",
|
|
2927
3051
|
at,
|
|
2928
3052
|
`ResumeRefused (halted): the stored run was HALTED by a refusal \u2014 its element at position ${at} is \`${String(e.kind)}\`${e.kind === "stopped" ? " (the name 0.7.0 wrote for `halted`, ADR 0102)" : ""} (${JSON.stringify(e.reason)}). A halted run is not resumable (P23 decision 10): the refusal that halted it would halt it again, and the one class whose cause never reached the log \u2014 a result OR-R6 refused \u2014 cannot be regenerated at all.`
|
|
2929
3053
|
);
|
|
2930
3054
|
}
|
|
2931
|
-
const c =
|
|
3055
|
+
const c = events.findIndex((e) => {
|
|
2932
3056
|
const x = e;
|
|
2933
3057
|
return x?.kind === "cancelRequested" || x?.kind === "outcome" && x.record?.terminalState === "cancelled";
|
|
2934
3058
|
});
|
|
2935
3059
|
if (c >= 0) {
|
|
2936
|
-
const e =
|
|
3060
|
+
const e = events[c];
|
|
2937
3061
|
const what = e?.kind === "cancelRequested" ? `the request of ${JSON.stringify(e.actor)} to cancel it (\`cancelRequested\`)` : `the settlement of ${JSON.stringify(e?.record?.taskId)} as \`cancelled\``;
|
|
2938
3062
|
return new ResumeRefused(
|
|
2939
3063
|
"cancelled",
|
|
@@ -2969,6 +3093,11 @@ function sameData(a, b) {
|
|
|
2969
3093
|
const kb = Object.keys(rb).filter((k) => rb[k] !== void 0);
|
|
2970
3094
|
return ka.length === kb.length && ka.every((k) => Object.hasOwn(rb, k) && sameData(ra[k], rb[k]));
|
|
2971
3095
|
}
|
|
3096
|
+
function storedAskedAt(e) {
|
|
3097
|
+
if (e === void 0 || e === null || typeof e !== "object" || e.kind !== "asked") return void 0;
|
|
3098
|
+
const t = e.observedAt;
|
|
3099
|
+
return typeof t === "number" ? t : void 0;
|
|
3100
|
+
}
|
|
2972
3101
|
function isFed(e) {
|
|
2973
3102
|
return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed" || e.kind === "interrupted" || e.kind === "paused" || e.kind === "unpaused" || e.kind === "attempted";
|
|
2974
3103
|
}
|
|
@@ -2993,6 +3122,25 @@ function describeElement(e) {
|
|
|
2993
3122
|
}
|
|
2994
3123
|
return s.length > 160 ? `${s.slice(0, 157)}...` : s;
|
|
2995
3124
|
}
|
|
3125
|
+
function deepFreeze(root) {
|
|
3126
|
+
const stack = [root];
|
|
3127
|
+
while (stack.length > 0) {
|
|
3128
|
+
const v = stack.pop();
|
|
3129
|
+
if (v === null || typeof v !== "object" || ArrayBuffer.isView(v) || v instanceof RegExp || Object.isFrozen(v)) continue;
|
|
3130
|
+
Object.freeze(v);
|
|
3131
|
+
const proto = Object.getPrototypeOf(v);
|
|
3132
|
+
if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) continue;
|
|
3133
|
+
for (const k of Object.keys(v)) {
|
|
3134
|
+
const x = v[k];
|
|
3135
|
+
if (x !== null && typeof x === "object") stack.push(x);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
function sharedCopy(v) {
|
|
3140
|
+
const copy = cloneValue(v);
|
|
3141
|
+
deepFreeze(copy);
|
|
3142
|
+
return copy;
|
|
3143
|
+
}
|
|
2996
3144
|
var NO_OUTPUTS = Object.freeze([]);
|
|
2997
3145
|
function groupPosition(group, path) {
|
|
2998
3146
|
let lo = 0;
|
|
@@ -3005,6 +3153,15 @@ function groupPosition(group, path) {
|
|
|
3005
3153
|
}
|
|
3006
3154
|
return lo;
|
|
3007
3155
|
}
|
|
3156
|
+
var InputCut = class extends Error {
|
|
3157
|
+
};
|
|
3158
|
+
var DEAF_SCHEDULER = {
|
|
3159
|
+
addTask() {
|
|
3160
|
+
throw new Error("a reading hands nothing to a scheduler (RN-R45, ADR 0128) \u2014 this is a defect in the driver");
|
|
3161
|
+
},
|
|
3162
|
+
addEventListener() {
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3008
3165
|
var UndispatchedTask = class extends Error {
|
|
3009
3166
|
constructor(message, options) {
|
|
3010
3167
|
super(message, options);
|
|
@@ -3144,7 +3301,7 @@ var Fifo = class {
|
|
|
3144
3301
|
for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
|
|
3145
3302
|
}
|
|
3146
3303
|
};
|
|
3147
|
-
var Driver = class {
|
|
3304
|
+
var Driver = class _Driver {
|
|
3148
3305
|
/**
|
|
3149
3306
|
* `OR-R7` — nothing outside the inbound API writes to run state. Private, and
|
|
3150
3307
|
* `snapshot()` is the only way out: a public `readonly` array still accepts
|
|
@@ -3276,7 +3433,8 @@ var Driver = class {
|
|
|
3276
3433
|
* - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
|
|
3277
3434
|
* by a run and by a resume's replay alike, so a replay REBUILDS it (`RN-R19`; P38's brief: *taken at rest*, never by
|
|
3278
3435
|
* a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
|
|
3279
|
-
* `needs` composition's
|
|
3436
|
+
* `needs` composition's value — since W202 an element of a reference's SHARED copy, which this keeps alive while held),
|
|
3437
|
+
* and as its OWN copy only where `kept` says that element is the caller's
|
|
3280
3438
|
* (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
|
|
3281
3439
|
* **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
|
|
3282
3440
|
* `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
|
|
@@ -3698,7 +3856,7 @@ var Driver = class {
|
|
|
3698
3856
|
const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
|
|
3699
3857
|
const schema = worker?.inputSchema;
|
|
3700
3858
|
if (schema === void 0) continue;
|
|
3701
|
-
const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
|
|
3859
|
+
const received = this.compose(n, value, EMPTY_INDICES, () => cloneValue(payload), () => value);
|
|
3702
3860
|
const fs = findings(schema, received);
|
|
3703
3861
|
if (fs.length) {
|
|
3704
3862
|
throw new BoundaryRefused(
|
|
@@ -3795,7 +3953,7 @@ var Driver = class {
|
|
|
3795
3953
|
const entryPayload = structuredClone(checkpoint.header.entryPayload);
|
|
3796
3954
|
this.header = { format: CHECKPOINT_FORMAT, runId: this.runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
|
|
3797
3955
|
this.started = true;
|
|
3798
|
-
const replay = { stored, feeding: -1, held: /* @__PURE__ */ new Map() };
|
|
3956
|
+
const replay = { stored, feeding: -1, held: /* @__PURE__ */ new Map(), reading: null };
|
|
3799
3957
|
this.replay = replay;
|
|
3800
3958
|
try {
|
|
3801
3959
|
this.replayStored(stored);
|
|
@@ -3860,6 +4018,7 @@ var Driver = class {
|
|
|
3860
4018
|
if (!isFed(e)) continue;
|
|
3861
4019
|
if (this.inputs.length !== i) throw this.diverged(this.inputs.length, "the replay produced nothing there");
|
|
3862
4020
|
replay.feeding = i;
|
|
4021
|
+
if (replay.reading !== null) replay.reading.fed += 1;
|
|
3863
4022
|
this.feed(() => this.feedOne(e, i));
|
|
3864
4023
|
}
|
|
3865
4024
|
if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
|
|
@@ -3913,12 +4072,15 @@ var Driver = class {
|
|
|
3913
4072
|
this.appendResolution(cloneRecord(record), nodeId, branchPath);
|
|
3914
4073
|
this.advance(runId, nodeId, branchPath);
|
|
3915
4074
|
}
|
|
3916
|
-
/**
|
|
4075
|
+
/**
|
|
4076
|
+
* The listener's boundary, for the replay: a refusal routing raises stops the run (`OR-R10`); a refused resume passes
|
|
4077
|
+
* through — and so does a reading's cut (`InputCut`, W205), which is no refusal and halts nothing.
|
|
4078
|
+
*/
|
|
3917
4079
|
feed(step2) {
|
|
3918
4080
|
try {
|
|
3919
4081
|
step2();
|
|
3920
4082
|
} catch (e) {
|
|
3921
|
-
if (e instanceof ResumeRefused) throw e;
|
|
4083
|
+
if (e instanceof ResumeRefused || e instanceof InputCut) throw e;
|
|
3922
4084
|
this.failRun(e);
|
|
3923
4085
|
}
|
|
3924
4086
|
}
|
|
@@ -3931,6 +4093,119 @@ var Driver = class {
|
|
|
3931
4093
|
`ResumeRefused (diverged): replaying run "${this.runId}" does not reproduce the stored element at position ${at} \u2014 stored ${describeElement(stored[at])}, replayed ${describeElement(this.inputs[at])} (${why}). P23 decision 9: a stored run the replay cannot regenerate \u2014 a predicate, a definition or an entry payload that is not what produced it \u2014 is refused rather than resumed on a different road. Nothing was handed to the scheduler.`
|
|
3932
4094
|
);
|
|
3933
4095
|
}
|
|
4096
|
+
/**
|
|
4097
|
+
* **A cut site offers what a node was handed** (`RN-R45`, W205). An offer for another path is dropped. `own` is the
|
|
4098
|
+
* position of the node's OWN element where it is already appended — a task's `dispatched`, a join's firing — and `null`
|
|
4099
|
+
* where it is the NEXT append, which `append` confirms (`Reading.pending`). Either way it is TAKEN only at a STORED
|
|
4100
|
+
* position, so a composition whose element lies past the prefix the reading verifies is never answered.
|
|
4101
|
+
*/
|
|
4102
|
+
offer(r, input, own3) {
|
|
4103
|
+
if (r.branchPath !== null && input.branchPath !== r.branchPath) return;
|
|
4104
|
+
const stored = this.replay.stored.length;
|
|
4105
|
+
if (own3 === null) {
|
|
4106
|
+
if (this.inputs.length < stored) r.pending = { at: this.inputs.length, input };
|
|
4107
|
+
return;
|
|
4108
|
+
}
|
|
4109
|
+
if (own3 < stored) this.take(r, input);
|
|
4110
|
+
}
|
|
4111
|
+
/** **An offer taken** — kept, and for a target with a path the replay ends HERE: nothing after it is replayed (`InputCut`). */
|
|
4112
|
+
take(r, input) {
|
|
4113
|
+
r.found.push(input);
|
|
4114
|
+
if (r.branchPath !== null) throw new InputCut();
|
|
4115
|
+
}
|
|
4116
|
+
/**
|
|
4117
|
+
* **A READING — what a node was handed, by replaying the run to that node's composition** — `RN-R45`, W205,
|
|
4118
|
+
* [ADR 0128](../../doc/decisions/0128-what-a-node-was-handed-is-read-by-replaying-the-run-to-its-composition.md), P42
|
|
4119
|
+
* decisions 1-7. The export {@link inputOf} and `RunHandle.inputOf` are this and nothing else. In order:
|
|
4120
|
+
*
|
|
4121
|
+
* 1. **The header**, refused by `resumeRefusal`'s own header half (`format`, `flow`) — never its halt half.
|
|
4122
|
+
* 2. **The target**, refused `unknown` where it cannot name anything in this run.
|
|
4123
|
+
* 3. **A driver built exactly as `runtime.resume` builds one** (`RN-R25`) — the definition, the registry's members, a
|
|
4124
|
+
* `joinPolicy` defaulting to `liveToken` — with NO callback and a scheduler that is handed nothing (`DEAF_SCHEDULER`).
|
|
4125
|
+
* 4. **The replay** (`readStep`) over the stored PREFIX it can verify, the cut armed: it stops at the target's
|
|
4126
|
+
* composition and returns what it found.
|
|
4127
|
+
*
|
|
4128
|
+
* Private and STATIC, so the export and the handle reach one road, and a test reaches the reading's own driver — its
|
|
4129
|
+
* count — by a cast (ADR 0121 rule 7): `driver` is `null` where a refusal came before one was built.
|
|
4130
|
+
*/
|
|
4131
|
+
static read(flow, options, run, target) {
|
|
4132
|
+
const refused = headerRefusal(flow, run);
|
|
4133
|
+
if (refused !== null) {
|
|
4134
|
+
return { answer: { ok: false, reason: refused.reason, index: null, message: `a reading refuses this header as a resume does \u2014 ${refused.message}` }, driver: null };
|
|
4135
|
+
}
|
|
4136
|
+
const header = run.header;
|
|
4137
|
+
try {
|
|
4138
|
+
assertRunId(header.runId);
|
|
4139
|
+
} catch (e) {
|
|
4140
|
+
return { answer: { ok: false, reason: "format", index: null, message: `the stored header's runId is not a run id (ID-R2): ${e instanceof Error ? e.message : String(e)}` }, driver: null };
|
|
4141
|
+
}
|
|
4142
|
+
const t = readTarget(flow, header.runId, target);
|
|
4143
|
+
if ("ok" in t) return { answer: t, driver: null };
|
|
4144
|
+
const { predicates, workers, nodeTypes, pools, groups, languages, gateTypes, joinPolicy, defaultRetryPolicy, validateBoundaries } = options;
|
|
4145
|
+
const driver = new _Driver(flow, {
|
|
4146
|
+
runId: header.runId,
|
|
4147
|
+
scheduler: DEAF_SCHEDULER,
|
|
4148
|
+
predicates,
|
|
4149
|
+
...workers !== void 0 ? { workers } : {},
|
|
4150
|
+
...nodeTypes !== void 0 ? { nodeTypes } : {},
|
|
4151
|
+
...pools !== void 0 ? { pools } : {},
|
|
4152
|
+
...groups !== void 0 ? { groups } : {},
|
|
4153
|
+
...languages !== void 0 ? { languages } : {},
|
|
4154
|
+
...gateTypes !== void 0 ? { gateTypes } : {},
|
|
4155
|
+
joinPolicy: joinPolicy ?? liveToken,
|
|
4156
|
+
...defaultRetryPolicy !== void 0 ? { defaultRetryPolicy } : {},
|
|
4157
|
+
...validateBoundaries !== void 0 ? { validateBoundaries } : {}
|
|
4158
|
+
});
|
|
4159
|
+
return { answer: driver.readStep(run, t), driver };
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* **The reading's replay** — `resume()`'s first half (`resumeStep` to `replayStored`) with the cut armed, and nothing of
|
|
4163
|
+
* its second: no in-flight set, no `resumed`, nothing handed on. **The prefix it verifies ENDS at the first `halted`,
|
|
4164
|
+
* `stopped` or `cancelRequested` element** (P42 decision 5): after one a halted driver composes nothing more, whether the
|
|
4165
|
+
* replay regenerates it (a predicate's throw) or cannot (a consumer's callback, a refused result, an owner's call) —
|
|
4166
|
+
* what follows is `not-reached`, never `diverged` (P42 retro (a1) corrected the reason). Everything
|
|
4167
|
+
* before is compared as `RN-R20` compares it.
|
|
4168
|
+
*/
|
|
4169
|
+
readStep(run, t) {
|
|
4170
|
+
const events = run.events;
|
|
4171
|
+
let end = events.length;
|
|
4172
|
+
for (let i = 0; i < events.length; i += 1) {
|
|
4173
|
+
const e = events[i];
|
|
4174
|
+
if (typeof e !== "object" || e === null) {
|
|
4175
|
+
return { ok: false, reason: "format", index: null, message: `the stored element at position ${i} is ${JSON.stringify(e) ?? String(e)}, which is not a fold input (RN-R14, RN-R17)` };
|
|
4176
|
+
}
|
|
4177
|
+
const k = e.kind;
|
|
4178
|
+
if (k === "halted" || k === "stopped" || k === "cancelRequested") {
|
|
4179
|
+
end = i;
|
|
4180
|
+
break;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
let entryPayload;
|
|
4184
|
+
try {
|
|
4185
|
+
entryPayload = structuredClone(run.header.entryPayload);
|
|
4186
|
+
} catch (e) {
|
|
4187
|
+
return { ok: false, reason: "format", index: null, message: `the stored header's entry payload does not clone (RN-R15, RN-R17): ${e instanceof Error ? e.message : String(e)}` };
|
|
4188
|
+
}
|
|
4189
|
+
this.header = { format: CHECKPOINT_FORMAT, runId: this.runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
|
|
4190
|
+
this.started = true;
|
|
4191
|
+
const reading = { nodeId: t.nodeId, branchPath: t.branchPath, pending: null, found: [], fed: 0, elements: 0 };
|
|
4192
|
+
this.replay = { stored: end === events.length ? events : events.slice(0, end), feeding: -1, held: /* @__PURE__ */ new Map(), reading };
|
|
4193
|
+
try {
|
|
4194
|
+
this.replayStored(this.replay.stored);
|
|
4195
|
+
} catch (e) {
|
|
4196
|
+
if (e instanceof InputCut) return { ok: true, inputs: [...reading.found] };
|
|
4197
|
+
if (e instanceof ResumeRefused) return { ok: false, reason: "diverged", index: e.index, message: e.message };
|
|
4198
|
+
throw e;
|
|
4199
|
+
}
|
|
4200
|
+
if (reading.found.length > 0) return { ok: true, inputs: [...reading.found] };
|
|
4201
|
+
const where = t.branchPath === null ? `node "${t.nodeId}" on any branch path` : `node "${t.nodeId}" at branch path "${t.branchPath}"`;
|
|
4202
|
+
return {
|
|
4203
|
+
ok: false,
|
|
4204
|
+
reason: "not-reached",
|
|
4205
|
+
index: end,
|
|
4206
|
+
message: `the replay handed ${where} nothing within the ${end} stored element(s) a reading verifies` + (end < events.length ? ` \u2014 the prefix ends at position ${end}, \`${String(events[end].kind)}\`, after which a halted driver composes nothing more (RN-R45)` : "") + "."
|
|
4207
|
+
};
|
|
4208
|
+
}
|
|
3934
4209
|
/**
|
|
3935
4210
|
* **What is IN FLIGHT: dispatched, and no settlement in the log** — `RN-R21`, in dispatch order. A
|
|
3936
4211
|
* `user_action` settlement IS a settlement: a parked task is waiting on a person, not on a worker, and
|
|
@@ -4804,7 +5079,12 @@ var Driver = class {
|
|
|
4804
5079
|
if (this.shut()) return;
|
|
4805
5080
|
const node = this.graph.byId.get(nodeId);
|
|
4806
5081
|
if (!node) return;
|
|
4807
|
-
const received = this.compose(node, payload, itemIndices, this.
|
|
5082
|
+
const received = this.compose(node, payload, itemIndices, () => this.sharedPayload(), () => this.itemOf(node.id, itemIndices));
|
|
5083
|
+
const reading = this.replay?.reading ?? null;
|
|
5084
|
+
if (reading !== null && node.kind !== "task" && node.id === reading.nodeId) {
|
|
5085
|
+
const path = node.kind === "join" || node.kind === "end" ? fromBranch : this.pathOf(node.id, itemIndices);
|
|
5086
|
+
this.offer(reading, { nodeId: node.id, kind: node.kind, branchPath: path, handed: node.kind === "join" ? "arrival" : "received", value: received }, null);
|
|
5087
|
+
}
|
|
4808
5088
|
switch (node.kind) {
|
|
4809
5089
|
case "task":
|
|
4810
5090
|
return this.dispatch(runId, node, received, itemIndices);
|
|
@@ -5112,7 +5392,8 @@ var Driver = class {
|
|
|
5112
5392
|
* described **today** before this method existed, and this adds **no retention**.
|
|
5113
5393
|
* *(Q19 is RESOLVED since W53 —
|
|
5114
5394
|
* [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
|
|
5115
|
-
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference
|
|
5395
|
+
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference** — and since W202 (ADR 0126) one shared
|
|
5396
|
+
* COPY per output a reference has read, the one thing in it that is not the log's object,
|
|
5116
5397
|
* asserted by IDENTITY at ladder L2 rather than by a byte count, because a
|
|
5117
5398
|
* reachability SET cannot tell an alias from a copy. W53's readers found that
|
|
5118
5399
|
* from both ends — the rule's stated test returned a third name on a `needs`
|
|
@@ -5140,7 +5421,7 @@ var Driver = class {
|
|
|
5140
5421
|
* node or an edge that declares a `needs` asks, and a run that declares none never builds it.
|
|
5141
5422
|
*/
|
|
5142
5423
|
outputs() {
|
|
5143
|
-
const c = this.
|
|
5424
|
+
const c = this.cache();
|
|
5144
5425
|
for (; c.at < this.log.length; c.at += 1) {
|
|
5145
5426
|
const r = this.log[c.at];
|
|
5146
5427
|
let t = c.tasks.get(r.taskId);
|
|
@@ -5160,9 +5441,26 @@ var Driver = class {
|
|
|
5160
5441
|
}
|
|
5161
5442
|
return c.byNode;
|
|
5162
5443
|
}
|
|
5444
|
+
/**
|
|
5445
|
+
* The projection, made empty on first need and NOT caught up — `outputs()` catches it up; a `"$payload"` read, which
|
|
5446
|
+
* reads no output, only needs somewhere to keep its shared copy (W202).
|
|
5447
|
+
*/
|
|
5448
|
+
cache() {
|
|
5449
|
+
return this.outputsCache ??= {
|
|
5450
|
+
at: 0,
|
|
5451
|
+
byNode: /* @__PURE__ */ new Map(),
|
|
5452
|
+
tasks: /* @__PURE__ */ new Map(),
|
|
5453
|
+
orders: /* @__PURE__ */ new Map(),
|
|
5454
|
+
index: /* @__PURE__ */ new Map(),
|
|
5455
|
+
depths: /* @__PURE__ */ new Map(),
|
|
5456
|
+
examined: 0,
|
|
5457
|
+
payload: null,
|
|
5458
|
+
copies: 0
|
|
5459
|
+
};
|
|
5460
|
+
}
|
|
5163
5461
|
/**
|
|
5164
5462
|
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
5165
|
-
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
5463
|
+
* 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
|
|
5166
5464
|
* in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
|
|
5167
5465
|
*
|
|
5168
5466
|
* **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
|
|
@@ -5174,6 +5472,10 @@ var Driver = class {
|
|
|
5174
5472
|
* that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
|
|
5175
5473
|
* so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
|
|
5176
5474
|
* none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
|
|
5475
|
+
*
|
|
5476
|
+
* **Both a replacement and a removal let go of the entry OBJECT, and with it the entry's shared copy** (W202, ADR 0126,
|
|
5477
|
+
* `OutputEntry.shared`): a replaced output's next read makes a new copy of the new result, and a removed one's copy is
|
|
5478
|
+
* held nowhere — `RN-R13`'s v0.33 term, released with what it belongs to (ADR 0046).
|
|
5177
5479
|
*/
|
|
5178
5480
|
place(c, t, eff) {
|
|
5179
5481
|
const list = c.byNode.get(t.nodeId);
|
|
@@ -5191,7 +5493,7 @@ var Driver = class {
|
|
|
5191
5493
|
}
|
|
5192
5494
|
return;
|
|
5193
5495
|
}
|
|
5194
|
-
const entry = { path: t.path, result: eff.result };
|
|
5496
|
+
const entry = { path: t.path, result: eff.result, error: eff.error };
|
|
5195
5497
|
if (t.entry !== null && list !== void 0) {
|
|
5196
5498
|
list[list.indexOf(t.entry)] = entry;
|
|
5197
5499
|
this.reindex(c, t.nodeId, t.entry, entry);
|
|
@@ -5288,7 +5590,8 @@ var Driver = class {
|
|
|
5288
5590
|
/**
|
|
5289
5591
|
* The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
|
|
5290
5592
|
* it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
|
|
5291
|
-
*
|
|
5593
|
+
* not a copy of them; since W202 an entry a reference has read also holds its ONE shared copy (`OutputEntry.shared`,
|
|
5594
|
+
* `RN-R13` v0.33), and `payload` the run's payload's — `tasks` is one slot per task id
|
|
5292
5595
|
* read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
|
|
5293
5596
|
* node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
|
|
5294
5597
|
* read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
|
|
@@ -5324,7 +5627,7 @@ var Driver = class {
|
|
|
5324
5627
|
continue;
|
|
5325
5628
|
}
|
|
5326
5629
|
if (t.form === "payload") {
|
|
5327
|
-
out[key] =
|
|
5630
|
+
out[key] = runPayload();
|
|
5328
5631
|
continue;
|
|
5329
5632
|
}
|
|
5330
5633
|
if (t.form === "item") {
|
|
@@ -5343,7 +5646,7 @@ var Driver = class {
|
|
|
5343
5646
|
*
|
|
5344
5647
|
* **Extracted by [W42](../../doc/work-items/w42-what-a-decision-sees.md) rather
|
|
5345
5648
|
* than copied.** An EDGE's `needs` resolves by exactly these rules — same grammar,
|
|
5346
|
-
* same visibility predicate, same order, same
|
|
5649
|
+
* same visibility predicate, same order, same shared copy (since W202, ADR 0126) — and the ONE difference
|
|
5347
5650
|
* is that an edge has no `"."` form, which is a property of the caller rather than
|
|
5348
5651
|
* of this expression. *Two callers, one resolution, so a change to what a reference
|
|
5349
5652
|
* means cannot mean two different things to a worker and to a predicate*, which is
|
|
@@ -5363,15 +5666,41 @@ var Driver = class {
|
|
|
5363
5666
|
`node "${atNodeId}" at branch path "${here}" declares \`${site}[${JSON.stringify(key)}]\` naming "${t.node}", and no output of that node is on this path. \`FD-V27\` checked that this node is reachable FROM it and \`FD-V29\` that it must have RUN, which is everything the graph can say; whether any of its outputs is on YOUR path is a statement about this run \u2014 a reader inside a region whose indices do not line up with the target\`s, or a join policy that fired before that node settled. FD-R3.`
|
|
5364
5667
|
);
|
|
5365
5668
|
}
|
|
5366
|
-
|
|
5367
|
-
|
|
5669
|
+
const c = this.outputsCache;
|
|
5670
|
+
c.examined += t.form === "all" ? visible.length : 1;
|
|
5671
|
+
if (t.part === "error") {
|
|
5672
|
+
return t.form === "all" ? visible.map((e) => e.error) : visible[0].error;
|
|
5673
|
+
}
|
|
5674
|
+
return t.form === "all" ? visible.map((e) => this.sharedOf(c, e)) : this.sharedOf(c, visible[0]);
|
|
5675
|
+
}
|
|
5676
|
+
/** **An output entry's shared copy**, made at its first read and kept on the entry (`OutputEntry.shared`) — counted. */
|
|
5677
|
+
sharedOf(c, e) {
|
|
5678
|
+
if (!("shared" in e)) {
|
|
5679
|
+
e.shared = sharedCopy(e.result);
|
|
5680
|
+
c.copies += 1;
|
|
5681
|
+
}
|
|
5682
|
+
return e.shared;
|
|
5683
|
+
}
|
|
5684
|
+
/**
|
|
5685
|
+
* **`"$payload"`'s value in a run: the run's ONE shared copy** — W202, ADR 0126, P41 decision 2: `sharedCopy` of the
|
|
5686
|
+
* header's `entryPayload` (`runPayload()`), made at the first `"$payload"` read of the run — or of a resume, whose driver
|
|
5687
|
+
* is a new one — and handed to every reader at both sites. Counted once. `FD-R4` does not come here: it composes from
|
|
5688
|
+
* the caller's own value, before a header exists, and keeps its clone.
|
|
5689
|
+
*/
|
|
5690
|
+
sharedPayload() {
|
|
5691
|
+
const c = this.cache();
|
|
5692
|
+
if (c.payload === null) {
|
|
5693
|
+
c.payload = { value: sharedCopy(this.runPayload()) };
|
|
5694
|
+
c.copies += 1;
|
|
5695
|
+
}
|
|
5696
|
+
return c.payload.value;
|
|
5368
5697
|
}
|
|
5369
5698
|
/**
|
|
5370
5699
|
* **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
|
|
5371
5700
|
* caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
|
|
5372
5701
|
* reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
|
|
5373
|
-
* header exists. Read only where a node or an edge composes the token — a run declaring none never
|
|
5374
|
-
* (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
|
|
5702
|
+
* header exists. Read only where a node or an edge composes the token — a run declaring none never copies it
|
|
5703
|
+
* (P38 decision 2); since v0.47 read ONCE, by `sharedPayload`, whose one shared copy every reader is handed. The header is set before the first `enter` in both doors, so `null` here is not a run.
|
|
5375
5704
|
*/
|
|
5376
5705
|
runPayload() {
|
|
5377
5706
|
return this.header?.entryPayload;
|
|
@@ -5407,8 +5736,8 @@ var Driver = class {
|
|
|
5407
5736
|
* received anyway* is `ctx.outcome`, which it already has. So there is no
|
|
5408
5737
|
* `edgeValue` parameter here and none is derivable — see the call site. **A
|
|
5409
5738
|
* `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
|
|
5410
|
-
* the NODE received but read from the run's header
|
|
5411
|
-
* copy
|
|
5739
|
+
* the NODE received but read from the run's header — since v0.47 the run's ONE shared
|
|
5740
|
+
* copy of it, `sharedPayload()` above, the object a worker's `"$payload"` key is handed (ADR 0126).
|
|
5412
5741
|
*
|
|
5413
5742
|
* @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
|
|
5414
5743
|
*/
|
|
@@ -5421,7 +5750,7 @@ var Driver = class {
|
|
|
5421
5750
|
for (const [key, ref] of Object.entries(needs)) {
|
|
5422
5751
|
const t = referenceTarget(ref);
|
|
5423
5752
|
if (t.form === "payload") {
|
|
5424
|
-
out[key] =
|
|
5753
|
+
out[key] = this.sharedPayload();
|
|
5425
5754
|
continue;
|
|
5426
5755
|
}
|
|
5427
5756
|
if (t.form === "item") {
|
|
@@ -5452,7 +5781,7 @@ var Driver = class {
|
|
|
5452
5781
|
}
|
|
5453
5782
|
this.dispatched.add(taskId);
|
|
5454
5783
|
if (this.replay === null) this.unhanded.add(taskId);
|
|
5455
|
-
this.append({ kind: "dispatched", taskId });
|
|
5784
|
+
const dispatchedAt = this.append({ kind: "dispatched", taskId });
|
|
5456
5785
|
const type = this.nodeTypes.get(node.type);
|
|
5457
5786
|
const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
|
|
5458
5787
|
const config = {
|
|
@@ -5464,7 +5793,11 @@ var Driver = class {
|
|
|
5464
5793
|
...node.workerGroups !== void 0 ? { workerGroups: node.workerGroups } : {}
|
|
5465
5794
|
};
|
|
5466
5795
|
if (this.replay !== null) {
|
|
5467
|
-
this.replay.
|
|
5796
|
+
const reading = this.replay.reading;
|
|
5797
|
+
if (reading === null) this.replay.held.set(taskId, config);
|
|
5798
|
+
else if (node.id === reading.nodeId) {
|
|
5799
|
+
this.offer(reading, { nodeId: node.id, kind: "task", branchPath: this.pathOf(node.id, itemIndices), handed: "envelope", value: config.payload }, dispatchedAt);
|
|
5800
|
+
}
|
|
5468
5801
|
return;
|
|
5469
5802
|
}
|
|
5470
5803
|
if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
|
|
@@ -5504,7 +5837,10 @@ var Driver = class {
|
|
|
5504
5837
|
throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
|
|
5505
5838
|
}
|
|
5506
5839
|
this.asked.add(taskId);
|
|
5507
|
-
|
|
5840
|
+
const replay = this.replay;
|
|
5841
|
+
const at = this.inputs.length;
|
|
5842
|
+
const observedAt = replay !== null && at < replay.stored.length ? storedAskedAt(replay.stored[at]) : this.now();
|
|
5843
|
+
this.append(observedAt === void 0 ? { kind: "asked", taskId, input: cloneValue(received) } : { kind: "asked", taskId, input: cloneValue(received), observedAt });
|
|
5508
5844
|
if (this.shut()) return;
|
|
5509
5845
|
const region = this.regionOf(node.id);
|
|
5510
5846
|
if (region) {
|
|
@@ -5826,6 +6162,15 @@ var Driver = class {
|
|
|
5826
6162
|
if (replay !== null && seq < replay.stored.length) {
|
|
5827
6163
|
const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
|
|
5828
6164
|
if (!same) throw this.diverged(seq, "a different element there");
|
|
6165
|
+
const r = replay.reading;
|
|
6166
|
+
if (r !== null) {
|
|
6167
|
+
r.elements += 1;
|
|
6168
|
+
if (r.pending !== null && r.pending.at === seq) {
|
|
6169
|
+
const taken = r.pending.input;
|
|
6170
|
+
r.pending = null;
|
|
6171
|
+
this.take(r, taken);
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
5829
6174
|
}
|
|
5830
6175
|
if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
|
|
5831
6176
|
return seq;
|
|
@@ -5909,7 +6254,7 @@ var Driver = class {
|
|
|
5909
6254
|
return;
|
|
5910
6255
|
}
|
|
5911
6256
|
this.firedAdd(region.fanoutId, rp);
|
|
5912
|
-
this.append({
|
|
6257
|
+
const firedAt = this.append({
|
|
5913
6258
|
kind: "joinSettled",
|
|
5914
6259
|
regionId: region.fanoutId,
|
|
5915
6260
|
regionPath: rp,
|
|
@@ -5919,6 +6264,10 @@ var Driver = class {
|
|
|
5919
6264
|
if (this.shut()) return;
|
|
5920
6265
|
const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
|
|
5921
6266
|
this.releaseArrivals(region.fanoutId, rp);
|
|
6267
|
+
const reading = this.replay?.reading ?? null;
|
|
6268
|
+
if (reading !== null && region.joinId === reading.nodeId) {
|
|
6269
|
+
this.offer(reading, { nodeId: region.joinId, kind: "join", branchPath: rp, handed: "collection", value: collected }, firedAt);
|
|
6270
|
+
}
|
|
5922
6271
|
const join = this.graph.byId.get(region.joinId);
|
|
5923
6272
|
if (!join || join.kind !== "join") return;
|
|
5924
6273
|
const edge = join.edges?.[0];
|
|
@@ -6053,6 +6402,46 @@ var Driver = class {
|
|
|
6053
6402
|
return this.effectiveOutcomeOf(taskId)?.terminalState ?? null;
|
|
6054
6403
|
}
|
|
6055
6404
|
};
|
|
6405
|
+
function readTarget(flow, runId, target) {
|
|
6406
|
+
const unknown = (why) => ({ ok: false, reason: "unknown", index: null, message: `the target names nothing this run can have: ${why} (RN-R45)` });
|
|
6407
|
+
const t = target;
|
|
6408
|
+
const nodes = flow.nodes;
|
|
6409
|
+
if (typeof t?.taskId === "string") {
|
|
6410
|
+
let parts;
|
|
6411
|
+
try {
|
|
6412
|
+
parts = parseTaskId(t.taskId);
|
|
6413
|
+
} catch (e) {
|
|
6414
|
+
return unknown(`task id ${JSON.stringify(t.taskId)} does not parse \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
6415
|
+
}
|
|
6416
|
+
if (parts.runId !== runId) return unknown(`task id ${JSON.stringify(t.taskId)} is of run "${parts.runId}", and this run is "${runId}"`);
|
|
6417
|
+
if (parts.attempt !== 0) return unknown(`task id ${JSON.stringify(t.taskId)} carries attempt ${parts.attempt}, and a run mints attempt 0`);
|
|
6418
|
+
const node = nodes.find((n) => n.id === parts.nodeId);
|
|
6419
|
+
if (node === void 0 || node.kind !== "task" && node.kind !== "ask") {
|
|
6420
|
+
return unknown(`task id ${JSON.stringify(t.taskId)} names ${node === void 0 ? "no node of the definition" : `a ${node.kind}, which has no task id`}`);
|
|
6421
|
+
}
|
|
6422
|
+
return { nodeId: parts.nodeId, branchPath: parts.branchPath };
|
|
6423
|
+
}
|
|
6424
|
+
if (typeof t?.nodeId === "string") {
|
|
6425
|
+
const nodeId = t.nodeId;
|
|
6426
|
+
if (!nodes.some((n) => n.id === nodeId)) return unknown(`the definition has no node "${nodeId}"`);
|
|
6427
|
+
if (t.branchPath === void 0) return { nodeId, branchPath: null };
|
|
6428
|
+
const path = t.branchPath;
|
|
6429
|
+
let ok = typeof path === "string";
|
|
6430
|
+
if (ok) {
|
|
6431
|
+
try {
|
|
6432
|
+
parseTaskId(`${runId}:${nodeId}:${path}:0`);
|
|
6433
|
+
} catch {
|
|
6434
|
+
ok = false;
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
if (!ok) return unknown(`branch path ${JSON.stringify(path) ?? String(path)} is not spec/task-id.md's grammar ("-", or dot-separated non-negative integers)`);
|
|
6438
|
+
return { nodeId, branchPath: path };
|
|
6439
|
+
}
|
|
6440
|
+
return unknown("a target is { taskId } or { nodeId, branchPath? }");
|
|
6441
|
+
}
|
|
6442
|
+
function inputOf(flow, options, run, target) {
|
|
6443
|
+
return Driver.read(flow, options, run, target).answer;
|
|
6444
|
+
}
|
|
6056
6445
|
|
|
6057
6446
|
// src/fold/accumulator.ts
|
|
6058
6447
|
var Suspended = class {
|
|
@@ -6345,7 +6734,9 @@ var Accumulator = class _Accumulator {
|
|
|
6345
6734
|
state: "suspended",
|
|
6346
6735
|
result: null,
|
|
6347
6736
|
error: null,
|
|
6348
|
-
input: structuredClone(input.input)
|
|
6737
|
+
input: structuredClone(input.input),
|
|
6738
|
+
// `RN-R36` v0.35 (W208, ADR 0130): WHEN it was asked, where the element says — and only there.
|
|
6739
|
+
...input.observedAt !== void 0 ? { askedAt: input.observedAt } : {}
|
|
6349
6740
|
});
|
|
6350
6741
|
return this.withOutcome();
|
|
6351
6742
|
}
|
|
@@ -6413,7 +6804,8 @@ var Accumulator = class _Accumulator {
|
|
|
6413
6804
|
state: r.resolvedAs,
|
|
6414
6805
|
result: r.result,
|
|
6415
6806
|
error: r.error,
|
|
6416
|
-
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
6807
|
+
...existing !== void 0 && "input" in existing ? { input: existing.input } : {},
|
|
6808
|
+
...existing?.askedAt !== void 0 ? { askedAt: existing.askedAt } : {}
|
|
6417
6809
|
});
|
|
6418
6810
|
}
|
|
6419
6811
|
this.runId = this.runId ?? runId;
|
|
@@ -6564,74 +6956,6 @@ function nodeOf(s, taskId) {
|
|
|
6564
6956
|
return s.nodes[taskId] ?? null;
|
|
6565
6957
|
}
|
|
6566
6958
|
|
|
6567
|
-
// src/fold/policies.ts
|
|
6568
|
-
var arrivalCount = {
|
|
6569
|
-
id: "arrival-count",
|
|
6570
|
-
// **`region` is deliberately unread.** It was the source of `need` and is now
|
|
6571
|
-
// the source of nothing: `branches` cannot answer *how many branches did this
|
|
6572
|
-
// region open* for a map, and a policy that read it for a fanout and counted for
|
|
6573
|
-
// a map would be branching on the node kind — which it cannot see
|
|
6574
|
-
// ([Q159](../../doc/questions/q159.md)) and, per that row, should not.
|
|
6575
|
-
open: () => ({ need: 0, arrived: [] }),
|
|
6576
|
-
step: (s, e) => {
|
|
6577
|
-
if (e.type === "branch.opened") return { ...s, need: s.need + 1 };
|
|
6578
|
-
if (e.type !== "branch.arrived" || s.arrived.includes(e.branchPath)) return s;
|
|
6579
|
-
return { ...s, arrived: [...s.arrived, e.branchPath] };
|
|
6580
|
-
},
|
|
6581
|
-
// **`s.need > 0` is a PIN, not a refinement**, and it is exactly `JP-R8`'s own
|
|
6582
|
-
// *"having been positive"* argument in the policy that argument was denied to.
|
|
6583
|
-
// A bare `arrived.length >= need` is true at `0 >= 0`, so without it `open`
|
|
6584
|
-
// itself reports `satisfied` — and at a map of zero items this policy would fire
|
|
6585
|
-
// a join **no branch ever reached**, which is character for character what
|
|
6586
|
-
// [ADR 0025](../../doc/decisions/0025-the-zero-arrival-join-is-reportable-and-arrival-counting-is-not-repaired.md)
|
|
6587
|
-
// REFUSED `JP-R7`'s variant for. It is vacuous on every `fanout` region, because
|
|
6588
|
-
// `FD-V9`'s floor puts `need` at 2 or more.
|
|
6589
|
-
status: (s) => s.need > 0 && s.arrived.length >= s.need ? "satisfied" : "waiting",
|
|
6590
|
-
// Never `unsatisfiable`: this policy has no way to know a join can never fire,
|
|
6591
|
-
// which is precisely the argument Q4 states as a testable claim. **At a map of
|
|
6592
|
-
// zero items it says `waiting`, exactly as the other two do** — the region is
|
|
6593
|
-
// reported dead by the FOLD (`RN-R10`, via the `regionEntered` input) rather
|
|
6594
|
-
// than by any policy, which is `spec/join-policy.md` section 5's own case.
|
|
6595
|
-
describe: (s) => ({ need: s.need, arrived: [...s.arrived] })
|
|
6596
|
-
};
|
|
6597
|
-
var liveToken = {
|
|
6598
|
-
id: "live-token",
|
|
6599
|
-
open: () => ({ live: 0, positive: false }),
|
|
6600
|
-
step: (s, e) => {
|
|
6601
|
-
if (e.type === "branch.opened") return { live: s.live + 1, positive: true };
|
|
6602
|
-
if (e.type === "branch.arrived" || e.type === "branch.exited" || e.type === "branch.lost") {
|
|
6603
|
-
return { ...s, live: s.live - 1 };
|
|
6604
|
-
}
|
|
6605
|
-
return s;
|
|
6606
|
-
},
|
|
6607
|
-
status: (s) => s.positive && s.live === 0 ? "satisfied" : "waiting",
|
|
6608
|
-
// NOT requiring an arrival before reporting `satisfied`, and NOT returning
|
|
6609
|
-
// `unsatisfiable` on zero-with-no-arrivals. That is JP-R8's third policy, and
|
|
6610
|
-
// it is the one that would hand live-token a graceful answer arrival-count is
|
|
6611
|
-
// structurally denied — which is the asymmetry JP-R8 exists to forbid.
|
|
6612
|
-
describe: (s) => ({ live: s.live, positive: s.positive })
|
|
6613
|
-
};
|
|
6614
|
-
var liveTokenRequiringArrival = {
|
|
6615
|
-
id: "live-token-requiring-arrival",
|
|
6616
|
-
open: () => ({ live: 0, positive: false, arrived: 0 }),
|
|
6617
|
-
step: (s, e) => {
|
|
6618
|
-
if (e.type === "branch.opened") return { ...s, live: s.live + 1, positive: true };
|
|
6619
|
-
if (e.type === "branch.arrived") return { ...s, live: s.live - 1, arrived: s.arrived + 1 };
|
|
6620
|
-
if (e.type === "branch.exited" || e.type === "branch.lost") return { ...s, live: s.live - 1 };
|
|
6621
|
-
return s;
|
|
6622
|
-
},
|
|
6623
|
-
status: (s) => {
|
|
6624
|
-
if (!s.positive || s.live !== 0) return "waiting";
|
|
6625
|
-
return s.arrived > 0 ? "satisfied" : "unsatisfiable";
|
|
6626
|
-
},
|
|
6627
|
-
describe: (s) => ({ live: s.live, positive: s.positive, arrived: s.arrived })
|
|
6628
|
-
};
|
|
6629
|
-
var POLICIES = {
|
|
6630
|
-
"arrival-count": arrivalCount,
|
|
6631
|
-
"live-token": liveToken,
|
|
6632
|
-
"live-token-requiring-arrival": liveTokenRequiringArrival
|
|
6633
|
-
};
|
|
6634
|
-
|
|
6635
6959
|
export {
|
|
6636
6960
|
PREDICATE_OPS,
|
|
6637
6961
|
PREDICATE_ROOTS,
|
|
@@ -6657,10 +6981,16 @@ export {
|
|
|
6657
6981
|
findings,
|
|
6658
6982
|
describeFindings,
|
|
6659
6983
|
readKeyPath,
|
|
6984
|
+
ID_RE_COPY,
|
|
6985
|
+
KINDS_COPY,
|
|
6986
|
+
ROUTABLE_STATES,
|
|
6660
6987
|
ASK_REFUSED_KEYS,
|
|
6661
6988
|
EDGE_KEYS_DECLARED,
|
|
6662
6989
|
TASK_KEYS_DECLARED,
|
|
6663
6990
|
NODE_KEYS_DECLARED,
|
|
6991
|
+
NODE_KEYS_REQUIRED,
|
|
6992
|
+
EDGE_KEYS_REQUIRED,
|
|
6993
|
+
DOC_KEYS_DECLARED,
|
|
6664
6994
|
RETRY_POLICY_KEYS,
|
|
6665
6995
|
retryPolicyProblem,
|
|
6666
6996
|
BUILT_IN_NODE_TYPES,
|
|
@@ -6753,6 +7083,10 @@ export {
|
|
|
6753
7083
|
selectEdge,
|
|
6754
7084
|
TERMINAL_EVENTS,
|
|
6755
7085
|
isTerminalEvent,
|
|
7086
|
+
arrivalCount,
|
|
7087
|
+
liveToken,
|
|
7088
|
+
liveTokenRequiringArrival,
|
|
7089
|
+
POLICIES,
|
|
6756
7090
|
InvalidDefinition,
|
|
6757
7091
|
DuplicateTaskId,
|
|
6758
7092
|
UnresolvedMapArity,
|
|
@@ -6768,15 +7102,12 @@ export {
|
|
|
6768
7102
|
UndispatchedTask,
|
|
6769
7103
|
MissingJoinPolicy,
|
|
6770
7104
|
Driver,
|
|
7105
|
+
inputOf,
|
|
6771
7106
|
Accumulator,
|
|
6772
7107
|
emptyState,
|
|
6773
7108
|
step,
|
|
6774
7109
|
fold,
|
|
6775
7110
|
foldLog,
|
|
6776
7111
|
isTerminal,
|
|
6777
|
-
nodeOf
|
|
6778
|
-
arrivalCount,
|
|
6779
|
-
liveToken,
|
|
6780
|
-
liveTokenRequiringArrival,
|
|
6781
|
-
POLICIES
|
|
7112
|
+
nodeOf
|
|
6782
7113
|
};
|