@cyoda/workflow-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,6 +30,14 @@ var OPERATOR_TYPES = /* @__PURE__ */ new Set([
30
30
  "IS_CHANGED"
31
31
  ]);
32
32
 
33
+ // src/types/transaction.ts
34
+ var PatchConflictError = class extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "PatchConflictError";
38
+ }
39
+ };
40
+
33
41
  // src/types/api.ts
34
42
  var WorkflowApiConflictError = class extends Error {
35
43
  constructor(entity, serverConcurrencyToken, message = "Workflow save conflict: server state has changed.") {
@@ -144,11 +152,17 @@ var FunctionCriterionSchema = z3.lazy(
144
152
 
145
153
  // src/schema/processor.ts
146
154
  import { z as z4 } from "zod";
147
- var ExecutionModeSchema = z4.enum(["SYNC", "ASYNC_SAME_TX", "ASYNC_NEW_TX"]);
155
+ var ExecutionModeSchema = z4.enum([
156
+ "SYNC",
157
+ "ASYNC_SAME_TX",
158
+ "ASYNC_NEW_TX",
159
+ "COMMIT_BEFORE_DISPATCH"
160
+ ]);
148
161
  var ExternalizedProcessorSchema = z4.object({
149
162
  type: z4.literal("externalized"),
150
163
  name: NameSchema,
151
164
  executionMode: ExecutionModeSchema.optional(),
165
+ startNewTxOnDispatch: z4.boolean().optional(),
152
166
  config: FunctionConfigSchema.and(
153
167
  z4.object({
154
168
  asyncResult: z4.boolean().optional(),
@@ -176,7 +190,7 @@ var TransitionSchema = z5.object({
176
190
  name: NameSchema,
177
191
  next: NameSchema,
178
192
  manual: z5.boolean(),
179
- disabled: z5.boolean(),
193
+ disabled: z5.boolean().default(false),
180
194
  criterion: CriterionSchema.optional(),
181
195
  processors: z5.array(ProcessorSchema).optional()
182
196
  });
@@ -389,6 +403,118 @@ function mintCriterionIds(c, host, path, ids) {
389
403
  }
390
404
  }
391
405
 
406
+ // src/criteria/operators.ts
407
+ var SUPPORTED_SIMPLE_OPERATORS = /* @__PURE__ */ new Set([
408
+ "EQUALS",
409
+ "NOT_EQUAL",
410
+ "GREATER_THAN",
411
+ "LESS_THAN",
412
+ "GREATER_OR_EQUAL",
413
+ "LESS_OR_EQUAL",
414
+ "CONTAINS",
415
+ "NOT_CONTAINS",
416
+ "STARTS_WITH",
417
+ "NOT_STARTS_WITH",
418
+ "ENDS_WITH",
419
+ "NOT_ENDS_WITH",
420
+ "LIKE",
421
+ "IS_NULL",
422
+ "NOT_NULL",
423
+ "BETWEEN",
424
+ "BETWEEN_INCLUSIVE",
425
+ "MATCHES_PATTERN",
426
+ "IEQUALS",
427
+ "INOT_EQUAL",
428
+ "ICONTAINS",
429
+ "INOT_CONTAINS",
430
+ "ISTARTS_WITH",
431
+ "INOT_STARTS_WITH",
432
+ "IENDS_WITH",
433
+ "INOT_ENDS_WITH"
434
+ ]);
435
+ var UNSUPPORTED_OPERATORS = /* @__PURE__ */ new Set([
436
+ "IS_UNCHANGED",
437
+ "IS_CHANGED"
438
+ ]);
439
+ var SUPPORTED_GROUP_OPERATORS = ["AND", "OR"];
440
+ var MAX_CRITERION_DEPTH = 50;
441
+ var CRITERION_DEPTH_WARNING_THRESHOLD = 5;
442
+ var OPERATOR_GROUPS = [
443
+ {
444
+ id: "equality",
445
+ label: "Equality",
446
+ operators: ["EQUALS", "NOT_EQUAL", "IEQUALS", "INOT_EQUAL"]
447
+ },
448
+ {
449
+ id: "ordering",
450
+ label: "Ordering",
451
+ operators: ["GREATER_THAN", "LESS_THAN", "GREATER_OR_EQUAL", "LESS_OR_EQUAL"]
452
+ },
453
+ {
454
+ id: "range",
455
+ label: "Range",
456
+ operators: ["BETWEEN", "BETWEEN_INCLUSIVE"]
457
+ },
458
+ {
459
+ id: "substring",
460
+ label: "Substring",
461
+ operators: [
462
+ "CONTAINS",
463
+ "NOT_CONTAINS",
464
+ "ICONTAINS",
465
+ "INOT_CONTAINS",
466
+ "STARTS_WITH",
467
+ "NOT_STARTS_WITH",
468
+ "ISTARTS_WITH",
469
+ "INOT_STARTS_WITH",
470
+ "ENDS_WITH",
471
+ "NOT_ENDS_WITH",
472
+ "IENDS_WITH",
473
+ "INOT_ENDS_WITH"
474
+ ]
475
+ },
476
+ {
477
+ id: "pattern",
478
+ label: "Pattern",
479
+ operators: ["LIKE", "MATCHES_PATTERN"]
480
+ },
481
+ {
482
+ id: "null",
483
+ label: "Null",
484
+ operators: ["IS_NULL", "NOT_NULL"]
485
+ }
486
+ ];
487
+ var OPERATOR_VALUE_SHAPE = {
488
+ EQUALS: "scalar",
489
+ NOT_EQUAL: "scalar",
490
+ GREATER_THAN: "scalar",
491
+ LESS_THAN: "scalar",
492
+ GREATER_OR_EQUAL: "scalar",
493
+ LESS_OR_EQUAL: "scalar",
494
+ CONTAINS: "scalar",
495
+ NOT_CONTAINS: "scalar",
496
+ STARTS_WITH: "scalar",
497
+ NOT_STARTS_WITH: "scalar",
498
+ ENDS_WITH: "scalar",
499
+ NOT_ENDS_WITH: "scalar",
500
+ LIKE: "scalar",
501
+ MATCHES_PATTERN: "scalar",
502
+ IEQUALS: "scalar",
503
+ INOT_EQUAL: "scalar",
504
+ ICONTAINS: "scalar",
505
+ INOT_CONTAINS: "scalar",
506
+ ISTARTS_WITH: "scalar",
507
+ INOT_STARTS_WITH: "scalar",
508
+ IENDS_WITH: "scalar",
509
+ INOT_ENDS_WITH: "scalar",
510
+ BETWEEN: "range",
511
+ BETWEEN_INCLUSIVE: "range",
512
+ IS_NULL: "none",
513
+ NOT_NULL: "none",
514
+ IS_UNCHANGED: "none",
515
+ IS_CHANGED: "none"
516
+ };
517
+
392
518
  // src/normalize/input.ts
393
519
  function normalizeWorkflowInput(workflow) {
394
520
  const out = {
@@ -431,12 +557,21 @@ function normalizeWorkflowInput(workflow) {
431
557
  }
432
558
  return out;
433
559
  }
434
- function normalizeCriterion(criterion) {
560
+ function normalizeCriterion(criterion, depth = 0) {
561
+ if (depth >= MAX_CRITERION_DEPTH) {
562
+ return criterion;
563
+ }
435
564
  switch (criterion.type) {
436
565
  case "simple":
566
+ if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
567
+ return { ...criterion, value: null };
568
+ }
437
569
  return criterion;
438
570
  case "group":
439
- return { ...criterion, conditions: criterion.conditions.map(normalizeCriterion) };
571
+ return {
572
+ ...criterion,
573
+ conditions: criterion.conditions.map((c) => normalizeCriterion(c, depth + 1))
574
+ };
440
575
  case "function": {
441
576
  const fn = criterion.function;
442
577
  const out = {
@@ -444,12 +579,15 @@ function normalizeCriterion(criterion) {
444
579
  function: {
445
580
  name: fn.name.trim(),
446
581
  ...fn.config !== void 0 ? { config: fn.config } : {},
447
- ...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion) } : {}
582
+ ...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion, depth + 1) } : {}
448
583
  }
449
584
  };
450
585
  return out;
451
586
  }
452
587
  case "lifecycle":
588
+ if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
589
+ return { ...criterion, value: null };
590
+ }
453
591
  return criterion;
454
592
  case "array":
455
593
  return criterion;
@@ -466,6 +604,79 @@ function normalizeProcessor(p) {
466
604
  };
467
605
  }
468
606
 
607
+ // src/criteria/jsonPathSubset.ts
608
+ var SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
609
+ var INDEX_RE = /^(?:\d+|\*)$/;
610
+ function validateJsonPathSubset(path) {
611
+ if (path.length === 0) return { ok: false, reason: "empty" };
612
+ if (path[0] !== "$") return { ok: false, reason: "missing-root" };
613
+ if (path === "$") return { ok: true };
614
+ let i = 1;
615
+ while (i < path.length) {
616
+ const ch = path[i];
617
+ if (ch === ".") {
618
+ if (path[i + 1] === ".") return { ok: false, reason: "recursive-descent" };
619
+ i += 1;
620
+ const start = i;
621
+ while (i < path.length && path[i] !== "." && path[i] !== "[") i += 1;
622
+ const segment = path.slice(start, i);
623
+ if (!SEGMENT_RE.test(segment)) return { ok: false, reason: "malformed" };
624
+ continue;
625
+ }
626
+ if (ch === "[") {
627
+ if (path[i + 1] === "?") return { ok: false, reason: "filter-expression" };
628
+ if (path[i + 1] === "'" || path[i + 1] === '"') return { ok: false, reason: "malformed" };
629
+ const end = path.indexOf("]", i);
630
+ if (end === -1) return { ok: false, reason: "malformed" };
631
+ const inner = path.slice(i + 1, end);
632
+ if (!INDEX_RE.test(inner)) return { ok: false, reason: "malformed" };
633
+ i = end + 1;
634
+ continue;
635
+ }
636
+ return { ok: false, reason: "malformed" };
637
+ }
638
+ return { ok: true };
639
+ }
640
+
641
+ // src/identity/id-for.ts
642
+ function idFor(meta, ref) {
643
+ switch (ref.kind) {
644
+ case "workflow":
645
+ return meta.ids.workflows[ref.workflow] ?? null;
646
+ case "state": {
647
+ for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
648
+ if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
649
+ }
650
+ return null;
651
+ }
652
+ case "transition": {
653
+ const matches = [];
654
+ for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
655
+ if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
656
+ matches.push(uuid);
657
+ }
658
+ }
659
+ return matches[ref.ordinal] ?? null;
660
+ }
661
+ case "processor": {
662
+ const matches = [];
663
+ for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
664
+ if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
665
+ }
666
+ return matches[ref.ordinal] ?? null;
667
+ }
668
+ case "criterion": {
669
+ const target = JSON.stringify(ref.path);
670
+ for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
671
+ if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
672
+ return uuid;
673
+ }
674
+ }
675
+ return null;
676
+ }
677
+ }
678
+ }
679
+
469
680
  // src/validate/helpers.ts
