@orkestrel/workflow 0.0.10 → 0.0.12

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.
@@ -110,6 +110,204 @@ var DEFAULT_PHASE_CONCURRENCY = 1024;
110
110
  */
111
111
  var MAX_TIMER_MS = 2147483647;
112
112
  //#endregion
113
+ //#region src/core/errors.ts
114
+ /**
115
+ * An error raised by the workflow runtime.
116
+ *
117
+ * @remarks
118
+ * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
119
+ * offending node id / status / parameter. Raised for an illegal lifecycle transition
120
+ * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
121
+ * boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), or a host
122
+ * schedule refused before arming because the caller's `signal` is not a native
123
+ * `AbortSignal` (`SCHEDULE`, delivered as a rejected promise).
124
+ */
125
+ var WorkflowError = class extends Error {
126
+ code;
127
+ context;
128
+ constructor(code, message, context) {
129
+ super(message);
130
+ this.name = "WorkflowError";
131
+ this.code = code;
132
+ if (context !== void 0) this.context = context;
133
+ }
134
+ };
135
+ /**
136
+ * Narrow an unknown caught value to a {@link WorkflowError}.
137
+ *
138
+ * @param value - The value to test (typically a `catch` binding)
139
+ * @returns `true` when `value` is a {@link WorkflowError}
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * try {
144
+ * task.complete('done')
145
+ * } catch (error) {
146
+ * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
147
+ * }
148
+ * ```
149
+ */
150
+ function isWorkflowError(value) {
151
+ try {
152
+ return value instanceof WorkflowError;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ //#endregion
158
+ //#region src/core/validators.ts
159
+ /** Test the workflow lifecycle vocabulary. */
160
+ function isLifecycleStatus(value) {
161
+ return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
162
+ }
163
+ /** Test a normalized persisted task failure. */
164
+ function isTaskFailure(value) {
165
+ try {
166
+ return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ /**
172
+ * Validate a safe owned JSON graph as a coherent workflow snapshot.
173
+ *
174
+ * @remarks
175
+ * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
176
+ * graph first so this semantic pass never observes accessors or prototypes.
177
+ */
178
+ function isOwnedWorkflowSnapshot(value) {
179
+ try {
180
+ if (!(0, _orkestrel_contract.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") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.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" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
181
+ const phaseIds = /* @__PURE__ */ new Set();
182
+ const derivations = [];
183
+ let frontier = false;
184
+ let running = false;
185
+ let vacuous = true;
186
+ for (const phase of value.phases) {
187
+ if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return false;
188
+ const forced = phase.override === "skipped" || phase.override === "stopped";
189
+ const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
190
+ if (!forced && frontier && started || phase.status === "running" && running) return false;
191
+ if (phase.status === "running") running = true;
192
+ if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
193
+ phaseIds.add(phase.id);
194
+ const taskIds = /* @__PURE__ */ new Set();
195
+ const statuses = [];
196
+ if (phase.tasks.length > 0) vacuous = false;
197
+ for (const task of phase.tasks) {
198
+ if (!(0, _orkestrel_contract.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") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
199
+ const budget = (task.retries ?? 0) + 1;
200
+ if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
201
+ if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
202
+ if (task.status === "running" || task.status === "completed" || task.status === "failed") {
203
+ if (task.attempts < 1 || task.activity === void 0) return false;
204
+ }
205
+ if (task.status === "pending" && task.activity !== void 0) return false;
206
+ if (task.status === "completed" || task.status === "failed") {
207
+ if (!isTaskResult(task.result, value, phase, task)) return false;
208
+ } else if (task.result !== void 0) return false;
209
+ taskIds.add(task.id);
210
+ statuses.push(task.status);
211
+ }
212
+ const derived = derivePhaseStatus(statuses);
213
+ if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
214
+ derivations.push({
215
+ status: phase.status,
216
+ bail: phase.bail
217
+ });
218
+ }
219
+ const derived = deriveWorkflowStatus(derivations);
220
+ if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
221
+ return value.status === (value.override ?? derived);
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+ /** Total hostile-boundary workflow snapshot guard. */
227
+ function isWorkflowSnapshot(value) {
228
+ const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
229
+ return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
230
+ }
231
+ /**
232
+ * Test whether an unknown value is a valid whole-frame activity report.
233
+ */
234
+ function isTaskActivityInput(value) {
235
+ try {
236
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
237
+ const prototype = Object.getPrototypeOf(value);
238
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
239
+ const note = value.note;
240
+ const progress = value.progress;
241
+ const operations = value.operations;
242
+ const constraints = value.constraints;
243
+ if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
244
+ if (progress !== void 0) {
245
+ if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
246
+ const progressPrototype = Object.getPrototypeOf(progress);
247
+ if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
248
+ const current = progress.current;
249
+ const total = progress.total;
250
+ const unit = progress.unit;
251
+ if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
252
+ }
253
+ if (operations !== void 0) {
254
+ if (!(0, _orkestrel_contract.isArray)(operations)) return false;
255
+ const ids = /* @__PURE__ */ new Set();
256
+ for (const operation of operations) {
257
+ if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
258
+ const operationPrototype = Object.getPrototypeOf(operation);
259
+ if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
260
+ const id = operation.id;
261
+ const name = operation.name;
262
+ const started = operation.started;
263
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
264
+ ids.add(id);
265
+ }
266
+ }
267
+ if (constraints !== void 0) {
268
+ if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
269
+ const ids = /* @__PURE__ */ new Set();
270
+ for (const constraint of constraints) {
271
+ if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
272
+ const constraintPrototype = Object.getPrototypeOf(constraint);
273
+ if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
274
+ const id = constraint.id;
275
+ const name = constraint.name;
276
+ const started = constraint.started;
277
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
278
+ ids.add(id);
279
+ }
280
+ }
281
+ return true;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+ /**
287
+ * Test whether an unknown value is valid persisted task activity.
288
+ */
289
+ function isTaskActivity(value) {
290
+ try {
291
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
292
+ const prototype = Object.getPrototypeOf(value);
293
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
294
+ const note = value.note;
295
+ const progress = value.progress;
296
+ const operations = value.operations;
297
+ const constraints = value.constraints;
298
+ const updated = value.updated;
299
+ if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
300
+ return isTaskActivityInput({
301
+ ...note === void 0 ? {} : { note },
302
+ ...progress === void 0 ? {} : { progress },
303
+ operations,
304
+ constraints
305
+ });
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+ //#endregion
113
311
  //#region src/core/helpers.ts
114
312
  /**
115
313
  * Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
@@ -583,6 +781,53 @@ function recoverWorkflowSnapshot(snapshot) {
583
781
  updated: now
584
782
  };
585
783
  }
784
+ /** Compare two optional description values. */
785
+ function matchesDescription(left, right) {
786
+ return left === right && (left === void 0 || typeof left === "string");
787
+ }
788
+ /** Test a result's lineage against its containing snapshot nodes. */
789
+ function isTaskResult(value, workflow, phase, task) {
790
+ try {
791
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.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 || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.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;
792
+ 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;
793
+ if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.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;
794
+ 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;
795
+ if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
796
+ if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
797
+ return false;
798
+ } catch {
799
+ return false;
800
+ }
801
+ }
802
+ function hasWorkflowHandlers(workflow, functions) {
803
+ if ("destroyed" in workflow) {
804
+ for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
805
+ return true;
806
+ }
807
+ const runs = /* @__PURE__ */ new Set();
808
+ for (const phase of workflow.phases) for (const task of phase.tasks) {
809
+ if (task.run === void 0 || runs.has(task.run)) continue;
810
+ runs.add(task.run);
811
+ if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
812
+ }
813
+ return true;
814
+ }
815
+ /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
816
+ function workflowSnapshotContext(value) {
817
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
818
+ for (const phase of value.phases) {
819
+ if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
820
+ const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
821
+ if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
822
+ for (const task of phase.tasks) {
823
+ if (!(0, _orkestrel_contract.isRecord)(task)) continue;
824
+ if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
825
+ ...phaseContext ?? {},
826
+ ...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
827
+ };
828
+ }
829
+ }
830
+ }
586
831
  /**
587
832
  * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
588
833
  * — the workflow tier of the result tree, built from each phase's `results()`.
@@ -672,6 +917,16 @@ function createDeferred() {
672
917
  * Schedule one cancellable host operation behind an owned settlement signal.
673
918
  *
674
919
  * @remarks
920
+ * A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
921
+ * rejected promise carrying a {@link import('./errors.js').WorkflowError} with the `SCHEDULE` code.
922
+ * Rejecting rather than throwing keeps every caller on one settlement path, so a backend never has
923
+ * to guard the call itself.
924
+ *
925
+ * The guard is necessary but not sufficient, so linking stays contained. A `Proxy` over a native
926
+ * signal passes the guard and can still make linking throw from a trap, and that escape would be
927
+ * synchronous — the one shape every caller here is built not to expect. Containment turns it into
928
+ * the same `SCHEDULE` rejection, so setup has exactly one failure shape however hostile the input.
929
+ *
675
930
  * The completion and failure paths each own an {@link AbortController}; their native composite is
676
931
  * linked to the optional caller signal before `start` can arm host work. Scheduler backends attach
677
932
  * only to that safe composite, so caller mutation of `addEventListener` or `removeEventListener`
@@ -683,16 +938,18 @@ function createDeferred() {
683
938
  *
684
939
  * @param start - Arm host work and return its cancellation closure
685
940
  * @param signal - Optional caller cancellation signal
686
- * @returns A promise settled exactly once by completion, host failure, or caller abort
941
+ * @returns A promise settled exactly once by an invalid-signal refusal, completion, host failure,
942
+ * or caller abort
687
943
  */
688
944
  function scheduleHost(start, signal) {
945
+ if (signal !== void 0 && !(0, _orkestrel_abort.isAbortSignal)(signal)) return Promise.reject(new WorkflowError("SCHEDULE", "scheduleHost signal must be an AbortSignal", { signal: typeof signal }));
689
946
  const completion = new AbortController();
690
947
  const failed = new AbortController();
691
948
  let settled;
692
949
  try {
693
950
  settled = (0, _orkestrel_abort.linkSignal)(AbortSignal.any([completion.signal, failed.signal]), signal);
694
- } catch (error) {
695
- return Promise.reject(error);
951
+ } catch {
952
+ return Promise.reject(new WorkflowError("SCHEDULE", "scheduleHost could not link the caller signal", { signal: typeof signal }));
696
953
  }
697
954
  if (settled.aborted) return Promise.reject(settled.reason);
698
955
  return new Promise((resolve, reject) => {
@@ -815,249 +1072,6 @@ var Scheduler = class {
815
1072
  }
816
1073
  };
817
1074
  //#endregion
818
- //#region src/core/errors.ts
819
- /**
820
- * An error raised by the workflow runtime.
821
- *
822
- * @remarks
823
- * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
824
- * offending node id / status. Raised for an illegal lifecycle transition
825
- * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
826
- * boundary (`RESTORE`), or a refused structural/activity edit (`MUTATION`).
827
- */
828
- var WorkflowError = class extends Error {
829
- code;
830
- context;
831
- constructor(code, message, context) {
832
- super(message);
833
- this.name = "WorkflowError";
834
- this.code = code;
835
- if (context !== void 0) this.context = context;
836
- }
837
- };
838
- /**
839
- * Narrow an unknown caught value to a {@link WorkflowError}.
840
- *
841
- * @param value - The value to test (typically a `catch` binding)
842
- * @returns `true` when `value` is a {@link WorkflowError}
843
- *
844
- * @example
845
- * ```ts
846
- * try {
847
- * task.complete('done')
848
- * } catch (error) {
849
- * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
850
- * }
851
- * ```
852
- */
853
- function isWorkflowError(value) {
854
- try {
855
- return value instanceof WorkflowError;
856
- } catch {
857
- return false;
858
- }
859
- }
860
- //#endregion
861
- //#region src/core/validators.ts
862
- /** Test the workflow lifecycle vocabulary. */
863
- function isLifecycleStatus(value) {
864
- return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
865
- }
866
- /** Test a normalized persisted task failure. */
867
- function isTaskFailure(value) {
868
- try {
869
- return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
870
- } catch {
871
- return false;
872
- }
873
- }
874
- /** Compare two optional description values. */
875
- function matchesDescription(left, right) {
876
- return left === right && (left === void 0 || typeof left === "string");
877
- }
878
- /** Test a result's lineage against its containing snapshot nodes. */
879
- function isTaskResult(value, workflow, phase, task) {
880
- try {
881
- if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.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 || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.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;
882
- 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;
883
- if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.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;
884
- 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;
885
- if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
886
- if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
887
- return false;
888
- } catch {
889
- return false;
890
- }
891
- }
892
- /**
893
- * Validate a safe owned JSON graph as a coherent workflow snapshot.
894
- *
895
- * @remarks
896
- * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
897
- * graph first so this semantic pass never observes accessors or prototypes.
898
- */
899
- function isOwnedWorkflowSnapshot(value) {
900
- try {
901
- if (!(0, _orkestrel_contract.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") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.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" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
902
- const phaseIds = /* @__PURE__ */ new Set();
903
- const derivations = [];
904
- let frontier = false;
905
- let running = false;
906
- let vacuous = true;
907
- for (const phase of value.phases) {
908
- if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return false;
909
- const forced = phase.override === "skipped" || phase.override === "stopped";
910
- const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
911
- if (!forced && frontier && started || phase.status === "running" && running) return false;
912
- if (phase.status === "running") running = true;
913
- if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
914
- phaseIds.add(phase.id);
915
- const taskIds = /* @__PURE__ */ new Set();
916
- const statuses = [];
917
- if (phase.tasks.length > 0) vacuous = false;
918
- for (const task of phase.tasks) {
919
- if (!(0, _orkestrel_contract.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") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
920
- const budget = (task.retries ?? 0) + 1;
921
- if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
922
- if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
923
- if (task.status === "running" || task.status === "completed" || task.status === "failed") {
924
- if (task.attempts < 1 || task.activity === void 0) return false;
925
- }
926
- if (task.status === "pending" && task.activity !== void 0) return false;
927
- if (task.status === "completed" || task.status === "failed") {
928
- if (!isTaskResult(task.result, value, phase, task)) return false;
929
- } else if (task.result !== void 0) return false;
930
- taskIds.add(task.id);
931
- statuses.push(task.status);
932
- }
933
- const derived = derivePhaseStatus(statuses);
934
- if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
935
- derivations.push({
936
- status: phase.status,
937
- bail: phase.bail
938
- });
939
- }
940
- const derived = deriveWorkflowStatus(derivations);
941
- if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
942
- return value.status === (value.override ?? derived);
943
- } catch {
944
- return false;
945
- }
946
- }
947
- /** Total hostile-boundary workflow snapshot guard. */
948
- function isWorkflowSnapshot(value) {
949
- const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
950
- return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
951
- }
952
- function hasWorkflowHandlers(workflow, functions) {
953
- if ("destroyed" in workflow) {
954
- for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
955
- return true;
956
- }
957
- const runs = /* @__PURE__ */ new Set();
958
- for (const phase of workflow.phases) for (const task of phase.tasks) {
959
- if (task.run === void 0 || runs.has(task.run)) continue;
960
- runs.add(task.run);
961
- if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
962
- }
963
- return true;
964
- }
965
- /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
966
- function workflowSnapshotContext(value) {
967
- if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
968
- for (const phase of value.phases) {
969
- if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
970
- const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
971
- if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
972
- for (const task of phase.tasks) {
973
- if (!(0, _orkestrel_contract.isRecord)(task)) continue;
974
- if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
975
- ...phaseContext ?? {},
976
- ...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
977
- };
978
- }
979
- }
980
- }
981
- /**
982
- * Test whether an unknown value is a valid whole-frame activity report.
983
- */
984
- function isTaskActivityInput(value) {
985
- try {
986
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
987
- const prototype = Object.getPrototypeOf(value);
988
- if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
989
- const note = value.note;
990
- const progress = value.progress;
991
- const operations = value.operations;
992
- const constraints = value.constraints;
993
- if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
994
- if (progress !== void 0) {
995
- if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
996
- const progressPrototype = Object.getPrototypeOf(progress);
997
- if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
998
- const current = progress.current;
999
- const total = progress.total;
1000
- const unit = progress.unit;
1001
- if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
1002
- }
1003
- if (operations !== void 0) {
1004
- if (!(0, _orkestrel_contract.isArray)(operations)) return false;
1005
- const ids = /* @__PURE__ */ new Set();
1006
- for (const operation of operations) {
1007
- if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
1008
- const operationPrototype = Object.getPrototypeOf(operation);
1009
- if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
1010
- const id = operation.id;
1011
- const name = operation.name;
1012
- const started = operation.started;
1013
- if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
1014
- ids.add(id);
1015
- }
1016
- }
1017
- if (constraints !== void 0) {
1018
- if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
1019
- const ids = /* @__PURE__ */ new Set();
1020
- for (const constraint of constraints) {
1021
- if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
1022
- const constraintPrototype = Object.getPrototypeOf(constraint);
1023
- if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
1024
- const id = constraint.id;
1025
- const name = constraint.name;
1026
- const started = constraint.started;
1027
- if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
1028
- ids.add(id);
1029
- }
1030
- }
1031
- return true;
1032
- } catch {
1033
- return false;
1034
- }
1035
- }
1036
- /**
1037
- * Test whether an unknown value is valid persisted task activity.
1038
- */
1039
- function isTaskActivity(value) {
1040
- try {
1041
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
1042
- const prototype = Object.getPrototypeOf(value);
1043
- if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
1044
- const note = value.note;
1045
- const progress = value.progress;
1046
- const operations = value.operations;
1047
- const constraints = value.constraints;
1048
- const updated = value.updated;
1049
- if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
1050
- return isTaskActivityInput({
1051
- ...note === void 0 ? {} : { note },
1052
- ...progress === void 0 ? {} : { progress },
1053
- operations,
1054
- constraints
1055
- });
1056
- } catch {
1057
- return false;
1058
- }
1059
- }
1060
- //#endregion
1061
1075
  //#region src/core/cloners.ts
1062
1076
  /**
1063
1077
  * Validate and own a workflow snapshot before live construction.
@@ -1314,18 +1328,18 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
1314
1328
  * idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
1315
1329
  * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
1316
1330
  * §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
1317
- * snapshot back and rebuild the live tree with {@link import('../factories.js').restoreWorkflow}.
1331
+ * snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1318
1332
  *
1319
1333
  * @example
1320
1334
  * ```ts
1321
1335
  * import { createMemoryDriver } from '@orkestrel/database'
1322
- * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
1336
+ * import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
1323
1337
  *
1324
1338
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
1325
1339
  * const workflow = createWorkflow(definition)
1326
1340
  * await store.set(workflow.snapshot()) // persist the run state (one JSON column)
1327
1341
  * const snapshot = await store.get(definition.id)
1328
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
1342
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
1329
1343
  * await store.delete(definition.id) // drop it
1330
1344
  * ```
1331
1345
  */
@@ -1385,17 +1399,17 @@ var DatabaseWorkflowStore = class {
1385
1399
  *
1386
1400
  * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
1387
1401
  * bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
1388
- * back and rebuild the live tree with {@link import('../factories.js').restoreWorkflow}.
1402
+ * back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
1389
1403
  *
1390
1404
  * @example
1391
1405
  * ```ts
1392
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
1406
+ * import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
1393
1407
  *
1394
1408
  * const store = createMemoryWorkflowStore()
1395
1409
  * const workflow = createWorkflow(definition)
1396
1410
  * await store.set(workflow.snapshot()) // persist the run state
1397
1411
  * const snapshot = await store.get(definition.id)
1398
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
1412
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
1399
1413
  * await store.delete(definition.id) // drop it
1400
1414
  * ```
1401
1415
  */
@@ -2237,7 +2251,7 @@ var PhaseManager = class {
2237
2251
  * @remarks
2238
2252
  * - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
2239
2253
  * {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
2240
- * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').restoreWorkflow}
2254
+ * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').createRestoredWorkflow}
2241
2255
  * passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
2242
2256
  * - **Derived status.** `status` is `#override` when forced, else
2243
2257
  * {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
@@ -2252,7 +2266,7 @@ var PhaseManager = class {
2252
2266
  * workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
2253
2267
  * navigate UP.
2254
2268
  * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
2255
- * JSON); {@link import('./factories.js').restoreWorkflow} rebuilds an equivalent live tree.
2269
+ * JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
2256
2270
  * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
2257
2271
  * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
2258
2272
  * corresponding status or runtime-gate change; the emitter isolates a listener throw and
@@ -2656,7 +2670,7 @@ var WorkflowManager = class {
2656
2670
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2657
2671
  let workflow;
2658
2672
  try {
2659
- workflow = restoreWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2673
+ workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2660
2674
  } catch (error) {
2661
2675
  if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2662
2676
  throw error;
@@ -3965,12 +3979,12 @@ function createWorkflow(definition, options) {
3965
3979
  return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
3966
3980
  }
3967
3981
  /**
3968
- * Rebuild an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
3982
+ * Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
3969
3983
  * inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
3970
3984
  * + recorded results + positional order + the persisted `#override`.
3971
3985
  *
3972
3986
  * @remarks
3973
- * Round-trip fidelity is paramount: a `snapshot()` → `restoreWorkflow()` reproduces the
3987
+ * Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
3974
3988
  * same status at every node (each `#override` restored DIRECTLY from the snapshot's own
3975
3989
  * `override` field, not guessed from a status divergence), the same recorded
3976
3990
  * {@link import('./types.js').TaskResult}s, and the same positional order (an interior
@@ -3990,18 +4004,18 @@ function createWorkflow(definition, options) {
3990
4004
  *
3991
4005
  * @example
3992
4006
  * ```ts
3993
- * import { restoreWorkflow } from '@orkestrel/workflow'
4007
+ * import { createRestoredWorkflow } from '@orkestrel/workflow'
3994
4008
  *
3995
- * const restored = restoreWorkflow(workflow.snapshot()) // bail comes from the snapshot
4009
+ * const restored = createRestoredWorkflow(workflow.snapshot()) // bail comes from the snapshot
3996
4010
  * restored.status === workflow.status // true
3997
4011
  * ```
3998
4012
  */
3999
- function restoreWorkflow(snapshot, options) {
4013
+ function createRestoredWorkflow(snapshot, options) {
4000
4014
  const captured = captureWorkflowOptions(options);
4001
4015
  return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
4002
4016
  }
4003
4017
  /**
4004
- * Rebuild an interrupted workflow at its remaining retry budget.
4018
+ * Build an interrupted workflow back to life at its remaining retry budget.
4005
4019
  *
4006
4020
  * @remarks
4007
4021
  * Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
@@ -4010,9 +4024,17 @@ function restoreWorkflow(snapshot, options) {
4010
4024
  *
4011
4025
  * @param snapshot - The hostile persisted snapshot
4012
4026
  * @param options - Runtime handlers and entity options
4013
- * @returns A recoverable live workflow
4027
+ * @returns A recoverable live {@link WorkflowInterface} root
4028
+ *
4029
+ * @example
4030
+ * ```ts
4031
+ * import { createRecoveredWorkflow } from '@orkestrel/workflow'
4032
+ *
4033
+ * const recovered = createRecoveredWorkflow(snapshot, { functions })
4034
+ * recovered.status // 'pending' — interrupted running work returned to its remaining budget
4035
+ * ```
4014
4036
  */
4015
- function recoverWorkflow(snapshot, options) {
4037
+ function createRecoveredWorkflow(snapshot, options) {
4016
4038
  const captured = captureWorkflowOptions(options);
4017
4039
  const owned = cloneWorkflowSnapshot(snapshot);
4018
4040
  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 });
@@ -4021,29 +4043,6 @@ function recoverWorkflow(snapshot, options) {
4021
4043
  return workflow;
4022
4044
  }
4023
4045
  /**
4024
- * Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
4025
- * on every phase — and that its every node's status (and its `override`, when present) is drawn
4026
- * from the lifecycle vocabulary, throwing a `RESTORE` {@link WorkflowError} otherwise.
4027
- *
4028
- * @remarks
4029
- * The boundary-narrowing guard (AGENTS §14) for {@link restoreWorkflow}: a snapshot is
4030
- * untrusted JSON, so a status (or an override) outside
4031
- * {@link import('./constants.js').WORKFLOW_STATUSES} /
4032
- * {@link import('./constants.js').PHASE_STATUSES} / {@link import('./constants.js').TASK_STATUSES},
4033
- * a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
4034
- * present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
4035
- * `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
4036
- * is rejected loudly (naming the offending node) rather than silently producing a broken tree.
4037
- * The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
4038
- * checked WHEN present. Structural shape beyond these fields is the contract's concern; this
4039
- * guards exactly the fields the live state machine reads back.
4040
- *
4041
- * @param snapshot - The snapshot to validate
4042
- */
4043
- function assertSnapshot(snapshot) {
4044
- cloneWorkflowSnapshot(snapshot);
4045
- }
4046
- /**
4047
4046
  * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
4048
4047
  * {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
4049
4048
  * backend behind the W-d persistence seam.
@@ -4056,19 +4055,19 @@ function assertSnapshot(snapshot) {
4056
4055
  * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
4057
4056
  * table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
4058
4057
  * driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
4059
- * concern: read a snapshot back and rebuild the live tree with {@link restoreWorkflow}.
4058
+ * concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
4060
4059
  *
4061
4060
  * @returns A memory-backed {@link WorkflowStoreInterface}
4062
4061
  *
4063
4062
  * @example
4064
4063
  * ```ts
4065
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
4064
+ * import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
4066
4065
  *
4067
4066
  * const store = createMemoryWorkflowStore()
4068
4067
  * const workflow = createWorkflow(definition)
4069
4068
  * await store.set(workflow.snapshot()) // persist the run state
4070
4069
  * const snapshot = await store.get(definition.id)
4071
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
4070
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
4072
4071
  * ```
4073
4072
  */
4074
4073
  function createMemoryWorkflowStore() {
@@ -4099,13 +4098,13 @@ function createMemoryWorkflowStore() {
4099
4098
  * @example
4100
4099
  * ```ts
4101
4100
  * import { createMemoryDriver } from '@orkestrel/database'
4102
- * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
4101
+ * import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
4103
4102
  *
4104
4103
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
4105
4104
  * const workflow = createWorkflow(definition)
4106
4105
  * await store.set(workflow.snapshot()) // persist the run state (one JSON column)
4107
4106
  * const snapshot = await store.get(definition.id)
4108
- * const restored = snapshot && restoreWorkflow(snapshot) // an identical live tree
4107
+ * const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
4109
4108
  * ```
4110
4109
  */
4111
4110
  function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
@@ -4174,11 +4173,11 @@ function createWorkflowRunner(options) {
4174
4173
  * @remarks
4175
4174
  * `options.functions` flows into every workflow the manager mints (`add`, via
4176
4175
  * {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
4177
- * {@link restoreWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
4176
+ * {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
4178
4177
  * mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
4179
4178
  * manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
4180
4179
  * is PURELY ADDITIVE: direct {@link WorkflowStoreInterface} use and
4181
- * {@link restoreWorkflow} remain valid — the manager is one more caller-driven persistence
4180
+ * {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
4182
4181
  * seam, not a replacement.
4183
4182
  *
4184
4183
  * @param options - The optional `store` seam and the `functions` registry threaded into every mint/hydrate
@@ -4317,7 +4316,6 @@ exports.WorkflowError = WorkflowError;
4317
4316
  exports.WorkflowManager = WorkflowManager;
4318
4317
  exports.WorkflowPersistence = WorkflowPersistence;
4319
4318
  exports.WorkflowRunner = WorkflowRunner;
4320
- exports.assertSnapshot = assertSnapshot;
4321
4319
  exports.buildPhaseContext = buildPhaseContext;
4322
4320
  exports.buildTaskContext = buildTaskContext;
4323
4321
  exports.buildWorkflowContext = buildWorkflowContext;
@@ -4329,6 +4327,8 @@ exports.collectResults = collectResults;
4329
4327
  exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
4330
4328
  exports.createDeferred = createDeferred;
4331
4329
  exports.createMemoryWorkflowStore = createMemoryWorkflowStore;
4330
+ exports.createRecoveredWorkflow = createRecoveredWorkflow;
4331
+ exports.createRestoredWorkflow = createRestoredWorkflow;
4332
4332
  exports.createRunner = createRunner;
4333
4333
  exports.createScheduler = createScheduler;
4334
4334
  exports.createWorkflow = createWorkflow;
@@ -4359,10 +4359,8 @@ exports.parkSignal = parkSignal;
4359
4359
  exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
4360
4360
  exports.phaseShape = phaseShape;
4361
4361
  exports.phaseUpdateShape = phaseUpdateShape;
4362
- exports.recoverWorkflow = recoverWorkflow;
4363
4362
  exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
4364
4363
  exports.resolveTaskSilence = resolveTaskSilence;
4365
- exports.restoreWorkflow = restoreWorkflow;
4366
4364
  exports.scheduleHost = scheduleHost;
4367
4365
  exports.success = success;
4368
4366
  exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;