@rytass/bpm-core-shared 0.21.0 → 0.21.1

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.
@@ -35,10 +35,15 @@ exports.toggleSelectedEdgeId = toggleSelectedEdgeId;
35
35
  exports.readWorkflowDefinitionIssue = readWorkflowDefinitionIssue;
36
36
  exports.readNotifyServiceTaskIssue = readNotifyServiceTaskIssue;
37
37
  exports.readExcludeInitiatorIssue = readExcludeInitiatorIssue;
38
+ exports.readExcludeNodeActorsShapeIssue = readExcludeNodeActorsShapeIssue;
39
+ exports.readExcludeNodeActorsIssues = readExcludeNodeActorsIssues;
38
40
  exports.isOrgManagerChainDecisionPolicyInvalid = isOrgManagerChainDecisionPolicyInvalid;
41
+ exports.readOrgManagerChainIssue = readOrgManagerChainIssue;
39
42
  exports.readApproverResolverIssue = readApproverResolverIssue;
40
43
  exports.readNodeActorSourceSkipReasons = readNodeActorSourceSkipReasons;
41
44
  exports.readNodeActorSourceIssues = readNodeActorSourceIssues;
45
+ exports.canUserTaskHaveMultipleApprovers = canUserTaskHaveMultipleApprovers;
46
+ exports.readSingleApproverSourceIssues = readSingleApproverSourceIssues;
42
47
  exports.hasConfiguredConditionEdges = hasConfiguredConditionEdges;
43
48
  exports.readServiceTaskMemberIds = readServiceTaskMemberIds;
44
49
  exports.defaultNotifyWebhookTargetId = defaultNotifyWebhookTargetId;
@@ -71,6 +76,7 @@ exports.readConditionExpressionOperator = readConditionExpressionOperator;
71
76
  exports.readConditionExpressionValue = readConditionExpressionValue;
72
77
  const form_1 = require("./form");
73
78
  const workflow_1 = require("./workflow");
79
+ const field_access_1 = require("./field-access");
74
80
  const workflow_reachability_1 = require("./workflow-reachability");
75
81
  exports.EXCLUDE_INITIATOR_WHEN_EMPTY_VALUES = ['FAIL', 'INITIATOR_MANAGER', 'SKIP_NODE'];
76
82
  exports.WORKFLOW_INPUT_HANDLE_ID = 'input';
@@ -658,16 +664,23 @@ function toggleSelectedEdgeId(edgeIds, edgeId) {
658
664
  }
659
665
  // ── Validation ─────────────────────────────────────────────────────────────