470
681
  function isValidName(name) {
471
682
  return NAME_REGEX.test(name);
@@ -491,12 +702,15 @@ function* walkCriteria(session) {
491
702
  }
492
703
  }
493
704
  }
494
- function* walkInner(c, where) {
705
+ function* walkInner(c, where, depth = 0) {
495
706
  yield { criterion: c, where };
707
+ if (depth >= MAX_CRITERION_DEPTH) {
708
+ return;
709
+ }
496
710
  if (c.type === "group") {
497
- for (const child of c.conditions) yield* walkInner(child, where);
711
+ for (const child of c.conditions) yield* walkInner(child, where, depth + 1);
498
712
  } else if (c.type === "function" && c.function.criterion) {
499
- yield* walkInner(c.function.criterion, where);
713
+ yield* walkInner(c.function.criterion, where, depth + 1);
500
714
  }
501
715
  }
502
716
 
@@ -509,6 +723,8 @@ function validateSemantics(session, doc) {
509
723
  issues.push(...validateWorkflow(wf, doc));
510
724
  }
511
725
  issues.push(...criterionRules(session));
726
+ issues.push(...criterionDepthRules(session));
727
+ issues.push(...automatedOrderingRules(session, doc));
512
728
  if (session.workflows.length === 1) {
513
729
  const only = session.workflows[0];
514
730
  if (only && only.criterion !== void 0) {
@@ -546,14 +762,14 @@ function validateWorkflow(wf, doc) {
546
762
  severity: "error",
547
763
  code: "missing-initial-state",
548
764
  message: `Workflow "${wf.name}" has no initialState.`,
549
- ...idFor(doc, wf.name, "workflow")
765
+ ...idFor2(doc, wf.name, "workflow")
550
766
  });
551
767
  } else if (!(wf.initialState in wf.states)) {
552
768
  issues.push({
553
769
  severity: "error",
554
770
  code: "unknown-initial-state",
555
771
  message: `Workflow "${wf.name}" initialState "${wf.initialState}" is not a state.`,
556
- ...idFor(doc, wf.name, "workflow")
772
+ ...idFor2(doc, wf.name, "workflow")
557
773
  });
558
774
  }
559
775
  if (!isValidName(wf.name)) {
@@ -606,6 +822,13 @@ function validateWorkflow(wf, doc) {
606
822
  message: `Scheduled processor "${p.name}" has empty target transition.`
607
823
  });
608
824
  }
