@stalfh233/omc-cli 0.4.98 → 0.4.99

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.
@@ -954,6 +954,33 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
954
954
  const agg = parseOptionsObject(config.aggregation);
955
955
  if (!agg || !isNonEmptyString(agg.schemaCode) || !isNonEmptyString(agg.dataItemCode) || !isNonEmptyString(agg.formula))
956
956
  refuseFieldOptions("AGGREGATION aggregation is missing schemaCode/dataItemCode/formula", 'rejected: aggregation 形状 {schemaCode:"<目标模型schemaCode>", dataItemCode:"<目标字段code>", formula:"SUM({<目标模型>.<字段>})", calculateType:"SUM", queryConditions:[[…]]}(FormulaProcessor.executeMainAggregation + AggregationFormula)');
957
+ // r18b (2026-10-26 live): LARGE/SMALL take a position argument and the
958
+ // platform evaluates the FORMULA, so `LARGE({col})` (no k) throws
959
+ // NumberFormatException internally and the target write answers 50000.
960
+ const calcType = typeof agg.calculateType === "string" ? agg.calculateType.toUpperCase() : "";
961
+ if (["LARGE", "SMALL"].includes(calcType) && !/(LARGE|SMALL)\s*\(\s*[^,()]+\s*,/.test(String(agg.formula)))
962
+ throw new ToolRefusalError({
963
+ status: "refused",
964
+ blocker: "aggregation-large-missing-k",
965
+ schemaCode: schemaCode ?? null,
966
+ calculateType: calcType,
967
+ formula: String(agg.formula),
968
+ message: `${calcType} 必须带位置参数 k:写成 ${calcType}({<来源模型>.<数值列>},<k>)(k=1 是最大/最小)。` +
969
+ `缺 k 时平台内部抛 NumberFormatException(日志可见),目标模型写入一律 50000(r18b 真机)。` +
970
+ `真机验证:LARGE({src.amt},2) 对 10/30/20 返回 20;SMALL({src.amt},1) 返回 10。`,
971
+ });
972
+ // UNION concatenates DISTINCT text values ("去重后拼接文本"), so a numeric
973
+ // resultType makes the aggregation parse text as a number and the write
974
+ // 50000s. Text result types: SHORT_TEXT(0) / LONG_TEXT(1).
975
+ if (calcType === "UNION" && ![0, 1].includes(Number(agg.resultType ?? 2)))
976
+ throw new ToolRefusalError({
977
+ status: "refused",
978
+ blocker: "aggregation-union-result-type",
979
+ schemaCode: schemaCode ?? null,
980
+ resultType: Number(agg.resultType ?? 2),
981
+ message: `UNION 是「去重后拼接文本」,resultType 必须是文本类型(0=SHORT_TEXT / 1=LONG_TEXT;顶层 formulaConfig.resultType 决定)。` +
982
+ `用数值 resultType 时平台把拼接结果按数字解析 → NumberFormatException → 目标写入 50000(r18b 真机:resultType:2 失败,0 成功,读回 "P1")。`,
983
+ });
957
984
  if (String(agg.dataItemCode).includes("."))
958
985
  throw new ToolRefusalError({
959
986
  status: "refused",
@@ -40,7 +40,7 @@ export function dataSubmitDescription() {
40
40
  `就地语义:${wire.submitInPlace};无流程 readback 必须确认 objectId 与传入 bizObjectId 相同;无 bizObjectId 的 workflowCode 新建单 → PROCESSING + workflowInstanceId,无流程 → 该行 COMPLETED。`,
41
41
  `既有草稿发起流程的唯一支持形状是两段式:先 data.save 带 workflowCode 暂存(${wire.workflowDraftSave}),再以返回的 workItemId 提交。`,
42
42
  `仅无 workflowCode/workItemId/workflowInstanceId 且无 bizObjectId 时保留 save→submit 便捷链(新建记录);只要存在 workItemId 或 workflowInstanceId 就 fail-closed 拒绝(blocker submit-biz-object-id-required),零 runtime/form/submit wire。`,
43
- `formType 自动判定:无任何 workflowCode/workItemId/workflowInstanceId 时**先按模型自动判定**——GET workflow/isworkflow?schemaCode=true 则用 workflow/getWorkflow 解析已发布流程码并**发起流程**(formType="${wire.formTypeWorkflow}",自动先 GET getReplayToken 注入 replayToken,否则 ${wire.replayTokenErrcode} 频繁重复提交);模型无流程 → "${wire.formTypeNoWorkflow}"(无流程提交,走 live 已证链 ${wire.submitChain};单发新建对象会 ${wire.typeCastErrcode},bizObject 勿带字符串 status)或 workItemId+workflowInstanceId(审批待办,agree 通过/驳回,**必须带 bizObjectId 指向在途单据**,否则引擎 NPE ${wire.approvalNpeErrcode})→ "${wire.formTypeWorkflow}"。即「无流程直接提交、有流程发起流程」,不再把有流程模型的新建数据落成游离 sheet 行。`,
43
+ `formType 自动判定:无任何 workflowCode/workItemId/workflowInstanceId 时**先按模型自动判定**——GET workflow/isworkflow?schemaCode=true 则用 workflow/getWorkflow 解析已发布流程码并**发起流程**(formType="${wire.formTypeWorkflow}",自动先 GET getReplayToken 注入 replayToken,否则 ${wire.replayTokenErrcode} 频繁重复提交);模型无流程 → "${wire.formTypeNoWorkflow}"(无流程提交,走 live 已证链 ${wire.submitChain};单发新建对象会 ${wire.typeCastErrcode},bizObject 勿带字符串 status)或 workItemId+workflowInstanceId(审批待办,agree **顶层**通过/驳回(approval 对象里放 agree 会被静默忽略并按通过处理),**必须带 bizObjectId 指向在途单据**,否则引擎 NPE ${wire.approvalNpeErrcode})→ "${wire.formTypeWorkflow}"。即「无流程直接提交、有流程发起流程」,不再把有流程模型的新建数据落成游离 sheet 行。`,
44
44
  "根因提示(环境相关,ticket 78 观测):**某些环境**里模型所在应用未发布时运行时提交稳定 50000,工具会反推并拒 app-not-published。该根因非普遍规律(见 data.save 的同条提示与 docs/issues/omc-cli-findings-2026-09-19.md B17)——遇 50000 先只读核对再判断。",
45
45
  "Pass `sheetCode` explicitly when the model has multiple sheets; otherwise the default-form sheetCode is resolved from bizsheet/get.",
46
46
  `计算字段:${wire.formulaFieldNote}`,
@@ -1290,6 +1290,27 @@ export function registerRuntimeDataLane(tools) {
1290
1290
  `而错误里不会说是哪一个。要么给出真实 id,要么整个省略该键(省略 ≠ 传空串)。`,
1291
1291
  });
1292
1292
  const hasWorkItem = typeof args.workItemId === "string" && args.workItemId.trim().length > 0;
1293
+ // r18b: pure shape check — run before any read/wire so a wrong-result
1294
+ // payload never costs a round trip.
1295
+ if (args.approval && typeof args.approval === "object") {
1296
+ const misplaced = args.approval;
1297
+ if (misplaced.agree !== undefined)
1298
+ throw new ToolRefusalError({
1299
+ status: "refused",
1300
+ blocker: "submit-approval-agree-misplaced",
1301
+ schemaCode: args.schemaCode,
1302
+ message: `approval.agree 不生效:平台只读**顶层** agree(true=通过 / false=驳回)。approval 对象里放 agree 会被**静默忽略并按通过处理**` +
1303
+ `(r18b 真机:approval:{agree:false} 仍写入 result=APPROVE 并把流程走完)。改成顶层参数 {"agree":false,...}。`,
1304
+ });
1305
+ if (misplaced.opinion !== undefined)
1306
+ throw new ToolRefusalError({
1307
+ status: "refused",
1308
+ blocker: "submit-approval-opinion-misplaced",
1309
+ schemaCode: args.schemaCode,
1310
+ message: `approval.opinion 不是平台键,会被静默丢弃(r18b 真机:意见不进 h_biz_comment,content 为 NULL)。` +
1311
+ `审批意见(BizCommentModel)用 **approval.content**;附件 approval.resources;下一步审批人 approval.relUsers。`,
1312
+ });
1313
+ }
1293
1314
  const hasInstance = typeof args.workflowInstanceId === "string" && args.workflowInstanceId.trim().length > 0;
1294
1315
  const targetObjectId = typeof args.bizObjectId === "string" && args.bizObjectId.trim() ? args.bizObjectId.trim() : null;
1295
1316
  if ((hasWorkItem || hasInstance) && !targetObjectId) {
@@ -1688,6 +1709,15 @@ export function registerRuntimeDataLane(tools) {
1688
1709
  // SEPARATE concern from server-side business rules (which DO run on
1689
1710
  // submit) — name that so `portalRulesExecuted:false` is not misread as
1690
1711
  // "no rule ran" (dogfood R2).
1712
+ // r18b: an approval submit that omits `agree` is treated as APPROVE by
1713
+ // the platform (`agree: args.agree !== false`). That default is right
1714
+ // for the portal button, but for an API caller "I forgot the flag" and
1715
+ // "I approve" must not look identical in the receipt.
1716
+ ...(hasWorkItem && args.agree === undefined
1717
+ ? {
1718
+ approvalNote: "本次是审批提交但未传 agree——平台按**通过**处理(顶层 agree 缺省 true)。要驳回必须显式 agree:false;approval.agree 不生效。",
1719
+ }
1720
+ : {}),
1691
1721
  portalRulesExecuted: false,
1692
1722
  portalRulesNote: "portalRulesExecuted 只指门户客户端的必填/控件校验未在 REST 提交时执行;服务端业务规则(含流程 end 规则)照常运行。",
1693
1723
  requiredFields: validation.requiredFields,
@@ -259,7 +259,7 @@ const formulaAggregationCrossModel = Object.freeze({
259
259
  conditionalRequirements: [
260
260
  "**DYNAMIC 条件的两个 code 分属两侧,写反了就静默 0**:`propertyCode` = **来源模型**的字段(这里是 r17_c.nm),`currentPropertyCode` = **目标模型**的字段(这里是 r17_d.srcNo)。语义 = 「来源行的 nm 等于本目标行的 srcNo」。",
261
261
  "`propertyType`/`currentPropertyType` 是 BizPropertyType 的**整数码**(0=SHORT_TEXT 2=NUMERICAL 9=WORK_SHEET…),`operatorType` 是 OperatorType 的**整数码**(1=EQ 6=ITEQ 9=IS_NULL 10=IS_NOT_NULL…)。写名字会被 ObjectMapperUtils 抛 json processing exception 并掩码成 305003;工具会自动转码。",
262
- "`calculateType` 是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION),不是枚举 code。",
262
+ "`calculateType` 是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/**LARGE/SMALL/UNION**),不是枚举 code。r18b 真机三种特殊算法:LARGE/SMALL **必须带位置参数** `LARGE({<来源>.<列>},2)`(缺 k → 平台 NumberFormatException → 目标写入 50000);**UNION 是文本拼接**,顶层 `resultType` 必须是 0/LONG_TEXT=1(写 2 会 50000,读回是去重串,如单号本身);SUM/AVG/MAX/MIN/COUNT 用 resultType:2。",
263
263
  "发布后平台会**自动生成一条 t100 CALCULATE_RULE 镜像行**(h_biz_data_rule,propertyCode=字段 code),它是回算触发的前置条件,别手动删。",
264
264
  "**回算时机(r17b 2026-10-26 真机完整行为图谱)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠,不依赖事件);② 来源行 submit/update/delete 触发 `RollUpEventListener`→`FormulaPropertySupport` 事件回算——FIXED+DYNAMIC 混用、多组 OR、COUNTIF/MAX、子表行级条件(propertyCode 带 `.`)都验证过,DELETE 也会回算;③ ⚠️ 事件回算**只在目标模型发布后的几分钟内可靠**,之后静默停止(源端数据变了目标字段不更新、无报错)——重新 `field.publish --force` 目标模型即可恢复。定时巡检/对账场景务必重发或依赖目标行写入路径。",
265
265
  ],
@@ -48,7 +48,9 @@ const workflowSubmit = Object.freeze({
48
48
  tool: "data.submit",
49
49
  args: { environment, schemaCode: model, workflowCode: "<workflow.code>", bizObjectId: "<data.save.performed.bizObjectId>", workflowInstanceId: "<data.save.performed.workflowInstanceId>", workItemId: "<data.save.performed.workItemId>", formData: { name: "recipe draft" } },
50
50
  preconditions: ["同一 recipe 的 data.save 已成功并读回 DRAFT;workflowInstanceId/workItemId/bizObjectId 均非空"],
51
- conditionalRequirements: ["workItemId 或 workflowInstanceId 出现时必须传 bizObjectId;身份只能复制 data.save 的 authoritative readback"],
51
+ conditionalRequirements: [
52
+ "workItemId 或 workflowInstanceId 出现时必须传 bizObjectId;身份只能复制 data.save 的 authoritative readback",
53
+ ],
52
54
  priorToolReferences: [
53
55
  { tool: "data.save", args: { environment, schemaCode: model, workflowCode: "<workflow.code>", formData: { name: "recipe draft" } }, purpose: "提供三个环境生成的提交身份" },
54
56
  { tool: "workitem.list", args: { environment }, purpose: "当 data.save 未直接返回 workItemId 时按 workflowInstanceId 解析待办" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.98",
3
+ "version": "0.4.99",
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.98",
53
+ "@stalfh233/omc-fake-platform": "0.4.99",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.98",
55
+ "omc-core": "0.4.99",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -156,7 +156,7 @@ rules:
156
156
  - **`queryConditions` 不能为空**:它是反向过滤器的唯一输入,空数组时回算日志照打但目标字段恒 0(r17 实测)。
157
157
  - **聚合目标必须是来源主表的标量列**(r18 2026-10-26 真机):`aggregation.schemaCode` 指向**子表 schema** 时能发布,但目标模型从此整个不可写(`data.submit`/`data.update` 全回 40000 无明细),且子表无父单单号列、匹配恒 0;`dataItemCode` 写 `<子表>.<子列>` 则 publish 回 309004。要子表合计:先在来源主模型物化一个数值字段,再聚合该标量。
158
158
  - **DYNAMIC 条件的两个 code 分属两侧,写反了静默 0**:`propertyCode` = 来源模型字段,`currentPropertyCode` = 目标模型字段;语义「来源行的 X 等于本目标行的 Y」。
159
- - **`propertyType`/`currentPropertyType`(BizPropertyType)与 `operatorType`(OperatorType)必须写整数**——两者都是 `@JsonCreator get(int)`:propertyType 0=SHORT_TEXT 2=NUMERICAL 3=DATE 9=WORK_SHEET…;operatorType 1=EQ 2=NEQ 3=GT 4=IT(小于)5=GTEQ 6=ITEQ(小于等于)9=IS_NULL 10=IS_NOT_NULL…。写名字会被 `ObjectMapperUtils` 抛 json processing exception,再被死循环校验掩码成 305003(工具接受名字并自动转码)。`calculateType` 相反,是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION)。
159
+ - **`propertyType`/`currentPropertyType`(BizPropertyType)与 `operatorType`(OperatorType)必须写整数**——两者都是 `@JsonCreator get(int)`:propertyType 0=SHORT_TEXT 2=NUMERICAL 3=DATE 9=WORK_SHEET…;operatorType 1=EQ 2=NEQ 3=GT 4=IT(小于)5=GTEQ 6=ITEQ(小于等于)9=IS_NULL 10=IS_NOT_NULL…。写名字会被 `ObjectMapperUtils` 抛 json processing exception,再被死循环校验掩码成 305003(工具接受名字并自动转码)。`calculateType` 相反,是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION);LARGE/SMALL 必须带 k(`LARGE({来源.列},2)`),UNION 是文本拼接、resultType 必须是 0/1。
160
160
  - **回算时机(r17b 2026-10-26 完整行为图谱)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠);② 来源行 submit/update/**delete** 触发 `RollUpEventListener`→`FormulaPropertySupport` 事件回算(FIXED+DYNAMIC 混用、多组 OR、COUNTIF/MAX、子表行级条件都验过);③ ⚠️ 事件回算**只在目标模型发布后几分钟内可靠**,之后静默停止(无报错)——`field.publish --force` 目标模型恢复。发布后平台自动生成 t100 `CALCULATE_RULE` 镜像行,别删。r17 真机:目标行 srcNo=C1、来源 C1 行 amt=100 → 读 100;再提交一条 C1/amt=5 → 读 105。
161
161
  - **普通数值字段 + 计算规则(type2)不能替代**:REST 路径永不执行(`CalculateHandler.java:50` 的 `forceExecuteCalculateRule` 从不置真),门户里才算。
162
162
  - **DYNAMIC 来源不要写点号**:`dataActions` 的 `currentDataItemValue` 写 `items.sealNo` 会被引擎当成**子表列集合**(DatasetImpl),标量目标收到集合 → 运行期 `submit/update` 报 50000(`WorkSheetBizField.fillClientContent: Incorrect value type`)。单跳关联字段直接写字段 code(引擎取 `.id`);子表列只能用于公式/聚合。`rule.save` 已写前拒绝(`rule-graph-create-dynamic-source-is-child-collection`)。
@@ -97,6 +97,16 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
97
97
 
98
98
  - **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish` 各失败一次)。读可并行,写不要。
99
99
 
100
+ ## 审批提交的真实形状(r18b 2026-10-26 真机,1.1.25)
101
+
102
+ | 参数 | 位置 | 语义 |
103
+ |---|---|---|
104
+ | `agree` | **顶层** | true=通过 / false=驳回;**缺省=true**(驳回后实例仍 PROCESSING,别当成功) |
105
+ | `approval.content` / `relUsers` / `resources` | approval 对象 | 意见 / 下一步审批人 / 附件 |
106
+ | `approval.agree`、`approval.opinion` | — | **静默忽略**(真机:agree:false 仍 APPROVE 且走完流程;opinion 不落库)。OMC 已写前拒绝 |
107
+
108
+ 链:`data.save`(draft→workItemId) → `data.submit`(顶层 agree) → 下一级待办。验收看 `sequenceStatus`。
109
+
100
110
  ## 计算规则(datarule)的取值时机
101
111
 
102
112
  - **字段计算规则(`datarule.create` `dataRuleType:2`)在 REST 路径上不会执行、也不会回填**(r17 2026-10-25 复核,AI-Platform 1.1.25):
@@ -112,15 +122,13 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
112
122
  **回算可靠性(重要)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠);② 来源行 submit/update/delete 的事件回算**只在目标模型发布后几分钟内可靠**,之后静默停止(无报错,源变了目标不更新)——重新 `field.publish --force` 目标模型恢复。验收:发布后立刻改来源行并读目标字段;长期对账别依赖事件回算。
113
123
  - **同模型(含自己的子表)聚合不可用**:`field.publish` 必抛 305003 —— `publishFormula` 把相对码改写成子表父模型(= 本模型),
114
124
  `checkCycleOfAggregation:1032` 判成环。与 payload 无关,8.6.26 与 1.1.25 代码相同。OMC 已写前拒绝 `aggregation-self-schema-cycle`。
115
- - **聚合目标不能是任何子表 schema,数据项必须是来源主表的标量列**(r18 2026-10-26 真机,代价约 40 次调用):
116
- `aggregation.schemaCode` 指向子表时**能发布**,但目标模型从此整个不可写 —— `data.submit`/`data.update` 全部回
117
- `40000 调用数据库发生异常`(无明细);且子表 schema 无父单单号列,按父单匹配恒为 0。
118
- `dataItemCode` 写成 `<子表>.<子列>` 则在 publish 回 309004「使用的聚合数据项不存在」。
119
- 正解:要子表合计,先在来源**主模型**物化一个数值字段(门户计算规则 / 写入方维护),再聚合该标量。
120
- OMC 已写前拒绝 `aggregation-target-is-child-schema`(读一次来源字段清单识别 parentId+sortKey)与
121
- `aggregation-data-item-not-scalar`(点号)。
122
- - **`40000` 的无明细掩码怎么查**:先怀疑字段级配置(上面的聚合/公式),用「删/禁一个公式字段再写一次」二分,
123
- 不要从表单/权限开始查。OMC 现在会把 40000 转成 `runtime-write-db-exception` 并列出该模型的公式字段。
125
+ - **聚合必须指向来源主表的标量列**(r18/r18b 真机):`schemaCode` 指向**子表 schema** 能发布,但目标模型从此整个不可写
126
+ (写入一律 `40000`,无明细),且子表无父单单号列、匹配恒 0;`dataItemCode` 写 `<子表>.<子列>` 则 publish 回 309004。
127
+ 要子表合计:先在来源主表物化一个数值字段,再聚合该标量。写前拒绝:`aggregation-target-is-child-schema` / `aggregation-data-item-not-scalar`。
128
+ - **`40000` 无明细时**先怀疑字段级配置(上面的聚合、公式序列化),用「删/禁一个公式字段再写一次」二分,别从表单/权限查。
129
+ OMC 会转成 `runtime-write-db-exception` 并列出该模型的公式字段。
130
+ - **`LARGE/SMALL` 必须带 k**:`LARGE({来源.列},2)`(缺 k → NumberFormatException → 写入 50000);
131
+ **`UNION` 是文本拼接**,顶层 `resultType` 必须 0/1(写 2 会 50000)。拒绝:`aggregation-large-missing-k` / `aggregation-union-result-type`。
124
132
  - **区别:`FORMULA`(计算公式,`propertyType:100`)字段的 EXPRESSION 模式仍只在前端算** —— API 写入不回填。
125
133
 
126
134
  ## 规则与业务集成