@stalfh233/omc-cli 0.4.64 → 0.4.65

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.
@@ -1664,7 +1664,7 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
1664
1664
  code: "rule-graph-cross-model-child-source-fanout",
1665
1665
  nodeCode,
1666
1666
  key: "filterCondition.mainConditions",
1667
- message: `${nodeCode}: 跨模型 UPDATE 用子表字段做定位条件(${childSourcedLeaves.join("、")})——1.1.25 把 "子表.字段" 解析成**集合**,目标主表仍只查询一次,不会按每条子表行各更新一次,运行结果是 0 行命中(r5-asset 2026-10-20 真机:保存/发布全绿、状态不变)。单行回写改用主表字段做 DYNAMIC 来源;一张单多行逐件回写需二开(BIZ_ACTION 自调接口一次处理多行)。`,
1667
+ message: `${nodeCode}: 跨模型 UPDATE 的定位条件用了点号来源(${childSourcedLeaves.join("、")})——1.1.25 的 DatasetImpl 把 "x.y" 当作**子表路径**解析(parts[0] 找子表、收集该列所有行的值),既不是"关联字段.id",也不会按每行各更新一次:目标主表仍只查询一次,结果 0 行命中(r5/r6 真机:保存发布全绿、字段不变)。单跳关联字段直接用字段 code(引擎会取关联对象的 id,真机 2026-10-21 已验证);多跳(a.b)没有可用形状——把目标 id 用表单映射落到本模型的一个字段上再引用它,或走二开。`,
1668
1668
  });
1669
1669
  if (!targetIsChild && Array.isArray(filterRecord.sheetConditions) && filterRecord.sheetConditions.length > 0)