825
+ if (p.type === "externalized" && p.startNewTxOnDispatch === true && p.executionMode !== "COMMIT_BEFORE_DISPATCH") {
826
+ issues.push({
827
+ severity: "warning",
828
+ code: "start-new-tx-without-commit-before-dispatch",
829
+ message: `Processor "${p.name}" sets startNewTxOnDispatch but executionMode is not COMMIT_BEFORE_DISPATCH.`
830
+ });
831
+ }
609
832
  if (p.type === "externalized" && p.config) {
610
833
  if (p.config.crossoverToAsyncMs !== void 0 && p.config.asyncResult !== true) {
611
834
  issues.push({
@@ -748,7 +971,16 @@ function criterionRules(session) {
748
971
  });
749
972
  }
750
973
  break;
751
- case "array":
974
+ case "array": {
975
+ const arrPathCheck = validateJsonPathSubset(criterion.jsonPath);
976
+ if (!arrPathCheck.ok) {
977
+ issues.push({
978
+ severity: "error",
979
+ code: "invalid-jsonpath-subset",
980
+ message: `Array criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${arrPathCheck.reason}) (at ${describe(where)}).`,
981
+ detail: { jsonPath: criterion.jsonPath, reason: arrPathCheck.reason }
982
+ });
983
+ }
752
984
  for (const v of criterion.value) {
753
985
  if (typeof v !== "string") {
754
986
  issues.push({
@@ -760,6 +992,7 @@ function criterionRules(session) {
760
992
  }
761
993
  }
762
994
  break;
995
+ }
763
996
  case "lifecycle":
764
997
  if (!LIFECYCLE_FIELDS.has(criterion.field)) {
765
998
  issues.push({
@@ -768,22 +1001,144 @@ function criterionRules(session) {
768
1001
  message: `Lifecycle criterion field "${criterion.field}" is invalid.`
769
1002
  });
770
1003
  }
1004
+ if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
1005
+ issues.push({
1006
+ severity: "warning",
1007
+ code: "unsupported-operator",
1008
+ message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
1009
+ detail: { operation: criterion.operation }
1010
+ });
1011
+ }
771
1012
  break;
772
1013
  case "group":
773
- if (criterion.operator === "NOT" && criterion.conditions.length > 1) {
1014
+ if (criterion.operator === "NOT") {
774
1015
  issues.push({
775
1016
  severity: "warning",
776
- code: "not-with-multiple-conditions",
777
- message: `NOT group has ${criterion.conditions.length} conditions; should have exactly one.`
1017
+ code: "unsupported-group-operator",
1018
+ message: `Group operator "NOT" is not implemented by the engine (at ${describe(where)}).`,
1019
+ detail: { operator: "NOT" }
778
1020
  });
1021
+ if (criterion.conditions.length > 1) {
1022
+ issues.push({
1023
+ severity: "warning",
1024
+ code: "not-with-multiple-conditions",
1025
+ message: `NOT group has ${criterion.conditions.length} conditions; should have exactly one.`
1026
+ });
1027
+ }
779
1028
  }
780
1029
  break;
781
- case "simple":
1030
+ case "simple": {
1031
+ const pathCheck = validateJsonPathSubset(criterion.jsonPath);
1032
+ if (!pathCheck.ok) {
1033
+ issues.push({
1034
+ severity: "error",
1035
+ code: "invalid-jsonpath-subset",
1036
+ message: `Simple criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${pathCheck.reason}) (at ${describe(where)}).`,
1037
+ detail: { jsonPath: criterion.jsonPath, reason: pathCheck.reason }
1038
+ });
1039
+ } else if (criterion.jsonPath.startsWith("$._meta")) {
1040
+ issues.push({
1041
+ severity: "warning",
1042
+ code: "lifecycle-path-in-simple",
1043
+ message: `Simple criterion path "${criterion.jsonPath}" looks like a lifecycle path; use a lifecycle criterion instead (at ${describe(where)}).`,
1044
+ detail: { jsonPath: criterion.jsonPath }
1045
+ });
1046
+ }
1047
+ if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
1048
+ issues.push({
1049
+ severity: "warning",
1050
+ code: "unsupported-operator",
1051
+ message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
1052
+ detail: { operation: criterion.operation }
1053
+ });
1054
+ }
1055
+ if (criterion.operation === "BETWEEN" || criterion.operation === "BETWEEN_INCLUSIVE") {
1056
+ if (!Array.isArray(criterion.value) || criterion.value.length !== 2) {
1057
+ issues.push({
1058
+ severity: "error",
1059
+ code: "simple-between-shape",
1060
+ message: `Operator "${criterion.operation}" requires a two-element [low, high] array value (at ${describe(where)}).`,
1061
+ detail: { operation: criterion.operation }
1062
+ });
1063
+ }
1064
+ }
1065
+ if (criterion.operation === "LIKE" && typeof criterion.value === "string" && /[%_]/.test(criterion.value)) {
1066
+ issues.push({
1067
+ severity: "warning",
1068
+ code: "like-wildcard-warning",
1069
+ message: `LIKE pattern contains "%" or "_" which are always wildcards (no escape mechanism) (at ${describe(where)}).`
1070
+ });
1071
+ }
1072
+ if (criterion.operation === "MATCHES_PATTERN" && typeof criterion.value === "string" && criterion.value.length > 0 && !criterion.value.startsWith("^") && !criterion.value.endsWith("$")) {
1073
+ issues.push({
1074
+ severity: "warning",
1075
+ code: "matches-pattern-unanchored",
1076
+ message: `MATCHES_PATTERN regex is unanchored; include "^"/"$" for whole-string match (at ${describe(where)}).`
1077
+ });
1078
+ }
782
1079
  break;
1080
+ }
783
1081
  }
784
1082
  }
785
1083
  return issues;
786
1084
  }
1085
+ function criterionDepthRules(session) {
1086
+ const issues = [];
1087
+ for (const wf of session.workflows) {
1088
+ if (wf.criterion) {
1089
+ pushDepthIssue(issues, criterionMaxDepth(wf.criterion), {
1090
+ kind: "workflow",
1091
+ workflow: wf.name
1092
+ });
1093
+ }
1094
+ for (const [stateCode, state] of Object.entries(wf.states)) {
1095
+ for (let i = 0; i < state.transitions.length; i++) {
1096
+ const t = state.transitions[i];
1097
+ if (!t.criterion) continue;
1098
+ pushDepthIssue(issues, criterionMaxDepth(t.criterion), {
1099
+ kind: "transition",
1100
+ workflow: wf.name,
1101
+ state: stateCode,
1102
+ transitionIndex: i,
1103
+ transitionName: t.name
1104
+ });
1105
+ }
1106
+ }
1107
+ }
1108
+ return issues;
1109
+ }
1110
+ function pushDepthIssue(issues, maxDepth, where) {
1111
+ if (maxDepth >= MAX_CRITERION_DEPTH) {
1112
+ issues.push({
1113
+ severity: "error",
1114
+ code: "criterion-depth-limit",
1115
+ message: `Criterion tree depth ${maxDepth} exceeds engine limit ${MAX_CRITERION_DEPTH} (at ${describe(where)}).`,
1116
+ detail: { maxDepth, threshold: MAX_CRITERION_DEPTH }
1117
+ });
1118
+ }
1119
+ if (maxDepth >= CRITERION_DEPTH_WARNING_THRESHOLD) {
1120
+ issues.push({
1121
+ severity: "warning",
1122
+ code: "criterion-depth-warning",
1123
+ message: `Criterion tree depth ${maxDepth} is hard to read; consider flattening (at ${describe(where)}).`,
1124
+ detail: { maxDepth, threshold: CRITERION_DEPTH_WARNING_THRESHOLD }
1125
+ });
1126
+ }
1127
+ }
1128
+ function criterionMaxDepth(root) {
1129
+ const stack = [{ node: root, depth: 1 }];
1130
+ let max = 0;
1131
+ while (stack.length > 0) {
1132
+ const { node, depth } = stack.pop();
1133
+ if (depth > max) max = depth;
1134
+ if (node.type === "group") {
1135
+ for (const child of node.conditions) stack.push({ node: child, depth: depth + 1 });
1136
+ } else if (node.type === "function" && node.function.criterion) {
1137
+ stack.push({ node: node.function.criterion, depth: depth + 1 });
1138
+ }
1139
+ }
1140
+ return max;
1141
+ }
787
1142
  function reachableStates(wf) {
788
1143
  const visited = /* @__PURE__ */ new Set();
789
1144
  if (!(wf.initialState in wf.states)) return visited;
@@ -832,11 +1187,65 @@ function describe(w) {
832
1187
  if (w.kind === "workflow") return `workflow "${w.workflow}"`;
833
1188
  return `transition "${w.transitionName}" on "${w.workflow}:${w.state}"`;
834
1189
  }
835
- function idFor(doc, workflowName, _kind) {
1190
+ function idFor2(doc, workflowName, _kind) {
836
1191
  if (!doc) return {};
837
1192
  const id = doc.meta.ids.workflows[workflowName];
838
1193
  return id ? { targetId: id } : {};
839
1194
  }
1195
+ function transitionTargetId(doc, workflow, state, declarationIndex) {
1196
+ if (!doc) return {};
1197
+ const id = idFor(doc.meta, {
1198
+ kind: "transition",
1199
+ workflow,
1200
+ state,
1201
+ transitionName: "",
1202
+ ordinal: declarationIndex
1203
+ });
1204
+ return id ? { targetId: id } : {};
1205
+ }
1206
+ function automatedOrderingRules(session, doc) {
1207
+ const issues = [];
1208
+ for (const wf of session.workflows) {
1209
+ for (const [stateCode, state] of Object.entries(wf.states)) {
1210
+ const automated = [];
1211
+ state.transitions.forEach((t, index) => {
1212
+ if (t.manual !== true && t.disabled !== true) {
1213
+ automated.push({ index, t });
1214
+ }
1215
+ });
1216
+ const nullIdx = automated.findIndex(({ t }) => t.criterion === void 0);
1217
+ if (nullIdx === -1 || nullIdx === automated.length - 1) continue;
1218
+ const nullEntry = automated[nullIdx];
1219
+ issues.push({
1220
+ severity: "warning",
1221
+ code: "null-criterion-not-last",
1222
+ message: `Transition "${nullEntry.t.name}" on state "${stateCode}" is automated and has no criterion, so it always fires; later automated transitions on this state are unreachable.`,
1223
+ ...transitionTargetId(doc, wf.name, stateCode, nullEntry.index),
1224
+ detail: {
1225
+ workflow: wf.name,
1226
+ state: stateCode,
1227
+ transitionName: nullEntry.t.name
1228
+ }
1229
+ });
1230
+ for (let j = nullIdx + 1; j < automated.length; j++) {
1231
+ const dead = automated[j];
1232
+ issues.push({
1233
+ severity: "warning",
1234
+ code: "unreachable-automated-transition",
1235
+ message: `Transition "${dead.t.name}" on state "${stateCode}" is unreachable: an earlier automated transition ("${nullEntry.t.name}") has no criterion and will always fire first.`,
1236
+ ...transitionTargetId(doc, wf.name, stateCode, dead.index),
1237
+ detail: {
1238
+ workflow: wf.name,
1239
+ state: stateCode,
1240
+ transitionName: dead.t.name,
1241
+ blockedBy: nullEntry.t.name
1242
+ }
1243
+ });
1244
+ }
1245
+ }
1246
+ }
1247
+ return issues;
1248
+ }
840
1249
 
841
1250
  // src/validate/schema.ts
842
1251
  function zodErrorToIssues(err) {
@@ -851,6 +1260,8 @@ function zodErrorToIssues(err) {
851
1260
  }
852
1261
 
853
1262
  // src/parse/parse-import.ts
1263
+ var MAX_JSON_BYTES = 5 * 1024 * 1024;
1264
+ var MAX_JSON_OBJECT_DEPTH = 200;
854
1265
  function parseJsonSafe(json) {
855
1266
  try {
856
1267
  return { ok: true, value: JSON.parse(json) };
@@ -858,11 +1269,36 @@ function parseJsonSafe(json) {
858
1269
  return { ok: false, err: e.message };
859
1270
  }
860
1271
  }
1272
+ function exceedsObjectDepth(value, limit) {
1273
+ const stack = [{ val: value, depth: 1 }];
1274
+ while (stack.length > 0) {
1275
+ const { val, depth } = stack.pop();
1276
+ if (depth > limit) return true;
1277
+ if (typeof val !== "object" || val === null) continue;
1278
+ const children = Array.isArray(val) ? val : Object.values(val);
1279
+ for (const child of children) {
1280
+ if (typeof child === "object" && child !== null) {
1281
+ stack.push({ val: child, depth: depth + 1 });
1282
+ }
1283
+ }
1284
+ }
1285
+ return false;
1286
+ }
861
1287
  function parseImportPayload(json, prior) {
1288
+ if (json.length > MAX_JSON_BYTES) {
1289
+ throw new ParseJsonError(
1290
+ `Workflow JSON exceeds the maximum allowed size of ${MAX_JSON_BYTES / (1024 * 1024)} MB.`
1291
+ );
1292
+ }
862
1293
  const parsed = parseJsonSafe(json);
863
1294
  if (!parsed.ok) {
864
1295
  throw new ParseJsonError(`Invalid JSON: ${parsed.err}`);
865
1296
  }
1297
+ if (exceedsObjectDepth(parsed.value, MAX_JSON_OBJECT_DEPTH)) {
1298
+ throw new ParseJsonError(
1299
+ `Workflow JSON nesting depth exceeds the maximum allowed depth of ${MAX_JSON_OBJECT_DEPTH}.`
1300
+ );
1301
+ }
866
1302
  let aliased;
867
1303
  try {
868
1304
  aliased = normalizeOperatorAlias(parsed.value);
@@ -878,7 +1314,7 @@ function parseImportPayload(json, prior) {
878
1314
  ]
879
1315
  };
880
1316
  }
881
- const schemaResult = ImportPayloadSchema.safeParse(aliased);
1317
+ const schemaResult = ImportPayloadSchema.safeParse(coerceCanonicalDefaults(aliased));
882
1318
  if (!schemaResult.success) {
883
1319
  return { ok: false, issues: zodErrorToIssues(schemaResult.error) };
884
1320
  }
@@ -899,6 +1335,53 @@ function parseImportPayload(json, prior) {
899
1335
  issues
900
1336
  };
901
1337
  }
1338
+ function coerceCanonicalDefaults(value) {
1339
+ if (!isObj(value)) return value;
1340
+ const v = value;
1341
+ if (!Array.isArray(v["workflows"])) return value;
1342
+ return {
1343
+ ...v,
1344
+ workflows: v["workflows"].map((wf) => {
1345
+ if (!isObj(wf)) return wf;
1346
+ const w = wf;
1347
+ if (!isObj(w["states"])) return wf;
1348
+ const states = w["states"];
1349
+ const nextStates = {};
1350
+ for (const [code, state] of Object.entries(states)) {
1351
+ if (!isObj(state)) {
1352
+ nextStates[code] = state;
1353
+ continue;
1354
+ }
1355
+ const s = state;
1356
+ if (!Array.isArray(s["transitions"])) {
1357
+ nextStates[code] = state;
1358
+ continue;
1359
+ }
1360
+ nextStates[code] = {
1361
+ ...s,
1362
+ transitions: s["transitions"].map((t) => {
1363
+ if (!isObj(t)) return t;
1364
+ const tx = t;
1365
+ if (!Array.isArray(tx["processors"])) return t;
1366
+ return {
1367
+ ...tx,
1368
+ processors: tx["processors"].map((p) => {
1369
+ if (!isObj(p)) return p;
1370
+ const proc = p;
1371
+ if (typeof proc["type"] === "string") return p;
1372
+ return { type: "externalized", ...proc };
1373
+ })
1374
+ };
1375
+ })
1376
+ };
1377
+ }
1378
+ return { ...w, states: nextStates };
1379
+ })
1380
+ };
1381
+ }
1382
+ function isObj(v) {
1383
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1384
+ }
902
1385
 
903
1386
  // src/parse/parse-export.ts
904
1387
  function parseExportPayload(json, prior) {
@@ -1049,7 +1532,11 @@ function outputCriterion(c) {
1049
1532
  jsonPath: c.jsonPath,
1050
1533
  operation: c.operation
1051
1534
  };
1052
- if (c.value !== void 0) out["value"] = c.value;
1535
+ if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
1536
+ out["value"] = null;
1537
+ } else if (c.value !== void 0) {
1538
+ out["value"] = c.value;
1539
+ }
1053
1540
  return out;
1054
1541
  }
1055
1542
  case "group":
@@ -1075,7 +1562,11 @@ function outputCriterion(c) {
1075
1562
  field: c.field,
1076
1563
  operation: c.operation
1077
1564
  };
1078
- if (c.value !== void 0) out["value"] = c.value;
1565
+ if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
1566
+ out["value"] = null;
1567
+ } else if (c.value !== void 0) {
1568
+ out["value"] = c.value;
1569
+ }
1079
1570
  return out;
1080
1571
  }
1081
1572
  case "array":
@@ -1108,8 +1599,9 @@ function outputExternalizedProcessor(p) {
1108
1599
  type: "externalized",
1109
1600
  name: p.name
1110
1601
  };
1111
- if (p.executionMode !== void 0 && p.executionMode !== "ASYNC_NEW_TX") {
1112
- out["executionMode"] = p.executionMode;
1602
+ out["executionMode"] = p.executionMode ?? "ASYNC_NEW_TX";
1603
+ if ("startNewTxOnDispatch" in p && p.startNewTxOnDispatch !== void 0) {
1604
+ out["startNewTxOnDispatch"] = p.startNewTxOnDispatch;
1113
1605
  }
1114
1606
  if (p.config !== void 0) {
1115
1607
  const cfg = outputExternalizedConfig(p.config);
@@ -1304,45 +1796,6 @@ function walkPath(root, path) {
1304
1796
  return null;
1305
1797
  }
1306
1798
 
1307
- // src/identity/id-for.ts
1308
- function idFor2(meta, ref) {
1309
- switch (ref.kind) {
1310
- case "workflow":
1311
- return meta.ids.workflows[ref.workflow] ?? null;
1312
- case "state": {
1313
- for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
1314
- if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
1315
- }
1316
- return null;
1317
- }
1318
- case "transition": {
1319
- const matches = [];
1320
- for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
1321
- if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
1322
- matches.push(uuid);
1323
- }
1324
- }
1325
- return matches[ref.ordinal] ?? null;
1326
- }
1327
- case "processor": {
1328
- const matches = [];
1329
- for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
1330
- if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
1331
- }
1332
- return matches[ref.ordinal] ?? null;
1333
- }
1334
- case "criterion": {
1335
- const target = JSON.stringify(ref.path);
1336
- for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
1337
- if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
1338
- return uuid;
1339
- }
1340
- }
1341
- return null;
1342
- }
1343
- }
1344
- }
1345
-
1346
1799
  // src/validate/index.ts
