@stalfh233/omc-cli 0.4.61 → 0.4.63

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.
@@ -300,10 +300,10 @@ async function stampChildRowStatuses(session, schemaCode, formData) {
300
300
  }
301
301
  }
302
302
  catch {
303
- return formData;
303
+ return { formData, childCodes: [] };
304
304
  }
305
305
  if (childCodes.size === 0)
306
- return formData;
306
+ return { formData, childCodes: [] };
307
307
  let changed = false;
308
308
  const next = { ...formData };
309
309
  for (const code of childCodes) {
@@ -321,7 +321,7 @@ async function stampChildRowStatuses(session, schemaCode, formData) {
321
321
  return { ...row, rowStatus: hasId ? "Modified" : "Added" };
322
322
  });
323
323
  }
324
- return changed ? next : formData;
324
+ return { formData: changed ? next : formData, childCodes: [...childCodes] };
325
325
  }
326
326
  /** Runtime business-object data keyed by field code. */
327
327
  export function registerRuntimeDataLane(tools) {
@@ -570,6 +570,7 @@ export function registerRuntimeDataLane(tools) {
570
570
  schemaCode: z.string(),
571
571
  bizObjectId: z.string({ required_error: "bizObjectId 必填(参数名就是 bizObjectId——不是 id 也不是 objectId,r44 两个 agent 都猜了 objectId;取值来自 data.submit 读回的 objectId/sequenceNo 或 data.list 的 row.bizObjectId)" }),
572
572
  full: z.boolean().optional().describe("返回未裁剪的完整业务对象(含 owner/creater 等系统字段;默认返回业务字段+身份的精简记录)"),
573
+ fields: z.array(z.string().min(1)).optional().describe("只保留列出的字段——核验状态时传 [\"sequenceStatus\",\"<业务状态字段>\"],别整条 data.load(r4-training 2026-10-20:同一对象被读 74 次);缺失字段回在 missingFields"),
573
574
  capability: capabilityArg,
574
575
  },
575
576
  handler: async (ctx, args, self) => {
@@ -603,7 +604,27 @@ export function registerRuntimeDataLane(tools) {
603
604
  }
604
605
  if (includeSchema)
605
606
  ctx.schemaCache.add(args.schemaCode);
606
- return { schemaCode: args.schemaCode, bizObjectId: args.bizObjectId, exists: data != null, ...(includeSchema ? {} : { schemaCached: true }), data };
607
+ // r4-training 2026-10-20: most repeat loads only ask "did it move?".
608
+ // Trim on the CLI side so the caller pays for the state bits, not the
609
+ // whole record (74 reads of one object in the r4 session).
610
+ let missingFields;
611
+ if (Array.isArray(args.fields) && args.fields.length > 0) {
612
+ if (data && typeof data === "object" && !Array.isArray(data)) {
613
+ const source = data;
614
+ const projected = {};
615
+ const missing = [];
616
+ for (const field of args.fields) {
617
+ if (field in source)
618
+ projected[field] = source[field];
619
+ else
620
+ missing.push(field);
621
+ }
622
+ data = projected;
623
+ if (missing.length > 0)
624
+ missingFields = missing;
625
+ }
626
+ }
627
+ return { schemaCode: args.schemaCode, bizObjectId: args.bizObjectId, exists: data != null, ...(missingFields ? { missingFields, availableFields: data && typeof data === "object" ? Object.keys(data) : [] } : {}), ...(includeSchema ? {} : { schemaCached: true }), data };
607
628
  }, { full: args.full === true, target: `data.load:${args.schemaCode}:${args.bizObjectId}` });
608
629
  },
609
630
  }));
@@ -625,7 +646,7 @@ export function registerRuntimeDataLane(tools) {
625
646
  message: "submit:true 与 workflowCode 互斥——workflowCode 是流程草稿两段式的第一段(返回 workItemId 后用 data.submit 提交);无流程模型的一调完成态用 submit:true 且不带 workflowCode",
626
647
  });
627
648
  const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
