@orkestrel/workflow 0.0.11 → 0.0.13

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.
@@ -154,6 +154,159 @@ function isWorkflowError(value) {
154
154
  }
155
155
  }
156
156
  //#endregion
157
+ //#region src/core/validators.ts
158
+ /** Test the workflow lifecycle vocabulary. */
159
+ function isLifecycleStatus(value) {
160
+ return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
161
+ }
162
+ /** Test a normalized persisted task failure. */
163
+ function isTaskFailure(value) {
164
+ try {
165
+ return isRecord(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && isNonEmptyString(value.message);
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+ /**
171
+ * Validate a safe owned JSON graph as a coherent workflow snapshot.
172
+ *
173
+ * @remarks
174
+ * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
175
+ * graph first so this semantic pass never observes accessors or prototypes.
176
+ */
177
+ function isOwnedWorkflowSnapshot(value) {
178
+ try {
179
+ if (!isRecord(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !isNonEmptyString(value.id) || !isNonEmptyString(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !isBoolean(value.bail) || !isArray(value.phases) || !isFiniteNumber(value.created) || value.created < 0 || !isFiniteNumber(value.updated) || value.updated < value.created) return false;
180
+ const phaseIds = /* @__PURE__ */ new Set();
181
+ const derivations = [];
182
+ let frontier = false;
183
+ let running = false;
184
+ let vacuous = true;
185
+ for (const phase of value.phases) {
186
+ if (!isRecord(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !isNonEmptyString(phase.id) || phaseIds.has(phase.id) || !isNonEmptyString(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return false;
187
+ const forced = phase.override === "skipped" || phase.override === "stopped";
188
+ const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
189
+ if (!forced && frontier && started || phase.status === "running" && running) return false;
190
+ if (phase.status === "running") running = true;
191
+ if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
192
+ phaseIds.add(phase.id);
193
+ const taskIds = /* @__PURE__ */ new Set();
194
+ const statuses = [];
195
+ if (phase.tasks.length > 0) vacuous = false;
196
+ for (const task of phase.tasks) {
197
+ if (!isRecord(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !isNonEmptyString(task.id) || taskIds.has(task.id) || !isNonEmptyString(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !isRecord(task.metadata) || !isJSONValue(task.metadata) || !isInteger(task.attempts) || task.attempts < 0 || task.run !== void 0 && !isNonEmptyString(task.run) || task.retries !== void 0 && (!isInteger(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!isInteger(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
198
+ const budget = (task.retries ?? 0) + 1;
199
+ if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
200
+ if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
201
+ if (task.status === "running" || task.status === "completed" || task.status === "failed") {
202
+ if (task.attempts < 1 || task.activity === void 0) return false;
203
+ }
204
+ if (task.status === "pending" && task.activity !== void 0) return false;
205
+ if (task.status === "completed" || task.status === "failed") {
206
+ if (!isTaskResult(task.result, value, phase, task)) return false;
207
+ } else if (task.result !== void 0) return false;
208
+ taskIds.add(task.id);
209
+ statuses.push(task.status);
210
+ }
211
+ const derived = derivePhaseStatus(statuses);
212
+ if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
213
+ derivations.push({
214
+ status: phase.status,
215
+ bail: phase.bail
216
+ });
217
+ }
218
+ const derived = deriveWorkflowStatus(derivations);
219
+ if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
220
+ return value.status === (value.override ?? derived);
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ /** Total hostile-boundary workflow snapshot guard. */
226
+ function isWorkflowSnapshot(value) {
227
+ const cloned = attempt(() => cloneJSONValue(value));
228
+ return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
229
+ }
230
+ /**
231
+ * Test whether an unknown value is a valid whole-frame activity report.
232
+ */
233
+ function isTaskActivityInput(value) {
234
+ try {
235
+ if (!isRecord(value)) return false;
236
+ const prototype = Object.getPrototypeOf(value);
237
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
238
+ const note = value.note;
239
+ const progress = value.progress;
240
+ const operations = value.operations;
241
+ const constraints = value.constraints;
242
+ if (note !== void 0 && !isNonEmptyString(note)) return false;
243
+ if (progress !== void 0) {
244
+ if (!isRecord(progress)) return false;
245
+ const progressPrototype = Object.getPrototypeOf(progress);
246
+ if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
247
+ const current = progress.current;
248
+ const total = progress.total;
249
+ const unit = progress.unit;
250
+ if (!isFiniteNumber(current) || current < 0 || total !== void 0 && (!isFiniteNumber(total) || total < current) || unit !== void 0 && !isNonEmptyString(unit)) return false;
251
+ }
252
+ if (operations !== void 0) {
253
+ if (!isArray(operations)) return false;
254
+ const ids = /* @__PURE__ */ new Set();
255
+ for (const operation of operations) {
256
+ if (!isRecord(operation)) return false;
257
+ const operationPrototype = Object.getPrototypeOf(operation);
258
+ if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
259
+ const id = operation.id;
260
+ const name = operation.name;
261
+ const started = operation.started;
262
+ if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
263
+ ids.add(id);
264
+ }
265
+ }
266
+ if (constraints !== void 0) {
267
+ if (!isArray(constraints)) return false;
268
+ const ids = /* @__PURE__ */ new Set();
269
+ for (const constraint of constraints) {
270
+ if (!isRecord(constraint)) return false;
271
+ const constraintPrototype = Object.getPrototypeOf(constraint);
272
+ if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
273
+ const id = constraint.id;
274
+ const name = constraint.name;
275
+ const started = constraint.started;
276
+ if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
277
+ ids.add(id);
278
+ }
279
+ }
280
+ return true;
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
285
+ /**
286
+ * Test whether an unknown value is valid persisted task activity.
287
+ */
288
+ function isTaskActivity(value) {
289
+ try {
290
+ if (!isRecord(value)) return false;
291
+ const prototype = Object.getPrototypeOf(value);
292
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
293
+ const note = value.note;
294
+ const progress = value.progress;
295
+ const operations = value.operations;
296
+ const constraints = value.constraints;
297
+ const updated = value.updated;
298
+ if (operations === void 0 || constraints === void 0 || !isFiniteNumber(updated) || updated < 0) return false;
299
+ return isTaskActivityInput({
300
+ ...note === void 0 ? {} : { note },
301
+ ...progress === void 0 ? {} : { progress },
302
+ operations,
303
+ constraints
304
+ });
305
+ } catch {
306
+ return false;
307
+ }
308
+ }
309
+ //#endregion
157
310
  //#region src/core/helpers.ts
158
311
  /**
159
312
  * Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
@@ -627,6 +780,53 @@ function recoverWorkflowSnapshot(snapshot) {
627
780
  updated: now
628
781
  };
629
782
  }
783
+ /** Compare two optional description values. */
784
+ function matchesDescription(left, right) {
785
+ return left === right && (left === void 0 || typeof left === "string");
786
+ }
787
+ /** Test a result's lineage against its containing snapshot nodes. */
788
+ function isTaskResult(value, workflow, phase, task) {
789
+ try {
790
+ if (!isRecord(value) || !isRecord(workflow) || !isRecord(phase) || !isRecord(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !isFiniteNumber(value.timestamp) || value.timestamp < 0 || !isRecord(value.task) || !isRecord(value.phase) || !isRecord(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
791
+ if (value.task.id !== task.id || value.task.name !== task.name || !matchesDescription(value.task.description, task.description) || value.phase.id !== phase.id || value.phase.name !== phase.name || !matchesDescription(value.phase.description, phase.description) || value.workflow.id !== workflow.id || value.workflow.name !== workflow.name || !matchesDescription(value.workflow.description, workflow.description)) return false;
792
+ if (!isRecord(value.task.phase) || !isRecord(value.task.phase.workflow) || !isRecord(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
793
+ if (value.task.phase.id !== phase.id || value.task.phase.name !== phase.name || !matchesDescription(value.task.phase.description, phase.description) || value.phase.workflow.id !== workflow.id || value.phase.workflow.name !== workflow.name || !matchesDescription(value.phase.workflow.description, workflow.description) || value.task.phase.workflow.id !== workflow.id || value.task.phase.workflow.name !== workflow.name || !matchesDescription(value.task.phase.workflow.description, workflow.description)) return false;
794
+ if (value.status === "completed") return isRecord(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && isJSONValue(value.result.value);
795
+ if (value.status === "failed") return isRecord(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
796
+ return false;
797
+ } catch {
798
+ return false;
799
+ }
800
+ }
801
+ function hasWorkflowHandlers(workflow, functions) {
802
+ if ("destroyed" in workflow) {
803
+ for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !isFunction(task.handler)) return false;
804
+ return true;
805
+ }
806
+ const runs = /* @__PURE__ */ new Set();
807
+ for (const phase of workflow.phases) for (const task of phase.tasks) {
808
+ if (task.run === void 0 || runs.has(task.run)) continue;
809
+ runs.add(task.run);
810
+ if (!isFunction(functions?.[task.run])) return false;
811
+ }
812
+ return true;
813
+ }
814
+ /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
815
+ function workflowSnapshotContext(value) {
816
+ if (!isRecord(value) || !isArray(value.phases)) return void 0;
817
+ for (const phase of value.phases) {
818
+ if (!isRecord(phase)) continue;
819
+ const phaseContext = isNonEmptyString(phase.id) ? { phase: phase.id } : void 0;
820
+ if (!isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return phaseContext;
821
+ for (const task of phase.tasks) {
822
+ if (!isRecord(task)) continue;
823
+ if (task.run !== void 0 && !isNonEmptyString(task.run) || task.retries !== void 0 && (!isInteger(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!isInteger(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !isInteger(task.attempts) || task.attempts < 0) return {
824
+ ...phaseContext ?? {},
825
+ ...isNonEmptyString(task.id) ? { task: task.id } : {}
826
+ };
827
+ }
828
+ }
829
+ }
630
830
  /**
631
831
  * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
632
832
  * — the workflow tier of the result tree, built from each phase's `results()`.
@@ -871,206 +1071,6 @@ var Scheduler = class {
871
1071
  }
872
1072
  };
873
1073
  //#endregion
874
- //#region src/core/validators.ts
875
- /** Test the workflow lifecycle vocabulary. */
876
- function isLifecycleStatus(value) {
877
- return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
878
- }
879
- /** Test a normalized persisted task failure. */
880
- function isTaskFailure(value) {
881
- try {
882
- return isRecord(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && isNonEmptyString(value.message);
883
- } catch {
884
- return false;
885
- }
886
- }
887
- /** Compare two optional description values. */
888
- function matchesDescription(left, right) {
889
- return left === right && (left === void 0 || typeof left === "string");
890
- }
891
- /** Test a result's lineage against its containing snapshot nodes. */
892
- function isTaskResult(value, workflow, phase, task) {
893
- try {
894
- if (!isRecord(value) || !isRecord(workflow) || !isRecord(phase) || !isRecord(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !isFiniteNumber(value.timestamp) || value.timestamp < 0 || !isRecord(value.task) || !isRecord(value.phase) || !isRecord(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
895
- if (value.task.id !== task.id || value.task.name !== task.name || !matchesDescription(value.task.description, task.description) || value.phase.id !== phase.id || value.phase.name !== phase.name || !matchesDescription(value.phase.description, phase.description) || value.workflow.id !== workflow.id || value.workflow.name !== workflow.name || !matchesDescription(value.workflow.description, workflow.description)) return false;
896
- if (!isRecord(value.task.phase) || !isRecord(value.task.phase.workflow) || !isRecord(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
897
- if (value.task.phase.id !== phase.id || value.task.phase.name !== phase.name || !matchesDescription(value.task.phase.description, phase.description) || value.phase.workflow.id !== workflow.id || value.phase.workflow.name !== workflow.name || !matchesDescription(value.phase.workflow.description, workflow.description) || value.task.phase.workflow.id !== workflow.id || value.task.phase.workflow.name !== workflow.name || !matchesDescription(value.task.phase.workflow.description, workflow.description)) return false;
898
- if (value.status === "completed") return isRecord(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && isJSONValue(value.result.value);
899
- if (value.status === "failed") return isRecord(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
900
- return false;
901
- } catch {
902
- return false;
903
- }
904
- }
905
- /**
906
- * Validate a safe owned JSON graph as a coherent workflow snapshot.
907
- *
908
- * @remarks
909
- * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
910
- * graph first so this semantic pass never observes accessors or prototypes.
911
- */
912
- function isOwnedWorkflowSnapshot(value) {
913
- try {
914
- if (!isRecord(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !isNonEmptyString(value.id) || !isNonEmptyString(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !isBoolean(value.bail) || !isArray(value.phases) || !isFiniteNumber(value.created) || value.created < 0 || !isFiniteNumber(value.updated) || value.updated < value.created) return false;
915
- const phaseIds = /* @__PURE__ */ new Set();
916
- const derivations = [];
917
- let frontier = false;
918
- let running = false;
919
- let vacuous = true;
920
- for (const phase of value.phases) {
921
- if (!isRecord(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !isNonEmptyString(phase.id) || phaseIds.has(phase.id) || !isNonEmptyString(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return false;
922
- const forced = phase.override === "skipped" || phase.override === "stopped";
923
- const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
924
- if (!forced && frontier && started || phase.status === "running" && running) return false;
925
- if (phase.status === "running") running = true;
926
- if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
927
- phaseIds.add(phase.id);
928
- const taskIds = /* @__PURE__ */ new Set();
929
- const statuses = [];
930
- if (phase.tasks.length > 0) vacuous = false;
931
- for (const task of phase.tasks) {
932
- if (!isRecord(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !isNonEmptyString(task.id) || taskIds.has(task.id) || !isNonEmptyString(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !isRecord(task.metadata) || !isJSONValue(task.metadata) || !isInteger(task.attempts) || task.attempts < 0 || task.run !== void 0 && !isNonEmptyString(task.run) || task.retries !== void 0 && (!isInteger(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!isInteger(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
933
- const budget = (task.retries ?? 0) + 1;
934
- if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
935
- if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
936
- if (task.status === "running" || task.status === "completed" || task.status === "failed") {
937
- if (task.attempts < 1 || task.activity === void 0) return false;
938
- }
939
- if (task.status === "pending" && task.activity !== void 0) return false;
940
- if (task.status === "completed" || task.status === "failed") {
941
- if (!isTaskResult(task.result, value, phase, task)) return false;
942
- } else if (task.result !== void 0) return false;
943
- taskIds.add(task.id);
944
- statuses.push(task.status);
945
- }
946
- const derived = derivePhaseStatus(statuses);
947
- if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
948
- derivations.push({
949
- status: phase.status,
950
- bail: phase.bail
951
- });
952
- }
953
- const derived = deriveWorkflowStatus(derivations);
954
- if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
955
- return value.status === (value.override ?? derived);
956
- } catch {
957
- return false;
958
- }
959
- }
960
- /** Total hostile-boundary workflow snapshot guard. */
961
- function isWorkflowSnapshot(value) {
962
- const cloned = attempt(() => cloneJSONValue(value));
963
- return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
964
- }
965
- function hasWorkflowHandlers(workflow, functions) {
966
- if ("destroyed" in workflow) {
967
- for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !isFunction(task.handler)) return false;
968
- return true;
969
- }
970
- const runs = /* @__PURE__ */ new Set();
971
- for (const phase of workflow.phases) for (const task of phase.tasks) {
972
- if (task.run === void 0 || runs.has(task.run)) continue;
973
- runs.add(task.run);
974
- if (!isFunction(functions?.[task.run])) return false;
975
- }
976
- return true;
977
- }
978
- /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
979
- function workflowSnapshotContext(value) {
980
- if (!isRecord(value) || !isArray(value.phases)) return void 0;
981
- for (const phase of value.phases) {
982
- if (!isRecord(phase)) continue;
983
- const phaseContext = isNonEmptyString(phase.id) ? { phase: phase.id } : void 0;
984
- if (!isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return phaseContext;
985
- for (const task of phase.tasks) {
986
- if (!isRecord(task)) continue;
987
- if (task.run !== void 0 && !isNonEmptyString(task.run) || task.retries !== void 0 && (!isInteger(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!isInteger(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !isInteger(task.attempts) || task.attempts < 0) return {
988
- ...phaseContext ?? {},
989
- ...isNonEmptyString(task.id) ? { task: task.id } : {}
990
- };
991
- }
992
- }
993
- }
994
- /**
995
- * Test whether an unknown value is a valid whole-frame activity report.
996
- */
997
- function isTaskActivityInput(value) {
998
- try {
999
- if (!isRecord(value)) return false;
1000
- const prototype = Object.getPrototypeOf(value);
1001
- if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
1002
- const note = value.note;
1003
- const progress = value.progress;
1004
- const operations = value.operations;
1005
- const constraints = value.constraints;
1006
- if (note !== void 0 && !isNonEmptyString(note)) return false;
1007
- if (progress !== void 0) {
1008
- if (!isRecord(progress)) return false;
1009
- const progressPrototype = Object.getPrototypeOf(progress);
1010
- if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
1011
- const current = progress.current;
1012
- const total = progress.total;
1013
- const unit = progress.unit;
1014
- if (!isFiniteNumber(current) || current < 0 || total !== void 0 && (!isFiniteNumber(total) || total < current) || unit !== void 0 && !isNonEmptyString(unit)) return false;
1015
- }
1016
- if (operations !== void 0) {
1017
- if (!isArray(operations)) return false;
1018
- const ids = /* @__PURE__ */ new Set();
1019
- for (const operation of operations) {
1020
- if (!isRecord(operation)) return false;
1021
- const operationPrototype = Object.getPrototypeOf(operation);
1022
- if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
1023
- const id = operation.id;
1024
- const name = operation.name;
1025
- const started = operation.started;
1026
- if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
1027
- ids.add(id);
1028
- }
1029
- }
1030
- if (constraints !== void 0) {
1031
- if (!isArray(constraints)) return false;
1032
- const ids = /* @__PURE__ */ new Set();
1033
- for (const constraint of constraints) {
1034
- if (!isRecord(constraint)) return false;
1035
- const constraintPrototype = Object.getPrototypeOf(constraint);
1036
- if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
1037
- const id = constraint.id;
1038
- const name = constraint.name;
1039
- const started = constraint.started;
1040
- if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
1041
- ids.add(id);
1042
- }
1043
- }
1044
- return true;
1045
- } catch {
1046
- return false;
1047
- }
1048
- }
1049
- /**
1050
- * Test whether an unknown value is valid persisted task activity.
1051
- */
1052
- function isTaskActivity(value) {
1053
- try {
1054
- if (!isRecord(value)) return false;
1055
- const prototype = Object.getPrototypeOf(value);
1056
- if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
1057
- const note = value.note;
1058
- const progress = value.progress;
1059
- const operations = value.operations;
1060
- const constraints = value.constraints;
1061
- const updated = value.updated;
1062
- if (operations === void 0 || constraints === void 0 || !isFiniteNumber(updated) || updated < 0) return false;
1063
- return isTaskActivityInput({
1064
- ...note === void 0 ? {} : { note },
1065
- ...progress === void 0 ? {} : { progress },
1066
- operations,
1067
- constraints
1068
- });
1069
- } catch {
1070
- return false;
1071
- }
1072
- }
1073
- //#endregion
1074
1074
  //#region src/core/cloners.ts
1075
1075
  /**
1076
1076
  * Validate and own a workflow snapshot before live construction.
@@ -1327,18 +1327,18 @@ var phaseUpdateShape = objectShape({
1327
1327
  * idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
1328
1328
  * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
1329
1329
  * §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
1330
- * snapshot back and rebuild the live tree with {@link import('../factories.js').restoreWorkflow}.
1330
+ * snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1331
1331
  *
1332
1332
  * @example
1333
1333
  * ```ts
1334
1334
  * import { createMemoryDriver } from '@orkestrel/database'
1335
- * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
1335
+ * import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
1336
1336
  *
1337
1337
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
1338
1338
  * const workflow = createWorkflow(definition)
1339
1339
  * await store.set(workflow.snapshot()) // persist the run state (one JSON column)
1340
1340
  * const snapshot = await store.get(definition.id)
1341
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
1341
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
1342
1342
  * await store.delete(definition.id) // drop it
1343
1343
  * ```
1344
1344
  */
@@ -1398,17 +1398,17 @@ var DatabaseWorkflowStore = class {
1398
1398
  *
1399
1399
  * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
1400
1400
  * bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
1401
- * back and rebuild the live tree with {@link import('../factories.js').restoreWorkflow}.
1401
+ * back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1402
1402
  *
1403
1403
  * @example
1404
1404
  * ```ts
1405
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
1405
+ * import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
1406
1406
  *
1407
1407
  * const store = createMemoryWorkflowStore()
1408
1408
  * const workflow = createWorkflow(definition)
1409
1409
  * await store.set(workflow.snapshot()) // persist the run state
1410
1410
  * const snapshot = await store.get(definition.id)
1411
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
1411
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
1412
1412
  * await store.delete(definition.id) // drop it
1413
1413
  * ```
1414
1414
  */
@@ -2250,7 +2250,7 @@ var PhaseManager = class {
2250
2250
  * @remarks
2251
2251
  * - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
2252
2252
  * {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
2253
- * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').restoreWorkflow}
2253
+ * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').createRestoredWorkflow}
2254
2254
  * passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
2255
2255
  * - **Derived status.** `status` is `#override` when forced, else
2256
2256
  * {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
@@ -2265,7 +2265,7 @@ var PhaseManager = class {
2265
2265
  * workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
2266
2266
  * navigate UP.
2267
2267
  * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
2268
- * JSON); {@link import('./factories.js').restoreWorkflow} rebuilds an equivalent live tree.
2268
+ * JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
2269
2269
  * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
2270
2270
  * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
2271
2271
  * corresponding status or runtime-gate change; the emitter isolates a listener throw and
@@ -2669,7 +2669,7 @@ var WorkflowManager = class {
2669
2669
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2670
2670
  let workflow;
2671
2671
  try {
2672
- workflow = restoreWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2672
+ workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2673
2673
  } catch (error) {
2674
2674
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2675
2675
  throw error;
@@ -3978,12 +3978,12 @@ function createWorkflow(definition, options) {
3978
3978
  return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
3979
3979
  }
3980
3980
  /**
3981
- * Rebuild an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
3981
+ * Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
3982
3982
  * inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
3983
3983
  * + recorded results + positional order + the persisted `#override`.
3984
3984
  *
3985
3985
  * @remarks
3986
- * Round-trip fidelity is paramount: a `snapshot()` → `restoreWorkflow()` reproduces the
3986
+ * Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
3987
3987
  * same status at every node (each `#override` restored DIRECTLY from the snapshot's own
3988
3988
  * `override` field, not guessed from a status divergence), the same recorded
3989
3989
  * {@link import('./types.js').TaskResult}s, and the same positional order (an interior
@@ -4003,18 +4003,18 @@ function createWorkflow(definition, options) {
4003
4003
  *
4004
4004
  * @example
4005
4005
  * ```ts
4006
- * import { restoreWorkflow } from '@orkestrel/workflow'
4006
+ * import { createRestoredWorkflow } from '@orkestrel/workflow'
4007
4007
  *
4008
- * const restored = restoreWorkflow(workflow.snapshot()) // bail comes from the snapshot
4008
+ * const restored = createRestoredWorkflow(workflow.snapshot()) // bail comes from the snapshot
4009
4009
  * restored.status === workflow.status // true
4010
4010
  * ```
4011
4011
  */
4012
- function restoreWorkflow(snapshot, options) {
4012
+ function createRestoredWorkflow(snapshot, options) {
4013
4013
  const captured = captureWorkflowOptions(options);
4014
4014
  return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
4015
4015
  }
4016
4016
  /**
4017
- * Rebuild an interrupted workflow at its remaining retry budget.
4017
+ * Build an interrupted workflow back to life at its remaining retry budget.
4018
4018
  *
4019
4019
  * @remarks
4020
4020
  * Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
@@ -4023,9 +4023,17 @@ function restoreWorkflow(snapshot, options) {
4023
4023
  *
4024
4024
  * @param snapshot - The hostile persisted snapshot
4025
4025
  * @param options - Runtime handlers and entity options
4026
- * @returns A recoverable live workflow
4026
+ * @returns A recoverable live {@link WorkflowInterface} root
4027
+ *
4028
+ * @example
4029
+ * ```ts
4030
+ * import { createRecoveredWorkflow } from '@orkestrel/workflow'
4031
+ *
4032
+ * const recovered = createRecoveredWorkflow(snapshot, { functions })
4033
+ * recovered.status // 'pending' — interrupted running work returned to its remaining budget
4034
+ * ```
4027
4035
  */
4028
- function recoverWorkflow(snapshot, options) {
4036
+ function createRecoveredWorkflow(snapshot, options) {
4029
4037
  const captured = captureWorkflowOptions(options);
4030
4038
  const owned = cloneWorkflowSnapshot(snapshot);
4031
4039
  if (owned.override !== void 0 || owned.phases.some((phase) => phase.override !== void 0)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has a terminal override`, { workflow: owned.id });
@@ -4034,29 +4042,6 @@ function recoverWorkflow(snapshot, options) {
4034
4042
  return workflow;
4035
4043
  }
4036
4044
  /**
4037
- * Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
4038
- * on every phase — and that its every node's status (and its `override`, when present) is drawn
4039
- * from the lifecycle vocabulary, throwing a `RESTORE` {@link WorkflowError} otherwise.
4040
- *
4041
- * @remarks
4042
- * The boundary-narrowing guard (AGENTS §14) for {@link restoreWorkflow}: a snapshot is
4043
- * untrusted JSON, so a status (or an override) outside
4044
- * {@link import('./constants.js').WORKFLOW_STATUSES} /
4045
- * {@link import('./constants.js').PHASE_STATUSES} / {@link import('./constants.js').TASK_STATUSES},
4046
- * a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
4047
- * present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
4048
- * `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
4049
- * is rejected loudly (naming the offending node) rather than silently producing a broken tree.
4050
- * The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
4051
- * checked WHEN present. Structural shape beyond these fields is the contract's concern; this
4052
- * guards exactly the fields the live state machine reads back.
4053
- *
4054
- * @param snapshot - The snapshot to validate
4055
- */
4056
- function assertSnapshot(snapshot) {
4057
- cloneWorkflowSnapshot(snapshot);
4058
- }
4059
- /**
4060
4045
  * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
4061
4046
  * {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
4062
4047
  * backend behind the W-d persistence seam.
@@ -4069,19 +4054,19 @@ function assertSnapshot(snapshot) {
4069
4054
  * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
4070
4055
  * table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
4071
4056
  * driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
4072
- * concern: read a snapshot back and rebuild the live tree with {@link restoreWorkflow}.
4057
+ * concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
4073
4058
  *
4074
4059
  * @returns A memory-backed {@link WorkflowStoreInterface}
4075
4060
  *
4076
4061
  * @example
4077
4062
  * ```ts
4078
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
4063
+ * import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
4079
4064
  *
4080
4065
  * const store = createMemoryWorkflowStore()
4081
4066
  * const workflow = createWorkflow(definition)
4082
4067
  * await store.set(workflow.snapshot()) // persist the run state
4083
4068
  * const snapshot = await store.get(definition.id)
4084
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
4069
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
4085
4070
  * ```
4086
4071
  */
4087
4072
  function createMemoryWorkflowStore() {
@@ -4112,13 +4097,13 @@ function createMemoryWorkflowStore() {
4112
4097
  * @example
4113
4098
  * ```ts
4114
4099
  * import { createMemoryDriver } from '@orkestrel/database'
4115
- * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
4100
+ * import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
4116
4101
  *
4117
4102
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
4118
4103
  * const workflow = createWorkflow(definition)
4119
4104
  * await store.set(workflow.snapshot()) // persist the run state (one JSON column)
4120
4105
  * const snapshot = await store.get(definition.id)
4121
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
4106
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
4122
4107
  * ```
4123
4108
  */
4124
4109
  function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
@@ -4187,11 +4172,11 @@ function createWorkflowRunner(options) {
4187
4172
  * @remarks
4188
4173
  * `options.functions` flows into every workflow the manager mints (`add`, via
4189
4174
  * {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
4190
- * {@link restoreWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
4175
+ * {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
4191
4176
  * mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
4192
4177
  * manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
4193
4178
  * is PURELY ADDITIVE: direct {@link WorkflowStoreInterface} use and
4194
- * {@link restoreWorkflow} remain valid — the manager is one more caller-driven persistence
4179
+ * {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
4195
4180
  * seam, not a replacement.
4196
4181
  *
4197
4182
  * @param options - The optional `store` seam and the `functions` registry threaded into every mint/hydrate
@@ -4307,6 +4292,6 @@ function createRunner(options) {
4307
4292
  return new Runner(options);
4308
4293
  }
4309
4294
  //#endregion
4310
- export { Controller, DEFAULT_BAIL, DEFAULT_PHASE_CONCURRENCY, DatabaseWorkflowStore, MAX_TIMER_MS, MemoryWorkflowStore, PHASE_STATUSES, Phase, PhaseManager, Runner, Scheduler, TASK_STATUSES, TASK_TRANSITIONS, TERMINAL_TASK_STATUSES, Task, TaskController, TaskManager, WORKFLOW_STATUSES, Workflow, WorkflowError, WorkflowManager, WorkflowPersistence, WorkflowRunner, assertSnapshot, buildPhaseContext, buildTaskContext, buildWorkflowContext, canTransitionTask, captureWorkflowOptions, cloneTaskActivity, cloneWorkflowSnapshot, collectResults, createDatabaseWorkflowStore, createDeferred, createMemoryWorkflowStore, createRunner, createScheduler, createWorkflow, createWorkflowContract, createWorkflowManager, createWorkflowRunner, definitionToSnapshot, deriveBoundary, derivePhaseStatus, deriveWorkflowStatus, errorToMessage, failure, findFailure, hasWorkflowHandlers, insertEntry, isLifecycleStatus, isOwnedWorkflowSnapshot, isTaskActivity, isTaskActivityInput, isTaskFailure, isTaskResult, isTerminalStatus, isWorkflowError, isWorkflowSnapshot, matchesDescription, moveEntry, parkSignal, phaseDefinitionToSnapshot, phaseShape, phaseUpdateShape, recoverWorkflow, recoverWorkflowSnapshot, resolveTaskSilence, restoreWorkflow, scheduleHost, success, taskDefinitionToSnapshot, taskShape, taskUpdateShape, workflowShape, workflowSnapshotContext };
4295
+ export { Controller, DEFAULT_BAIL, DEFAULT_PHASE_CONCURRENCY, DatabaseWorkflowStore, MAX_TIMER_MS, MemoryWorkflowStore, PHASE_STATUSES, Phase, PhaseManager, Runner, Scheduler, TASK_STATUSES, TASK_TRANSITIONS, TERMINAL_TASK_STATUSES, Task, TaskController, TaskManager, WORKFLOW_STATUSES, Workflow, WorkflowError, WorkflowManager, WorkflowPersistence, WorkflowRunner, buildPhaseContext, buildTaskContext, buildWorkflowContext, canTransitionTask, captureWorkflowOptions, cloneTaskActivity, cloneWorkflowSnapshot, collectResults, createDatabaseWorkflowStore, createDeferred, createMemoryWorkflowStore, createRecoveredWorkflow, createRestoredWorkflow, createRunner, createScheduler, createWorkflow, createWorkflowContract, createWorkflowManager, createWorkflowRunner, definitionToSnapshot, deriveBoundary, derivePhaseStatus, deriveWorkflowStatus, errorToMessage, failure, findFailure, hasWorkflowHandlers, insertEntry, isLifecycleStatus, isOwnedWorkflowSnapshot, isTaskActivity, isTaskActivityInput, isTaskFailure, isTaskResult, isTerminalStatus, isWorkflowError, isWorkflowSnapshot, matchesDescription, moveEntry, parkSignal, phaseDefinitionToSnapshot, phaseShape, phaseUpdateShape, recoverWorkflowSnapshot, resolveTaskSilence, scheduleHost, success, taskDefinitionToSnapshot, taskShape, taskUpdateShape, workflowShape, workflowSnapshotContext };
4311
4296
 
4312
4297
  //# sourceMappingURL=index.js.map