1347
1800
  function validateImportSchema(raw) {
1348
1801
  const parsed = ImportPayloadSchema.safeParse(raw);
@@ -1362,9 +1815,13 @@ function validateSession(session) {
1362
1815
  // src/patch/apply.ts
1363
1816
  import { produce } from "immer";
1364
1817
  function applyPatch(doc, patch) {
1365
- if (patch.op === "setEdgeAnchors") {
1366
- return applySetEdgeAnchors(doc, patch);
1367
- }
1818
+ if (patch.op === "setEdgeAnchors") return applySetEdgeAnchors(doc, patch);
1819
+ if (patch.op === "setNodePosition") return applySetNodePosition(doc, patch);
1820
+ if (patch.op === "removeNodePosition") return applyRemoveNodePosition(doc, patch);
1821
+ if (patch.op === "resetLayout") return applyResetLayout(doc, patch);
1822
+ if (patch.op === "addComment") return applyAddComment(doc, patch);
1823
+ if (patch.op === "updateComment") return applyUpdateComment(doc, patch);
1824
+ if (patch.op === "removeComment") return applyRemoveComment(doc, patch);
1368
1825
  const nextSession = produce(doc.session, (d) => {
1369
1826
  const draft = d;
1370
1827
  switch (patch.op) {
@@ -1410,6 +1867,11 @@ function applyPatch(doc, patch) {
1410
1867
  case "renameState": {
1411
1868
  const wf = draft.workflows.find((w) => w.name === patch.workflow);
1412
1869
  if (!wf) return;
1870
+ if (patch.to !== patch.from && patch.to in wf.states) {
1871
+ throw new PatchConflictError(
1872
+ `State "${patch.to}" already exists in workflow "${patch.workflow}"`
1873
+ );
1874
+ }
1413
1875
  renameStateCascading(wf, patch.from, patch.to);
1414
1876
  return;
1415
1877
  }
@@ -1457,6 +1919,23 @@ function applyPatch(doc, patch) {
1457
1919
  state.transitions.splice(patch.toIndex, 0, item);
1458
1920
  return;
1459
1921
  }
1922
+ case "moveTransitionSource": {
1923
+ const wf = draft.workflows.find((w) => w.name === patch.workflow);
1924
+ if (!wf) return;
1925
+ const fromState = wf.states[patch.fromState];
1926
+ const toState = wf.states[patch.toState];
1927
+ if (!fromState || !toState) return;
1928
+ const idx = fromState.transitions.findIndex((t) => t.name === patch.transitionName);
1929
+ if (idx < 0) return;
1930
+ if (patch.fromState !== patch.toState && toState.transitions.some((t) => t.name === patch.transitionName)) {
1931
+ throw new PatchConflictError(
1932
+ `Transition "${patch.transitionName}" already exists in state "${patch.toState}"`
1933
+ );
1934
+ }
1935
+ const [transition] = fromState.transitions.splice(idx, 1);
1936
+ if (transition) toState.transitions.push(transition);
1937
+ return;
1938
+ }
1460
1939
  case "addProcessor": {
1461
1940
  const loc = locateTransition(doc, patch.transitionUuid);
1462
1941
  if (!loc) return;
@@ -1539,9 +2018,11 @@ function applyPatch(doc, patch) {
1539
2018
  }
1540
2019
  });
1541
2020
  const nextMeta = assignSyntheticIds(nextSession, doc.meta);
2021
+ preserveMovedTransitionUuid(doc, patch, nextSession, nextMeta);
2022
+ const cleanedWorkflowUi = cleanupWorkflowUi(nextMeta.workflowUi, nextSession, nextMeta);
1542
2023
  return {
1543
2024
  session: nextSession,
1544
- meta: { ...nextMeta, revision: doc.meta.revision + 1 }
2025
+ meta: { ...nextMeta, workflowUi: cleanedWorkflowUi, revision: doc.meta.revision + 1 }
1545
2026
  };
1546
2027
  }
1547
2028
  function applyPatches(doc, patches) {
@@ -1574,6 +2055,124 @@ function applySetEdgeAnchors(doc, patch) {
1574
2055
  }
1575
2056
  };
1576
2057
  }
2058
+ function applySetNodePosition(doc, patch) {
2059
+ const workflowUi = { ...doc.meta.workflowUi };
2060
+ const current = workflowUi[patch.workflow] ?? {};
2061
+ const nodes = { ...current.layout?.nodes ?? {} };
2062
+ nodes[patch.stateCode] = { x: patch.x, y: patch.y, pinned: patch.pinned ?? true };
2063
+ workflowUi[patch.workflow] = { ...current, layout: { nodes } };
2064
+ return {
2065
+ session: doc.session,
2066
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2067
+ };
2068
+ }
2069
+ function applyRemoveNodePosition(doc, patch) {
2070
+ const workflowUi = { ...doc.meta.workflowUi };
2071
+ const current = workflowUi[patch.workflow] ?? {};
2072
+ const nodes = { ...current.layout?.nodes ?? {} };
2073
+ delete nodes[patch.stateCode];
2074
+ workflowUi[patch.workflow] = {
2075
+ ...current,
2076
+ layout: Object.keys(nodes).length > 0 ? { nodes } : void 0
2077
+ };
2078
+ return {
2079
+ session: doc.session,
2080
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2081
+ };
2082
+ }
2083
+ function applyResetLayout(doc, patch) {
2084
+ const workflowUi = { ...doc.meta.workflowUi };
2085
+ const current = workflowUi[patch.workflow] ?? {};
2086
+ workflowUi[patch.workflow] = { ...current, layout: void 0 };
2087
+ return {
2088
+ session: doc.session,
2089
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2090
+ };
2091
+ }
2092
+ function applyAddComment(doc, patch) {
2093
+ const workflowUi = { ...doc.meta.workflowUi };
2094
+ const current = workflowUi[patch.workflow] ?? {};
2095
+ const comments = { ...current.comments ?? {}, [patch.comment.id]: patch.comment };
2096
+ workflowUi[patch.workflow] = { ...current, comments };
2097
+ return {
2098
+ session: doc.session,
2099
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2100
+ };
2101
+ }
2102
+ function applyUpdateComment(doc, patch) {
2103
+ const workflowUi = { ...doc.meta.workflowUi };
2104
+ const current = workflowUi[patch.workflow] ?? {};
2105
+ const existing = current.comments?.[patch.commentId];
2106
+ if (!existing) return { ...doc, meta: { ...doc.meta, revision: doc.meta.revision + 1 } };
2107
+ const updated = { ...existing, ...patch.updates, id: existing.id };
2108
+ const comments = { ...current.comments ?? {}, [patch.commentId]: updated };
2109
+ workflowUi[patch.workflow] = { ...current, comments };
2110
+ return {
2111
+ session: doc.session,
2112
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2113
+ };
2114
+ }
2115
+ function applyRemoveComment(doc, patch) {
2116
+ const workflowUi = { ...doc.meta.workflowUi };
2117
+ const current = workflowUi[patch.workflow] ?? {};
2118
+ const comments = { ...current.comments ?? {} };
2119
+ delete comments[patch.commentId];
2120
+ workflowUi[patch.workflow] = {
2121
+ ...current,
2122
+ comments: Object.keys(comments).length > 0 ? comments : void 0
2123
+ };
2124
+ return {
2125
+ session: doc.session,
2126
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2127
+ };
2128
+ }
2129
+ function preserveMovedTransitionUuid(priorDoc, patch, nextSession, nextMeta) {
2130
+ if (patch.op !== "moveTransitionSource") return;
2131
+ const oldUuid = transitionUuidByName(
2132
+ priorDoc,
2133
+ patch.workflow,
2134
+ patch.fromState,
2135
+ patch.transitionName
2136
+ );
2137
+ if (!oldUuid) return;
2138
+ const newUuid = transitionUuidByNameInSession(
2139
+ nextSession,
2140
+ nextMeta,
2141
+ patch.workflow,
2142
+ patch.toState,
2143
+ patch.transitionName
2144
+ );
2145
+ if (!newUuid || newUuid === oldUuid) return;
2146
+ const newPtr = nextMeta.ids.transitions[newUuid];
2147
+ if (!newPtr) return;
2148
+ delete nextMeta.ids.transitions[newUuid];
2149
+ nextMeta.ids.transitions[oldUuid] = {
2150
+ ...newPtr,
2151
+ transitionUuid: oldUuid
2152
+ };
2153
+ for (const processorPtr of Object.values(nextMeta.ids.processors)) {
2154
+ if (processorPtr.transitionUuid === newUuid) {
2155
+ processorPtr.transitionUuid = oldUuid;
2156
+ }
2157
+ }
2158
+ for (const criterionPtr of Object.values(nextMeta.ids.criteria)) {
2159
+ const host = criterionPtr.host;
2160
+ if ((host.kind === "transition" || host.kind === "processorConfig") && host.transitionUuid === newUuid) {
2161
+ host.transitionUuid = oldUuid;
2162
+ }
2163
+ }
2164
+ }
2165
+ function transitionUuidByName(doc, workflow, state, transitionName) {
2166
+ return transitionUuidByNameInSession(doc.session, doc.meta, workflow, state, transitionName);
2167
+ }
2168
+ function transitionUuidByNameInSession(session, meta, workflow, state, transitionName) {
2169
+ const wf = session.workflows.find((candidate) => candidate.name === workflow);
2170
+ const transitions = wf?.states[state]?.transitions ?? [];
2171
+ const index = transitions.findIndex((transition) => transition.name === transitionName);
2172
+ if (index < 0) return null;
2173
+ const ordered = Object.entries(meta.ids.transitions).filter(([, ptr]) => ptr.workflow === workflow && ptr.state === state).map(([uuid]) => uuid);
2174
+ return ordered[index] ?? null;
2175
+ }
1577
2176
  function renameStateCascading(wf, from, to) {
1578
2177
  if (!(from in wf.states) || from === to) return;
1579
2178
  wf.states[to] = wf.states[from];
@@ -1622,6 +2221,65 @@ function locateProcessor(doc, processorUuid) {
1622
2221
  processorIndex: pIdx
1623
2222
  };
1624
2223
  }
2224
+ function cleanupWorkflowUi(workflowUi, session, meta) {
2225
+ const wfNames = new Set(session.workflows.map((w) => w.name));
2226
+ const result = {};
2227
+ const validTransitionIdsByWorkflow = /* @__PURE__ */ new Map();
2228
+ if (meta) {
2229
+ for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
2230
+ let set = validTransitionIdsByWorkflow.get(ptr.workflow);
2231
+ if (!set) {
2232
+ set = /* @__PURE__ */ new Set();
2233
+ validTransitionIdsByWorkflow.set(ptr.workflow, set);
2234
+ }
2235
+ set.add(uuid);
2236
+ }
2237
+ }
2238
+ for (const [wfName, ui] of Object.entries(workflowUi)) {
2239
+ if (!wfNames.has(wfName)) continue;
2240
+ const wf = session.workflows.find((w) => w.name === wfName);
2241
+ const existingStates = wf ? new Set(Object.keys(wf.states)) : /* @__PURE__ */ new Set();
2242
+ const allTransitionNames = /* @__PURE__ */ new Set();
2243
+ if (wf) {
2244
+ for (const state of Object.values(wf.states)) {
2245
+ for (const t of state.transitions) allTransitionNames.add(t.name);
2246
+ }
2247
+ }
2248
+ let layout = ui.layout;
2249
+ if (layout?.nodes) {
2250
+ const cleanNodes = Object.fromEntries(
2251
+ Object.entries(layout.nodes).filter(([code]) => existingStates.has(code))
2252
+ );
2253
+ layout = Object.keys(cleanNodes).length > 0 ? { nodes: cleanNodes } : void 0;
2254
+ }
2255
+ let comments = ui.comments;
2256
+ if (comments) {
2257
+ const cleanComments = {};
2258
+ for (const [id, c] of Object.entries(comments)) {
2259
+ if (c.attachedTo?.kind === "state" && !existingStates.has(c.attachedTo.stateCode)) {
2260
+ cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
2261
+ } else if (c.attachedTo?.kind === "transition" && !allTransitionNames.has(c.attachedTo.transitionName)) {
2262
+ cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
2263
+ } else {
2264
+ cleanComments[id] = c;
2265
+ }
2266
+ }
2267
+ comments = Object.keys(cleanComments).length > 0 ? cleanComments : void 0;
2268
+ }
2269
+ let edgeAnchors = ui.edgeAnchors;
2270
+ if (edgeAnchors && meta) {
2271
+ const validTransitionIds = validTransitionIdsByWorkflow.get(wfName) ?? /* @__PURE__ */ new Set();
2272
+ const cleanAnchors = Object.fromEntries(
2273
+ Object.entries(edgeAnchors).filter(
2274
+ ([transitionUuid]) => validTransitionIds.has(transitionUuid)
2275
+ )
2276
+ );
2277
+ edgeAnchors = Object.keys(cleanAnchors).length > 0 ? cleanAnchors : void 0;
2278
+ }
2279
+ result[wfName] = { ...ui, layout, comments, edgeAnchors };
2280
+ }
2281
+ return result;
2282
+ }
1625
2283
  function applyCriterionAtPath(container, path, criterion) {
1626
2284
  if (path.length === 0) return;
1627
2285
  let node = container;
@@ -1658,7 +2316,6 @@ function invertPatch(doc, patch) {
1658
2316
  case "removeWorkflow":
1659
2317
  case "renameWorkflow":
1660
2318
  case "removeState":
1661
- case "renameState":
1662
2319
  case "replaceSession":
1663
2320
  return { op: "replaceSession", session: cloneSession(doc) };
1664
2321
  case "updateWorkflowMeta": {
@@ -1673,30 +2330,19 @@ function invertPatch(doc, patch) {
1673
2330
  case "setInitialState": {
1674
2331
  const wf = findWorkflow(doc, patch.workflow);
1675
2332
  if (!wf) return noop();
1676
- return {
1677
- op: "setInitialState",
1678
- workflow: patch.workflow,
1679
- stateCode: wf.initialState
1680
- };
2333
+ return { op: "setInitialState", workflow: patch.workflow, stateCode: wf.initialState };
1681
2334
  }
1682
2335
  case "setWorkflowCriterion": {
1683
2336
  const wf = findWorkflow(doc, patch.workflow);
1684
2337
  if (!wf) return noop();
1685
- return wf.criterion ? {
1686
- op: "setWorkflowCriterion",
1687
- workflow: patch.workflow,
1688
- criterion: cloneCriterion(wf.criterion)
1689
- } : { op: "setWorkflowCriterion", workflow: patch.workflow };
2338
+ return wf.criterion ? { op: "setWorkflowCriterion", workflow: patch.workflow, criterion: cloneCriterion(wf.criterion) } : { op: "setWorkflowCriterion", workflow: patch.workflow };
1690
2339
  }
1691
2340
  case "addState":
1692
- return {
1693
- op: "removeState",
1694
- workflow: patch.workflow,
1695
- stateCode: patch.stateCode
1696
- };
1697
- case "addTransition": {
2341
+ return { op: "removeState", workflow: patch.workflow, stateCode: patch.stateCode };
2342
+ case "renameState":
2343
+ return { op: "renameState", workflow: patch.workflow, from: patch.to, to: patch.from };
2344
+ case "addTransition":
1698
2345
  return { op: "replaceSession", session: cloneSession(doc) };
1699
- }
1700
2346
  case "updateTransition": {
1701
2347
  const t = findTransition(doc, patch.transitionUuid);
1702
2348
  if (!t) return noop();
@@ -1704,15 +2350,48 @@ function invertPatch(doc, patch) {
1704
2350
  for (const key of Object.keys(patch.updates)) {
1705
2351
  prior[key] = t[key];
1706
2352
  }
2353
+ return { op: "updateTransition", transitionUuid: patch.transitionUuid, updates: prior };
2354
+ }
2355
+ case "removeTransition": {
2356
+ const loc = locateTransition2(doc, patch.transitionUuid);
2357
+ if (!loc) return noop();
2358
+ const wf = findWorkflow(doc, loc.workflow);
2359
+ const state = wf?.states[loc.state];
2360
+ const t = state?.transitions[loc.index];
2361
+ if (!t) return noop();
1707
2362
  return {
1708
- op: "updateTransition",
1709
- transitionUuid: patch.transitionUuid,
1710
- updates: prior
2363
+ op: "addTransition",
2364
+ workflow: loc.workflow,
2365
+ fromState: loc.state,
2366
+ transition: structuredClone(t)
1711
2367
  };
1712
2368
  }
1713
- case "removeTransition":
1714
- case "reorderTransition":
1715
- return { op: "replaceSession", session: cloneSession(doc) };
2369
+ case "reorderTransition": {
2370
+ const loc = locateTransition2(doc, patch.transitionUuid);
2371
+ if (!loc) return noop();
2372
+ const orderedForState = [];
2373
+ for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
2374
+ if (p.workflow === patch.workflow && p.state === patch.fromState) {
2375
+ orderedForState.push(uuid);
2376
+ }
2377
+ }
2378
+ const uuidAtTarget = orderedForState[patch.toIndex] ?? patch.transitionUuid;
2379
+ return {
2380
+ op: "reorderTransition",
2381
+ workflow: patch.workflow,
2382
+ fromState: patch.fromState,
2383
+ transitionUuid: uuidAtTarget,
2384
+ toIndex: loc.index
2385
+ };
2386
+ }
2387
+ case "moveTransitionSource":
2388
+ return {
2389
+ op: "moveTransitionSource",
2390
+ workflow: patch.workflow,
2391
+ fromState: patch.toState,
2392
+ toState: patch.fromState,
2393
+ transitionName: patch.transitionName
2394
+ };
1716
2395
  case "addProcessor":
1717
2396
  return { op: "replaceSession", session: cloneSession(doc) };
1718
2397
  case "updateProcessor": {
@@ -1722,23 +2401,43 @@ function invertPatch(doc, patch) {
1722
2401
  for (const key of Object.keys(patch.updates)) {
1723
2402
  prior[key] = p[key];
1724
2403
  }
2404
+ return { op: "updateProcessor", processorUuid: patch.processorUuid, updates: prior };
2405
+ }
2406
+ case "removeProcessor": {
2407
+ const ptr = doc.meta.ids.processors[patch.processorUuid];
2408
+ if (!ptr) return noop();
2409
+ const procLoc = locateProcessor2(doc, patch.processorUuid);
2410
+ if (!procLoc) return noop();
2411
+ const wf = findWorkflow(doc, procLoc.workflow);
2412
+ const state = wf?.states[procLoc.state];
2413
+ const t = state?.transitions[procLoc.transitionIndex];
2414
+ const p = t?.processors?.[procLoc.processorIndex];
2415
+ if (!p) return noop();
1725
2416
  return {
1726
- op: "updateProcessor",
1727
- processorUuid: patch.processorUuid,
1728
- updates: prior
2417
+ op: "addProcessor",
2418
+ transitionUuid: ptr.transitionUuid,
2419
+ processor: structuredClone(p),
2420
+ index: procLoc.processorIndex
2421
+ };
2422
+ }
2423
+ case "reorderProcessor": {
2424
+ const procLoc = locateProcessor2(doc, patch.processorUuid);
2425
+ if (!procLoc) return noop();
2426
+ const orderedForTransition = [];
2427
+ for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
2428
+ if (p.transitionUuid === patch.transitionUuid) orderedForTransition.push(uuid);
2429
+ }
2430
+ const uuidAtTarget = orderedForTransition[patch.toIndex] ?? patch.processorUuid;
2431
+ return {
2432
+ op: "reorderProcessor",
2433
+ transitionUuid: patch.transitionUuid,
2434
+ processorUuid: uuidAtTarget,
2435
+ toIndex: procLoc.processorIndex
1729
2436
  };
1730
2437
  }
1731
- case "removeProcessor":
1732
- case "reorderProcessor":
1733
- return { op: "replaceSession", session: cloneSession(doc) };
1734
2438
  case "setCriterion": {
1735
2439
  const prior = readCriterionAt(doc, patch.host, patch.path);
1736
- return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : {
1737
- op: "setCriterion",
1738
- host: patch.host,
1739
- path: patch.path,
1740
- criterion: cloneCriterion(prior)
1741
- };
2440
+ return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : { op: "setCriterion", host: patch.host, path: patch.path, criterion: cloneCriterion(prior) };
1742
2441
  }
1743
2442
  case "setImportMode":
1744
2443
  return { op: "setImportMode", mode: doc.session.importMode };
@@ -1754,6 +2453,36 @@ function invertPatch(doc, patch) {
1754
2453
  anchors: prior ? { ...prior } : null
1755
2454
  };
1756
2455
  }
2456
+ case "setNodePosition": {
2457
+ const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
2458
+ if (!prior) {
2459
+ return { op: "removeNodePosition", workflow: patch.workflow, stateCode: patch.stateCode };
2460
+ }
2461
+ return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
2462
+ }
2463
+ case "removeNodePosition": {
2464
+ const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
2465
+ if (!prior) return noop();
2466
+ return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
2467
+ }
2468
+ case "resetLayout":
2469
+ return noop();
2470
+ case "addComment":
2471
+ return { op: "removeComment", workflow: patch.workflow, commentId: patch.comment.id };
2472
+ case "updateComment": {
2473
+ const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
2474
+ if (!prior) return noop();
2475
+ const priorUpdates = {};
2476
+ for (const key of Object.keys(patch.updates)) {
2477
+ priorUpdates[key] = prior[key];
2478
+ }
2479
+ return { op: "updateComment", workflow: patch.workflow, commentId: patch.commentId, updates: priorUpdates };
2480
+ }
2481
+ case "removeComment": {
2482
+ const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
2483
+ if (!prior) return noop();
2484
+ return { op: "addComment", workflow: patch.workflow, comment: structuredClone(prior) };
2485
+ }
1757
2486
  }
1758
2487
  }
1759
2488
  function noop() {
@@ -1762,30 +2491,42 @@ function noop() {
1762
2491
  function findWorkflow(doc, name) {
1763
2492
  return doc.session.workflows.find((w) => w.name === name);
1764
2493
  }
1765
- function findTransition(doc, transitionUuid) {
2494
+ function locateTransition2(doc, transitionUuid) {
1766
2495
  const ptr = doc.meta.ids.transitions[transitionUuid];
1767
- if (!ptr) return void 0;
1768
- const wf = findWorkflow(doc, ptr.workflow);
1769
- const state = wf?.states[ptr.state];
1770
- if (!state) return void 0;
2496
+ if (!ptr) return null;
1771
2497
  const ordered = [];
1772
2498
  for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
1773
2499
  if (p.workflow === ptr.workflow && p.state === ptr.state) ordered.push(uuid);
1774
2500
  }
1775
2501
  const idx = ordered.indexOf(transitionUuid);
1776
- return state.transitions[idx];
2502
+ if (idx < 0) return null;
2503
+ return { workflow: ptr.workflow, state: ptr.state, index: idx };
1777
2504
  }
1778
- function findProcessor(doc, processorUuid) {
2505
+ function locateProcessor2(doc, processorUuid) {
1779
2506
  const ptr = doc.meta.ids.processors[processorUuid];
1780
- if (!ptr) return void 0;
1781
- const t = findTransition(doc, ptr.transitionUuid);
1782
- if (!t?.processors) return void 0;
2507
+ if (!ptr) return null;
2508
+ const tLoc = locateTransition2(doc, ptr.transitionUuid);
2509
+ if (!tLoc) return null;
1783
2510
  const ordered = [];
1784
2511
  for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
1785
2512
  if (p.transitionUuid === ptr.transitionUuid) ordered.push(uuid);
1786
2513
  }
1787
- const idx = ordered.indexOf(processorUuid);
1788
- return t.processors[idx];
2514
+ const pIdx = ordered.indexOf(processorUuid);
2515
+ if (pIdx < 0) return null;
2516
+ return { workflow: tLoc.workflow, state: tLoc.state, transitionIndex: tLoc.index, processorIndex: pIdx };
2517
+ }
2518
+ function findTransition(doc, transitionUuid) {
2519
+ const loc = locateTransition2(doc, transitionUuid);
2520
+ if (!loc) return void 0;
2521
+ const wf = findWorkflow(doc, loc.workflow);
2522
+ return wf?.states[loc.state]?.transitions[loc.index];
2523
+ }
2524
+ function findProcessor(doc, processorUuid) {
2525
+ const loc = locateProcessor2(doc, processorUuid);
2526
+ if (!loc) return void 0;
2527
+ const wf = findWorkflow(doc, loc.workflow);
2528
+ const t = wf?.states[loc.state]?.transitions[loc.transitionIndex];
2529
+ return t?.processors?.[loc.processorIndex];
1789
2530
  }
1790
2531
  function readCriterionAt(doc, host, path) {
1791
2532
  const wf = findWorkflow(doc, host.workflow);
@@ -1820,6 +2561,26 @@ function cloneCriterion(c) {
1820
2561
  return structuredClone(c);
1821
2562
  }
1822
2563
 
2564
+ // src/patch/transaction.ts
2565
+ function applyTransaction(doc, tx) {
2566
+ return tx.patches.reduce((d, p) => applyPatch(d, p), doc);
2567
+ }
2568
+ function invertTransaction(doc, tx) {
2569
+ if (tx.inverses.length > 0) {
2570
+ return {
2571
+ summary: `Undo: ${tx.summary}`,
2572
+ patches: tx.inverses,
2573
+ inverses: tx.patches
2574
+ };
2575
+ }
2576
+ const computed = tx.patches.map((p) => invertPatch(doc, p)).reverse();
2577
+ return {
2578
+ summary: `Undo: ${tx.summary}`,
2579
+ patches: computed,
2580
+ inverses: tx.patches
2581
+ };
2582
+ }
2583
+
1823
2584
  // src/migrate/registry.ts
1824
2585
  var registry = [];
1825
2586
  function registerMigration(entry) {
@@ -1857,8 +2618,59 @@ function migrateSession(session, from, to) {
1857
2618
  return path.reduce((s, entry) => entry.migrate(s), session);
1858
2619
  }
1859
2620
  registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
2621
+
2622
+ // src/criteria/describe.ts
2623
+ var OPERATOR_SYMBOL = {
2624
+ EQUALS: "=",
2625
+ NOT_EQUAL: "\u2260",
2626
+ GREATER_THAN: ">",
2627
+ LESS_THAN: "<",
2628
+ GREATER_OR_EQUAL: "\u2265",
2629
+ LESS_OR_EQUAL: "\u2264",
2630
+ IEQUALS: "=",
2631
+ INOT_EQUAL: "\u2260"
2632
+ };
2633
+ function describeCriterion(c) {
2634
+ switch (c.type) {
2635
+ case "simple":
2636
+ return describeBinary(c.jsonPath, c.operation, c.value);
2637
+ case "group": {
2638
+ const n = c.conditions.length;
2639
+ return `${c.operator} (${n} condition${n === 1 ? "" : "s"})`;
2640
+ }
2641
+ case "function":
2642
+ return `Function: ${c.function.name || "<unnamed>"}`;
2643
+ case "lifecycle":
2644
+ return describeBinary(c.field, c.operation, c.value);
2645
+ case "array": {
2646
+ const n = c.value.length;
2647
+ return `${c.jsonPath} ${c.operation} [${n} value${n === 1 ? "" : "s"}]`;
2648
+ }
2649
+ }
2650
+ }
2651
+ function describeBinary(lhs, op, value) {
2652
+ if (op === "IS_NULL") return `${lhs} IS NULL`;
2653
+ if (op === "NOT_NULL") return `${lhs} IS NOT NULL`;
2654
+ if (op === "IS_CHANGED") return `${lhs} CHANGED`;
2655
+ if (op === "IS_UNCHANGED") return `${lhs} UNCHANGED`;
2656
+ if (op === "BETWEEN" || op === "BETWEEN_INCLUSIVE") {
2657
+ const inclusive = op === "BETWEEN_INCLUSIVE";
2658
+ if (Array.isArray(value) && value.length === 2) {
2659
+ return `${lhs} \u2208 ${inclusive ? "[" : "("}${formatValue(value[0])}, ${formatValue(value[1])}${inclusive ? "]" : ")"}`;
2660
+ }
2661
+ return `${lhs} ${op} ${formatValue(value)}`;
2662
+ }
2663
+ const symbol = OPERATOR_SYMBOL[op] ?? op;
2664
+ return `${lhs} ${symbol} ${formatValue(value)}`;
2665
+ }
2666
+ function formatValue(v) {
2667
+ if (v === void 0) return "?";
2668
+ if (typeof v === "string") return JSON.stringify(v);
2669
+ return JSON.stringify(v);
2670
+ }
1860
2671
  export {
1861
2672
  ArrayCriterionSchema,
2673
+ CRITERION_DEPTH_WARNING_THRESHOLD,
1862
2674
  CriterionSchema,
1863
2675
  ExecutionModeSchema,
1864
2676
  ExportPayloadSchema,
@@ -1868,26 +2680,38 @@ export {
1868
2680
  GroupCriterionSchema,
1869
2681
  ImportPayloadSchema,
1870
2682
  LifecycleCriterionSchema,
2683
+ MAX_CRITERION_DEPTH,
2684
+ MAX_JSON_BYTES,
2685
+ MAX_JSON_OBJECT_DEPTH,
1871
2686
  NAME_REGEX,
1872
2687
  NameSchema,
2688
+ OPERATOR_GROUPS,
1873
2689
  OPERATOR_TYPES,
2690
+ OPERATOR_VALUE_SHAPE,
1874
2691
  OperatorEnum,
1875
2692
  ParseJsonError,
2693
+ PatchConflictError,
1876
2694
  ProcessorSchema,
2695
+ SUPPORTED_GROUP_OPERATORS,
2696
+ SUPPORTED_SIMPLE_OPERATORS,
1877
2697
  ScheduledProcessorSchema,
1878
2698
  SchemaError,
1879
2699
  SimpleCriterionSchema,
1880
2700
  StateSchema,
1881
2701
  TransitionSchema,
2702
+ UNSUPPORTED_OPERATORS,
1882
2703
  WorkflowApiConflictError,
1883
2704
  WorkflowApiTransportError,
1884
2705
  WorkflowSchema,
1885
2706
  applyPatch,
1886
2707
  applyPatches,
2708
+ applyTransaction,
1887
2709
  assignSyntheticIds,
2710
+ describeCriterion,
1888
2711
  findMigrationPath,
1889
- idFor2 as idFor,
2712
+ idFor,
1890
2713
  invertPatch,
2714
+ invertTransaction,
1891
2715
  listMigrations,
1892
2716
  lookupById,
1893
2717
  migrateSession,
@@ -1913,6 +2737,7 @@ export {
1913
2737
  validateAll,
1914
2738
  validateExportSchema,
1915
2739
  validateImportSchema,
2740
+ validateJsonPathSubset,
1916
2741
  validateSemantics,
1917
2742
  validateSession,
1918
2743
  zodErrorToIssues