660
666
  function readWorkflowDefinitionIssue(definition) {
661
- var _a, _b;
667
+ var _a, _b, _c;
662
668
  const incompleteUserTaskNode = definition.nodes.find((node) => node.type === 'userTask' &&
663
669
  Boolean(readApproverResolverIssue(node.data.approverResolver)));
664
670
  const unsatisfiableQuorumNode = definition.nodes.find((node) => node.type === 'userTask' &&
665
671
  isDecisionPolicyUnsatisfiable(node.data.decisionPolicy, node.data.approverResolver));
666
672
  const excludeInitiatorNode = definition.nodes.find((node) => node.type === 'userTask' &&
667
673
  Boolean(readExcludeInitiatorIssue(node.data.excludeInitiator)));
668
- const notifyNodeIssue = (_a = definition.nodes
669
- .map((node) => readNotifyServiceTaskIssue(node))
674
+ // Shape only: whether the stages sit upstream is a publish-time graph rule
675
+ // (`readExcludeNodeActorsIssues`), so a draft can be saved unfinished.
676
+ const excludeNodeActorsIssue = (_a = definition.nodes
677
+ .map((node) => node.type === 'userTask'
678
+ ? readExcludeNodeActorsShapeIssue(node.data.excludeNodeActors)
679
+ : null)
670
680
  .find((issue) => issue !== null)) !== null && _a !== void 0 ? _a : null;
681
+ const notifyNodeIssue = (_b = definition.nodes
682
+ .map((node) => readNotifyServiceTaskIssue(node))
683
+ .find((issue) => issue !== null)) !== null && _b !== void 0 ? _b : null;
671
684
  const incompleteConditionEdge = definition.edges.find((edge) => isExclusiveGatewaySourceEdge(edge, definition.nodes) &&
672
685
  !edge.data.isDefault &&
673
686
  !edge.data.condition);
@@ -690,7 +703,7 @@ function readWorkflowDefinitionIssue(definition) {
690
703
  }
691
704
  if (unsatisfiableQuorumNode &&
692
705
  unsatisfiableQuorumNode.type === 'userTask' &&
693
- ((_b = unsatisfiableQuorumNode.data.decisionPolicy) === null || _b === void 0 ? void 0 : _b.type) === 'QUORUM') {
706
+ ((_c = unsatisfiableQuorumNode.data.decisionPolicy) === null || _c === void 0 ? void 0 : _c.type) === 'QUORUM') {
694
707
  return (`簽核節點「${unsatisfiableQuorumNode.data.label}」的門檻人數為 ` +
695
708
  `${unsatisfiableQuorumNode.data.decisionPolicy.threshold},` +
696
709
  `但只指定了 ${readDesignTimeApproverCount(unsatisfiableQuorumNode.data.approverResolver)} 位簽核者,` +
@@ -699,6 +712,9 @@ function readWorkflowDefinitionIssue(definition) {
699
712
  if (excludeInitiatorNode && excludeInitiatorNode.type === 'userTask') {
700
713
  return readExcludeInitiatorIssue(excludeInitiatorNode.data.excludeInitiator);
701
714
  }
715
+ if (excludeNodeActorsIssue) {
716
+ return excludeNodeActorsIssue;
717
+ }
702
718
  if (notifyNodeIssue) {
703
719
  return notifyNodeIssue;
704
720
  }
@@ -750,6 +766,106 @@ function readExcludeInitiatorIssue(config) {
750
766
  ? null
751
767
  : '簽核節點的「申請人不可簽核自己的單」需要指定沒有其他簽核人時的處理方式。';
752
768
  }
769
+ /**
770
+ * The shape problem of a node's `excludeNodeActors` setting, or `null` when it
771
+ * is absent or well-formed: at least one id, text only, no repeats, and a
772
+ * known `whenEmpty`. Needs no graph, so it can block saving a draft the way
773
+ * {@link readExcludeInitiatorIssue} does; the graph rules are
774
+ * {@link readExcludeNodeActorsIssues}.
775
+ */
776
+ function readExcludeNodeActorsShapeIssue(config) {
777
+ if (config === undefined) {
778
+ return null;
779
+ }
780
+ // Raw JSON that has not been linted yet can carry anything here.
781
+ const rawNodeIds = config === null || config === void 0 ? void 0 : config.nodeIds;
782
+ const nodeIds = Array.isArray(rawNodeIds)
783
+ ? rawNodeIds
784
+ : [];
785
+ if (nodeIds.length === 0) {
786
+ return '簽核節點的「不可與前面關卡同一人」需要指定前面的簽核關卡。';
787
+ }
788
+ if (nodeIds.some((id) => typeof id !== 'string' || id.trim() === '') ||
789
+ new Set(nodeIds).size !== nodeIds.length) {
790
+ return '「不可與前面關卡同一人」所選的前面關卡不可重複,也不可留白。';
791
+ }
792
+ return exports.EXCLUDE_INITIATOR_WHEN_EMPTY_VALUES.includes(config.whenEmpty)
793
+ ? null
794
+ : '簽核節點的「不可與前面關卡同一人」需要指定沒有其他簽核人時的處理方式。';
795
+ }
796
+ /**
797
+ * The designer-facing issues for a node's `excludeNodeActors` setting ("this
798
+ * stage's approver cannot be the person who approved an earlier stage");
799
+ * `[]` when it is absent or sound. `nodeId` is the node that carries the
800
+ * setting. The setting is checked in this order and stops at the first group
801
+ * that fails:
802
+ *
803
+ * 1. shape ({@link readExcludeNodeActorsShapeIssue}) and never the node itself;
804
+ * 2. every listed node exists and is a `userTask`;
805
+ * 3. each listed node on its own passes the source rules that
806
+ * {@link readNodeActorSourceIssues} applies to a `NODE_ACTOR` source (same
807
+ * judgement, wording about the exclusion):
808
+ * - it may not be skipped or pass without a signer
809
+ * ({@link readNodeActorSourceSkipReasons});
810
+ * - every path from the start to `nodeId` goes through it, checked for
811
+ * each listed node separately (never "one of them covers it");
812
+ * - it is upstream of `nodeId`;
813
+ * 4. `whenEmpty: 'SKIP_NODE'` is refused on a node whose approver fills
814
+ * fields in, for the same reason `excludeInitiator` is.
815
+ *
816
+ * Why every listed node has to be on every path: the engine reads the newest
817
+ * round of approvals of a node, and a return can send the instance back to the
818
+ * start. A listed node that this round did not pass would leave its previous
819
+ * round's approver in the exclusion list, and the node would refuse a person
820
+ * who never signed an earlier stage in this round. Requiring a listed node to
821
+ * be run, and signed by a person, every time rules that out.
822
+ *
823
+ * The `NODE_ACTOR` rule "two sources on different branches of one parallel
824
+ * split" is not applied a second time. A listed node that sits on a parallel
825
+ * branch is still refused by the coverage rule above, though: coverage reads a
826
+ * parallel gateway like an exclusive one, the same limit `NODE_ACTOR` has, so
827
+ * such stages have to be chained one after another instead.
828
+ */
829
+ function readExcludeNodeActorsIssues(definition, nodeId, config) {
830
+ if (config === undefined) {
831
+ return [];
832
+ }
833
+ const shapeIssue = readExcludeNodeActorsShapeIssue(config);
834
+ if (shapeIssue !== null) {
835
+ return [shapeIssue];
836
+ }
837
+ const sourceNodeIds = config.nodeIds;
838
+ if (sourceNodeIds.includes(nodeId)) {
839
+ return ['「不可與前面關卡同一人」所選的前面關卡不可以是自己。'];
840
+ }
841
+ const labelOf = (id) => {
842
+ const node = definition.nodes.find((candidate) => candidate.id === id);
843
+ return (node === null || node === void 0 ? void 0 : node.data.label) ? `「${node.data.label}」` : `「${id}」`;
844
+ };
845
+ const sourceNodes = sourceNodeIds.map((id) => definition.nodes.find((candidate) => candidate.id === id));
846
+ const missingId = sourceNodeIds.find((id, index) => sourceNodes[index] === undefined);
847
+ if (missingId !== undefined) {
848
+ return [`「不可與前面關卡同一人」所選的前面關卡「${missingId}」不存在。`];
849
+ }
850
+ const notUserTaskId = sourceNodeIds.find((id, index) => { var _a; return ((_a = sourceNodes[index]) === null || _a === void 0 ? void 0 : _a.type) !== 'userTask'; });
851
+ if (notUserTaskId !== undefined) {
852
+ return [
853
+ `「不可與前面關卡同一人」所選的前面關卡 ${labelOf(notUserTaskId)} 不是簽核關卡。`,
854
+ ];
855
+ }
856
+ const stageIssues = readSourceStageIssues(definition, nodeId, sourceNodeIds, sourceNodeIds.map((id) => [id]), EXCLUDE_NODE_ACTORS_STAGE_TEXTS);
857
+ if (stageIssues.length > 0) {
858
+ return stageIssues;
859
+ }
860
+ const self = definition.nodes.find((node) => node.id === nodeId);
861
+ return config.whenEmpty === 'SKIP_NODE' &&
862
+ (self === null || self === void 0 ? void 0 : self.type) === 'userTask' &&
863
+ (0, field_access_1.readNodeFillableFieldKeys)(self).size > 0
864
+ ? [
865
+ `簽核節點 ${labelOf(nodeId)} 有可由簽核人填寫的欄位,「不可與前面關卡同一人」不能選擇略過此節點。`,
866
+ ]
867
+ : [];
868
+ }
753
869
  /**
754
870
  * `true` when an `ORG_MANAGER_CHAIN` node uses a decision policy that lets one
755
871
  * manager close the node (`SINGLE`, `PARALLEL_ANY`, `QUORUM`) or has none at
@@ -765,6 +881,57 @@ function isOrgManagerChainDecisionPolicyInvalid(resolver, decisionPolicy) {
765
881
  (decisionPolicy === null || decisionPolicy === void 0 ? void 0 : decisionPolicy.type) !== 'SEQUENTIAL' &&
766
882
  (decisionPolicy === null || decisionPolicy === void 0 ? void 0 : decisionPolicy.type) !== 'PARALLEL_ALL');
767
883
  }
884
+ /**
885
+ * The designer-facing issue with the two optional settings of an
886
+ * `ORG_MANAGER_CHAIN` (`minPositionLevel`, `startFrom`, `startPositionLevel`),
887
+ * or `null` when all are well-formed. `subject` names the node kind in the
888
+ * sentence; a notify node's chain may only start at the initiator (it has no
889
+ * earlier stage and no form pick to start from). Reads defensively: on the
890
+ * backend the definition is parsed JSON that no DTO has validated. The form
891
+ * field and the earlier stages the start names are checked elsewhere, against
892
+ * the form schema and the graph.
893
+ */
894
+ function readOrgManagerChainIssue(resolver, subject) {
895
+ // Checked first, for both subjects: without a whole-number level the climb
896
+ // never stops (`level >= undefined` is always false) and collects every
897
+ // manager up to the top.
898
+ if (!Number.isInteger(resolver.minPositionLevel)) {
899
+ return `${subject}需要指定有效的職等門檻。`;
900
+ }
901
+ const startFrom = resolver.startFrom;
902
+ if (startFrom !== undefined && startFrom !== null) {
903
+ const start = startFrom;
904
+ if (typeof start !== 'object' ||
905
+ typeof start.type !== 'string' ||
906
+ (0, workflow_1.readUnsupportedOrgManagerChainStartType)({ type: start.type }) !== null) {
907
+ return `${subject}的主管鏈起點不在這個版本支援的範圍內,請改選其他起點。`;
908
+ }
909
+ if (start.type === 'FORM_MEMBER' &&
910
+ (typeof start.formPath !== 'string' || !start.formPath.trim())) {
911
+ return `${subject}需要指定主管鏈起點的表單欄位。`;
912
+ }
913
+ if (start.type === 'NODE_ACTOR' &&
914
+ (!Array.isArray(start.nodeIds) || start.nodeIds.length === 0)) {
915
+ return `${subject}需要指定主管鏈起點的前面簽核關卡。`;
916
+ }
917
+ }
918
+ if (subject === '知會節點' &&
919
+ !(0, workflow_1.isOrgManagerChainStartedFromInitiator)(resolver)) {
920
+ return '知會節點的主管鏈只能從發起人開始。';
921
+ }
922
+ const rawStartLevel = resolver.startPositionLevel;
923
+ if (rawStartLevel === undefined || rawStartLevel === null) {
924
+ return null;
925
+ }
926
+ const startLevel = (0, workflow_1.readOrgManagerChainStartPositionLevel)(resolver);
927
+ if (startLevel === null || !Number.isInteger(startLevel)) {
928
+ return `${subject}需要指定有效的起始職等。`;
929
+ }
930
+ return Number.isInteger(resolver.minPositionLevel) &&
931
+ startLevel > resolver.minPositionLevel
932
+ ? `${subject}的起始職等不可高於職等門檻。`
933
+ : null;
934
+ }
768
935
  function readApproverResolverIssue(resolver) {
769
936
  var _a, _b;
770
937
  if ((0, workflow_1.readUnsupportedApproverResolverType)(resolver) !== null) {
@@ -786,9 +953,12 @@ function readApproverResolverIssue(resolver) {
786
953
  (!resolver.orgUnitId.trim() || !resolver.positionId.trim())) {
787
954
  return '簽核節點需要指定組織與職位。';
788
955
  }
789
- if (resolver.type === 'ORG_MANAGER_CHAIN' &&
790
- !Number.isInteger(resolver.minPositionLevel)) {
791
- return '簽核節點需要指定有效的職等門檻。';
956
+ if (resolver.type === 'ORG_MANAGER_CHAIN') {
957
+ // The level is checked first inside `readOrgManagerChainIssue`.
958
+ const chainIssue = readOrgManagerChainIssue(resolver, '簽核節點');
959
+ if (chainIssue !== null) {
960
+ return chainIssue;
961
+ }
792
962
  }
793
963
  if ((resolver.type === 'FORM_MEMBER_MANAGER' ||
794
964
  resolver.type === 'FORM_ORG_UNIT_MANAGER') &&
@@ -826,13 +996,16 @@ function readApproverResolverIssue(resolver) {
826
996
  return null;
827
997
  }
828
998
  function readNodeActorSourceSkipReasons(node) {
829
- var _a, _b, _c;
999
+ var _a, _b, _c, _d;
830
1000
  return [
831
1001
  ...(((_a = node.data.entryCondition) === null || _a === void 0 ? void 0 : _a.trim()) ? ['entryCondition'] : []),
832
1002
  ...(((_b = node.data.excludeInitiator) === null || _b === void 0 ? void 0 : _b.whenEmpty) === 'SKIP_NODE'
833
1003
  ? ['skipNode']
834
1004
  : []),
835
- ...(((_c = node.data.sla) === null || _c === void 0 ? void 0 : _c.onTimeout) === 'AUTO_APPROVE'
1005
+ ...(((_c = node.data.excludeNodeActors) === null || _c === void 0 ? void 0 : _c.whenEmpty) === 'SKIP_NODE'
1006
+ ? ['skipNodeByNodeActors']
1007
+ : []),
1008
+ ...(((_d = node.data.sla) === null || _d === void 0 ? void 0 : _d.onTimeout) === 'AUTO_APPROVE'
836
1009
  ? ['autoApprove']
837
1010
  : []),
838
1011
  ];
@@ -841,7 +1014,61 @@ const NODE_ACTOR_SOURCE_SKIP_ISSUES = {
841
1014
  autoApprove: (label) => `簽核來源的前面關卡 ${label} 設有逾時自動核准,可能沒有人實際簽核。`,
842
1015
  entryCondition: (label) => `簽核來源的前面關卡 ${label} 設有進入條件,可能被跳過、沒有人簽核。`,
843
1016
  skipNode: (label) => `簽核來源的前面關卡 ${label} 在排除發起人後沒人時會跳過此關,可能沒有人簽核。`,
1017
+ skipNodeByNodeActors: (label) => `簽核來源的前面關卡 ${label} 在排除前面關卡的簽核者後沒人時會跳過此關,可能沒有人簽核。`,
1018
+ };
1019
+ const NODE_ACTOR_SOURCE_STAGE_TEXTS = {
1020
+ coverage: (stageLabels, nodeLabel) => `有一條路徑不會經過所選的前面關卡(${stageLabels.join('、')})就走到 ${nodeLabel},那一趟沒有人可以當簽核人。`,
1021
+ notUpstream: (stageLabel, nodeLabel) => `前面關卡 ${stageLabel} 不在 ${nodeLabel} 的上游,走不到這個節點,不能當簽核來源。`,
1022
+ skip: NODE_ACTOR_SOURCE_SKIP_ISSUES,
844
1023
  };
1024
+ const EXCLUDE_NODE_ACTORS_STAGE_TEXTS = {
1025
+ coverage: (stageLabels, nodeLabel) => `有一條路徑不會經過排除對象的前面關卡 ${stageLabels.join('、')} 就走到 ${nodeLabel},那一趟排除不到本輪的簽核者(退回重送後還會讀到上一輪的簽核者)。請改用分流,把兩關放在同一條路線上。`,
1026
+ notUpstream: (stageLabel, nodeLabel) => `前面關卡 ${stageLabel} 不在 ${nodeLabel} 的上游,走不到這個節點,無法排除該關的簽核者。`,
1027
+ skip: {
1028
+ autoApprove: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 設有逾時自動核准,可能沒有人實際簽核,這一關就排除不到任何人。請移除逾時自動核准,或改選其他關卡。`,
1029
+ entryCondition: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 設有進入條件,可能被跳過;被跳過時會改讀到上一輪的簽核者。請改用分流,把兩關放在同一條路線上。`,
1030
+ skipNode: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 在排除發起人後沒人時會跳過此關,可能被跳過;被跳過時會改讀到上一輪的簽核者。請改選其他處理方式。`,
1031
+ skipNodeByNodeActors: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 在排除前面關卡的簽核者後沒人時會跳過此關,可能被跳過;被跳過時會改讀到上一輪的簽核者。請改選其他處理方式。`,
1032
+ },
1033
+ };
1034
+ /**
1035
+ * The checks every "who signed an earlier stage" reader needs, shared by the
1036
+ * `NODE_ACTOR` source ({@link readNodeActorSourceIssues}) and
1037
+ * `excludeNodeActors` ({@link readExcludeNodeActorsIssues}). In this order,
1038
+ * all of them reported together:
1039
+ *
1040
+ * 1. a stage that may be skipped or pass without a signer
1041
+ * ({@link readNodeActorSourceSkipReasons});
1042
+ * 2. coverage: for each group in `coverageGroups`, walking from the start
1043
+ * without entering any stage of the group must not reach `nodeId`. A
1044
+ * `NODE_ACTOR` source passes its whole list as one group ("one of them
1045
+ * covers every path"); an exclusion passes one group per stage;
1046
+ * 3. every stage is upstream of `nodeId`.
1047
+ *
1048
+ * The caller has already checked that every id exists and is a `userTask`.
1049
+ */
1050
+ function readSourceStageIssues(definition, nodeId, stageIds, coverageGroups, texts) {
1051
+ const labelOf = (id) => {
1052
+ const node = definition.nodes.find((candidate) => candidate.id === id);
1053
+ return (node === null || node === void 0 ? void 0 : node.data.label) ? `「${node.data.label}」` : `「${id}」`;
1054
+ };
1055
+ const skippableIssues = stageIds.flatMap((id) => {
1056
+ const node = definition.nodes.find((candidate) => candidate.id === id);
1057
+ return (node === null || node === void 0 ? void 0 : node.type) === 'userTask'
1058
+ ? readNodeActorSourceSkipReasons(node).map((reason) => texts.skip[reason](labelOf(node.id)))
1059
+ : [];
1060
+ });
1061
+ const startNodeIds = definition.nodes
1062
+ .filter((node) => node.type === 'startEvent')
1063
+ .map((node) => node.id);
1064
+ const coverageIssues = coverageGroups.flatMap((group) => (0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, startNodeIds, new Set(group)).has(nodeId)
1065
+ ? [texts.coverage(group.map(labelOf), labelOf(nodeId))]
1066
+ : []);
1067
+ const notUpstreamIssues = stageIds
1068
+ .filter((id) => !(0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, [id]).has(nodeId))
1069
+ .map((id) => texts.notUpstream(labelOf(id), labelOf(nodeId)));
1070
+ return [...skippableIssues, ...coverageIssues, ...notUpstreamIssues];
1071
+ }
845
1072
  /**
846
1073
  * The graph checks behind a `NODE_ACTOR` / `NODE_ACTOR_MANAGER` node: the
847
1074
  * "who signed earlier" source only works when every case reaches `nodeId`
@@ -886,34 +1113,88 @@ function readNodeActorSourceIssues(definition, nodeId, sourceNodeIds) {
886
1113
  if (notUserTaskId !== undefined) {
887
1114
  return [`簽核來源的前面關卡 ${labelOf(notUserTaskId)} 不是簽核關卡。`];
888
1115
  }
889
- const skippableIssues = sourceNodes.flatMap((node) => (node === null || node === void 0 ? void 0 : node.type) === 'userTask'
890
- ? readNodeActorSourceSkipReasons(node).map((reason) => NODE_ACTOR_SOURCE_SKIP_ISSUES[reason](labelOf(node.id)))
891
- : []);
892
- const startNodeIds = definition.nodes
893
- .filter((node) => node.type === 'startEvent')
894
- .map((node) => node.id);
895
- const reachableWithoutSource = (0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, startNodeIds, new Set(sourceNodeIds));
896
- const coverageIssues = reachableWithoutSource.has(nodeId)
897
- ? [
898
- `有一條路徑不會經過所選的前面關卡(${sourceNodeIds
899
- .map(labelOf)
900
- .join('、')})就走到 ${labelOf(nodeId)},那一趟沒有人可以當簽核人。`,
901
- ]
902
- : [];
903
- const notUpstreamIssues = sourceNodeIds
904
- .filter((id) => !(0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, [id]).has(nodeId))
905
- .map((id) => `前面關卡 ${labelOf(id)} 不在 ${labelOf(nodeId)} 的上游,走不到這個節點,不能當簽核來源。`);
1116
+ const stageIssues = readSourceStageIssues(definition, nodeId, sourceNodeIds, [sourceNodeIds], NODE_ACTOR_SOURCE_STAGE_TEXTS);
906
1117
  const parallelBranches = (0, workflow_reachability_1.readWorkflowParallelBranchSets)(definition);
907
1118
  const parallelIssues = sourceNodeIds.flatMap((id, index) => sourceNodeIds
908
1119
  .slice(index + 1)
909
1120
  .filter((otherId) => parallelBranches.some((branches) => (0, workflow_reachability_1.isOnOwnParallelBranch)(branches, id, otherId)))
910
1121
  .map((otherId) => `前面關卡 ${labelOf(id)} 與 ${labelOf(otherId)} 在平行分支的不同路線上,兩關會同時簽核,不能一起當簽核來源。`));
911
- return [
912
- ...skippableIssues,
913
- ...coverageIssues,
914
- ...notUpstreamIssues,
915
- ...parallelIssues,
916
- ];
1122
+ return [...stageIssues, ...parallelIssues];
1123
+ }
1124
+ /**
1125
+ * `true` when a user task can end with more than one person having approved
1126
+ * it, judged from its definition alone. The engine reads "who signed this
1127
+ * stage" as the people with an approval in its newest batch, so a source that
1128
+ * several people approve gives a start that is not one person.
1129
+ *
1130
+ * - `SINGLE` and `PARALLEL_ANY` end on the first approval: one person;
1131
+ * - `PARALLEL_ALL` and `SEQUENTIAL` wait for every seat: several;
1132
+ * - `QUORUM` ends once the threshold is met: several when a `COUNT` threshold
1133
+ * is above 1 (the engine reads anything below 1 as 1), and always for a
1134
+ * `PERCENTAGE` threshold, whose head count only the org chart at run time
1135
+ * knows;
1136
+ * - a `DIRECT` resolver that lists one member can only ever give one person,
1137
+ * whatever the policy ({@link readDesignTimeApproverCount}), unless the
1138
+ * stage keeps the initiator (`excludeInitiator`) or an earlier stage's
1139
+ * approvers (`excludeNodeActors`) out with `whenEmpty: 'INITIATOR_MANAGER'`:
1140
+ * when that one member is excluded the stage goes to the initiator's
1141
+ * manager instead, who may be several people, so the policy decides.
1142
+ *
1143
+ * Deliberately conservative on the unknown side: a policy this build does not
1144
+ * recognise reads as one person, because the structural lint reports it.
1145
+ * The designer's stage picker for a manager chain's `NODE_ACTOR` start reads
1146
+ * this too, so it and {@link readSingleApproverSourceIssues} agree.
1147
+ */
1148
+ function canUserTaskHaveMultipleApprovers(node) {
1149
+ var _a, _b;
1150
+ // `approverResolver` and `decisionPolicy` can be missing on raw JSON that has
1151
+ // not been linted yet.
1152
+ const resolver = node.data.approverResolver;
1153
+ const policy = node.data.decisionPolicy;
1154
+ const designTimeCount = resolver
1155
+ ? readDesignTimeApproverCount(resolver)
1156
+ : null;
1157
+ // Who the initiator's manager is (and how many share the level) is only known
1158
+ // at run time, so a lone listed member is no guarantee of one person here.
1159
+ // Either exclusion can empty the stage and hand it over.
1160
+ const mayHandOverToInitiatorManagers = ((_a = node.data.excludeInitiator) === null || _a === void 0 ? void 0 : _a.whenEmpty) === 'INITIATOR_MANAGER' ||
1161
+ ((_b = node.data.excludeNodeActors) === null || _b === void 0 ? void 0 : _b.whenEmpty) === 'INITIATOR_MANAGER';
1162
+ if (!mayHandOverToInitiatorManagers &&
1163
+ designTimeCount !== null &&
1164
+ designTimeCount <= 1) {
1165
+ return false;
1166
+ }
1167
+ if ((policy === null || policy === void 0 ? void 0 : policy.type) === 'PARALLEL_ALL' || (policy === null || policy === void 0 ? void 0 : policy.type) === 'SEQUENTIAL') {
1168
+ return true;
1169
+ }
1170
+ return ((policy === null || policy === void 0 ? void 0 : policy.type) === 'QUORUM' &&
1171
+ (policy.thresholdType === 'PERCENTAGE' || policy.threshold > 1));
1172
+ }
1173
+ /**
1174
+ * The publish-time issues with the stages a manager chain's `NODE_ACTOR`
1175
+ * start names, beyond what {@link readNodeActorSourceIssues} checks: every
1176
+ * stage must end with one person's approval. A stage several people approve
1177
+ * would make the start ambiguous, and the engine can only say so when the last
1178
+ * of them approves, inside that decision's own transaction, which then never
1179
+ * commits. Returns Chinese issue lines; `[]` means every named stage is a
1180
+ * single-signer stage.
1181
+ *
1182
+ * `NODE_ACTOR_MANAGER` takes the managers of everyone who signed and so has no
1183
+ * such limit; this check is deliberately not part of
1184
+ * {@link readNodeActorSourceIssues}. A stage that does not exist or is not a
1185
+ * user task is reported there, not here.
1186
+ */
1187
+ function readSingleApproverSourceIssues(definition, sourceNodeIds) {
1188
+ return sourceNodeIds.flatMap((id) => {
1189
+ const node = definition.nodes.find((candidate) => candidate.id === id);
1190
+ if ((node === null || node === void 0 ? void 0 : node.type) !== 'userTask' || !canUserTaskHaveMultipleApprovers(node)) {
1191
+ return [];
1192
+ }
1193
+ const label = node.data.label ? `「${node.data.label}」` : `「${id}」`;
1194
+ return [
1195
+ `主管鏈起點的前面關卡 ${label} 可能有多位實際簽核人,無法決定從誰的主管開始。請改選只會有一位簽核人的關卡。`,
1196
+ ];
1197
+ });
917
1198
  }
918
1199
  function hasConfiguredConditionEdges(definition) {
919
1200
  return definition.edges.some((edge) => isExclusiveGatewaySourceEdge(edge, definition.nodes) &&
@@ -988,12 +1269,20 @@ function readNotifyRecipientsIssue(recipients) {
988
1269
  (!recipients.orgUnitId.trim() || !recipients.positionId.trim())) {
989
1270
  return '知會節點需要指定組織與職位。';
990
1271
  }
1272
+ // Hand-written or AI-written JSON may carry no `orgUnitId` at all.
1273
+ if (recipients.type === 'ORG_UNIT_MANAGER' &&
1274
+ (typeof recipients.orgUnitId !== 'string' || !recipients.orgUnitId.trim())) {
1275
+ return '知會節點需要指定組織。';
1276
+ }
991
1277
  if (recipients.type === 'DYNAMIC_FORM' && !recipients.formPath.trim()) {
992
1278
  return '知會節點需要指定知會對象的表單欄位。';
993
1279
  }
994
1280
  if (recipients.type === 'EXPRESSION' && !recipients.expression.trim()) {
995
1281
  return '知會節點需要指定知會對象的運算式。';
996
1282
  }
1283
+ if (recipients.type === 'ORG_MANAGER_CHAIN') {
1284
+ return readOrgManagerChainIssue(recipients, '知會節點');
1285
+ }
997
1286
  return null;
998
1287
  }
999
1288
  /**