@stalfh233/omc-cli 0.4.59 → 0.4.60

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.
@@ -823,7 +823,7 @@ export const RULE_NODE_DEFAULT_NAMES = Object.freeze({
823
823
  /** Canonical node shapes per nodeType, transcribed from the production capture. */
824
824
  export const RULE_NODE_TEMPLATES = Object.freeze({
825
825
  UPDATE: {
826
- description: "数据更新:`filterCondition` = **查找满足条件的主表数据**(要更新哪条;DYNAMIC 过滤来源=currentSchemaDataItem,value 不出现);**空 mainConditions = 无条件下更新整表**(bench23 病灶,干 lint 给 warning)。`filterSubCondition` = 查找满足条件的子表数据。`dataActions` 赋值(DYNAMIC 来源=currentDataItemValue,value 留空;**EXPRESSION 公式赋值**:`ruleValueType:\"EXPRESSION\"` + `value` 为含非空 `formula` 的对象(可带 `editorText`/`editorMark`,平台编辑器解析要用到)。字段引用语法是 `{字段code}`,如 `{\"formula\":\"{amount}-{cost}\"}`;日期差用 **`DATEDIF({end},{begin},\"<unit>\")`**(unit 小写:`y` 年 / `M` 月 / `d` 日 / `h` 时 / `m` 分 / `s` 秒;两个日期字段直接相减不成立)。公式语言**支持 IF/比较运算符**(真机 2026-10-07:`IF(DATEDIF({actual},{eta},\"h\")>0,INTDOWN(DATEDIF({actual},{eta},\"h\")),0)` 计算正确);取整用 `INTDOWN`(floor)/`INTUP`(ceil)/`ROUND`;**`MAX`/`MIN` 是集合聚合函数,不是逐值函数**——`MAX(0,x)` 报 `Wrong number of args (2) passed to: MAX` 50000,两值取大用 `IF(x>y,x,y)`。无效公式会让整个 rule.save/update 50000(非静默)。公式只在**数据更新(Update)** 触发链持久化,Create 触发的自更新本 build 不落库;同批更新内公式读**更新前**值——`UPDATE 算 X → BRANCH 判 X` 这类同链两步也读旧值,不成立);`dataCondition` = 输入数据满足条件时才执行;`childTriggerConditionType` = 子表触发范围 ADD/MODIFY/DELETE。inputParam 必须是 \"_input\"(或 \"_<QUERY节点>_output\")。平台标签证据:i18n 模块 196233 节点属性表 + evidence/form-conditions-and-rules-truth.md §4。",
826
+ description: "数据更新:`filterCondition` = **查找满足条件的主表数据**(要更新哪条;DYNAMIC 过滤来源=currentSchemaDataItem,value 不出现);**空 mainConditions = 无条件下更新整表**(bench23 病灶,干 lint 给 warning)。`filterSubCondition` = 查找满足条件的子表数据。`dataActions` 赋值(DYNAMIC 来源=currentDataItemValue,value 留空;**EXPRESSION 公式赋值**:`ruleValueType:\"EXPRESSION\"` + `value` 为含非空 `formula` 的对象(可带 `editorText`/`editorMark`,平台编辑器解析要用到)。字段引用语法是 `{字段code}`,如 `{\"formula\":\"{amount}-{cost}\"}`;**文本合并只能写 `STRING({a},\"-\",{b})`**(真机 2026-10-20:`{a}+\"-X\"` 在文本目标上静默写 null,`+` 是数值加法;`CONCAT/CONCATENATE/STRCAT/JOIN` 未注册,提交报 101005 表达式格式错误);日期差用 **`DATEDIF({end},{begin},\"<unit>\")`**(unit 小写:`y` 年 / `M` 月 / `d` 日 / `h` 时 / `m` 分 / `s` 秒;两个日期字段直接相减不成立)。公式语言**支持 IF/比较运算符**(真机 2026-10-07:`IF(DATEDIF({actual},{eta},\"h\")>0,INTDOWN(DATEDIF({actual},{eta},\"h\")),0)` 计算正确);取整用 `INTDOWN`(floor)/`INTUP`(ceil)/`ROUND`;**`MAX`/`MIN` 是集合聚合函数,不是逐值函数**——`MAX(0,x)` 报 `Wrong number of args (2) passed to: MAX` 50000,两值取大用 `IF(x>y,x,y)`。无效公式会让整个 rule.save/update 50000(非静默)。公式只在**数据更新(Update)** 触发链持久化,Create 触发的自更新本 build 不落库;同批更新内公式读**更新前**值——`UPDATE 算 X → BRANCH 判 X` 这类同链两步也读旧值,不成立);`dataCondition` = 输入数据满足条件时才执行;`childTriggerConditionType` = 子表触发范围 ADD/MODIFY/DELETE。inputParam 必须是 \"_input\"(或 \"_<QUERY节点>_output\")。平台标签证据:i18n 模块 196233 节点属性表 + evidence/form-conditions-and-rules-truth.md §4。",
827
827
  template: {
828
828
  nodeCode: "UPDATE_1", nodeName: "数据更新", nodeType: "UPDATE", targetObjectCode: "<target schemaCode>", inputParam: "_input", updateNodeType: "UPDATE_MODEL", execFormMapping: false,
829
829
  filterCondition: { mainConditions: [[{ targetSchemaDataItem: "<target field>", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "<trigger field>" }]], sheetConditions: [] },
@@ -1091,6 +1091,58 @@ export function validateConditionLeaf(leaf, nodeCode, where, warnings, legacyTol
1091
1091
  }
1092
1092
  return blockers;
1093
1093
  }
1094
+ /**
1095
+ * The formula engine's registered function names — the `CalculateMethodType`
1096
+ * enum in cloudpivot-shared (`CalculateRuleExecutor` registers one Aviator
1097
+ * function per code). A name outside this set makes the platform answer
1098
+ * `errcode 101005 表达式格式错误` at submit time (1.1.25 真机 2026-10-20).
1099
+ */
1100
+ const FORMULA_FUNCTIONS = new Set([
1101
+ "STRING", "SUBSTRING", "REPLACE", "UPPERMONEY", "IF", "CONTAINS", "GETADDRESS", "LEN",
1102
+ "LOWER", "UPPER", "SEARCH", "STARTSWITH", "SUBSTITUTE", "TRIM", "GETUNITNAME", "LEFT",
1103
+ "RIGHT", "TEXT", "UNION", "DATE", "COUNTIF", "SUM", "AVG", "MAX", "MIN", "COUNT", "ROUND",
1104
+ "INTUP", "INTDOWN", "VALUE", "FIXED", "ABS", "CEILING", "FLOOR", "COS", "SIN", "LARGE",
1105
+ "SMALL", "LOG", "MOD", "PI", "POWER", "RAND", "SQRT", "SUMIF", "DATEDIF", "YEAR", "MONTH",
1106
+ "DAY", "WEEKNUM", "WEEKDAY", "QUARTER", "HOUR", "MINUTE", "SECOND", "ADDDAY", "ADDMONTH",
1107
+ "ADDYEAR", "TODAY", "BETWEEN", "NOTBETWEEN", "ADDHOUR", "ADDMINUTE", "NOW", "CASE", "ISNULL",
1108
+ ]);
1109
+ /** Names agents reach for that the engine does NOT register, with the real one. */
1110
+ const FORMULA_FUNCTION_ALIASES = Object.freeze({
1111
+ CONCAT: "STRING",
1112
+ CONCATENATE: "STRING",
1113
+ STRCAT: "STRING",
1114
+ JOIN: "STRING",
1115
+ });
1116
+ /**
1117
+ * Advisory lint for EXPRESSION formulas. The platform validates formulas only at
1118
+ * submit time and the failure is loud for unknown functions (101005) but SILENT
1119
+ * for text arithmetic: `{a}+"-X"` on a text target stores null, because `+` is
1120
+ * numeric. Text concatenation must use `STRING(a,b,...)` (真机 2026-10-20).
1121
+ */
1122
+ export function formulaWarnings(action, nodeCode, index) {
1123
+ if (String(action.ruleValueType ?? "") !== "EXPRESSION")
1124
+ return [];
1125
+ const value = action.value;
1126
+ if (!value || typeof value !== "object" || Array.isArray(value))
1127
+ return [];
1128
+ const formula = value.formula;
1129
+ if (typeof formula !== "string" || !formula.trim())
1130
+ return [];
1131
+ const warnings = [];
1132
+ const where = `dataActions[${index}]`;
1133
+ for (const match of formula.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*\(/g)) {
1134
+ const name = match[1].toUpperCase();
1135
+ const alias = FORMULA_FUNCTION_ALIASES[name];
1136
+ if (alias) {
1137
+ warnings.push({ code: "rule-graph-formula-function-alias", nodeCode, key: "value", severity: "warning", message: `${where}: 公式里的 ${match[1]}() 不是平台函数;文本合并用 ${alias}({a},"-",{b})。别名写法在提交时报 errcode 101005 表达式格式错误。` });
1138
+ continue;
1139
+ }
1140
+ if (FORMULA_FUNCTIONS.has(name))
1141
+ continue;
1142
+ warnings.push({ code: "rule-graph-formula-function-unknown", nodeCode, key: "value", severity: "warning", message: `${where}: 公式函数 ${match[1]}() 不在平台注册表(CalculateMethodType)——提交时会报 101005 表达式格式错误。可用函数见 omc help omc-business-configuration 或 cpdocs BusinessRulesTrigger.md「表达式函数」。` });
1143
+ }
1144
+ return warnings;
1145
+ }
1094
1146
  /** Validate one dataAction; returns blockers (empty = clean). */
1095
1147
  export function validateDataAction(action, nodeCode, index) {
1096
1148
  const blockers = [];
@@ -1479,6 +1531,21 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
1479
1531
  if (assignIndex >= 0 && assignIndex > systemIndex)
1480
1532
  blockers.push({ code: "rule-graph-assign-persistence-order", nodeCode: String(systemNode.nodeCode ?? ""), key: "nodes", message: `ASSIGN(CURRENT) 的赋值必须排在 ${String(systemNode.nodeType)} 之前(Start→ASSIGN→${String(systemNode.nodeType)}→End),否则赋值合并进 _input 的时机晚于持久化,读回无变化(真机 2026-09-28)。将 ASSIGN 移到系统持久化节点前。` });
1481
1533
  }
1534
+ // A SELF-update in a Create rule must run AFTER SYSTEM_CREATE: the UPDATE
1535
+ // node queries its target rows, and before the trigger row is persisted that
1536
+ // query matches nothing. Observed live 2026-10-20: UPDATE_1 before
1537
+ // SYSTEM_CREATE saved/published/enabled fine, but UpdateServiceExecutor
1538
+ // logged 「查找范围不存在数据」 and the field kept its old value; moving the
1539
+ // same node after SYSTEM_CREATE wrote it. This is the one action type that
1540
+ // must sit AFTER persistence (ASSIGN/BIZ_ACTION must sit before).
1541
+ if (String(systemNode.nodeType ?? "") === "SYSTEM_CREATE" && schemaCode) {
1542
+ const selfUpdateIndex = nodeRecords.findIndex((node) => {
1543
+ const target = String(node.targetObjectCode ?? "");
1544
+ return String(node.nodeType) === "UPDATE" && (target === "" || target === schemaCode);
1545
+ });
1546
+ if (selfUpdateIndex >= 0 && selfUpdateIndex < systemIndex)
1547
+ blockers.push({ code: "rule-graph-update-before-persistence-order", nodeCode: String(nodeRecords[selfUpdateIndex].nodeCode ?? ""), key: "nodes", message: `Create 规则里的自更新 UPDATE 必须排在 SYSTEM_CREATE 之后(Start→SYSTEM_CREATE→UPDATE→End)——排在前面时规则能存能启用,但运行时查不到触发行(日志「查找范围不存在数据」),字段静默不写回(真机 2026-10-20 前后对照)。把它挪到系统节点之后。` });
1548
+ }
1482
1549
  }
1483
1550
  // inputParam chaining: only "_input" or "_<existing nodeCode>_output".
1484
1551
  for (const node of nodeRecords) {
@@ -1535,8 +1602,10 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
1535
1602
  if (actions.length === 0)
1536
1603
  blockers.push({ code: "rule-graph-update-without-actions", nodeCode, key: "dataActions", message: "UPDATE 节点至少需要 1 个 dataAction" });
1537
1604
  actions.forEach((action, index) => {
1538
- if (action && typeof action === "object")
1605
+ if (action && typeof action === "object") {
1539
1606
  blockers.push(...validateDataAction(action, nodeCode, index));
1607
+ warnings.push(...formulaWarnings(action, nodeCode, index));
1608
+ }
1540
1609
  else
1541
1610
  blockers.push({ code: "rule-graph-action-not-object", nodeCode, key: `dataActions[${index}]`, message: "dataAction 必须是对象" });
1542
1611
  });
@@ -1690,14 +1759,18 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
1690
1759
  }
1691
1760
  if (Array.isArray(node.dataActions))
1692
1761
  node.dataActions.forEach((action, index) => {
1693
- if (action && typeof action === "object")
1762
+ if (action && typeof action === "object") {
1694
1763
  blockers.push(...validateDataAction(action, nodeCode, index));
1764
+ warnings.push(...formulaWarnings(action, nodeCode, index));
1765
+ }
1695
1766
  });
1696
1767
  }
1697
1768
  if (nodeType === "ASSIGN" && Array.isArray(node.dataActions)) {
1698
1769
  node.dataActions.forEach((action, index) => {
1699
- if (action && typeof action === "object")
1770
+ if (action && typeof action === "object") {
1700
1771
  blockers.push(...validateDataAction(action, nodeCode, index));
1772
+ warnings.push(...formulaWarnings(action, nodeCode, index));
1773
+ }
1701
1774
  });
1702
1775
  }
1703
1776
  // ASSIGN.filterCondition = 「满足条件时触发赋值」(与 UPDATE 同构的定位/触发条件),
@@ -1141,6 +1141,23 @@ export function registerRuntimeDataLane(tools) {
1141
1141
  note: "下一级审批:用这些字段调 data.submit(同意传 agree:true / formData 回填)。",
1142
1142
  };
1143
1143
  }
1144
+ else if (sequenceStatus === "PROCESSING") {
1145
+ // r4-training 2026-10-20: an unresolvable participant (role with no
1146
+ // members, member not attached to the originator's department, or a
1147
+ // participant function that is not registered) makes the engine
1148
+ // create NO next work item while the submit still answers errCode 0.
1149
+ // Without this line the caller cannot tell "flow advanced" from
1150
+ // "flow silently stalled" — surface it instead of staying quiet.
1151
+ readback.flowWarning = {
1152
+ code: "flow-no-next-workitem",
1153
+ message: "提交成功但该实例没有生成下一节点待办,流程停在 PROCESSING。常见原因:审批参与人解析为空(角色无成员 / 成员未挂发起人部门 / 参与人函数未注册)。",
1154
+ checks: [
1155
+ `omc call workitem.list --args '{"workflowInstanceId":"${effectiveInstanceId}"}'(稍后重查,排除异步派发延迟)`,
1156
+ "容器日志搜 GetParticipantInfo error / CustomParticipleMethod{...} -> []",
1157
+ "参与人优先用参与者对象数组(角色 id);内置函数只认 FindUserByRole(org, roleCode[, order])",
1158
+ ],
1159
+ };
1160
+ }
1144
1161
  }
1145
1162
  catch {
1146
1163
  // best-effort
@@ -47,7 +47,7 @@ export const WORKFLOW_WIRE = Object.freeze({
47
47
  /** Workflow template object (workflow/update|publish raw body). */
48
48
  export const workflowTemplateArg = z
49
49
  .record(z.string(), z.unknown())
50
- .describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色用不依赖 OU 的解析(二开 FindUsersSpecByRoleFunction(code) 全局找人,或参与人挂 OU 树可覆盖的部门)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template.`);
50
+ .describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空时实例同样卡 PROCESSING 且无待办,验收看 workitem.list 而不是 data.submit 返回(r4-training 实证 2026-10-20)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template.`);
51
51
  const WORKFLOW_SCOPE = "greenfield workflow authoring";
52
52
  export const WORKFLOW_LANE_CONTRACT = Object.freeze({
53
53
  lane: LANES.workflow,
@@ -492,6 +492,20 @@ function participantReferenceWarnings(template) {
492
492
  if (raw === undefined || raw === null || String(raw).trim().length === 0)
493
493
  continue; // platform may fill later
494
494
  const participant = String(raw).trim();
495
+ // r4-training 2026-10-20: two function names that show up in read-back /
496
+ // seeded templates are NOT registered at runtime in AI-Platform v1.1.25 —
497
+ // they publish fine, then ParticipantActivityRuntime throws
498
+ // FunctionNotFoundException, the instance stalls in PROCESSING and the
499
+ // submit still answers errCode 0. Flag them by name (advisory, not a block).
500
+ const unregistered = /^(FindUsersByRoleFunction|FindUsersSpecByRoleFunction)\s*\(/.exec(participant);
501
+ if (unregistered) {
502
+ warnings.push({
503
+ activityCode: String(activity.activityCode ?? ""),
504
+ participant,
505
+ warning: `参与者函数 ${unregistered[1]}(...) 在 AI-Platform v1.1.25 未注册(运行期 FunctionNotFoundException,实例卡 PROCESSING 且提交仍回 errCode 0)。内置写法只有 FindUserByRole(org, roleCode[, order]);跨部门/全局角色优先用参与者对象数组(角色 id)。不要从 workflow.get_published 或种子流程回抄这个函数名。`,
506
+ });
507
+ continue;
508
+ }
495
509
  if (isPlatformParticipantReference(participant))
496
510
  continue;
497
511
  warnings.push({
@@ -12,25 +12,28 @@ const ruleGraph = Object.freeze({
12
12
  nodeGraph: {
13
13
  nodes: [
14
14
  { nodeCode: "Start", nodeType: "START" },
15
- { nodeCode: "UPDATE_1", nodeType: "UPDATE", targetObjectCode: model, inputParam: "_input", filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] }, dataActions: [{ targetDataItem: "status", ruleValueType: "FIXED", value: "待处理", ruleActionType: "EQUALS" }] },
16
15
  { nodeCode: "Create", nodeName: "新增数据", nodeType: "SYSTEM_CREATE", isSystemNode: true },
16
+ { nodeCode: "UPDATE_1", nodeType: "UPDATE", targetObjectCode: model, inputParam: "_input", filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] }, dataActions: [{ targetDataItem: "status", ruleValueType: "FIXED", value: "待处理", ruleActionType: "EQUALS" }] },
17
17
  { nodeCode: "End", nodeType: "END" },
18
18
  ],
19
19
  routes: [
20
- { preNode: "Start", postNode: "UPDATE_1" },
21
- { preNode: "UPDATE_1", postNode: "Create" },
22
- { preNode: "Create", postNode: "End" },
20
+ { preNode: "Start", postNode: "Create" },
21
+ { preNode: "Create", postNode: "UPDATE_1" },
22
+ { preNode: "UPDATE_1", postNode: "End" },
23
23
  ],
24
24
  },
25
25
  publish: true,
26
26
  },
27
27
  preconditions: ["模型和 target field 已发布;Create 是平台预置触发规则;目标字段可由规则动作写入"],
28
- conditionalRequirements: ["BIZ_ACTION 的 outputMappings 必须在 SYSTEM_CREATE/SYSTEM_UPDATE 前被消费;自定义 ruleCode 不会被数据事件自动分发"],
28
+ conditionalRequirements: [
29
+ "**自更新 UPDATE 必须排在 SYSTEM_CREATE 之后**(Start→SYSTEM_CREATE→UPDATE→End)——排在前面时规则能存能启用,运行时 `UpdateServiceExecutor` 查不到触发行(日志「查找范围不存在数据,不再做修改数据动作」),字段静默不写回。真机对照 2026-10-20:UPDATE 在 SYSTEM_CREATE 前 → 值不变;挪到其后 → 值写入成功。注意与 ASSIGN/BIZ_ACTION 相反,那两类要排在系统节点之前。",
30
+ "BIZ_ACTION 的 outputMappings 必须在 SYSTEM_CREATE/SYSTEM_UPDATE 前被消费;自定义 ruleCode 不会被数据事件自动分发",
31
+ ],
29
32
  priorToolReferences: [
30
33
  { tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认 status 已发布" },
31
34
  ],
32
35
  dryRun: { supported: true, command: `omc call rule.save --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 执行 rule graph dry lint;不会保存/发布规则" },
33
- expectedReadback: { performed: { published: true, enabled: false }, readback: { published: true, next: "rule.enable", inputParam: "_input", systemNodeOrder: "UPDATE_1 → SYSTEM_CREATE" } },
36
+ expectedReadback: { performed: { published: true, enabled: false }, readback: { published: true, next: "rule.enable", inputParam: "_input", systemNodeOrder: "SYSTEM_CREATE → UPDATE_1" } },
34
37
  refusalRecovery: [
35
38
  { code: "rule-graph-validation-failed", action: "按 validation 修正 nodeType、路线、_input 和系统节点顺序后重新 dry-run" },
36
39
  { code: "rule-schema-unresolved", action: "先确认模型存在,不要让 list/save 为幻影 schema 补建预置规则" },
@@ -27,6 +27,8 @@ const workflowTemplateUpdate = Object.freeze({
27
27
  "多级审批:为每个审批节点各写一个 PARTICIPANT 活动(activityCode 唯一),END 放最后;数组里按顺序就是审批链顺序;新增的审批节点由工具自动接入链路(补全到 END 的连线),无需自己写 routes;",
28
28
  "**{Originator}=发起人自己**:审批节点用它=提交人自审自己——提交人与审批人同账号时平台可能直接自动通过(COMPLETED,无待办;r40 实证,与节点结构有关)。需要独立审批人时绑定具体用户 id 或 `FindUserByRole({Originator.OU},\"<角色code>\",\"random\")`;验收要 PENDING 待办时先确认审批人≠提交人;",
29
29
  "**participant 必须是平台可解析的三种形态之一(真实生产 2026-09-23 观察)**:(a) 关键字 `{Originator}`;(b) 参与者对象数组 JSON `[{\"code\":\"<角色code>\",\"corpId\":\"<corpId|other>\",\"id\":\"<roleId>\",\"name\":\"<角色名>\",\"type\":2}]`(type 2=角色,3=用户;多个用 `+` 连接);(c) 函数 `FindUserByRole({Originator.OU},\"<roleCode>\",\"random\")`。**自由文本角色名(如 `部门负责人`)能 publish 但解析不到人、不生成待办**——先用 `org.role.list` / `org.department.list` 拿 code/id 再构造。工具会在 readback 里以 `participantWarnings` 标出自由文本。",
30
+ "**不要从读回/种子配置回抄函数式参与人**:`workflow.get_published` 的摘要与平台种子流程(如 crm_contract_flow)会显示 `FindUsersByRoleFunction(...)` / `FindUsersSpecByRoleFunction(...)`,1.1.25 运行期**未注册这两个名字**,原样写回会在 ParticipantActivityRuntime 抛 `FunctionNotFoundException`;参与人表达式只认 `FindUserByRole(org, roleCode[, order])`(r4-training 实证 2026-10-20,`cloudpivot-workflow-AI-Platform-v1.1.25.jar` 只有 `FindUserByRoleFunction`)。",
31
+ "**参与人解析为空同样是静默失败**:`FindUserByRole({Originator.OU},...)` 只找该部门范围内的角色成员;角色成员未挂部门(或角色零成员)时返回 `[]`,实例卡 PROCESSING、无待办、接口仍回 errCode 0。验收必须看 `workitem.list` 或容器日志 `CustomParticipleMethod{...} -> []`,不能只看 `data.submit` 返回;需要跨部门/全局角色时优先用参与者对象数组(角色 id),或先给角色补齐部门范围成员。",
30
32
  "**审批节点必须带 sheetCode(=模型默认表单槽位,通常等于 schemaCode)——S29 契约:participant sheetCode 为空 publish 会 401026 WORKFLOW_ACTIVITY_NO_SHEET。工具已自动回填,正常无需手填**;",
31
33
  "节点类型 START / PARTICIPANT / END;省略的既有节点由 merge 保留;",
32
34
  "**平台 workflow.create 会自动生成含「填写申请单」发起节点的草稿(START → 填写申请单 → 审批 → END);workflow.update 是 merge,不删除既有节点——要严格三节点需在设计器删该节点,或如实接受该发起节点**",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.59",
3
+ "version": "0.4.60",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,9 +50,9 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@stalfh233/omc-fake-platform": "0.4.59",
53
+ "@stalfh233/omc-fake-platform": "0.4.60",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.59",
55
+ "omc-core": "0.4.60",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -13,6 +13,7 @@ A change is accepted only when every gate below holds.
13
13
  2. **Authoritative readback** — prove the intended state from the authoritative face (`field.list`, `form.get`, `data.load`, `onlinejs.get` for Online JS, `model.inspect` for workflow/rules), pairing each write with its same-family reader (`onlinejs.compose`→`onlinejs.get`; `report.datasource.link`→`linkedCount>0`). "Called without error" is not acceptance.
14
14
  2a. **Judge must be decidable** — prove the field exists first; all-same values (`false`/`null`/`0`) mean a wrong judge, not a pass. `form.get` has no `customScript` key — use `onlinejs.get`.
15
15
  2b. **Reachable ≠ correct** — `method.test success:true` proves reachability only; without a contract report "接口连通,业务对接未验证(契约缺失)".
16
+ 2c. **Allowed ≠ advanced** — `data.submit` → `PROCESSING` only proves the instance exists. Prove the next task via `workitem.list` or a later submit; a stalled instance still answers `errCode 0`.
16
17
  3. **Delivery state recorded** — the `.omc/` artifact exists with `deliveryState: DELIVERED` and the environment fingerprint.
17
18
  4. **Zero residue** — fixtures removed and exact absence proven on every face (draft and published); final state CLEANED.
18
19
 
@@ -150,6 +150,7 @@ rules:
150
150
 
151
151
  - **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
152
152
  - 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
153
+ - **参与人解析失败是静默的**:角色零成员、成员未挂部门(`FindUserByRole({Originator.OU},...)` 只找部门范围)、或写了未注册函数(`FindUsersByRoleFunction` / `FindUsersSpecByRoleFunction`,1.1.25 未内置)时,`data.submit` 仍回 `errCode 0`,实例停在 PROCESSING 且 `biz_workitem` 无下一节点。验收看 `workitem.list` 是否有新待办,或容器日志的 `GetParticipantInfo error` / `CustomParticipleMethod{...} -> []`;不要从 `workflow.get_published` 的摘要回抄函数式参与人。内置写法只有 `FindUserByRole(org, roleCode[, order])`,跨部门/全局角色优先用参与者对象数组(角色 id)。
153
154
 
154
155
  ## 流程字段权限
155
156