@stalfh233/omc-cli 0.4.94 → 0.4.96
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.
- package/dist/capability-overview.js +6 -3
- package/dist/compute.js +47 -1
- package/dist/field-families.js +68 -9
- package/dist/goal-routes.js +10 -3
- package/dist/lanes/datarule.js +15 -0
- package/dist/lanes/field-contract.js +3 -3
- package/dist/lanes/field.js +55 -1
- package/dist/lanes/rule-graph.js +1 -1
- package/dist/recipes/datarule.js +2 -1
- package/dist/recipes/field.js +54 -1
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +7 -3
- package/skills/omc-capabilities.md +3 -1
- package/skills/omc-requirement-vocabulary.md +10 -5
- package/skills/omc-write-hazards.md +13 -7
|
@@ -71,7 +71,7 @@ const SURFACES = [
|
|
|
71
71
|
description: "字段族全链:create→publish→表单绑定;CHILD_TABLE 子列;FORMULA 计算(子表求和回主表、子表列同行运算)。",
|
|
72
72
|
operations: ["create", "update", "publish", "delete"],
|
|
73
73
|
intent: { level: "partial", note: "create/update(含 CHILD_TABLE subFields、FORMULA 计算)、publish 与 delete(absent:true)已声明式。" },
|
|
74
|
-
notes: "子表列是 schemaCode=父字段 code 上的普通字段;field.update 不接受 subFields。子列支持标量族 + WORK_SHEET 关联列(需 relativeCode + options)。**计算类已覆盖(默认走计算规则,非 FORMULA 字段)**:子表求和、子表列同行运算用「普通字段 + 计算规则」(`describe datarule.create --example field-compute-rule|field-compute-subtable-sum|field-compute-child-column
|
|
74
|
+
notes: "子表列是 schemaCode=父字段 code 上的普通字段;field.update 不接受 subFields。子列支持标量族 + WORK_SHEET 关联列(需 relativeCode + options)。**计算类已覆盖(默认走计算规则,非 FORMULA 字段)**:子表求和、子表列同行运算用「普通字段 + 计算规则」(`describe datarule.create --example field-compute-rule|field-compute-subtable-sum|field-compute-child-column`);计算规则**在 REST 路径上不会执行也不会回填**(r17 2026-10-25 复核:`data.submit`/`data.update` 后目标字段仍为传入值;门控 `CalculateHandler.java:50` 的 `forceExecuteCalculateRule` 在 REST 路径从不置真)。嵌套子表/MULT_WORK_SHEET/RELEVANCE_DATA 不覆盖。",
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
id: "form",
|
|
@@ -214,8 +214,11 @@ export const CAPABILITY_CUES = Object.freeze({
|
|
|
214
214
|
// (it matched 子表+字段) and answered with the "create a child table" recipe,
|
|
215
215
|
// so the agent hand-built an AGGREGATION rule node — a shape the platform
|
|
216
216
|
// accepts and then fails at runtime. Row aggregation is a FIELD COMPUTE RULE
|
|
217
|
-
// (`omc compute` / datarule type2)
|
|
218
|
-
//
|
|
217
|
+
// (`omc compute` / datarule type2) — the ONLY configurable path, and it runs
|
|
218
|
+
// in the portal form only (r17 2026-10-25: the REST submit/update path never
|
|
219
|
+
// computes it, and the FORMULA-field AGGREGATION alternative is refused at
|
|
220
|
+
// publish with 305003). These compound cues outscore field's 子表+字段 so the
|
|
221
|
+
// aggregate intent at least reaches the ladder that states the real limits.
|
|
219
222
|
datarule: ["字段级规则", "控件规则", "校验规则", "计算拼接", "显示规则", "规则", "type1", "type2", "type3",
|
|
220
223
|
"统计子表", "子表行数", "子表计数", "子表求和", "子表汇总", "行数统计", "计数写回", "汇总回主表", "求和回主表", "聚合计算",
|
|
221
224
|
"不早于", "不能早于", "不晚于", "不能晚于", "早于今天", "晚于今天", "日期校验", "提交校验", "校验不通过", "必填校验", "正则校验", "重复校验", "长度校验"],
|
package/dist/compute.js
CHANGED
|
@@ -106,6 +106,41 @@ export function composeComputeArgs(options) {
|
|
|
106
106
|
dryRun: !options.apply,
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The receipt for a `compute --apply` must not imply the rule will fire.
|
|
111
|
+
*
|
|
112
|
+
* 0.4.95 source evidence (8.6.26):
|
|
113
|
+
* - a type-2 rule whose carrier is `shortTextStitch` runs only when
|
|
114
|
+
* `CalcSwitchConf.forceExecuteCalculateRule` is true
|
|
115
|
+
* (`CalculateHandler.onHandler`, 8.6.26 :50);
|
|
116
|
+
* - that flag comes from `BizObjectOptions.executeCalRule`
|
|
117
|
+
* (`BizObjectOpConf.constructCalcSwitchConf` :95), and the only builder that
|
|
118
|
+
* sets it (`.executeCalRule()`) has NO caller in the tree — only the
|
|
119
|
+
* open-engine API can pass it.
|
|
120
|
+
* So data.save / data.submit / data.update never refresh the target field.
|
|
121
|
+
* r12 believed otherwise; r16 could not reproduce r12's success.
|
|
122
|
+
*/
|
|
123
|
+
export function computeExecutionNotice() {
|
|
124
|
+
return {
|
|
125
|
+
executionWarning: "字段计算规则已写入,但**提交/更新不会触发它**:type2 的 shortTextStitch 载体只在 " +
|
|
126
|
+
"CalcSwitchConf.forceExecuteCalculateRule=true 时求值,该开关来自 BizObjectOptions.executeCalRule," +
|
|
127
|
+
"而 8.6.26 全代码库没有任何 REST 路径把它置真(.executeCalRule() 仅 open-engine 显式 API 使用)。" +
|
|
128
|
+
"所以 data.save / data.submit / data.update 都不会刷新目标字段。",
|
|
129
|
+
executionEvidence: [
|
|
130
|
+
"BizObjectManagerImpl.java:377,424 — calculateHandler.onHandler(settings.getCalcSwitchConf(), …)",
|
|
131
|
+
"CalculateHandler.java:50 — if (switchConf.isForceExecuteCalculateRule())",
|
|
132
|
+
"BizObjectOpConf.java:95 — setForceExecuteCalculateRule(options.isExecuteCalRule())",
|
|
133
|
+
"engine/bizobject/BizObjectOptions.java:58 — 唯一的 executeCalRule() builder,全库无调用方",
|
|
134
|
+
],
|
|
135
|
+
aggregationPathNote: "AGGREGATION 形状(options.formulaType=\"AGGREGATION\" + aggregation)已对齐(0.4.96):" +
|
|
136
|
+
"propertyType/currentPropertyType(BizPropertyType) 与 operatorType(OperatorType) 必须写整数码," +
|
|
137
|
+
"写名字会让 ObjectMapperUtils 抛 json processing exception 并被掩码成 305003「死循环」;OMC 会把名字自动转成码。" +
|
|
138
|
+
"但**同模型子表聚合依然不可用**:平台 publishFormula 把 aggregation.schemaCode 改写成子表的父模型," +
|
|
139
|
+
"checkCycleOfAggregation:1032 随后判成环 → 无条件 305003(8.6.26 与 1.1.25 代码相同,r17 真机 A/B 已证)。" +
|
|
140
|
+
"跨模型聚合可以发布。服务端没有可用的子表聚合路径:计算规则不执行、AGGREGATION 同模型不发布、" +
|
|
141
|
+
"触发器回算只覆盖跨模型。替代:business rule(子表触发 → UPDATE 主表)或二开。",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
109
144
|
export async function cmdCompute(args) {
|
|
110
145
|
const request = requestId();
|
|
111
146
|
const flag = (name) => argvValue(args, name);
|
|
@@ -121,6 +156,8 @@ export async function cmdCompute(args) {
|
|
|
121
156
|
const usage = [
|
|
122
157
|
"用法:omc compute --schema <模型code> --target <目标字段> --formula \"<公式,{字段} 引用>\" [--name 规则名] [--env 环境] [--apply]",
|
|
123
158
|
"条件聚合:--formula \"{<子表code>.<子列code>}\" --where \"<子列code> = <值>\" [--aggregate count|sum] → 编译为 COUNTIF / SUMIF",
|
|
159
|
+
"⚠️ 这条规则**只在门户前端执行**:data.save/submit/update 不会触发它(源码取证:type2 的 shortTextStitch 载体只在 executeCalRule=true 时执行,REST 路径从不置真;r17 2026-10-25 真机复核:提交后目标字段仍为传入值)。API 调用方请自行算好再写。",
|
|
160
|
+
"⚠️ 也不能改用 FORMULA 字段的 AGGREGATION 兜底:同模型子表聚合在 field.publish 必抛 305003(aggregation-self-schema-cycle,平台 publishFormula/checkCycleOfAggregation 逻辑成环)。跨模型聚合能发布,但回算依赖门户/事件。汇总请走业务规则或二开。",
|
|
124
161
|
"公式语法:同类字段 {字段code};子表列 {子表code.子列code};无条件聚合 SUM/COUNT/MAX/MIN/AVG({子表.子列}) 需加 --allow-unfiltered 显式确认。",
|
|
125
162
|
"默认 dry-run,--apply 才写入。",
|
|
126
163
|
].join("\n");
|
|
@@ -167,7 +204,16 @@ export async function cmdCompute(args) {
|
|
|
167
204
|
return CLI_EXIT_CODES.internal;
|
|
168
205
|
}
|
|
169
206
|
const classified = classifyResult(result);
|
|
170
|
-
|
|
207
|
+
// 0.4.95 source-verified: a type-2 rule whose carrier is `shortTextStitch`
|
|
208
|
+
// (what this command writes) is executed ONLY when
|
|
209
|
+
// `CalcSwitchConf.forceExecuteCalculateRule` is true. That flag comes from
|
|
210
|
+
// `BizObjectOptions.executeCalRule`, and `.executeCalRule()` is never called
|
|
211
|
+
// anywhere in the 8.6.26 tree — only the open-engine API can pass it. So
|
|
212
|
+
// data.save / data.submit / data.update never refresh the target field, no
|
|
213
|
+
// matter how correct the formula is. Say so in the receipt instead of letting
|
|
214
|
+
// the caller believe the write took effect (r12 believed it; r16 disproved it).
|
|
215
|
+
const withWarning = apply && classified.status === "ok" ? { ...result, ...computeExecutionNotice() } : result;
|
|
216
|
+
writeEnvelope(envelope(classified.status, withWarning, request, classified.error));
|
|
171
217
|
return exitCode;
|
|
172
218
|
}
|
|
173
219
|
//# sourceMappingURL=compute.js.map
|
package/dist/field-families.js
CHANGED
|
@@ -689,13 +689,59 @@ function refuseFieldOptions(reason, message) {
|
|
|
689
689
|
*/
|
|
690
690
|
const AGGREGATION_OPERATOR_CODES = Object.freeze({
|
|
691
691
|
EQ: 1, NEQ: 2, GT: 3, IT: 4, GTEQ: 5, ITEQ: 6,
|
|
692
|
+
// Readable aliases for the two comparison names the enum spells oddly
|
|
693
|
+
// (`IT` = 小于, `ITEQ` = 小于等于). Writers reach for LT/LTEQ/GTE far more
|
|
694
|
+
// often than for the Java constant names, so accept both.
|
|
695
|
+
LT: 4, LTE: 6, LTEQ: 6, LE: 6, GTE: 5,
|
|
692
696
|
CONTAINS: 7, NOT_CONTAINS: 8, IS_NULL: 9, IS_NOT_NULL: 10,
|
|
693
697
|
OF: 11, NOT_OF: 12, HAVE: 13, NOT_HAVE: 14,
|
|
694
698
|
IN: 15, NOT_IN: 16, BETWEEN: 17, NOT_BETWEEN: 18,
|
|
695
699
|
CONTAINS_ALL: 19, NOT_CONTAINS_ALL: 20, CONTAINS_ANY: 21,
|
|
696
700
|
NOT_CONTAINS_ANY: 22, EQ_ANY: 23, NOT_EQ_ANY: 24,
|
|
697
701
|
});
|
|
698
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Platform `StripPrefixUtil.stripPrefix`: everything up to and INCLUDING the
|
|
704
|
+
* first dot is dropped (`r17_agg.items` → `items`; `items` → `items`).
|
|
705
|
+
*/
|
|
706
|
+
export function stripAggregationPrefix(code) {
|
|
707
|
+
const dot = code.indexOf(".");
|
|
708
|
+
return dot >= 0 ? code.slice(dot + 1) : code;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Does an aggregation target resolve to the schema the field itself sits on?
|
|
712
|
+
* Both spellings count: the bare schema code (`r17_agg`), the child path
|
|
713
|
+
* (`r17_agg.items` → parent `r17_agg`), and a bare child code that equals the
|
|
714
|
+
* model code (impossible, guarded for completeness).
|
|
715
|
+
*/
|
|
716
|
+
export function aggregationTargetsOwnSchema(schemaCode, aggregationSchemaCode) {
|
|
717
|
+
const raw = aggregationSchemaCode.trim();
|
|
718
|
+
if (raw === schemaCode)
|
|
719
|
+
return true;
|
|
720
|
+
if (raw.startsWith(`${schemaCode}.`))
|
|
721
|
+
return true;
|
|
722
|
+
return stripAggregationPrefix(raw) === schemaCode;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The refusal a same-model aggregation must carry. It names the platform
|
|
726
|
+
* mechanism so the reader stops looking for a payload fix, and gives the three
|
|
727
|
+
* paths that actually work.
|
|
728
|
+
*/
|
|
729
|
+
export function aggregationSelfSchemaMessage(aggregationSchemaCode, schemaCode) {
|
|
730
|
+
return (`rejected: 聚合字段不能聚合**自己所在模型**的数据(schemaCode=${aggregationSchemaCode} 解析到 ${schemaCode})。` +
|
|
731
|
+
`这不是 payload 写错:平台 AggregationFormula.schemaCode 指向「被聚合的模型」,` +
|
|
732
|
+
`指向自己的子表时 BizPropertyManagerImpl.publishFormula 会把它改写成子表的**父模型**(= 本模型),` +
|
|
733
|
+
`随后 checkCycleOfAggregation:1032 拿这个相对 code 和字段自己的 schemaCode 比较,两者相等即判定成环 → ` +
|
|
734
|
+
`**无条件抛 305003「规则配置出现死循环」**。8.6.26 与 AI-Platform 1.1.25 的这段代码完全相同(r17 2026-10-25 反编译比对),` +
|
|
735
|
+
`真机 A/B 也印证:聚合**别的模型**的子表发布 errcode:0,只差这一处相对 code。` +
|
|
736
|
+
`可行替代(按推荐序):` +
|
|
737
|
+
`① **跨模型汇总**——omc describe field.create --example formula-aggregation-cross-model,` +
|
|
738
|
+
`r17 已真机跑通(来源行 submit → 目标字段 100→105):schemaCode 指向**另一个模型**,` +
|
|
739
|
+
`queryConditions 用 DYNAMIC 条件把两侧对上(propertyCode=来源字段、currentPropertyCode=目标字段),` +
|
|
740
|
+
`空条件会静默得 0;` +
|
|
741
|
+
`② 业务规则(子表触发 → UPDATE 主表);③ 二开。` +
|
|
742
|
+
`另:普通数值字段 + 计算规则(datarule type2)在 REST 路径上也不会执行(executeCalRule 从不置真),不要用它兜底。`);
|
|
743
|
+
}
|
|
744
|
+
export function normalizeAggregationFormula(agg) {
|
|
699
745
|
const groups = agg.queryConditions;
|
|
700
746
|
if (groups === undefined)
|
|
701
747
|
return agg;
|
|
@@ -711,14 +757,14 @@ function normalizeAggregationFormula(agg) {
|
|
|
711
757
|
continue;
|
|
712
758
|
const code = typeof value === "number" ? value : FIELD_PROPERTY_TYPES[String(value).toUpperCase()];
|
|
713
759
|
if (typeof code !== "number" || !Number.isInteger(code))
|
|
714
|
-
refuseFieldOptions(`AGGREGATION queryConditions.${key} ${JSON.stringify(value)} is not a numeric BizPropertyType code`, `rejected: 聚合条件的 ${key} 必须是**整数**属性类型编码(
|
|
760
|
+
refuseFieldOptions(`AGGREGATION queryConditions.${key} ${JSON.stringify(value)} is not a numeric BizPropertyType code`, `rejected: 聚合条件的 ${key} 必须是**整数**属性类型编码(BizPropertyType 的 @JsonCreator 只接受 int;传 "NUMERICAL" 会让 ObjectMapperUtils 抛 json processing exception,被上层掩码成 305003 或 publish 50000);可用名字:${Object.keys(FIELD_PROPERTY_TYPES).join("、")}`);
|
|
715
761
|
record[key] = code;
|
|
716
762
|
}
|
|
717
763
|
const operator = record.operatorType;
|
|
718
764
|
if (operator !== undefined && operator !== null) {
|
|
719
765
|
const code = typeof operator === "number" ? operator : AGGREGATION_OPERATOR_CODES[String(operator).toUpperCase()];
|
|
720
766
|
if (typeof code !== "number" || !Number.isInteger(code) || code < 1 || code > 24)
|
|
721
|
-
refuseFieldOptions(`AGGREGATION queryConditions.operatorType ${JSON.stringify(operator)} is not an OperatorType code`, `rejected: 聚合条件的 operatorType 必须是整数 1–24(或名字:${Object.keys(AGGREGATION_OPERATOR_CODES).join("、")}
|
|
767
|
+
refuseFieldOptions(`AGGREGATION queryConditions.operatorType ${JSON.stringify(operator)} is not an OperatorType code`, `rejected: 聚合条件的 operatorType 必须是整数 1–24(或名字:${Object.keys(AGGREGATION_OPERATOR_CODES).join("、")});OperatorType 的 @JsonCreator 只接受 int,传 "GTEQ" 会抛 json processing exception 并被掩码成 305003/publish 50000`);
|
|
722
768
|
record.operatorType = code;
|
|
723
769
|
}
|
|
724
770
|
if (record.valueType === undefined)
|
|
@@ -889,12 +935,17 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
|
|
|
889
935
|
// empty-queryConditions probe evaluate to 0) — a FALSE NEGATIVE (the same
|
|
890
936
|
// 二开-vs-配置 call an agent wrongly made: 表单字段「计算」公式可直接做聚合).
|
|
891
937
|
// Validate the evidenced shape and allow it.
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
938
|
+
//
|
|
939
|
+
// SAME-model aggregation is NOT fabricable (r17 2026-10-25, source + live):
|
|
940
|
+
// AggregationFormula.schemaCode is the AGGREGATED model, so for the field's
|
|
941
|
+
// own child table `publishFormula` rewrites it to the child's PARENT
|
|
942
|
+
// (BizPropertyManagerImpl.publishFormula) and `checkCycleOfAggregation:1032`
|
|
943
|
+
// then compares that relative code with the field's own schemaCode — they
|
|
944
|
+
// are the same model, so publish throws 305003 unconditionally. The check is
|
|
945
|
+
// identical in 8.6.26 and AI-Platform 1.1.25, so this is a platform
|
|
946
|
+
// limitation, not a payload mistake. Live A/B: aggregating ANOTHER model's
|
|
947
|
+
// child table published errcode:0 (the only difference is the relative code).
|
|
948
|
+
// Refuse instead of letting publish answer the masked 305003.
|
|
898
949
|
const isAggregation = config.formulaType === "AGGREGATION";
|
|
899
950
|
if (isAggregation) {
|
|
900
951
|
if (!isNonEmptyString(config.aggregation))
|
|
@@ -903,6 +954,14 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
|
|
|
903
954
|
const agg = parseOptionsObject(config.aggregation);
|
|
904
955
|
if (!agg || !isNonEmptyString(agg.schemaCode) || !isNonEmptyString(agg.dataItemCode) || !isNonEmptyString(agg.formula))
|
|
905
956
|
refuseFieldOptions("AGGREGATION aggregation is missing schemaCode/dataItemCode/formula", 'rejected: aggregation 形状 {schemaCode:"<目标模型schemaCode>", dataItemCode:"<目标字段code>", formula:"SUM({<目标模型>.<字段>})", calculateType:"SUM", queryConditions:[[…]]}(FormulaProcessor.executeMainAggregation + AggregationFormula)');
|
|
957
|
+
if (schemaCode !== undefined && aggregationTargetsOwnSchema(schemaCode, String(agg.schemaCode)))
|
|
958
|
+
throw new ToolRefusalError({
|
|
959
|
+
status: "refused",
|
|
960
|
+
blocker: "aggregation-self-schema-cycle",
|
|
961
|
+
schemaCode,
|
|
962
|
+
aggregationSchemaCode: String(agg.schemaCode),
|
|
963
|
+
message: aggregationSelfSchemaMessage(String(agg.schemaCode), schemaCode),
|
|
964
|
+
});
|
|
906
965
|
const normalizedAgg = normalizeAggregationFormula(agg);
|
|
907
966
|
if (normalizedAgg !== agg) {
|
|
908
967
|
config.aggregation = JSON.stringify(normalizedAgg);
|
package/dist/goal-routes.js
CHANGED
|
@@ -392,9 +392,16 @@ const COMPOSER_ROUTES = Object.freeze([
|
|
|
392
392
|
id: "compute",
|
|
393
393
|
command: "omc compute --schema <模型code> --target <目标字段> --formula '<公式>' [--where '<子列> = <值>'] --apply",
|
|
394
394
|
usage: "omc compute --help",
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
// r17: this composer writes a datarule type2 row, which the platform only
|
|
396
|
+
// evaluates in the PORTAL form — never on the REST save/submit/update path
|
|
397
|
+
// (CalculateHandler is gated on executeCalRule, which no REST route sets).
|
|
398
|
+
// Claiming "服务端回填" here sent agents to build a rule and then read back 0.
|
|
399
|
+
summary: "字段计算规则一条命令(datarule type2):普通表达式、子表求和、条件计数 COUNTIF/SUMIF。⚠️ 只在门户表单里算,REST 提交不回填;需要 API 可读的跨模型汇总用 field.create --example formula-aggregation-cross-model",
|
|
400
|
+
groups: [["计算", "统计", "汇总", "求和", "聚合", "计数", "合计"], ["字段", "子表", "列", "回主表", "写入", "模型"]],
|
|
401
|
+
// r17: a CROSS-MODEL roll-up is not this composer's shape — `compute` writes
|
|
402
|
+
// a datarule type2 row, which cannot read another model at all. Those asks
|
|
403
|
+
// belong to field.create's formula-aggregation-cross-model recipe.
|
|
404
|
+
avoid: ["另一个模型", "其它模型", "其他模型", "跨模型", "另一张表", "另一个表"],
|
|
398
405
|
topics: ["omc-write-hazards"],
|
|
399
406
|
},
|
|
400
407
|
{
|
package/dist/lanes/datarule.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveDataRuleRow } from "../identity.js";
|
|
|
11
11
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
12
12
|
import { runtimeWriteWithAppMapping } from "../runtime-support.js";
|
|
13
13
|
import { DATARULE_LANE_CONTRACT, DATARULE_TYPE_OPTION_KEYS } from "./datarule-contract.js";
|
|
14
|
+
import { normalizeAggregationFormula } from "../field-families.js";
|
|
14
15
|
import { CONDITION_PROTOCOL_EXAMPLES, dataRuleOptionsShapeError } from "./datarule-protocol.js";
|
|
15
16
|
const DATARULE_LANE = DATARULE_LANE_CONTRACT.lane;
|
|
16
17
|
const DATARULE_TOOLS = DATARULE_LANE_CONTRACT.tools;
|
|
@@ -391,6 +392,20 @@ export function assertDataRuleOptions(dataRuleType, options, checkType) {
|
|
|
391
392
|
'正确形状:options:{shortTextStitch:"{\\"formula\\":\\"{字段code}\\",\\"editorText\\":\\"{字段名}\\",\\"editorMark\\":\\"{字段名}\\"}"}(editorText/editorMark 仅展示用;formula 是表达式)',
|
|
392
393
|
});
|
|
393
394
|
}
|
|
395
|
+
if (formulaType === "AGGREGATION" && typeof options.aggregation === "string") {
|
|
396
|
+
// r17 (2026-10-25): the SAME Jackson binding applies on this path.
|
|
397
|
+
// `propertyType` / `currentPropertyType` are BizPropertyType
|
|
398
|
+
// (@JsonCreator int) and `operatorType` is OperatorType (@JsonCreator int);
|
|
399
|
+
// the readable names are only a convenience the CLI adapts. Writing
|
|
400
|
+
// "EQ"/"SHORT_TEXT" through makes ObjectMapperUtils throw
|
|
401
|
+
// "json processing exception", which the dead-cycle check masks as
|
|
402
|
+
// 305003 死循环 AT CREATE TIME (live: this cost r16 45% of its refusals).
|
|
403
|
+
// Normalize to the integer codes the platform actually stores.
|
|
404
|
+
const parsed = JSON.parse(options.aggregation);
|
|
405
|
+
const normalized = normalizeAggregationFormula(parsed);
|
|
406
|
+
if (normalized !== parsed)
|
|
407
|
+
options.aggregation = JSON.stringify(normalized);
|
|
408
|
+
}
|
|
394
409
|
// editorText ↔ formula round-trip (display correctness). Skipped for the
|
|
395
410
|
// AGGREGATION carrier (its display object differs).
|
|
396
411
|
if (formulaType !== "AGGREGATION")
|
|
@@ -90,9 +90,9 @@ export const FIELD_FORMULA_WIRE = Object.freeze({
|
|
|
90
90
|
outerOptions: '{"formulaConfig":"<JSON string>","persistent":true[, "format":""|"percentile" — readback-only mirror, fwb §100]}',
|
|
91
91
|
innerConfig: '{"resultType":0|2,"resultFormat":""|"percentile","formulaType":"EXPRESSION","expression":"<JSON string {formula,editorText,editorMark}>"}',
|
|
92
92
|
expressionFormula: '<fieldCode> placeholders, e.g. {omc_a} + {omc_b}',
|
|
93
|
-
aggregationConfig: 'AGGREGATION nested {schemaCode,dataItemCode,calculateType,formula,queryConditions} = 表单「计算」控件的「聚合计算」(UI 实测 2026-10-07:跨模型聚合,求和/最大/最小/…). FormulaProcessor.executeMainAggregation 用 queryConditions 查该 schema、对匹配行的 dataItemCode 求值 formula. shape-validated & allowed. **计算类需求默认不用 FORMULA 字段**:子表求和回主表、子表列同行运算一律「普通字段 + 计算规则」(`datarule.create` dataRuleType:2,recipes `field-compute-subtable-sum` / `field-compute-child-column`,前台真机算得 7.00/350);FORMULA 字段仅作已存量读取。计算规则(`datarule.create` dataRuleType:2
|
|
93
|
+
aggregationConfig: 'AGGREGATION nested {schemaCode,dataItemCode,calculateType,formula,queryConditions} = 表单「计算」控件的「聚合计算」(UI 实测 2026-10-07:跨模型聚合,求和/最大/最小/…). FormulaProcessor.executeMainAggregation 用 queryConditions 查该 schema、对匹配行的 dataItemCode 求值 formula. shape-validated & allowed. **计算类需求默认不用 FORMULA 字段**:子表求和回主表、子表列同行运算一律「普通字段 + 计算规则」(`datarule.create` dataRuleType:2,recipes `field-compute-subtable-sum` / `field-compute-child-column`,前台真机算得 7.00/350);FORMULA 字段仅作已存量读取。计算规则(`datarule.create` dataRuleType:2)**在 REST 路径上不会执行,也不会回填**(r17 2026-10-25 复核:`data.submit` 写入后目标字段仍为传入值 0,`docker logs` 无任何 CalculateHandler/formula-recalc 记录;源码门控见 0.4.95:`CalculateHandler.java:50` 由 `forceExecuteCalculateRule` 门控,唯一开启入口 `BizObjectOptions.executeCalRule()` 全库 0 调用方)。本 FORMULA 类型字段同样只在门户前端算。**跨模型汇总在 REST 路径上可用(r17 2026-10-25 真机验证)**:FORMULA(100) + `formulaType:"AGGREGATION"`,`aggregation.schemaCode` 指向**另一个模型**,`queryConditions` 必须给 DYNAMIC 条件把两侧对上(`propertyCode` = 来源模型字段,`currentPropertyCode` = 目标模型字段;写反或留空则反向过滤器为空、目标字段恒 0),来源行 submit/update/delete 触发 `FormulaPropertySupport.onTriggerObjectChanged` 回算;发布时会自动生成 t100 CALCULATE_RULE 镜像行(回算前置)。实测 100→105。配方 `formula-aggregation-cross-model`。**同模型(含自己的子表)聚合仍然不可用**:`field.publish` 必抛 305003(`aggregation-self-schema-cycle`),这是平台 `publishFormula` 把相对码改写成子表父模型后 `checkCycleOfAggregation:1032` 判成环的必然结果。',
|
|
94
94
|
legacyRejected: "resultType/expression top-level keys are not read by the platform and are refused",
|
|
95
|
-
runtimeVerification: "本 FORMULA(100) 字段类型:计算由门户**前端**执行,API save/update/submit 不回填/重算(真机 2026-09-25/2026-10-07
|
|
95
|
+
runtimeVerification: "本 FORMULA(100) 字段类型:计算由门户**前端**执行,API save/update/submit 不回填/重算(真机 2026-09-25/2026-10-07,仅覆盖该字段类型)。字段计算规则(datarule dataRuleType:2)**同样不回填**:r17(2026-10-25)在 AI-Platform 1.1.25 上重建 `SUM({rows.amt})` 的 shortTextStitch 规则,`data.submit` 与 `data.update` 后 `total` 都仍等于传入值 0,`docker logs` 无 CalculateHandler/formula-recalc 记录;源码门控见 `CalculateHandler.java:50`(`forceExecuteCalculateRule`,唯一开启入口 `BizObjectOptions.executeCalRule()` 全库 0 调用方)。带子表计算字段的模型 API data.save/update 可能 50000(服务端重算 NPE),前台正常。",
|
|
96
96
|
});
|
|
97
97
|
/** RELEVANCE_DATA(16) options wire (8.6.26 RelevanceDataOptions + admin field serializer). */
|
|
98
98
|
export const FIELD_RELEVANCE_WIRE = Object.freeze({
|
|
@@ -152,7 +152,7 @@ export const FIELD_LANE_CONTRACT = Object.freeze({
|
|
|
152
152
|
"8.6.26 FormulaPropertyOptions (cloudpivot-shared-8.6.26): options={formulaConfig:string,persistent}; FormulaConfig={resultType,resultFormat,formulaType,expression,aggregation}; FormulaProcessor reads config.expression as a JSON string {formula,editorText,editorMark} and blanks {fieldCode} placeholders",
|
|
153
153
|
"omc-core formula-field-contract.ts v1 (live-verified): {formulaConfig,persistent:true}, resultType 0|2, resultFormat \"\"|\"percentile\"",
|
|
154
154
|
"field-families-fwb-20260914.md:170-180 (fwb readback 2026-09-14): FORMULA(100) field row options = {formulaConfig, persistent, format} with formulaConfig.resultFormat \"percentile\" and outer format \"percentile\" — the evidenced non-empty resultFormat and the readback-only outer format key are accepted; unknown values stay refused fail-closed",
|
|
155
|
-
"ticket 24 live (2026-09-12): FORMULA options read back verbatim via field.list; EXPRESSION field evaluated 3+4=7 on save. An empty-queryConditions AGGREGATION probe evaluated to 0 — REVERSED 2026-10-07: that was a false negative; AGGREGATION (表单「计算」控件「聚合计算」, 跨模型聚合) is supported; subtable→main roll-up and child-column arithmetic are delivered as 普通字段+计算规则 (datarule type2, recipes field-compute-subtable-sum / field-compute-child-column;
|
|
155
|
+
"ticket 24 live (2026-09-12): FORMULA options read back verbatim via field.list; EXPRESSION field evaluated 3+4=7 on save. An empty-queryConditions AGGREGATION probe evaluated to 0 — REVERSED 2026-10-07: that was a false negative; AGGREGATION (表单「计算」控件「聚合计算」, 跨模型聚合) is supported; subtable→main roll-up and child-column arithmetic are delivered as 普通字段+计算规则 (datarule type2, recipes field-compute-subtable-sum / field-compute-child-column; PORTAL-form verified 2026-10-08 — the API path never computes them, see the r17 2026-10-25 note below) — the earlier FORMULA-field recipes were removed as the wrong vehicle",
|
|
156
156
|
"8.6.26 RelevanceDataOptions (bizmodel): {relevanceField,referenceField,referenceFieldPropertyType,referenceFieldControlType}; validateRelevanceDataPropertyConfig requires referenceField+relevanceField; admin field serializer adds originalOptions/format1",
|
|
157
157
|
"ticket 24 live (2026-09-12): FORMULA options readback + RELEVANCE_DATA create/publish/form-binding readback",
|
|
158
158
|
"ticket 61 source (8.6.26): the runtime list face resolves a relation's displayed value from the field ROW's top-level relativePropertyCode, not options.displayField — QueryRuntimeController.java:1214-1226 (getRelativePropertyCode; empty → \"name\" data title); BizPropertyController create/update :851/:987 never auto-fill it, so every relation create/update must carry it",
|
package/dist/lanes/field.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
3
|
import { fieldChangeWireBody, fieldUpdateWireBody, prepareFieldChange, prepareFieldUpdate } from "../field-change.js";
|
|
4
|
-
import { choicesArg, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, subFieldArg, SYSTEM_FIELD_CODES, } from "../field-families.js";
|
|
4
|
+
import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, } from "../field-families.js";
|
|
5
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
6
6
|
import { governedRead } from "../read.js";
|
|
7
7
|
import { writeTool } from "../write.js";
|
|
@@ -141,6 +141,49 @@ function assertNameI18nArg(nameI18n) {
|
|
|
141
141
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
142
142
|
refuse("JSON 解析结果不是对象");
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* r17 (2026-10-25, live + decompiled A/B): a FORMULA field CANNOT aggregate its
|
|
146
|
+
* own model's child table. `BizPropertyManagerImpl.publishFormula` rewrites
|
|
147
|
+
* AggregationFormula.schemaCode → the child's PARENT, then
|
|
148
|
+
* `checkCycleOfAggregation:1032` compares that relative code with the field's
|
|
149
|
+
* own schemaCode; for a same-model child they are equal, so publish throws
|
|
150
|
+
* 305003「规则配置出现死循环」unconditionally (identical code in 8.6.26 and
|
|
151
|
+
* AI-Platform 1.1.25). Aggregating ANOTHER model's child table published
|
|
152
|
+
* errcode:0 in the same session — the relative code is the only difference.
|
|
153
|
+
*
|
|
154
|
+
* The static gate in field-families can only see the spelled-out target
|
|
155
|
+
* (`r17_agg` / `r17_agg.items`); a bare child code (`items`) is indistinguishable
|
|
156
|
+
* from an external model without a read. The create path already reads the
|
|
157
|
+
* published field list, so resolve it here for free and refuse before the write.
|
|
158
|
+
*/
|
|
159
|
+
function assertNoSelfSchemaAggregation(args, published) {
|
|
160
|
+
const raw = args.propertyType;
|
|
161
|
+
const propertyType = typeof raw === "number" ? raw : FIELD_PROPERTY_TYPES[String(raw)];
|
|
162
|
+
if (propertyType !== FIELD_PROPERTY_TYPES.FORMULA)
|
|
163
|
+
return;
|
|
164
|
+
const formulaConfig = args.options?.formulaConfig;
|
|
165
|
+
const config = decodeJsonObjectColumn(formulaConfig, { invalid: "undefined", array: "reject", object: "reject" });
|
|
166
|
+
if (!config || config.formulaType !== "AGGREGATION" || typeof config.aggregation !== "string")
|
|
167
|
+
return;
|
|
168
|
+
const agg = decodeJsonObjectColumn(config.aggregation, { invalid: "undefined", array: "reject", object: "reject" });
|
|
169
|
+
const target = typeof agg?.schemaCode === "string" ? agg.schemaCode.trim() : "";
|
|
170
|
+
if (!target)
|
|
171
|
+
return;
|
|
172
|
+
const childCodes = published
|
|
173
|
+
.filter((row) => Number(row.type ?? row.propertyType) === FIELD_PROPERTY_TYPES.CHILD_TABLE)
|
|
174
|
+
.map((row) => String(row.code ?? row.propertyCode ?? ""));
|
|
175
|
+
const resolvesToSelf = aggregationTargetsOwnSchema(args.schemaCode, target) || childCodes.includes(stripAggregationPrefix(target));
|
|
176
|
+
if (!resolvesToSelf)
|
|
177
|
+
return;
|
|
178
|
+
throw new ToolRefusalError({
|
|
179
|
+
status: "refused",
|
|
180
|
+
blocker: "aggregation-self-schema-cycle",
|
|
181
|
+
schemaCode: args.schemaCode,
|
|
182
|
+
aggregationSchemaCode: target,
|
|
183
|
+
childTableCodes: childCodes,
|
|
184
|
+
message: aggregationSelfSchemaMessage(target, args.schemaCode),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
144
187
|
export function registerFieldLane(tools) {
|
|
145
188
|
tools.push(defineTool({
|
|
146
189
|
name: "field.list",
|
|
@@ -300,6 +343,7 @@ export function registerFieldLane(tools) {
|
|
|
300
343
|
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST`, nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true, code: args.code })}'` });
|
|
301
344
|
// One seam owns field-change normalization, every reproduced gate, and the
|
|
302
345
|
// wire body (src/field-change.ts), so configure_model and intent share it.
|
|
346
|
+
assertNoSelfSchemaAggregation(args, published);
|
|
303
347
|
const prepared = prepareFieldChange(args);
|
|
304
348
|
let performed;
|
|
305
349
|
try {
|
|
@@ -384,6 +428,16 @@ async function runFieldUpdate(session, args) {
|
|
|
384
428
|
const record = rows.find((row) => (row.code ?? row.propertyCode) === args.code);
|
|
385
429
|
if (!record)
|
|
386
430
|
throw new WireError("business", `field ${args.code} not found on ${args.schemaCode}`, 301001);
|
|
431
|
+
if (args.options !== undefined) {
|
|
432
|
+
// Same platform cycle as field.create (r17). On UPDATE the caller may also
|
|
433
|
+
// be rewriting a previously-fine AGGREGATION, so resolve the child tables
|
|
434
|
+
// from the published face the same way; a read failure must not silently
|
|
435
|
+
// skip the gate, so fall back to the spelled-out target only.
|
|
436
|
+
const publishedRows = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })
|
|
437
|
+
.then((response) => wireRows(response.data))
|
|
438
|
+
.catch(() => []);
|
|
439
|
+
assertNoSelfSchemaAggregation({ schemaCode: args.schemaCode, propertyType: record.type ?? record.propertyType, options: args.options }, publishedRows);
|
|
440
|
+
}
|
|
387
441
|
const update = {
|
|
388
442
|
schemaCode: args.schemaCode,
|
|
389
443
|
code: args.code,
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -1237,7 +1237,7 @@ export function validateDataAction(action, nodeCode, index) {
|
|
|
1237
1237
|
code: "rule-graph-aggregation-is-a-field-feature",
|
|
1238
1238
|
nodeCode,
|
|
1239
1239
|
key: `dataActions[${index}].ruleValueType`,
|
|
1240
|
-
message: `${nodeCode}: 规则节点不支持 AGGREGATION 赋值——平台会存进去并启用,但运行期聚合结果是集合(ArrayList),标量目标收到集合即 50000「服务异常」(DecimalBizField.fillClientContent)。**子表计数/求和/取最值回主表用字段计算规则**:目标字段建成普通数值字段,然后 \`omc compute --schema <主表> --target <目标字段> --formula 'COUNTIF({<子表code>.<子列code>},"异常")'\`(默认 dry-run,--apply 写入;等价 datarule.create dataRuleType:2,配方 \`field-compute-subtable-sum\`)。规则图里只做 FIXED/DYNAMIC/EXPRESSION
|
|
1240
|
+
message: `${nodeCode}: 规则节点不支持 AGGREGATION 赋值——平台会存进去并启用,但运行期聚合结果是集合(ArrayList),标量目标收到集合即 50000「服务异常」(DecimalBizField.fillClientContent)。**子表计数/求和/取最值回主表用字段计算规则**:目标字段建成普通数值字段,然后 \`omc compute --schema <主表> --target <目标字段> --formula 'COUNTIF({<子表code>.<子列code>},"异常")'\`(默认 dry-run,--apply 写入;等价 datarule.create dataRuleType:2,配方 \`field-compute-subtable-sum\`)。规则图里只做 FIXED/DYNAMIC/EXPRESSION 的直接赋值。该计算规则**只在门户前端执行,REST 的 data.submit/data.update 不会回填**(r17 2026-10-25 复核:提交后目标字段仍为传入值),所以 API 里必须自己算好再写;正确性请用门户表单验证。来源:r11 巡检沙盒 2026-10-24(1.1.25)。`,
|
|
1241
1241
|
}];
|
|
1242
1242
|
if (!RULE_VALUE_TYPES.includes(valueType))
|
|
1243
1243
|
blockers.push({ code: "rule-graph-action-value-type", nodeCode, key: "ruleValueType", message: `${where}: ruleValueType "${valueType}" 不在 ${RULE_VALUE_TYPES.join("/")}` });
|
package/dist/recipes/datarule.js
CHANGED
|
@@ -55,7 +55,7 @@ const dataruleCalcRule = Object.freeze({
|
|
|
55
55
|
name: "c=a+b",
|
|
56
56
|
},
|
|
57
57
|
preconditions: ["**拼装器优先**:简单表达式用 `omc compute --formula '{a}*0.4+{b}*0.3'`;**条件聚合**用 `omc compute --formula '{<子表>.<子列>}' --where '<子列> = <值>'`(自动编译 COUNTIF/SUMIF)——两者都默认 dry-run、`--apply` 写入。本配方仅用于拼装器表达不了的条件分支等复杂场景。",
|
|
58
|
-
"**计算类需求:建普通业务字段(数值优先,文本亦可)+ 本计算规则;不要用「计算公式」字段类型(FORMULA
|
|
58
|
+
"**计算类需求:建普通业务字段(数值优先,文本亦可)+ 本计算规则;不要用「计算公式」字段类型(FORMULA)**——平台约定是普通字段顺带配计算规则(2026-10-08 门户复核:普通数值字段 + type2 计算规则,门户算出 7.00,设计器「计算规则」弹窗显示公式)。⚠️ r17 2026-10-25:这条规则**只在门户前端执行**,REST 的 `data.submit`/`data.update` 写完目标字段仍是传入值,API 调用方必须自己算好再写。",
|
|
59
59
|
"参与计算的字段已发布",
|
|
60
60
|
],
|
|
61
61
|
conditionalRequirements: [
|
|
@@ -98,6 +98,7 @@ const dataruleSubtableSum = Object.freeze({
|
|
|
98
98
|
preconditions: ["**拼装器优先**:简单表达式用 `omc compute --formula '{a}*0.4+{b}*0.3'`;**条件聚合**用 `omc compute --formula '{<子表>.<子列>}' --where '<子列> = <值>'`(自动编译 COUNTIF/SUMIF)——两者都默认 dry-run、`--apply` 写入。本配方仅用于拼装器表达不了的条件分支等复杂场景。",
|
|
99
99
|
"**目标 `total` 是普通数值字段(不是「计算公式」字段)**;`<childSchemaCode>` 是子表字段的 code(如 `prop_repair_detail` / `Sub_item_information_table`),`amount` 是其子列 code",
|
|
100
100
|
"子表与其列已发布",
|
|
101
|
+
"⚠️ r17 2026-10-25 复核:`SUM({子表.子列})` 的 type2 规则 `data.submit`/`data.update` 后目标字段**仍是传入值**(实测传 0 停在 0),`docker logs` 无任何 CalculateHandler/formula-recalc 记录。本配方只在**门户填表**时生效;API 路径请自行汇总后写入。同模型子表聚合也**不能**改用 FORMULA 字段的 AGGREGATION——`field.publish` 必抛 305003(`aggregation-self-schema-cycle`)。",
|
|
101
102
|
],
|
|
102
103
|
conditionalRequirements: [
|
|
103
104
|
"子表列引用用 `{子表字段code.子列code}` 点号全限定;求和 `SUM({子表.子列})`(真机核验:`SUM({prop_repair_detail.amount})`)。",
|
package/dist/recipes/field.js
CHANGED
|
@@ -223,8 +223,61 @@ const businessModelDropdown = Object.freeze({
|
|
|
223
223
|
requiresEnvironmentDerivedIds: true,
|
|
224
224
|
prerequisiteData: { source: "discover_environment → field.list(published:true)", fields: ["source.model", "source.businessField", "app.code"] },
|
|
225
225
|
});
|
|
226
|
+
const formulaAggregationCrossModel = Object.freeze({
|
|
227
|
+
id: "formula-aggregation-cross-model",
|
|
228
|
+
title: "跨模型汇总:FORMULA 字段 + AGGREGATION + DYNAMIC 条件(API 可回算,r17 真机 100→105 实测)",
|
|
229
|
+
aliases: ["跨模型汇总", "跨模型聚合", "另一个模型", "其它模型", "其他模型", "另一张表", "统计合计写入", "汇总到本模型", "API 回填聚合"],
|
|
230
|
+
status: "offline-valid",
|
|
231
|
+
tool: "field.create",
|
|
232
|
+
args: {
|
|
233
|
+
environment,
|
|
234
|
+
schemaCode: "r17_d",
|
|
235
|
+
code: "total3",
|
|
236
|
+
name: "按名称合计",
|
|
237
|
+
propertyType: "FORMULA",
|
|
238
|
+
options: {
|
|
239
|
+
formulaConfig: JSON.stringify({
|
|
240
|
+
formulaType: "AGGREGATION",
|
|
241
|
+
resultType: 2,
|
|
242
|
+
aggregation: JSON.stringify({
|
|
243
|
+
schemaCode: "r17_c",
|
|
244
|
+
dataItemCode: "amt",
|
|
245
|
+
calculateType: "SUM",
|
|
246
|
+
formula: "SUM({r17_c.amt})",
|
|
247
|
+
queryConditions: [[{ propertyCode: "nm", currentPropertyCode: "srcNo", operatorType: 1, valueType: "DYNAMIC", currentPropertyType: 0 }]],
|
|
248
|
+
}),
|
|
249
|
+
}),
|
|
250
|
+
persistent: true,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
preconditions: [
|
|
254
|
+
"**只能汇总「别的模型」**:同模型(含自己的子表)聚合在 field.publish 必抛 305003(平台 publishFormula 把相对码改写成子表父模型 = 本模型,checkCycleOfAggregation:1032 判成环)。工具已写前拒绝 `aggregation-self-schema-cycle`。",
|
|
255
|
+
"目标字段用 FORMULA(100)(不是普通字段 + 计算规则——type2 计算规则在 REST 路径永不执行)。",
|
|
256
|
+
"`schemaCode` 是被汇总的**来源模型**;`dataItemCode` 是来源模型的列;`queryConditions` **不能为空**,否则反向过滤器为空、找不到要回算的目标行(真机:空条件时 recalc 日志照常打印但目标字段恒为 0)。",
|
|
257
|
+
],
|
|
258
|
+
conditionalRequirements: [
|
|
259
|
+
"**DYNAMIC 条件的两个 code 分属两侧,写反了就静默 0**:`propertyCode` = **来源模型**的字段(这里是 r17_c.nm),`currentPropertyCode` = **目标模型**的字段(这里是 r17_d.srcNo)。语义 = 「来源行的 nm 等于本目标行的 srcNo」。",
|
|
260
|
+
"`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;工具会自动转码。",
|
|
261
|
+
"`calculateType` 是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION),不是枚举 code。",
|
|
262
|
+
"发布后平台会**自动生成一条 t100 CALCULATE_RULE 镜像行**(h_biz_data_rule,propertyCode=字段 code),它是回算触发的前置条件,别手动删。",
|
|
263
|
+
"回算时机 = **来源模型的行被 submit/update/delete**(`FormulaPropertySupport.onTriggerObjectChanged`)。目标行本身写入不会触发聚合。",
|
|
264
|
+
],
|
|
265
|
+
priorToolReferences: [
|
|
266
|
+
{ tool: "field.list", args: { environment, schemaCode: "r17_c", published: true }, purpose: "确认来源模型与其列已发布,取 dataItemCode 与 DYNAMIC 条件的 propertyCode" },
|
|
267
|
+
{ tool: "field.list", args: { environment, schemaCode: "r17_d", published: true }, purpose: "确认目标模型已有所承载匹配值的字段(currentPropertyCode)" },
|
|
268
|
+
],
|
|
269
|
+
dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 校验 options 形状与聚合目标;同模型聚合在此拒绝" },
|
|
270
|
+
expectedReadback: { readback: { fieldPresent: true, options: { formulaConfig: "<含 AGGREGATION + DYNAMIC 条件>" } }, next: ["field.publish", "data.submit 来源行 ×2 → data.load 目标行读到合计", "form.publish"] },
|
|
271
|
+
refusalRecovery: [
|
|
272
|
+
{ code: "aggregation-self-schema-cycle", action: "把 schemaCode 换成另一个模型;同模型/自己的子表聚合平台不支持" },
|
|
273
|
+
{ code: "duplicate-field", action: "字段已存在改用 field.update 或换 code" },
|
|
274
|
+
...commonRecovery,
|
|
275
|
+
],
|
|
276
|
+
requiresEnvironmentDerivedIds: true,
|
|
277
|
+
prerequisiteData: { source: "field.list(published:true) ×2(来源模型 + 目标模型)", fields: ["source.model", "source.dataItemCode", "source.matchField", "target.matchField"] },
|
|
278
|
+
});
|
|
226
279
|
export const FIELD_METADATA = Object.freeze({
|
|
227
280
|
"field.update": Object.freeze({ recipes: [fieldSerialNumber, formConditionalRequired, formAlwaysReadonly, formConditionalReadonly], relatedTools: ["field.list", "field.publish", "form.draft", "form.publish"] }),
|
|
228
|
-
"field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, childTable, businessModelDropdown], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
|
|
281
|
+
"field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, childTable, businessModelDropdown, formulaAggregationCrossModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
|
|
229
282
|
});
|
|
230
283
|
//# sourceMappingURL=field.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.96",
|
|
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.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.96",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.96",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -151,9 +151,13 @@ rules:
|
|
|
151
151
|
- **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
|
|
152
152
|
- 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
|
|
153
153
|
- **清理测试数据用批量删除**:`data.delete --args '{"schemaCode":"<code>","bizObjectIds":["id1","id2"]}'` 一次往返删多条(真机 r6:207 次逐条 `data.delete` 占 13.5% 调用)。
|
|
154
|
-
- **「聚合计算」是字段能力,不是规则节点**:FORMULA(计算公式)字段的 `options.formulaConfig.formulaType="AGGREGATION"` 就是 UI 里的「配置方式 →
|
|
155
|
-
`options = {persistent:true, formulaConfig: JSON字符串({resultType:2,
|
|
156
|
-
**`
|
|
154
|
+
- **「聚合计算」是字段能力,不是规则节点**:FORMULA(计算公式)字段的 `options.formulaConfig.formulaType="AGGREGATION"` 就是 UI 里的「配置方式 → 聚合计算」。**只能汇总另一个模型**(跨模型),**同模型/自己的子表会 305003**(`field.publish` 时 `publishFormula` 把相对码改写成子表父模型 = 本模型,`checkCycleOfAggregation:1032` 判成环;工具已写前拒绝 `aggregation-self-schema-cycle`)。可直接抄 `omc describe field.create --example formula-aggregation-cross-model`。形状:
|
|
155
|
+
`options = {persistent:true, formulaConfig: JSON字符串({resultType:2, formulaType:"AGGREGATION", aggregation: JSON字符串({schemaCode:"<来源模型>", dataItemCode:"<来源列>", calculateType:"SUM", formula:"SUM({<来源模型>.<列>})", queryConditions:[[{propertyCode:"<来源模型字段>", currentPropertyCode:"<目标模型字段>", operatorType:1, valueType:"DYNAMIC"}]]})})}`。
|
|
156
|
+
- **`queryConditions` 不能为空**:它是反向过滤器的唯一输入,空数组时回算日志照打但目标字段恒 0(r17 实测)。
|
|
157
|
+
- **DYNAMIC 条件的两个 code 分属两侧,写反了静默 0**:`propertyCode` = 来源模型字段,`currentPropertyCode` = 目标模型字段;语义「来源行的 X 等于本目标行的 Y」。
|
|
158
|
+
- **`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
|
+
- **回算时机**:来源模型的行 submit/update/delete 触发 `FormulaPropertySupport.onTriggerObjectChanged`;目标行自己写入不触发。发布后平台自动生成 t100 `CALCULATE_RULE` 镜像行,别删。r17 真机:目标行 srcNo=C1、来源 C1 行 amt=100 → 读 100;再提交一条 C1/amt=5 的来源行 → 读 105。
|
|
160
|
+
- **普通数值字段 + 计算规则(type2)不能替代**:REST 路径永不执行(`CalculateHandler.java:50` 的 `forceExecuteCalculateRule` 从不置真),门户里才算。
|
|
157
161
|
- **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`)。
|
|
158
162
|
- **单选字段的 `dataCondition` 在 1.1.25 不生效**:用 FIXED 值比较 RADIO/CHECKBOX/DROPDOWN 时,运行时日志「解析选框类型数据项的比较值失败」,条件被跳过、节点**无条件执行**(r8:异常归还被当成已归还回写)。`rule.save` 会把它放进 `readback.lintHazards`(`rule-graph-radio-condition-unparseable`);改用 BRANCH 判断该字段,或拆两条规则。
|
|
159
163
|
- **预置 Update 规则里的跨模型回写只在 `data.update` 触发**:`rule-graph-cross-model-update-trigger-inert` 现在是 hazard(在 `readback.lintHazards`,不混进 `lintWarnings`);create/submit 不执行它。要提交时生效,把节点放进 Create 规则或流程事件规则。
|
|
@@ -91,7 +91,9 @@ only the section for the capability you need —
|
|
|
91
91
|
- **边界 / 陷阱**:
|
|
92
92
|
- **计算类需求默认走「普通字段 + 计算规则」,不要建「计算公式」字段类型(FORMULA)**:建一个**数值/单行文本**业务字段,再配一条**字段级计算规则**(`datarule.create` `dataRuleType:2`,`options.shortTextStitch={"formula","editorText","editorMark"}`)。理由:平台约定用正常业务字段承载、顺带配计算规则(真机 2026-10-08 production 表单核验 + 设计器弹窗显示)。「计算公式」字段类型不是默认,仅当确需一列专用公式时才用。
|
|
93
93
|
- **计算规则公式引用**:同类字段裸 `{字段code}`(`{a}+{b}`);**子表列用 `{子表字段code.子列code}`**(`{Sub_item_information_table.taxInclusiveAmount}`);子表求和 `SUM({子表.子列})`;支持 `IF(cond,a,b)`、`CASE(v,c1,r1,c2,r2)`、`VALUE()`、`AND`、比较 `=`、字符串字面量与运算符空格。recipe:`omc describe datarule.create --example field-compute-rule|field-compute-subtable-sum|field-compute-child-column`。
|
|
94
|
-
- 计算规则(`dataRuleType:2
|
|
94
|
+
- 计算规则(`dataRuleType:2`)**在 REST 路径上不会执行也不会回填**(r17 2026-10-25 复核:`data.submit`/`data.update` 后目标字段仍为传入值)。
|
|
95
|
+
- **跨模型汇总可以用 FORMULA(100) + AGGREGATION,REST 能回算**(r17 真机 100→105):`omc describe field.create --example formula-aggregation-cross-model`;`queryConditions` 必须给 DYNAMIC 条件(propertyCode=来源字段、currentPropertyCode=目标字段),空条件静默得 0。
|
|
96
|
+
- **同模型(含自己的子表)聚合不可用**:`field.publish` 必抛 305003(`aggregation-self-schema-cycle`,平台 `checkCycleOfAggregation:1032` 逻辑成环)。
|
|
95
97
|
- 只有 `FORMULA`(计算公式,`propertyType:100`)字段是**门户前端**计算、API 不回填(旧证据 2026-09-25/10-07,仅覆盖该字段类型)。
|
|
96
98
|
- 子表列支持标量族 + WORK_SHEET 关联列(需 `relativeCode`+options);**嵌套子表 / MULT_WORK_SHEET / RELEVANCE_DATA 不覆盖**。
|
|
97
99
|
|
|
@@ -44,13 +44,18 @@ capability's coverage/tools/boundaries from `omc-capabilities`.
|
|
|
44
44
|
Coverage status and boundaries for every capability (including 权限/子表/聚合/
|
|
45
45
|
上线/内联/OCR) live in **`omc-capabilities`** — its 能力索引 table and
|
|
46
46
|
[边界] section are the authority; read the relevant section there rather than a
|
|
47
|
-
copy here. In particular,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
copy here. In particular, **跨模型汇总/求和是配置能力**(FORMULA(100) + AGGREGATION,REST 可回算),不要当二开;
|
|
48
|
+
**同模型(含自己的子表)聚合平台不支持**——`field.publish` 必抛 305003(`aggregation-self-schema-cycle`),别用 FORMULA 兜底。
|
|
49
|
+
|
|
50
|
+
跨模型汇总的正解(r17 2026-10-25 真机 100→105 验证):
|
|
51
|
+
`omc describe field.create --example formula-aggregation-cross-model`。要点:`aggregation.schemaCode` 指向**另一个模型**,
|
|
52
|
+
`queryConditions` 必须给 DYNAMIC 条件(`propertyCode`=来源字段、`currentPropertyCode`=目标字段),
|
|
53
|
+
`propertyType`/`operatorType` 写整数码(工具会自动转名字),`calculateType` 写字符串。
|
|
54
|
+
空 `queryConditions` 会静默得 0——它同时是「哪些目标行要回算」的反向过滤器输入。
|
|
55
|
+
|
|
56
|
+
字段计算规则(`dataRuleType:2`)在 REST 路径上**不会执行也不会回填**(r17 2026-10-25 复核:`data.submit`/`data.update` 后目标字段仍为传入值)。
|
|
51
57
|
**不要**在规则图(`rule.save`)里用 `ruleValueType:"AGGREGATION"` —— 平台会存进去并启用,运行期聚合结果是集合,
|
|
52
58
|
标量目标收到集合即 50000「服务异常」(r11 巡检沙盒 2026-10-24,1.1.25;OMC 现已写前 fail-closed)。
|
|
53
|
-
字段计算规则(`dataRuleType:2`)由**服务端**在 `data.submit`/`data.update` 时计算并回填(且覆盖传入值,真机 1.1.25 复核):写完直接 `data.load` 读目标字段即可验证。只有 `FORMULA`(100) 字段是前端计算、API 不回填。
|
|
54
59
|
|
|
55
60
|
Cue → capability is a starting point, never a substitute for 定面. A cue that
|
|
56
61
|
could name two faces is resolved by the 定面表 in the parent skill.
|
|
@@ -97,13 +97,19 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
97
97
|
|
|
98
98
|
## 计算规则(datarule)的取值时机
|
|
99
99
|
|
|
100
|
-
- **字段计算规则(`datarule.create` `dataRuleType:2
|
|
101
|
-
`data.submit` 与 `data.update`
|
|
102
|
-
- 实测:`
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
100
|
+
- **字段计算规则(`datarule.create` `dataRuleType:2`)在 REST 路径上不会执行、也不会回填**(r17 2026-10-25 复核,AI-Platform 1.1.25):
|
|
101
|
+
`data.submit` 与 `data.update` 写完目标字段**仍是调用方传入值**;`docker logs` 里没有任何 CalculateHandler/formula-recalc 记录。
|
|
102
|
+
- 实测:`SUM({rows.amt})` 的 type2 规则 + 两行明细(10+20),submit 传 `total:0` 回读仍是 `0`;`data.update` 传 `0` 也仍是 `0`。
|
|
103
|
+
(早前「覆盖传入值、API 可判定」的结论来自 r12,已被推翻——不要再用它做验收。)
|
|
104
|
+
- 源码门控:`CalculateHandler.java:50` 的 `forceExecuteCalculateRule` 只在 `BizObjectOptions.executeCalRule()` 为真时求值,而该 builder 全库 0 调用方(REST 路径从不置真)。
|
|
105
|
+
- 所以「计算规则是否生效」**不能用 API 判定**;正确性只能在门户表单里验。API 调用方请自己算好再写。
|
|
106
|
+
- **跨模型汇总可以走 FORMULA(100) + AGGREGATION,而且 REST 能回算**(r17 真机 100→105):
|
|
107
|
+
`aggregation.schemaCode` 指向**另一个模型**,`queryConditions` 必须给 DYNAMIC 条件
|
|
108
|
+
(`propertyCode`=来源模型字段、`currentPropertyCode`=目标模型字段);来源行 submit/update/delete 触发
|
|
109
|
+
`FormulaPropertySupport.onTriggerObjectChanged`。配方:`omc describe field.create --example formula-aggregation-cross-model`。
|
|
110
|
+
- **同模型(含自己的子表)聚合不可用**:`field.publish` 必抛 305003 —— `publishFormula` 把相对码改写成子表父模型(= 本模型),
|
|
111
|
+
`checkCycleOfAggregation:1032` 判成环。与 payload 无关,8.6.26 与 1.1.25 代码相同。OMC 已写前拒绝 `aggregation-self-schema-cycle`。
|
|
112
|
+
- **区别:`FORMULA`(计算公式,`propertyType:100`)字段的 EXPRESSION 模式仍只在前端算** —— API 写入不回填。
|
|
107
113
|
|
|
108
114
|
## 规则与业务集成
|
|
109
115
|
|