@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.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ArrayCriterionSchema: () => ArrayCriterionSchema,
24
+ CRITERION_DEPTH_WARNING_THRESHOLD: () => CRITERION_DEPTH_WARNING_THRESHOLD,
24
25
  CriterionSchema: () => CriterionSchema,
25
26
  ExecutionModeSchema: () => ExecutionModeSchema,
26
27
  ExportPayloadSchema: () => ExportPayloadSchema,
@@ -30,26 +31,38 @@ __export(index_exports, {
30
31
  GroupCriterionSchema: () => GroupCriterionSchema,
31
32
  ImportPayloadSchema: () => ImportPayloadSchema,
32
33
  LifecycleCriterionSchema: () => LifecycleCriterionSchema,
34
+ MAX_CRITERION_DEPTH: () => MAX_CRITERION_DEPTH,
35
+ MAX_JSON_BYTES: () => MAX_JSON_BYTES,
36
+ MAX_JSON_OBJECT_DEPTH: () => MAX_JSON_OBJECT_DEPTH,
33
37
  NAME_REGEX: () => NAME_REGEX,
34
38
  NameSchema: () => NameSchema,
39
+ OPERATOR_GROUPS: () => OPERATOR_GROUPS,
35
40
  OPERATOR_TYPES: () => OPERATOR_TYPES,
41
+ OPERATOR_VALUE_SHAPE: () => OPERATOR_VALUE_SHAPE,
36
42
  OperatorEnum: () => OperatorEnum,
37
43
  ParseJsonError: () => ParseJsonError,
44
+ PatchConflictError: () => PatchConflictError,
38
45
  ProcessorSchema: () => ProcessorSchema,
46
+ SUPPORTED_GROUP_OPERATORS: () => SUPPORTED_GROUP_OPERATORS,
47
+ SUPPORTED_SIMPLE_OPERATORS: () => SUPPORTED_SIMPLE_OPERATORS,
39
48
  ScheduledProcessorSchema: () => ScheduledProcessorSchema,
40
49
  SchemaError: () => SchemaError,
41
50
  SimpleCriterionSchema: () => SimpleCriterionSchema,
42
51
  StateSchema: () => StateSchema,
43
52
  TransitionSchema: () => TransitionSchema,
53
+ UNSUPPORTED_OPERATORS: () => UNSUPPORTED_OPERATORS,
44
54
  WorkflowApiConflictError: () => WorkflowApiConflictError,
45
55
  WorkflowApiTransportError: () => WorkflowApiTransportError,
46
56
  WorkflowSchema: () => WorkflowSchema,
47
57
  applyPatch: () => applyPatch,
48
58
  applyPatches: () => applyPatches,
59
+ applyTransaction: () => applyTransaction,
49
60
  assignSyntheticIds: () => assignSyntheticIds,
61
+ describeCriterion: () => describeCriterion,
50
62
  findMigrationPath: () => findMigrationPath,
51
- idFor: () => idFor2,
63
+ idFor: () => idFor,
52
64
  invertPatch: () => invertPatch,
65
+ invertTransaction: () => invertTransaction,
53
66
  listMigrations: () => listMigrations,
54
67
  lookupById: () => lookupById,
55
68
  migrateSession: () => migrateSession,
@@ -75,6 +88,7 @@ __export(index_exports, {
75
88
  validateAll: () => validateAll,
76
89
  validateExportSchema: () => validateExportSchema,
77
90
  validateImportSchema: () => validateImportSchema,
91
+ validateJsonPathSubset: () => validateJsonPathSubset,
78
92
  validateSemantics: () => validateSemantics,
79
93
  validateSession: () => validateSession,
80
94
  zodErrorToIssues: () => zodErrorToIssues
@@ -113,6 +127,14 @@ var OPERATOR_TYPES = /* @__PURE__ */ new Set([
113
127
  "IS_CHANGED"
114
128
  ]);
115
129
 
130
+ // src/types/transaction.ts
131
+ var PatchConflictError = class extends Error {
132
+ constructor(message) {
133
+ super(message);
134
+ this.name = "PatchConflictError";
135
+ }
136
+ };
137
+
116
138
  // src/types/api.ts
117
139
  var WorkflowApiConflictError = class extends Error {
118
140
  constructor(entity, serverConcurrencyToken, message = "Workflow save conflict: server state has changed.") {
@@ -227,11 +249,17 @@ var FunctionCriterionSchema = import_zod3.z.lazy(
227
249
 
228
250
  // src/schema/processor.ts
229
251
  var import_zod4 = require("zod");
230
- var ExecutionModeSchema = import_zod4.z.enum(["SYNC", "ASYNC_SAME_TX", "ASYNC_NEW_TX"]);
252
+ var ExecutionModeSchema = import_zod4.z.enum([
253
+ "SYNC",
254
+ "ASYNC_SAME_TX",
255
+ "ASYNC_NEW_TX",
256
+ "COMMIT_BEFORE_DISPATCH"
257
+ ]);
231
258
  var ExternalizedProcessorSchema = import_zod4.z.object({
232
259
  type: import_zod4.z.literal("externalized"),
233
260
  name: NameSchema,
234
261
  executionMode: ExecutionModeSchema.optional(),
262
+ startNewTxOnDispatch: import_zod4.z.boolean().optional(),
235
263
  config: FunctionConfigSchema.and(
236
264
  import_zod4.z.object({
237
265
  asyncResult: import_zod4.z.boolean().optional(),
@@ -259,7 +287,7 @@ var TransitionSchema = import_zod5.z.object({
259
287
  name: NameSchema,
260
288
  next: NameSchema,
261
289
  manual: import_zod5.z.boolean(),
262
- disabled: import_zod5.z.boolean(),
290
+ disabled: import_zod5.z.boolean().default(false),
263
291
  criterion: CriterionSchema.optional(),
264
292
  processors: import_zod5.z.array(ProcessorSchema).optional()
265
293
  });
@@ -472,6 +500,118 @@ function mintCriterionIds(c, host, path, ids) {
472
500
  }
473
501
  }
474
502
 
503
+ // src/criteria/operators.ts
504
+ var SUPPORTED_SIMPLE_OPERATORS = /* @__PURE__ */ new Set([
505
+ "EQUALS",
506
+ "NOT_EQUAL",
507
+ "GREATER_THAN",
508
+ "LESS_THAN",
509
+ "GREATER_OR_EQUAL",
510
+ "LESS_OR_EQUAL",
511
+ "CONTAINS",
512
+ "NOT_CONTAINS",
513
+ "STARTS_WITH",
514
+ "NOT_STARTS_WITH",
515
+ "ENDS_WITH",
516
+ "NOT_ENDS_WITH",
517
+ "LIKE",
518
+ "IS_NULL",
519
+ "NOT_NULL",
520
+ "BETWEEN",
521
+ "BETWEEN_INCLUSIVE",
522
+ "MATCHES_PATTERN",
523
+ "IEQUALS",
524
+ "INOT_EQUAL",
525
+ "ICONTAINS",
526
+ "INOT_CONTAINS",
527
+ "ISTARTS_WITH",
528
+ "INOT_STARTS_WITH",
529
+ "IENDS_WITH",
530
+ "INOT_ENDS_WITH"
531
+ ]);
532
+ var UNSUPPORTED_OPERATORS = /* @__PURE__ */ new Set([
533
+ "IS_UNCHANGED",
534
+ "IS_CHANGED"
535
+ ]);
536
+ var SUPPORTED_GROUP_OPERATORS = ["AND", "OR"];
537
+ var MAX_CRITERION_DEPTH = 50;
538
+ var CRITERION_DEPTH_WARNING_THRESHOLD = 5;
539
+ var OPERATOR_GROUPS = [
540
+ {
541
+ id: "equality",
542
+ label: "Equality",
543
+ operators: ["EQUALS", "NOT_EQUAL", "IEQUALS", "INOT_EQUAL"]
544
+ },
545
+ {
546
+ id: "ordering",
547
+ label: "Ordering",
548
+ operators: ["GREATER_THAN", "LESS_THAN", "GREATER_OR_EQUAL", "LESS_OR_EQUAL"]
549
+ },
550
+ {
551
+ id: "range",
552
+ label: "Range",
553
+ operators: ["BETWEEN", "BETWEEN_INCLUSIVE"]
554
+ },
555
+ {
556
+ id: "substring",
557
+ label: "Substring",
558
+ operators: [
559
+ "CONTAINS",
560
+ "NOT_CONTAINS",
561
+ "ICONTAINS",
562
+ "INOT_CONTAINS",
563
+ "STARTS_WITH",
564
+ "NOT_STARTS_WITH",
565
+ "ISTARTS_WITH",
566
+ "INOT_STARTS_WITH",
567
+ "ENDS_WITH",
568
+ "NOT_ENDS_WITH",
569
+ "IENDS_WITH",
570
+ "INOT_ENDS_WITH"
571
+ ]
572
+ },
573
+ {
574
+ id: "pattern",
575
+ label: "Pattern",
576
+ operators: ["LIKE", "MATCHES_PATTERN"]
577
+ },
578
+ {
579
+ id: "null",
580
+ label: "Null",
581
+ operators: ["IS_NULL", "NOT_NULL"]
582
+ }
583
+ ];
584
+ var OPERATOR_VALUE_SHAPE = {
585
+ EQUALS: "scalar",
586
+ NOT_EQUAL: "scalar",
587
+ GREATER_THAN: "scalar",
588
+ LESS_THAN: "scalar",
589
+ GREATER_OR_EQUAL: "scalar",
590
+ LESS_OR_EQUAL: "scalar",
591
+ CONTAINS: "scalar",
592
+ NOT_CONTAINS: "scalar",
593
+ STARTS_WITH: "scalar",
594
+ NOT_STARTS_WITH: "scalar",
595
+ ENDS_WITH: "scalar",
596
+ NOT_ENDS_WITH: "scalar",
597
+ LIKE: "scalar",
598
+ MATCHES_PATTERN: "scalar",
599
+ IEQUALS: "scalar",
600
+ INOT_EQUAL: "scalar",
601
+ ICONTAINS: "scalar",
602
+ INOT_CONTAINS: "scalar",
603
+ ISTARTS_WITH: "scalar",
604
+ INOT_STARTS_WITH: "scalar",
605
+ IENDS_WITH: "scalar",
606
+ INOT_ENDS_WITH: "scalar",
607
+ BETWEEN: "range",
608
+ BETWEEN_INCLUSIVE: "range",
609
+ IS_NULL: "none",
610
+ NOT_NULL: "none",
611
+ IS_UNCHANGED: "none",
612
+ IS_CHANGED: "none"
613
+ };
614
+
475
615
  // src/normalize/input.ts
476
616
  function normalizeWorkflowInput(workflow) {
477
617
  const out = {
@@ -514,12 +654,21 @@ function normalizeWorkflowInput(workflow) {
514
654
  }
515
655
  return out;
516
656
  }
517
- function normalizeCriterion(criterion) {
657
+ function normalizeCriterion(criterion, depth = 0) {
658
+ if (depth >= MAX_CRITERION_DEPTH) {
659
+ return criterion;
660
+ }
518
661
  switch (criterion.type) {
519
662
  case "simple":
663
+ if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
664
+ return { ...criterion, value: null };
665
+ }
520
666
  return criterion;
521
667
  case "group":
522
- return { ...criterion, conditions: criterion.conditions.map(normalizeCriterion) };
668
+ return {
669
+ ...criterion,
670
+ conditions: criterion.conditions.map((c) => normalizeCriterion(c, depth + 1))
671
+ };
523
672
  case "function": {
524
673
  const fn = criterion.function;
525
674
  const out = {
@@ -527,12 +676,15 @@ function normalizeCriterion(criterion) {
527
676
  function: {
528
677
  name: fn.name.trim(),
529
678
  ...fn.config !== void 0 ? { config: fn.config } : {},
530
- ...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion) } : {}
679
+ ...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion, depth + 1) } : {}
531
680
  }
532
681
  };
533
682
  return out;
534
683
  }
535
684
  case "lifecycle":
685
+ if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
686
+ return { ...criterion, value: null };
687
+ }
536
688
  return criterion;
537
689
  case "array":
538
690
  return criterion;
@@ -549,6 +701,79 @@ function normalizeProcessor(p) {
549
701
  };
550
702
  }
551
703
 
704
+ // src/criteria/jsonPathSubset.ts
705
+ var SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
706
+ var INDEX_RE = /^(?:\d+|\*)$/;
707
+ function validateJsonPathSubset(path) {
708
+ if (path.length === 0) return { ok: false, reason: "empty" };
709
+ if (path[0] !== "$") return { ok: false, reason: "missing-root" };
710
+ if (path === "$") return { ok: true };
711
+ let i = 1;
712
+ while (i < path.length) {
713
+ const ch = path[i];
714
+ if (ch === ".") {
715
+ if (path[i + 1] === ".") return { ok: false, reason: "recursive-descent" };
716
+ i += 1;
717
+ const start = i;
718
+ while (i < path.length && path[i] !== "." && path[i] !== "[") i += 1;
719
+ const segment = path.slice(start, i);
720
+ if (!SEGMENT_RE.test(segment)) return { ok: false, reason: "malformed" };
721
+ continue;
722
+ }
723
+ if (ch === "[") {
724
+ if (path[i + 1] === "?") return { ok: false, reason: "filter-expression" };
725
+ if (path[i + 1] === "'" || path[i + 1] === '"') return { ok: false, reason: "malformed" };
726
+ const end = path.indexOf("]", i);
727
+ if (end === -1) return { ok: false, reason: "malformed" };
728
+ const inner = path.slice(i + 1, end);
729
+ if (!INDEX_RE.test(inner)) return { ok: false, reason: "malformed" };
730
+ i = end + 1;
731
+ continue;
732
+ }
733
+ return { ok: false, reason: "malformed" };
734
+ }
735
+ return { ok: true };
736
+ }
737
+
738
+ // src/identity/id-for.ts
739
+ function idFor(meta, ref) {
740
+ switch (ref.kind) {
741
+ case "workflow":
742
+ return meta.ids.workflows[ref.workflow] ?? null;
743
+ case "state": {
744
+ for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
745
+ if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
746
+ }
747
+ return null;
748
+ }
749
+ case "transition": {
750
+ const matches = [];
751
+ for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
752
+ if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
753
+ matches.push(uuid);
754
+ }
755
+ }
756
+ return matches[ref.ordinal] ?? null;
757
+ }
758
+ case "processor": {
759
+ const matches = [];
760
+ for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
761
+ if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
762
+ }
763
+ return matches[ref.ordinal] ?? null;
764
+ }
765
+ case "criterion": {
766
+ const target = JSON.stringify(ref.path);
767
+ for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
768
+ if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
769
+ return uuid;
770
+ }
771
+ }
772
+ return null;
773
+ }
774
+ }
775
+ }
776
+
552
777
  // src/validate/helpers.ts
553
778
  function isValidName(name) {
554
779
  return NAME_REGEX.test(name);
@@ -574,12 +799,15 @@ function* walkCriteria(session) {
574
799
  }
575
800
  }
576
801
  }
577
- function* walkInner(c, where) {
802
+ function* walkInner(c, where, depth = 0) {
578
803
  yield { criterion: c, where };
804
+ if (depth >= MAX_CRITERION_DEPTH) {
805
+ return;
806
+ }
579
807
  if (c.type === "group") {
580
- for (const child of c.conditions) yield* walkInner(child, where);
808
+ for (const child of c.conditions) yield* walkInner(child, where, depth + 1);
581
809
  } else if (c.type === "function" && c.function.criterion) {
582
- yield* walkInner(c.function.criterion, where);
810
+ yield* walkInner(c.function.criterion, where, depth + 1);
583
811
  }
584
812
  }
585
813
 
@@ -592,6 +820,8 @@ function validateSemantics(session, doc) {
592
820
  issues.push(...validateWorkflow(wf, doc));
593
821
  }
594
822
  issues.push(...criterionRules(session));
823
+ issues.push(...criterionDepthRules(session));
824
+ issues.push(...automatedOrderingRules(session, doc));
595
825
  if (session.workflows.length === 1) {
596
826
  const only = session.workflows[0];
597
827
  if (only && only.criterion !== void 0) {
@@ -629,14 +859,14 @@ function validateWorkflow(wf, doc) {
629
859
  severity: "error",
630
860
  code: "missing-initial-state",
631
861
  message: `Workflow "${wf.name}" has no initialState.`,
632
- ...idFor(doc, wf.name, "workflow")
862
+ ...idFor2(doc, wf.name, "workflow")
633
863
  });
634
864
  } else if (!(wf.initialState in wf.states)) {
635
865
  issues.push({
636
866
  severity: "error",
637
867
  code: "unknown-initial-state",
638
868
  message: `Workflow "${wf.name}" initialState "${wf.initialState}" is not a state.`,
639
- ...idFor(doc, wf.name, "workflow")
869
+ ...idFor2(doc, wf.name, "workflow")
640
870
  });
641
871
  }
642
872
  if (!isValidName(wf.name)) {
@@ -689,6 +919,13 @@ function validateWorkflow(wf, doc) {
689
919
  message: `Scheduled processor "${p.name}" has empty target transition.`
690
920
  });
691
921
  }
922
+ if (p.type === "externalized" && p.startNewTxOnDispatch === true && p.executionMode !== "COMMIT_BEFORE_DISPATCH") {
923
+ issues.push({
924
+ severity: "warning",
925
+ code: "start-new-tx-without-commit-before-dispatch",
926
+ message: `Processor "${p.name}" sets startNewTxOnDispatch but executionMode is not COMMIT_BEFORE_DISPATCH.`
927
+ });
928
+ }
692
929
  if (p.type === "externalized" && p.config) {
693
930
  if (p.config.crossoverToAsyncMs !== void 0 && p.config.asyncResult !== true) {
694
931
  issues.push({
@@ -831,7 +1068,16 @@ function criterionRules(session) {
831
1068
  });
832
1069
  }
833
1070
  break;
834
- case "array":
1071
+ case "array": {
1072
+ const arrPathCheck = validateJsonPathSubset(criterion.jsonPath);
1073
+ if (!arrPathCheck.ok) {
1074
+ issues.push({
1075
+ severity: "error",
1076
+ code: "invalid-jsonpath-subset",
1077
+ message: `Array criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${arrPathCheck.reason}) (at ${describe(where)}).`,
1078
+ detail: { jsonPath: criterion.jsonPath, reason: arrPathCheck.reason }
1079
+ });
1080
+ }
835
1081
  for (const v of criterion.value) {
836
1082
  if (typeof v !== "string") {
837
1083
  issues.push({
@@ -843,6 +1089,7 @@ function criterionRules(session) {
843
1089
  }
844
1090
  }
845
1091
  break;
1092
+ }
846
1093
  case "lifecycle":
847
1094
  if (!LIFECYCLE_FIELDS.has(criterion.field)) {
848
1095
  issues.push({
@@ -851,22 +1098,144 @@ function criterionRules(session) {
851
1098
  message: `Lifecycle criterion field "${criterion.field}" is invalid.`
852
1099
  });
853
1100
  }
1101
+ if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
1102
+ issues.push({
1103
+ severity: "warning",
1104
+ code: "unsupported-operator",
1105
+ message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
1106
+ detail: { operation: criterion.operation }
1107
+ });
1108
+ }
854
1109
  break;
855
1110
  case "group":
856
- if (criterion.operator === "NOT" && criterion.conditions.length > 1) {
1111
+ if (criterion.operator === "NOT") {
857
1112
  issues.push({
858
1113
  severity: "warning",
859
- code: "not-with-multiple-conditions",
860
- message: `NOT group has ${criterion.conditions.length} conditions; should have exactly one.`
1114
+ code: "unsupported-group-operator",
1115
+ message: `Group operator "NOT" is not implemented by the engine (at ${describe(where)}).`,
1116
+ detail: { operator: "NOT" }
861
1117
  });
1118
+ if (criterion.conditions.length > 1) {
1119
+ issues.push({
1120
+ severity: "warning",
1121
+ code: "not-with-multiple-conditions",
1122
+ message: `NOT group has ${criterion.conditions.length} conditions; should have exactly one.`
1123
+ });
1124
+ }
862
1125
  }
863
1126
  break;
864
- case "simple":
1127
+ case "simple": {
1128
+ const pathCheck = validateJsonPathSubset(criterion.jsonPath);
1129
+ if (!pathCheck.ok) {
1130
+ issues.push({
1131
+ severity: "error",
1132
+ code: "invalid-jsonpath-subset",
1133
+ message: `Simple criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${pathCheck.reason}) (at ${describe(where)}).`,
1134
+ detail: { jsonPath: criterion.jsonPath, reason: pathCheck.reason }
1135
+ });
1136
+ } else if (criterion.jsonPath.startsWith("$._meta")) {
1137
+ issues.push({
1138
+ severity: "warning",
1139
+ code: "lifecycle-path-in-simple",
1140
+ message: `Simple criterion path "${criterion.jsonPath}" looks like a lifecycle path; use a lifecycle criterion instead (at ${describe(where)}).`,
1141
+ detail: { jsonPath: criterion.jsonPath }
1142
+ });
1143
+ }
1144
+ if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
1145
+ issues.push({
1146
+ severity: "warning",
1147
+ code: "unsupported-operator",
1148
+ message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
1149
+ detail: { operation: criterion.operation }
1150
+ });
1151
+ }
1152
+ if (criterion.operation === "BETWEEN" || criterion.operation === "BETWEEN_INCLUSIVE") {
1153
+ if (!Array.isArray(criterion.value) || criterion.value.length !== 2) {
1154
+ issues.push({
1155
+ severity: "error",
1156
+ code: "simple-between-shape",
1157
+ message: `Operator "${criterion.operation}" requires a two-element [low, high] array value (at ${describe(where)}).`,
1158
+ detail: { operation: criterion.operation }
1159
+ });
1160
+ }
1161
+ }
1162
+ if (criterion.operation === "LIKE" && typeof criterion.value === "string" && /[%_]/.test(criterion.value)) {
1163
+ issues.push({
1164
+ severity: "warning",
1165
+ code: "like-wildcard-warning",
1166
+ message: `LIKE pattern contains "%" or "_" which are always wildcards (no escape mechanism) (at ${describe(where)}).`
1167
+ });
1168
+ }
1169
+ if (criterion.operation === "MATCHES_PATTERN" && typeof criterion.value === "string" && criterion.value.length > 0 && !criterion.value.startsWith("^") && !criterion.value.endsWith("$")) {
1170
+ issues.push({
1171
+ severity: "warning",
1172
+ code: "matches-pattern-unanchored",
1173
+ message: `MATCHES_PATTERN regex is unanchored; include "^"/"$" for whole-string match (at ${describe(where)}).`
1174
+ });
1175
+ }
865
1176
  break;
1177
+ }
866
1178
  }
867
1179
  }
868
1180
  return issues;
869
1181
  }
1182
+ function criterionDepthRules(session) {
1183
+ const issues = [];
1184
+ for (const wf of session.workflows) {
1185
+ if (wf.criterion) {
1186
+ pushDepthIssue(issues, criterionMaxDepth(wf.criterion), {
1187
+ kind: "workflow",
1188
+ workflow: wf.name
1189
+ });
1190
+ }
1191
+ for (const [stateCode, state] of Object.entries(wf.states)) {
1192
+ for (let i = 0; i < state.transitions.length; i++) {
1193
+ const t = state.transitions[i];
1194
+ if (!t.criterion) continue;
1195
+ pushDepthIssue(issues, criterionMaxDepth(t.criterion), {
1196
+ kind: "transition",
1197
+ workflow: wf.name,
1198
+ state: stateCode,
1199
+ transitionIndex: i,
1200
+ transitionName: t.name
1201
+ });
1202
+ }
1203
+ }
1204
+ }
1205
+ return issues;
1206
+ }
1207
+ function pushDepthIssue(issues, maxDepth, where) {
1208
+ if (maxDepth >= MAX_CRITERION_DEPTH) {
1209
+ issues.push({
1210
+ severity: "error",
1211
+ code: "criterion-depth-limit",
1212
+ message: `Criterion tree depth ${maxDepth} exceeds engine limit ${MAX_CRITERION_DEPTH} (at ${describe(where)}).`,
1213
+ detail: { maxDepth, threshold: MAX_CRITERION_DEPTH }
1214
+ });
1215
+ }
1216
+ if (maxDepth >= CRITERION_DEPTH_WARNING_THRESHOLD) {
1217
+ issues.push({
1218
+ severity: "warning",
1219
+ code: "criterion-depth-warning",
1220
+ message: `Criterion tree depth ${maxDepth} is hard to read; consider flattening (at ${describe(where)}).`,
1221
+ detail: { maxDepth, threshold: CRITERION_DEPTH_WARNING_THRESHOLD }
1222
+ });
1223
+ }
1224
+ }
1225
+ function criterionMaxDepth(root) {
1226
+ const stack = [{ node: root, depth: 1 }];
1227
+ let max = 0;
1228
+ while (stack.length > 0) {
1229
+ const { node, depth } = stack.pop();
1230
+ if (depth > max) max = depth;
1231
+ if (node.type === "group") {
1232
+ for (const child of node.conditions) stack.push({ node: child, depth: depth + 1 });
1233
+ } else if (node.type === "function" && node.function.criterion) {
1234
+ stack.push({ node: node.function.criterion, depth: depth + 1 });
1235
+ }
1236
+ }
1237
+ return max;
1238
+ }
870
1239
  function reachableStates(wf) {
871
1240
  const visited = /* @__PURE__ */ new Set();
872
1241
  if (!(wf.initialState in wf.states)) return visited;
@@ -915,11 +1284,65 @@ function describe(w) {
915
1284
  if (w.kind === "workflow") return `workflow "${w.workflow}"`;
916
1285
  return `transition "${w.transitionName}" on "${w.workflow}:${w.state}"`;
917
1286
  }
918
- function idFor(doc, workflowName, _kind) {
1287
+ function idFor2(doc, workflowName, _kind) {
919
1288
  if (!doc) return {};
920
1289
  const id = doc.meta.ids.workflows[workflowName];
921
1290
  return id ? { targetId: id } : {};
922
1291
  }
1292
+ function transitionTargetId(doc, workflow, state, declarationIndex) {
1293
+ if (!doc) return {};
1294
+ const id = idFor(doc.meta, {
1295
+ kind: "transition",
1296
+ workflow,
1297
+ state,
1298
+ transitionName: "",
1299
+ ordinal: declarationIndex
1300
+ });
1301
+ return id ? { targetId: id } : {};
1302
+ }
1303
+ function automatedOrderingRules(session, doc) {
1304
+ const issues = [];
1305
+ for (const wf of session.workflows) {
1306
+ for (const [stateCode, state] of Object.entries(wf.states)) {
1307
+ const automated = [];
1308
+ state.transitions.forEach((t, index) => {
1309
+ if (t.manual !== true && t.disabled !== true) {
1310
+ automated.push({ index, t });
1311
+ }
1312
+ });
1313
+ const nullIdx = automated.findIndex(({ t }) => t.criterion === void 0);
1314
+ if (nullIdx === -1 || nullIdx === automated.length - 1) continue;
1315
+ const nullEntry = automated[nullIdx];
1316
+ issues.push({
1317
+ severity: "warning",
1318
+ code: "null-criterion-not-last",
1319
+ 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.`,
1320
+ ...transitionTargetId(doc, wf.name, stateCode, nullEntry.index),
1321
+ detail: {
1322
+ workflow: wf.name,
1323
+ state: stateCode,
1324
+ transitionName: nullEntry.t.name
1325
+ }
1326
+ });
1327
+ for (let j = nullIdx + 1; j < automated.length; j++) {
1328
+ const dead = automated[j];
1329
+ issues.push({
1330
+ severity: "warning",
1331
+ code: "unreachable-automated-transition",
1332
+ 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.`,
1333
+ ...transitionTargetId(doc, wf.name, stateCode, dead.index),
1334
+ detail: {
1335
+ workflow: wf.name,
1336
+ state: stateCode,
1337
+ transitionName: dead.t.name,
1338
+ blockedBy: nullEntry.t.name
1339
+ }
1340
+ });
1341
+ }
1342
+ }
1343
+ }
1344
+ return issues;
1345
+ }
923
1346
 
924
1347
  // src/validate/schema.ts
925
1348
  function zodErrorToIssues(err) {
@@ -934,6 +1357,8 @@ function zodErrorToIssues(err) {
934
1357
  }
935
1358
 
936
1359
  // src/parse/parse-import.ts
1360
+ var MAX_JSON_BYTES = 5 * 1024 * 1024;
1361
+ var MAX_JSON_OBJECT_DEPTH = 200;
937
1362
  function parseJsonSafe(json) {
938
1363
  try {
939
1364
  return { ok: true, value: JSON.parse(json) };
@@ -941,11 +1366,36 @@ function parseJsonSafe(json) {
941
1366
  return { ok: false, err: e.message };
942
1367
  }
943
1368
  }
1369
+ function exceedsObjectDepth(value, limit) {
1370
+ const stack = [{ val: value, depth: 1 }];
1371
+ while (stack.length > 0) {
1372
+ const { val, depth } = stack.pop();
1373
+ if (depth > limit) return true;
1374
+ if (typeof val !== "object" || val === null) continue;
1375
+ const children = Array.isArray(val) ? val : Object.values(val);
1376
+ for (const child of children) {
1377
+ if (typeof child === "object" && child !== null) {
1378
+ stack.push({ val: child, depth: depth + 1 });
1379
+ }
1380
+ }
1381
+ }
1382
+ return false;
1383
+ }
944
1384
  function parseImportPayload(json, prior) {
1385
+ if (json.length > MAX_JSON_BYTES) {
1386
+ throw new ParseJsonError(
1387
+ `Workflow JSON exceeds the maximum allowed size of ${MAX_JSON_BYTES / (1024 * 1024)} MB.`
1388
+ );
1389
+ }
945
1390
  const parsed = parseJsonSafe(json);
946
1391
  if (!parsed.ok) {
947
1392
  throw new ParseJsonError(`Invalid JSON: ${parsed.err}`);
948
1393
  }
1394
+ if (exceedsObjectDepth(parsed.value, MAX_JSON_OBJECT_DEPTH)) {
1395
+ throw new ParseJsonError(
1396
+ `Workflow JSON nesting depth exceeds the maximum allowed depth of ${MAX_JSON_OBJECT_DEPTH}.`
1397
+ );
1398
+ }
949
1399
  let aliased;
950
1400
  try {
951
1401
  aliased = normalizeOperatorAlias(parsed.value);
@@ -961,7 +1411,7 @@ function parseImportPayload(json, prior) {
961
1411
  ]
962
1412
  };
963
1413
  }
964
- const schemaResult = ImportPayloadSchema.safeParse(aliased);
1414
+ const schemaResult = ImportPayloadSchema.safeParse(coerceCanonicalDefaults(aliased));
965
1415
  if (!schemaResult.success) {
966
1416
  return { ok: false, issues: zodErrorToIssues(schemaResult.error) };
967
1417
  }
@@ -982,6 +1432,53 @@ function parseImportPayload(json, prior) {
982
1432
  issues
983
1433
  };
984
1434
  }
1435
+ function coerceCanonicalDefaults(value) {
1436
+ if (!isObj(value)) return value;
1437
+ const v = value;
1438
+ if (!Array.isArray(v["workflows"])) return value;
1439
+ return {
1440
+ ...v,
1441
+ workflows: v["workflows"].map((wf) => {
1442
+ if (!isObj(wf)) return wf;
1443
+ const w = wf;
1444
+ if (!isObj(w["states"])) return wf;
1445
+ const states = w["states"];
1446
+ const nextStates = {};
1447
+ for (const [code, state] of Object.entries(states)) {
1448
+ if (!isObj(state)) {
1449
+ nextStates[code] = state;
1450
+ continue;
1451
+ }
1452
+ const s = state;
1453
+ if (!Array.isArray(s["transitions"])) {
1454
+ nextStates[code] = state;
1455
+ continue;
1456
+ }
1457
+ nextStates[code] = {
1458
+ ...s,
1459
+ transitions: s["transitions"].map((t) => {
1460
+ if (!isObj(t)) return t;
1461
+ const tx = t;
1462
+ if (!Array.isArray(tx["processors"])) return t;
1463
+ return {
1464
+ ...tx,
1465
+ processors: tx["processors"].map((p) => {
1466
+ if (!isObj(p)) return p;
1467
+ const proc = p;
1468
+ if (typeof proc["type"] === "string") return p;
1469
+ return { type: "externalized", ...proc };
1470
+ })
1471
+ };
1472
+ })
1473
+ };
1474
+ }
1475
+ return { ...w, states: nextStates };
1476
+ })
1477
+ };
1478
+ }
1479
+ function isObj(v) {
1480
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1481
+ }
985
1482
 
986
1483
  // src/parse/parse-export.ts
987
1484
  function parseExportPayload(json, prior) {
@@ -1132,7 +1629,11 @@ function outputCriterion(c) {
1132
1629
  jsonPath: c.jsonPath,
1133
1630
  operation: c.operation
1134
1631
  };
1135
- if (c.value !== void 0) out["value"] = c.value;
1632
+ if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
1633
+ out["value"] = null;
1634
+ } else if (c.value !== void 0) {
1635
+ out["value"] = c.value;
1636
+ }
1136
1637
  return out;
1137
1638
  }
1138
1639
  case "group":
@@ -1158,7 +1659,11 @@ function outputCriterion(c) {
1158
1659
  field: c.field,
1159
1660
  operation: c.operation
1160
1661
  };
1161
- if (c.value !== void 0) out["value"] = c.value;
1662
+ if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
1663
+ out["value"] = null;
1664
+ } else if (c.value !== void 0) {
1665
+ out["value"] = c.value;
1666
+ }
1162
1667
  return out;
1163
1668
  }
1164
1669
  case "array":
@@ -1191,8 +1696,9 @@ function outputExternalizedProcessor(p) {
1191
1696
  type: "externalized",
1192
1697
  name: p.name
1193
1698
  };
1194
- if (p.executionMode !== void 0 && p.executionMode !== "ASYNC_NEW_TX") {
1195
- out["executionMode"] = p.executionMode;
1699
+ out["executionMode"] = p.executionMode ?? "ASYNC_NEW_TX";
1700
+ if ("startNewTxOnDispatch" in p && p.startNewTxOnDispatch !== void 0) {
1701
+ out["startNewTxOnDispatch"] = p.startNewTxOnDispatch;
1196
1702
  }
1197
1703
  if (p.config !== void 0) {
1198
1704
  const cfg = outputExternalizedConfig(p.config);
@@ -1387,45 +1893,6 @@ function walkPath(root, path) {
1387
1893
  return null;
1388
1894
  }
1389
1895
 
1390
- // src/identity/id-for.ts
1391
- function idFor2(meta, ref) {
1392
- switch (ref.kind) {
1393
- case "workflow":
1394
- return meta.ids.workflows[ref.workflow] ?? null;
1395
- case "state": {
1396
- for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
1397
- if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
1398
- }
1399
- return null;
1400
- }
1401
- case "transition": {
1402
- const matches = [];
1403
- for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
1404
- if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
1405
- matches.push(uuid);
1406
- }
1407
- }
1408
- return matches[ref.ordinal] ?? null;
1409
- }
1410
- case "processor": {
1411
- const matches = [];
1412
- for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
1413
- if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
1414
- }
1415
- return matches[ref.ordinal] ?? null;
1416
- }
1417
- case "criterion": {
1418
- const target = JSON.stringify(ref.path);
1419
- for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
1420
- if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
1421
- return uuid;
1422
- }
1423
- }
1424
- return null;
1425
- }
1426
- }
1427
- }
1428
-
1429
1896
  // src/validate/index.ts