628
- const formData = await stampChildRowStatuses(session, args.schemaCode, args.formData);
649
+ const formData = (await stampChildRowStatuses(session, args.schemaCode, args.formData)).formData;
629
650
  if (typeof args.workflowCode === "string" && args.workflowCode.trim()) {
630
651
  // Portal draft-save shape. The row id travels in data.id (the portal
631
652
  // form SDK receives a pre-generated id from form/load and echoes it);
@@ -787,7 +808,7 @@ export function registerRuntimeDataLane(tools) {
787
808
  message: "no live business object with this id; refusing to update (the platform would otherwise insert a new row)",
788
809
  });
789
810
  }
790
- const formData = await stampChildRowStatuses(session, args.schemaCode, args.formData);
811
+ const formData = (await stampChildRowStatuses(session, args.schemaCode, args.formData)).formData;
791
812
  const performed = await wireCall(session, {
792
813
  path: ENDPOINTS.runtimeFormSave,
793
814
  method: "POST",
@@ -886,6 +907,7 @@ export function registerRuntimeDataLane(tools) {
886
907
  workItemId: z.string().optional(),
887
908
  bizObjectId: z.string().optional().describe("existing record id; REQUIRED for workItem approval submits (formType=1 + workItemId) — the submit's bizObject must reference the in-flight record or the engine NPEs (DataChangeHandlerImpl.changeBizObject)"),
888
909
  agree: z.boolean().optional(),
910
+ allowChildRowAppend: z.boolean().optional().describe("默认 false:审批提交(带 workItemId/workflowInstanceId)时 formData 含子表数组会被拒——平台按追加插行,每次提交重复一份。确实要追加子行时才置 true。"),
889
911
  approval: z.record(z.string(), z.unknown()).optional(),
890
912
  runMode: z.string().optional(),
891
913
  isSimulative: z.boolean().optional(),
@@ -905,7 +927,27 @@ export function registerRuntimeDataLane(tools) {
905
927
  });
906
928
  }
907
929
  const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
908
- const formData = await stampChildRowStatuses(session, args.schemaCode, args.formData);
930
+ const stampedSubmit = await stampChildRowStatuses(session, args.schemaCode, args.formData);
931
+ const formData = stampedSubmit.formData;
932
+ // r5-asset 2026-10-20: the platform APPENDS child rows carried in an
933
+ // approval submit's formData (no upsert by row id, no error). Live: an
934
+ // approval that echoed the whole form twice turned 2 detail rows into 6
935
+ // and 30000 into 90000 while every call answered errcode 0 — node-level
936
+ // `editable:false` does not stop it server-side. Refuse before the wire.
937
+ if ((hasWorkItem || hasInstance) && args.allowChildRowAppend !== true) {
938
+ const carried = stampedSubmit.childCodes.filter((code) => {
939
+ const value = args.formData[code];
940
+ return Array.isArray(value) && value.length > 0;
941
+ });
942
+ if (carried.length > 0)
943
+ throw new ToolRefusalError({
944
+ status: "refused",
945
+ blocker: "submit-approval-child-rows",
946
+ schemaCode: args.schemaCode,
947
+ childTables: carried,
948
+ message: `审批提交(workItemId/workflowInstanceId)的 formData 不要带子表数组:${carried.join("、")}。平台按「追加行」处理——每提交一次就再插一份(真机:2 行 → 6 行,30000 → 90000),且不报错、审批节点的 editable:false 也拦不住。只传主表字段;确实要在审批中追加子行时显式传 allowChildRowAppend:true。`,
949
+ });
950
+ }
909
951
  // Portal required-field semantics: the REST submit does NOT enforce them,
910
952
  // so a missing required value must be REPORTED, not silently accepted as a
911
953
  // portal-valid submit (dogfood R3). Best-effort read; a failure never
@@ -691,7 +691,7 @@ function validateWorkflowRender(template, tool) {
691
691
  * chain order" — reusing any edge whose endpoints survived. A draft whose
692
692
  * (possibly branching) graph is untouched passes the chain check verbatim.
693
693
  */
694
- function completeWorkflowChain(rules, activities) {
694
+ export function completeWorkflowChain(rules, activities) {
695
695
  const codes = activities.map((activity) => String(activity.activityCode ?? "")).filter((code) => code.length > 0);
696
696
  if (codes.length < 2)
697
697
  return rules;
@@ -712,13 +712,24 @@ function completeWorkflowChain(rules, activities) {
712
712
  }
713
713
  for (const code of codes) {
714
714
  const type = typeOf(code);
715
- if (type !== "START" && (inbound.get(code) ?? 0) !== 1)
715
+ if (type !== "START" && (inbound.get(code) ?? 0) < 1)
716
716
  valid = false;
717
- if (type !== "END" && (outbound.get(code) ?? 0) !== 1)
717
+ if (type !== "END" && (outbound.get(code) ?? 0) < 1)
718
718
  valid = false;
719
719
  }
720
720
  if (valid)
721
721
  return rules;
722
+ // r5-asset 2026-10-20: a node with TWO outgoing edges is a conditional
723
+ // branch (condition edge + utilizeElse fallback), NOT an invalid chain. The
724
+ // old rule demanded outbound === 1, so every branched graph fell through to
725
+ // the linear rebuild below — which maps consecutive activity codes and
726
+ // silently DROPS the else edge. Live symptom: a workflow.update that sent
727
+ // activities without routes erased A3→A5, every low-amount instance stalled
728
+ // in PROCESSING with errcode 0, and the only trace was a missing edge in
729
+ // get_published. Never linearize a graph that already branches.
730
+ const branched = [...outbound.values()].some((count) => count > 1);
731
+ if (branched)
732
+ return rules;
722
733
  const byEdge = new Map(rules.map((rule) => [`${rule.preActivityCode}->${rule.postActivityCode}`, rule]));
723
734
  return codes.slice(0, -1).map((code, index) => byEdge.get(`${code}->${codes[index + 1]}`) ?? { preActivityCode: code, postActivityCode: codes[index + 1] });
724
735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.61",
3
+ "version": "0.4.63",
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.61",
53
+ "@stalfh233/omc-fake-platform": "0.4.63",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.61",
55
+ "omc-core": "0.4.63",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -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
+ - **核验状态用投影,别重复整条读**:`data.load --args '{"schemaCode":..,"bizObjectId":..,"fields":["sequenceStatus","<业务状态字段>"]}'` 只回指定字段(缺失项在 `missingFields`)——r4 实测同一对象被整条读 74 次;大结果先 `artifact get <id> --summary` → `--keys/--jq`,别整包内联进推理。
153
154
  - **参与人解析失败分两种,别当成同一种(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)。
154
155
 
155
156
  ## 流程字段权限
@@ -125,3 +125,18 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
125
125
  ## 固定验收顺序
126
126
 
127
127
  执行阶段以 `omc-five-piece-flow` 为准;写面安全只额外强调两点:写前先 `commands/describe/contract` + dry-run 静态校验,写后权威 readback,必要时再用 CLI 触发运行验证。
128
+
129
+ ## 审批提交回传子表 = 追加行(真机 2026-10-20)
130
+
131
+ 带 `workItemId` / `workflowInstanceId` 的 `data.submit` 里放子表数组,平台按**追加**处理,不按行 `id` upsert、也不报错:
132
+ 2 行明细回传两次 → 6 行,`totalValue` 30000 → 90000,审批节点的 `editable:false` 拦不住服务端写入。
133
+ **只传主表字段**;确需追加行时显式 `allowChildRowAppend:true`(0.4.63 起默认拒绝,blocker `submit-approval-child-rows`)。
134
+ 收尾时前后各读一次子表行数确认未变。
135
+
136
+ ## 跨表回写只能取主表字段(真机 2026-10-20)
137
+
138
+ - `id = {子表code}.asset` / `...asset.id`:保存发布全绿,**运行不命中**,无日志。
139
+ - 请求体直接引用子表关联(CREATE 的 dataActions 等):提交报 `550010 业务规则使用的数据项不存在`。
140
+ - BRANCH 后接两个跨表 UPDATE:`50000`。
141
+ - 固定 id / 主表字段条件:可运行。
142
+ 逐件联动(一张单多件资产)纯配置做不到,走二开 BIZ_ACTION 或人工。