1670
1670
  blockers.push({
@@ -85,6 +85,57 @@ const WORKFLOW_ROUTE_CONDITION_KEYS = ["text", "formula", "utilizeElse", "fixedP
85
85
  * and the line silently never fires (the same class of silent no-op that bit
86
86
  * checkType=2). Only caller-supplied `routes`/`rules` are checked.
87
87
  */
88
+ /**
89
+ * AI-Platform 1.1.25 rejects a template whose first activity after START is not
90
+ * the originator fill node: WorkflowTemplateValidateUtility.validateStartActivity
91
+ * emits 401035 「发起节点的参与者只能是{Originator}」 and publish answers
92
+ * errcode 0 with a ValidateMessage[] (nothing is stored). Live 2026-10-21: a
93
+ * role-approval node placed directly after START failed publish with exactly
94
+ * that code, and the generic message did not name the cause.
95
+ */
96
+ function assertFirstActivityIsOriginator(template, tool, merged) {
97
+ // Only gate AUTHORING intent: the caller must have sent both a START node and
98
+ // routes. A partial/stored-draft publish is left to the platform validator
99
+ // (401035), which the tool already surfaces faithfully.
100
+ const callerRows = callerActivities(template);
101
+ const callerRoutes = template.routes ?? template.rules ?? template.edges;
102
+ if (!Array.isArray(callerRows) || !Array.isArray(callerRoutes))
103
+ return;
104
+ if (!callerRows.some((activity) => String(activity.activityType ?? activity.type ?? "") === "START"))
105
+ return;
106
+ // Assert on the MERGED body: participant shorthands ({type:Originator,name:…})
107
+ // are normalized to "{Originator}" there.
108
+ const activities = callerActivities(merged ?? template);
109
+ const rawRoutes = merged ? (merged.routes ?? merged.rules ?? merged.edges) : callerRoutes;
110
+ if (!Array.isArray(activities) || !Array.isArray(rawRoutes))
111
+ return;
112
+ const start = activities.find((activity) => String(activity.activityType ?? activity.type ?? "") === "START");
113
+ if (!start)
114
+ return;
115
+ const startCode = activityCodeOf(start);
116
+ const first = rawRoutes
117
+ .map((entry) => (entry ?? {}))
118
+ .find((route) => String(route.preNode ?? route.preActivityCode ?? route.pre ?? route.from ?? "") === startCode);
119
+ if (!first)
120
+ return;
121
+ const nextCode = String(first.postNode ?? first.postActivityCode ?? first.post ?? first.to ?? "");
122
+ const next = activities.find((activity) => activityCodeOf(activity) === nextCode);
123
+ if (!next)
124
+ return;
125
+ const type = String(next.activityType ?? next.type ?? "");
126
+ const participant = String(next.participant ?? "").trim();
127
+ if (type === "PARTICIPANT" && /^\{Originator\}$/i.test(participant))
128
+ return;
129
+ throw new ToolRefusalError({
130
+ status: "refused",
131
+ blocker: "workflow-first-activity-not-originator",
132
+ workflowCode: String(template.workflowCode ?? template.code ?? ""),
133
+ activityCode: nextCode,
134
+ message: `${tool}: START 之后的第一个节点必须是参与者为 {Originator} 的填写节点(当前是 ${type || "?"}「${String(next.activityName ?? next.name ?? nextCode)}」${participant ? `,参与者 ${participant}` : ""})。` +
135
+ `平台校验 WorkflowTemplateValidateUtility.validateStartActivity 对此报 401035「发起节点的参与者只能是{Originator}」,publish 回 errcode 0 但一个 ValidateMessage[] 就把模板挡在门外、不落库(真机 2026-10-21)。` +
136
+ `正确形状:Start → 填写({Originator}) → 审批/条件分支 … → End;审批节点不能直接接在 START 后。`,
137
+ });
138
+ }
88
139
  function validateWorkflowRouteConditions(template, tool) {
89
140
  const raw = template.routes ?? template.rules ?? template.edges;
90
141
  if (!Array.isArray(raw))
@@ -1818,6 +1869,7 @@ export function registerWorkflowLane(tools) {
1818
1869
  const merge = args.merge === true || (args.merge !== false && activitiesLackBoundaries(template));
1819
1870
  const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, merge);
1820
1871
  await backfillDesignerDefaults(session, args.schemaCode, body, template);
1872
+ assertFirstActivityIsOriginator(template, "workflow.update", body);
1821
1873
  validateWorkflowRender(body, "workflow.update");
1822
1874
  // Live: workflow/update does NOT run the template validator — it stores a
1823
1875
  // refused participant silently and only /publish answers the
@@ -1868,6 +1920,7 @@ export function registerWorkflowLane(tools) {
1868
1920
  // boundaries is a partial edit → delta merge (see activitiesLackBoundaries).
1869
1921
  const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, activitiesLackBoundaries(template));
1870
1922
  await backfillDesignerDefaults(session, args.schemaCode, body, template);
1923
+ assertFirstActivityIsOriginator(template, "workflow.publish", body);
1871
1924
  const unregistered = unregisteredParticipantFunctions(body);
1872
1925
  if (unregistered.length > 0)
1873
1926
  throw new ToolRefusalError({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.64",
3
+ "version": "0.4.65",
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.64",
53
+ "@stalfh233/omc-fake-platform": "0.4.65",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.64",
55
+ "omc-core": "0.4.65",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -151,6 +151,8 @@ rules:
151
151
  - **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
152
152
  - 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
153
153
  - **核验状态用投影,别重复整条读**:`data.load --args '{"schemaCode":..,"bizObjectId":..,"fields":["sequenceStatus","<业务状态字段>"]}'` 只回指定字段(缺失项在 `missingFields`)——r4 实测同一对象被整条读 74 次;大结果先 `artifact get <id> --summary` → `--keys/--jq`,别整包内联进推理。
154
+ - **流程第一个节点必须是 `{Originator}` 填写节点**:平台 `WorkflowTemplateValidateUtility.validateStartActivity` 对「START 后直接接审批」报 401035「发起节点的参与者只能是{Originator}」,publish 回 errcode 0 但一个 ValidateMessage[] 就把模板挡在门外、不落库(真机 2026-10-21)。OMC 0.4.65 起在 `workflow.update`/`publish` 出门前拒绝(`workflow-first-activity-not-originator`)。
155
+ - **跨模型回写的目标行必须已提交(COMPLETED)**:DRAFT 行不参与目标查询,运行 0 行命中且日志只有「查找范围不存在数据」。单跳关联字段可直接作 DYNAMIC 来源(引擎取关联对象 id);点号路径按子表解析,`关联字段.id` / 两跳路径都不支持。
154
156
  - **参与人解析失败分两种,别当成同一种(1.1.25 实测)**:① **解析为空**(角色零成员、成员未挂部门、自由文本、角色码不存在)→ 引擎把任务**退回发起人**,流程继续,症状是「审批人显示成我自己」,`data.submit` 与 `workitem.list` 都正常;② **参与人函数未注册**(`FindUsersByRoleFunction` / `FindUsersSpecByRoleFunction`,1.1.25 未内置)→ `FunctionNotFoundException`、不生成待办、实例卡 PROCESSING 而接口仍回 `errCode 0`。区分用 `omc call workflow.instanceActivity --args '{"workflowInstanceId":"<id>"}'`:无 `status:1` 节点=卡住;`status:1` 且参与人全是发起人=`diagnosis.warnings` 给 `participant-fallback-to-originator`。不要从 `workflow.get_published` 的摘要回抄函数式参与人;内置写法只有 `FindUserByRole(org, roleCode[, order])`,跨部门/全局角色优先用参与者对象数组(角色 id)。
155
157
 
156
158
  ## 流程字段权限
@@ -144,6 +144,8 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
144
144
 
145
145
  对应源码:`UpdateServiceExecutor.getUpdatedMainBizObject()` 只把 `mainConditions` 交给目标主表查询;`sheetConditions` 只在 `doUpdateChildData()` 里被读取。
146
146
 
147
+ 两个前提(真机 2026-10-21):① **目标行必须已提交**——DRAFT 行不参与目标查询,`data.save` 建的行永远改不到,日志只有「查找范围不存在数据」;② **单跳关联字段可直接作 DYNAMIC 来源**(引擎取关联对象 id),点号路径按子表解析,`关联字段.id` 与两跳都不支持——用表单映射把目标 id 落到本模型字段再引用。
148
+
147
149
  由此得到四条使用纪律:
148
150
 
149
151
  1. **主表目标上的 `sheetConditions` 是死配置**——不会生效(OMC 0.4.64 起给 warning)。