1430
1897
  function validateImportSchema(raw) {
1431
1898
  const parsed = ImportPayloadSchema.safeParse(raw);
@@ -1445,9 +1912,13 @@ function validateSession(session) {
1445
1912
  // src/patch/apply.ts
1446
1913
  var import_immer = require("immer");
1447
1914
  function applyPatch(doc, patch) {
1448
- if (patch.op === "setEdgeAnchors") {
1449
- return applySetEdgeAnchors(doc, patch);
1450
- }
1915
+ if (patch.op === "setEdgeAnchors") return applySetEdgeAnchors(doc, patch);
1916
+ if (patch.op === "setNodePosition") return applySetNodePosition(doc, patch);
1917
+ if (patch.op === "removeNodePosition") return applyRemoveNodePosition(doc, patch);
1918
+ if (patch.op === "resetLayout") return applyResetLayout(doc, patch);
1919
+ if (patch.op === "addComment") return applyAddComment(doc, patch);
1920
+ if (patch.op === "updateComment") return applyUpdateComment(doc, patch);
1921
+ if (patch.op === "removeComment") return applyRemoveComment(doc, patch);
1451
1922
  const nextSession = (0, import_immer.produce)(doc.session, (d) => {
1452
1923
  const draft = d;
1453
1924
  switch (patch.op) {
@@ -1493,6 +1964,11 @@ function applyPatch(doc, patch) {
1493
1964
  case "renameState": {
1494
1965
  const wf = draft.workflows.find((w) => w.name === patch.workflow);
1495
1966
  if (!wf) return;
1967
+ if (patch.to !== patch.from && patch.to in wf.states) {
1968
+ throw new PatchConflictError(
1969
+ `State "${patch.to}" already exists in workflow "${patch.workflow}"`
1970
+ );
1971
+ }
1496
1972
  renameStateCascading(wf, patch.from, patch.to);
1497
1973
  return;
1498
1974
  }
@@ -1540,6 +2016,23 @@ function applyPatch(doc, patch) {
1540
2016
  state.transitions.splice(patch.toIndex, 0, item);
1541
2017
  return;
1542
2018
  }
2019
+ case "moveTransitionSource": {
2020
+ const wf = draft.workflows.find((w) => w.name === patch.workflow);
2021
+ if (!wf) return;
2022
+ const fromState = wf.states[patch.fromState];
2023
+ const toState = wf.states[patch.toState];
2024
+ if (!fromState || !toState) return;
2025
+ const idx = fromState.transitions.findIndex((t) => t.name === patch.transitionName);
2026
+ if (idx < 0) return;
2027
+ if (patch.fromState !== patch.toState && toState.transitions.some((t) => t.name === patch.transitionName)) {
2028
+ throw new PatchConflictError(
2029
+ `Transition "${patch.transitionName}" already exists in state "${patch.toState}"`
2030
+ );
2031
+ }
2032
+ const [transition] = fromState.transitions.splice(idx, 1);
2033
+ if (transition) toState.transitions.push(transition);
2034
+ return;
2035
+ }
1543
2036
  case "addProcessor": {
1544
2037
  const loc = locateTransition(doc, patch.transitionUuid);
1545
2038
  if (!loc) return;
@@ -1622,9 +2115,11 @@ function applyPatch(doc, patch) {
1622
2115
  }
1623
2116
  });
1624
2117
  const nextMeta = assignSyntheticIds(nextSession, doc.meta);
2118
+ preserveMovedTransitionUuid(doc, patch, nextSession, nextMeta);
2119
+ const cleanedWorkflowUi = cleanupWorkflowUi(nextMeta.workflowUi, nextSession, nextMeta);
1625
2120
  return {
1626
2121
  session: nextSession,
1627
- meta: { ...nextMeta, revision: doc.meta.revision + 1 }
2122
+ meta: { ...nextMeta, workflowUi: cleanedWorkflowUi, revision: doc.meta.revision + 1 }
1628
2123
  };
1629
2124
  }
1630
2125
  function applyPatches(doc, patches) {
@@ -1657,6 +2152,124 @@ function applySetEdgeAnchors(doc, patch) {
1657
2152
  }
1658
2153
  };
1659
2154
  }
2155
+ function applySetNodePosition(doc, patch) {
2156
+ const workflowUi = { ...doc.meta.workflowUi };
2157
+ const current = workflowUi[patch.workflow] ?? {};
2158
+ const nodes = { ...current.layout?.nodes ?? {} };
2159
+ nodes[patch.stateCode] = { x: patch.x, y: patch.y, pinned: patch.pinned ?? true };
2160
+ workflowUi[patch.workflow] = { ...current, layout: { nodes } };
2161
+ return {
2162
+ session: doc.session,
2163
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2164
+ };
2165
+ }
2166
+ function applyRemoveNodePosition(doc, patch) {
2167
+ const workflowUi = { ...doc.meta.workflowUi };
2168
+ const current = workflowUi[patch.workflow] ?? {};
2169
+ const nodes = { ...current.layout?.nodes ?? {} };
2170
+ delete nodes[patch.stateCode];
2171
+ workflowUi[patch.workflow] = {
2172
+ ...current,
2173
+ layout: Object.keys(nodes).length > 0 ? { nodes } : void 0
2174
+ };
2175
+ return {
2176
+ session: doc.session,
2177
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2178
+ };
2179
+ }
2180
+ function applyResetLayout(doc, patch) {
2181
+ const workflowUi = { ...doc.meta.workflowUi };
2182
+ const current = workflowUi[patch.workflow] ?? {};
2183
+ workflowUi[patch.workflow] = { ...current, layout: void 0 };
2184
+ return {
2185
+ session: doc.session,
2186
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2187
+ };
2188
+ }
2189
+ function applyAddComment(doc, patch) {
2190
+ const workflowUi = { ...doc.meta.workflowUi };
2191
+ const current = workflowUi[patch.workflow] ?? {};
2192
+ const comments = { ...current.comments ?? {}, [patch.comment.id]: patch.comment };
2193
+ workflowUi[patch.workflow] = { ...current, comments };
2194
+ return {
2195
+ session: doc.session,
2196
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2197
+ };
2198
+ }
2199
+ function applyUpdateComment(doc, patch) {
2200
+ const workflowUi = { ...doc.meta.workflowUi };
2201
+ const current = workflowUi[patch.workflow] ?? {};
2202
+ const existing = current.comments?.[patch.commentId];
2203
+ if (!existing) return { ...doc, meta: { ...doc.meta, revision: doc.meta.revision + 1 } };
2204
+ const updated = { ...existing, ...patch.updates, id: existing.id };
2205
+ const comments = { ...current.comments ?? {}, [patch.commentId]: updated };
2206
+ workflowUi[patch.workflow] = { ...current, comments };
2207
+ return {
2208
+ session: doc.session,
2209
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2210
+ };
2211
+ }
2212
+ function applyRemoveComment(doc, patch) {
2213
+ const workflowUi = { ...doc.meta.workflowUi };
2214
+ const current = workflowUi[patch.workflow] ?? {};
2215
+ const comments = { ...current.comments ?? {} };
2216
+ delete comments[patch.commentId];
2217
+ workflowUi[patch.workflow] = {
2218
+ ...current,
2219
+ comments: Object.keys(comments).length > 0 ? comments : void 0
2220
+ };
2221
+ return {
2222
+ session: doc.session,
2223
+ meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
2224
+ };
2225
+ }
2226
+ function preserveMovedTransitionUuid(priorDoc, patch, nextSession, nextMeta) {
2227
+ if (patch.op !== "moveTransitionSource") return;
2228
+ const oldUuid = transitionUuidByName(
2229
+ priorDoc,
2230
+ patch.workflow,
2231
+ patch.fromState,
2232
+ patch.transitionName
2233
+ );
2234
+ if (!oldUuid) return;
2235
+ const newUuid = transitionUuidByNameInSession(
2236
+ nextSession,
2237
+ nextMeta,
2238
+ patch.workflow,
2239
+ patch.toState,
2240
+ patch.transitionName
2241
+ );
2242
+ if (!newUuid || newUuid === oldUuid) return;
2243
+ const newPtr = nextMeta.ids.transitions[newUuid];
2244
+ if (!newPtr) return;
2245
+ delete nextMeta.ids.transitions[newUuid];
2246
+ nextMeta.ids.transitions[oldUuid] = {
2247
+ ...newPtr,
2248
+ transitionUuid: oldUuid
2249
+ };
2250
+ for (const processorPtr of Object.values(nextMeta.ids.processors)) {
2251
+ if (processorPtr.transitionUuid === newUuid) {
2252
+ processorPtr.transitionUuid = oldUuid;
2253
+ }
2254
+ }
2255
+ for (const criterionPtr of Object.values(nextMeta.ids.criteria)) {
2256
+ const host = criterionPtr.host;
2257
+ if ((host.kind === "transition" || host.kind === "processorConfig") && host.transitionUuid === newUuid) {
2258
+ host.transitionUuid = oldUuid;
2259
+ }
2260
+ }
2261
+ }
2262
+ function transitionUuidByName(doc, workflow, state, transitionName) {
2263
+ return transitionUuidByNameInSession(doc.session, doc.meta, workflow, state, transitionName);
2264
+ }
2265
+ function transitionUuidByNameInSession(session, meta, workflow, state, transitionName) {
2266
+ const wf = session.workflows.find((candidate) => candidate.name === workflow);
2267
+ const transitions = wf?.states[state]?.transitions ?? [];
2268
+ const index = transitions.findIndex((transition) => transition.name === transitionName);
2269
+ if (index < 0) return null;
2270
+ const ordered = Object.entries(meta.ids.transitions).filter(([, ptr]) => ptr.workflow === workflow && ptr.state === state).map(([uuid]) => uuid);
2271
+ return ordered[index] ?? null;
2272
+ }
1660
2273
  function renameStateCascading(wf, from, to) {
1661
2274
  if (!(from in wf.states) || from === to) return;
1662
2275
  wf.states[to] = wf.states[from];
@@ -1705,6 +2318,65 @@ function locateProcessor(doc, processorUuid) {
1705
2318
  processorIndex: pIdx
1706
2319
  };
1707
2320
  }
2321
+ function cleanupWorkflowUi(workflowUi, session, meta) {
2322
+ const wfNames = new Set(session.workflows.map((w) => w.name));
2323
+ const result = {};
2324
+ const validTransitionIdsByWorkflow = /* @__PURE__ */ new Map();
2325
+ if (meta) {
2326
+ for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
2327
+ let set = validTransitionIdsByWorkflow.get(ptr.workflow);
2328
+ if (!set) {
2329
+ set = /* @__PURE__ */ new Set();
2330
+ validTransitionIdsByWorkflow.set(ptr.workflow, set);
2331
+ }
2332
+ set.add(uuid);
2333
+ }
2334
+ }
2335
+ for (const [wfName, ui] of Object.entries(workflowUi)) {
2336
+ if (!wfNames.has(wfName)) continue;
2337
+ const wf = session.workflows.find((w) => w.name === wfName);
2338
+ const existingStates = wf ? new Set(Object.keys(wf.states)) : /* @__PURE__ */ new Set();
2339
+ const allTransitionNames = /* @__PURE__ */ new Set();
2340
+ if (wf) {
2341
+ for (const state of Object.values(wf.states)) {
2342
+ for (const t of state.transitions) allTransitionNames.add(t.name);
2343
+ }
2344
+ }
2345
+ let layout = ui.layout;
2346
+ if (layout?.nodes) {
2347
+ const cleanNodes = Object.fromEntries(
2348
+ Object.entries(layout.nodes).filter(([code]) => existingStates.has(code))
2349
+ );
2350
+ layout = Object.keys(cleanNodes).length > 0 ? { nodes: cleanNodes } : void 0;
2351
+ }
2352
+ let comments = ui.comments;
2353
+ if (comments) {
2354
+ const cleanComments = {};
2355
+ for (const [id, c] of Object.entries(comments)) {
2356
+ if (c.attachedTo?.kind === "state" && !existingStates.has(c.attachedTo.stateCode)) {
2357
+ cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
2358
+ } else if (c.attachedTo?.kind === "transition" && !allTransitionNames.has(c.attachedTo.transitionName)) {
2359
+ cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
2360
+ } else {
2361
+ cleanComments[id] = c;
2362
+ }
2363
+ }
2364
+ comments = Object.keys(cleanComments).length > 0 ? cleanComments : void 0;
2365
+ }
2366
+ let edgeAnchors = ui.edgeAnchors;
2367
+ if (edgeAnchors && meta) {
2368
+ const validTransitionIds = validTransitionIdsByWorkflow.get(wfName) ?? /* @__PURE__ */ new Set();
2369
+ const cleanAnchors = Object.fromEntries(
2370
+ Object.entries(edgeAnchors).filter(
2371
+ ([transitionUuid]) => validTransitionIds.has(transitionUuid)
2372
+ )
2373
+ );
2374
+ edgeAnchors = Object.keys(cleanAnchors).length > 0 ? cleanAnchors : void 0;
2375
+ }
2376
+ result[wfName] = { ...ui, layout, comments, edgeAnchors };
2377
+ }
2378
+ return result;
2379
+ }
1708
2380
  function applyCriterionAtPath(container, path, criterion) {
1709
2381
  if (path.length === 0) return;
1710
2382
  let node = container;
@@ -1741,7 +2413,6 @@ function invertPatch(doc, patch) {
1741
2413
  case "removeWorkflow":
1742
2414
  case "renameWorkflow":
1743
2415
  case "removeState":
1744
- case "renameState":
1745
2416
  case "replaceSession":
1746
2417
  return { op: "replaceSession", session: cloneSession(doc) };
1747
2418
  case "updateWorkflowMeta": {
@@ -1756,30 +2427,19 @@ function invertPatch(doc, patch) {
1756
2427
  case "setInitialState": {
1757
2428
  const wf = findWorkflow(doc, patch.workflow);
1758
2429
  if (!wf) return noop();
1759
- return {
1760
- op: "setInitialState",
1761
- workflow: patch.workflow,
1762
- stateCode: wf.initialState
1763
- };
2430
+ return { op: "setInitialState", workflow: patch.workflow, stateCode: wf.initialState };
1764
2431
  }
1765
2432
  case "setWorkflowCriterion": {
1766
2433
  const wf = findWorkflow(doc, patch.workflow);
1767
2434
  if (!wf) return noop();
1768
- return wf.criterion ? {
1769
- op: "setWorkflowCriterion",
1770
- workflow: patch.workflow,
1771
- criterion: cloneCriterion(wf.criterion)
1772
- } : { op: "setWorkflowCriterion", workflow: patch.workflow };
2435
+ return wf.criterion ? { op: "setWorkflowCriterion", workflow: patch.workflow, criterion: cloneCriterion(wf.criterion) } : { op: "setWorkflowCriterion", workflow: patch.workflow };
1773
2436
  }
1774
2437
  case "addState":
1775
- return {
1776
- op: "removeState",
1777
- workflow: patch.workflow,
1778
- stateCode: patch.stateCode
1779
- };
1780
- case "addTransition": {
2438
+ return { op: "removeState", workflow: patch.workflow, stateCode: patch.stateCode };
2439
+ case "renameState":
2440
+ return { op: "renameState", workflow: patch.workflow, from: patch.to, to: patch.from };
2441
+ case "addTransition":
1781
2442
  return { op: "replaceSession", session: cloneSession(doc) };
1782
- }
1783
2443
  case "updateTransition": {
1784
2444
  const t = findTransition(doc, patch.transitionUuid);
1785
2445
  if (!t) return noop();
@@ -1787,15 +2447,48 @@ function invertPatch(doc, patch) {
1787
2447
  for (const key of Object.keys(patch.updates)) {
1788
2448
  prior[key] = t[key];
1789
2449
  }
2450
+ return { op: "updateTransition", transitionUuid: patch.transitionUuid, updates: prior };
2451
+ }
2452
+ case "removeTransition": {
2453
+ const loc = locateTransition2(doc, patch.transitionUuid);
2454
+ if (!loc) return noop();
2455
+ const wf = findWorkflow(doc, loc.workflow);
2456
+ const state = wf?.states[loc.state];
2457
+ const t = state?.transitions[loc.index];
2458
+ if (!t) return noop();
1790
2459
  return {
1791
- op: "updateTransition",
1792
- transitionUuid: patch.transitionUuid,
1793
- updates: prior
2460
+ op: "addTransition",
2461
+ workflow: loc.workflow,
2462
+ fromState: loc.state,
2463
+ transition: structuredClone(t)
1794
2464
  };
1795
2465
  }
1796
- case "removeTransition":
1797
- case "reorderTransition":
1798
- return { op: "replaceSession", session: cloneSession(doc) };
2466
+ case "reorderTransition": {
2467
+ const loc = locateTransition2(doc, patch.transitionUuid);
2468
+ if (!loc) return noop();
2469
+ const orderedForState = [];
2470
+ for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
2471
+ if (p.workflow === patch.workflow && p.state === patch.fromState) {
2472
+ orderedForState.push(uuid);
2473
+ }
2474
+ }
2475
+ const uuidAtTarget = orderedForState[patch.toIndex] ?? patch.transitionUuid;
2476
+ return {
2477
+ op: "reorderTransition",
2478
+ workflow: patch.workflow,
2479
+ fromState: patch.fromState,
2480
+ transitionUuid: uuidAtTarget,
2481
+ toIndex: loc.index
2482
+ };
2483
+ }
2484
+ case "moveTransitionSource":
2485
+ return {
2486
+ op: "moveTransitionSource",
2487
+ workflow: patch.workflow,
2488
+ fromState: patch.toState,
2489
+ toState: patch.fromState,
2490
+ transitionName: patch.transitionName
2491
+ };
1799
2492
  case "addProcessor":
1800
2493
  return { op: "replaceSession", session: cloneSession(doc) };
1801
2494
  case "updateProcessor": {
@@ -1805,23 +2498,43 @@ function invertPatch(doc, patch) {
1805
2498
  for (const key of Object.keys(patch.updates)) {
1806
2499
  prior[key] = p[key];
1807
2500
  }
2501
+ return { op: "updateProcessor", processorUuid: patch.processorUuid, updates: prior };
2502
+ }
2503
+ case "removeProcessor": {
2504
+ const ptr = doc.meta.ids.processors[patch.processorUuid];
2505
+ if (!ptr) return noop();
2506
+ const procLoc = locateProcessor2(doc, patch.processorUuid);
2507
+ if (!procLoc) return noop();
2508
+ const wf = findWorkflow(doc, procLoc.workflow);
2509
+ const state = wf?.states[procLoc.state];
2510
+ const t = state?.transitions[procLoc.transitionIndex];
2511
+ const p = t?.processors?.[procLoc.processorIndex];
2512
+ if (!p) return noop();
1808
2513
  return {
1809
- op: "updateProcessor",
1810
- processorUuid: patch.processorUuid,
1811
- updates: prior
2514
+ op: "addProcessor",
2515
+ transitionUuid: ptr.transitionUuid,
2516
+ processor: structuredClone(p),
2517
+ index: procLoc.processorIndex
2518
+ };
2519
+ }
2520
+ case "reorderProcessor": {
2521
+ const procLoc = locateProcessor2(doc, patch.processorUuid);
2522
+ if (!procLoc) return noop();
2523
+ const orderedForTransition = [];
2524
+ for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
2525
+ if (p.transitionUuid === patch.transitionUuid) orderedForTransition.push(uuid);
2526
+ }
2527
+ const uuidAtTarget = orderedForTransition[patch.toIndex] ?? patch.processorUuid;
2528
+ return {
2529
+ op: "reorderProcessor",
2530
+ transitionUuid: patch.transitionUuid,
2531
+ processorUuid: uuidAtTarget,
2532
+ toIndex: procLoc.processorIndex
1812
2533
  };
1813
2534
  }
1814
- case "removeProcessor":
1815
- case "reorderProcessor":
1816
- return { op: "replaceSession", session: cloneSession(doc) };
1817
2535
  case "setCriterion": {
1818
2536
  const prior = readCriterionAt(doc, patch.host, patch.path);
1819
- return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : {
1820
- op: "setCriterion",
1821
- host: patch.host,
1822
- path: patch.path,
1823
- criterion: cloneCriterion(prior)
1824
- };
2537
+ return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : { op: "setCriterion", host: patch.host, path: patch.path, criterion: cloneCriterion(prior) };
1825
2538
  }
1826
2539
  case "setImportMode":
1827
2540
  return { op: "setImportMode", mode: doc.session.importMode };
@@ -1837,6 +2550,36 @@ function invertPatch(doc, patch) {
1837
2550
  anchors: prior ? { ...prior } : null
1838
2551
  };
1839
2552
  }
2553
+ case "setNodePosition": {
2554
+ const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
2555
+ if (!prior) {
2556
+ return { op: "removeNodePosition", workflow: patch.workflow, stateCode: patch.stateCode };
2557
+ }
2558
+ return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
2559
+ }
2560
+ case "removeNodePosition": {
2561
+ const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
2562
+ if (!prior) return noop();
2563
+ return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
2564
+ }
2565
+ case "resetLayout":
2566
+ return noop();
2567
+ case "addComment":
2568
+ return { op: "removeComment", workflow: patch.workflow, commentId: patch.comment.id };
2569
+ case "updateComment": {
2570
+ const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
2571
+ if (!prior) return noop();
2572
+ const priorUpdates = {};
2573
+ for (const key of Object.keys(patch.updates)) {
2574
+ priorUpdates[key] = prior[key];
2575
+ }
2576
+ return { op: "updateComment", workflow: patch.workflow, commentId: patch.commentId, updates: priorUpdates };
2577
+ }
2578
+ case "removeComment": {
2579
+ const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
2580
+ if (!prior) return noop();
2581
+ return { op: "addComment", workflow: patch.workflow, comment: structuredClone(prior) };
2582
+ }
1840
2583
  }
1841
2584
  }
1842
2585
  function noop() {
@@ -1845,30 +2588,42 @@ function noop() {
1845
2588
  function findWorkflow(doc, name) {
1846
2589
  return doc.session.workflows.find((w) => w.name === name);
1847
2590
  }
1848
- function findTransition(doc, transitionUuid) {
2591
+ function locateTransition2(doc, transitionUuid) {
1849
2592
  const ptr = doc.meta.ids.transitions[transitionUuid];
1850
- if (!ptr) return void 0;
1851
- const wf = findWorkflow(doc, ptr.workflow);
1852
- const state = wf?.states[ptr.state];
1853
- if (!state) return void 0;
2593
+ if (!ptr) return null;
1854
2594
  const ordered = [];
1855
2595
  for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
1856
2596
  if (p.workflow === ptr.workflow && p.state === ptr.state) ordered.push(uuid);
1857
2597
  }
1858
2598
  const idx = ordered.indexOf(transitionUuid);
1859
- return state.transitions[idx];
2599
+ if (idx < 0) return null;
2600
+ return { workflow: ptr.workflow, state: ptr.state, index: idx };
1860
2601
  }
1861
- function findProcessor(doc, processorUuid) {
2602
+ function locateProcessor2(doc, processorUuid) {
1862
2603
  const ptr = doc.meta.ids.processors[processorUuid];
1863
- if (!ptr) return void 0;
1864
- const t = findTransition(doc, ptr.transitionUuid);
1865
- if (!t?.processors) return void 0;
2604
+ if (!ptr) return null;
2605
+ const tLoc = locateTransition2(doc, ptr.transitionUuid);
2606
+ if (!tLoc) return null;
1866
2607
  const ordered = [];
1867
2608
  for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
1868
2609
  if (p.transitionUuid === ptr.transitionUuid) ordered.push(uuid);
1869
2610
  }
1870
- const idx = ordered.indexOf(processorUuid);
1871
- return t.processors[idx];
2611
+ const pIdx = ordered.indexOf(processorUuid);
2612
+ if (pIdx < 0) return null;
2613
+ return { workflow: tLoc.workflow, state: tLoc.state, transitionIndex: tLoc.index, processorIndex: pIdx };
2614
+ }
2615
+ function findTransition(doc, transitionUuid) {
2616
+ const loc = locateTransition2(doc, transitionUuid);
2617
+ if (!loc) return void 0;
2618
+ const wf = findWorkflow(doc, loc.workflow);
2619
+ return wf?.states[loc.state]?.transitions[loc.index];
2620
+ }
2621
+ function findProcessor(doc, processorUuid) {
2622
+ const loc = locateProcessor2(doc, processorUuid);
2623
+ if (!loc) return void 0;
2624
+ const wf = findWorkflow(doc, loc.workflow);
2625
+ const t = wf?.states[loc.state]?.transitions[loc.transitionIndex];
2626
+ return t?.processors?.[loc.processorIndex];
1872
2627
  }
1873
2628
  function readCriterionAt(doc, host, path) {
1874
2629
  const wf = findWorkflow(doc, host.workflow);
@@ -1903,6 +2658,26 @@ function cloneCriterion(c) {
1903
2658
  return structuredClone(c);
1904
2659
  }
1905
2660
 
2661
+ // src/patch/transaction.ts
2662
+ function applyTransaction(doc, tx) {
2663
+ return tx.patches.reduce((d, p) => applyPatch(d, p), doc);
2664
+ }
2665
+ function invertTransaction(doc, tx) {
2666
+ if (tx.inverses.length > 0) {
2667
+ return {
2668
+ summary: `Undo: ${tx.summary}`,
2669
+ patches: tx.inverses,
2670
+ inverses: tx.patches
2671
+ };
2672
+ }
2673
+ const computed = tx.patches.map((p) => invertPatch(doc, p)).reverse();
2674
+ return {
2675
+ summary: `Undo: ${tx.summary}`,
2676
+ patches: computed,
2677
+ inverses: tx.patches
2678
+ };
2679
+ }
2680
+
1906
2681
  // src/migrate/registry.ts
1907
2682
  var registry = [];
1908
2683
  function registerMigration(entry) {
@@ -1940,9 +2715,60 @@ function migrateSession(session, from, to) {
1940
2715
  return path.reduce((s, entry) => entry.migrate(s), session);
1941
2716
  }
1942
2717
  registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
2718
+
2719
+ // src/criteria/describe.ts
2720
+ var OPERATOR_SYMBOL = {
2721
+ EQUALS: "=",
2722
+ NOT_EQUAL: "\u2260",
2723
+ GREATER_THAN: ">",
2724
+ LESS_THAN: "<",
2725
+ GREATER_OR_EQUAL: "\u2265",
2726
+ LESS_OR_EQUAL: "\u2264",
2727
+ IEQUALS: "=",
2728
+ INOT_EQUAL: "\u2260"
2729
+ };
2730
+ function describeCriterion(c) {
2731
+ switch (c.type) {
2732
+ case "simple":
2733
+ return describeBinary(c.jsonPath, c.operation, c.value);
2734
+ case "group": {
2735
+ const n = c.conditions.length;
2736
+ return `${c.operator} (${n} condition${n === 1 ? "" : "s"})`;
2737
+ }
2738
+ case "function":
2739
+ return `Function: ${c.function.name || "<unnamed>"}`;
2740
+ case "lifecycle":
2741
+ return describeBinary(c.field, c.operation, c.value);
2742
+ case "array": {
2743
+ const n = c.value.length;
2744
+ return `${c.jsonPath} ${c.operation} [${n} value${n === 1 ? "" : "s"}]`;
2745
+ }
2746
+ }
2747
+ }
2748
+ function describeBinary(lhs, op, value) {
2749
+ if (op === "IS_NULL") return `${lhs} IS NULL`;
2750
+ if (op === "NOT_NULL") return `${lhs} IS NOT NULL`;
2751
+ if (op === "IS_CHANGED") return `${lhs} CHANGED`;
2752
+ if (op === "IS_UNCHANGED") return `${lhs} UNCHANGED`;
2753
+ if (op === "BETWEEN" || op === "BETWEEN_INCLUSIVE") {
2754
+ const inclusive = op === "BETWEEN_INCLUSIVE";
2755
+ if (Array.isArray(value) && value.length === 2) {
2756
+ return `${lhs} \u2208 ${inclusive ? "[" : "("}${formatValue(value[0])}, ${formatValue(value[1])}${inclusive ? "]" : ")"}`;
2757
+ }
2758
+ return `${lhs} ${op} ${formatValue(value)}`;
2759
+ }
2760
+ const symbol = OPERATOR_SYMBOL[op] ?? op;
2761
+ return `${lhs} ${symbol} ${formatValue(value)}`;
2762
+ }
2763
+ function formatValue(v) {
2764
+ if (v === void 0) return "?";
2765
+ if (typeof v === "string") return JSON.stringify(v);
2766
+ return JSON.stringify(v);
2767
+ }
1943
2768
  // Annotate the CommonJS export names for ESM import in node:
1944
2769
  0 && (module.exports = {
1945
2770
  ArrayCriterionSchema,
2771
+ CRITERION_DEPTH_WARNING_THRESHOLD,
1946
2772
  CriterionSchema,
1947
2773
  ExecutionModeSchema,
1948
2774
  ExportPayloadSchema,
@@ -1952,26 +2778,38 @@ registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
1952
2778
  GroupCriterionSchema,
1953
2779
  ImportPayloadSchema,
1954
2780
  LifecycleCriterionSchema,
2781
+ MAX_CRITERION_DEPTH,
2782
+ MAX_JSON_BYTES,
2783
+ MAX_JSON_OBJECT_DEPTH,
1955
2784
  NAME_REGEX,
1956
2785
  NameSchema,
2786
+ OPERATOR_GROUPS,
1957
2787
  OPERATOR_TYPES,
2788
+ OPERATOR_VALUE_SHAPE,
1958
2789
  OperatorEnum,
1959
2790
  ParseJsonError,
2791
+ PatchConflictError,
1960
2792
  ProcessorSchema,
2793
+ SUPPORTED_GROUP_OPERATORS,
2794
+ SUPPORTED_SIMPLE_OPERATORS,
1961
2795
  ScheduledProcessorSchema,
1962
2796
  SchemaError,
1963
2797
  SimpleCriterionSchema,
1964
2798
  StateSchema,
1965
2799
  TransitionSchema,
2800
+ UNSUPPORTED_OPERATORS,
1966
2801
  WorkflowApiConflictError,
1967
2802
  WorkflowApiTransportError,
1968
2803
  WorkflowSchema,
1969
2804
  applyPatch,
1970
2805
  applyPatches,
2806
+ applyTransaction,
1971
2807
  assignSyntheticIds,
2808
+ describeCriterion,
1972
2809
  findMigrationPath,
1973
2810
  idFor,
1974
2811
  invertPatch,
2812
+ invertTransaction,
1975
2813
  listMigrations,
1976
2814
  lookupById,
1977
2815
  migrateSession,
@@ -1997,6 +2835,7 @@ registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
1997
2835
  validateAll,
1998
2836
  validateExportSchema,
1999
2837
  validateImportSchema,
2838
+ validateJsonPathSubset,
2000
2839
  validateSemantics,
2001
2840
  validateSession,
2002
2841
  zodErrorToIssues