@stalfh233/omc-cli 0.0.0-stage → 0.4.10
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/LICENSE +21 -0
- package/README.md +293 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ToolRefusalError } from "./tool-types.js";
|
|
3
|
+
import { decodeJsonObjectColumn } from "./json-column.js";
|
|
4
|
+
import { FIELD_CONTROL_CODES, FIELD_DATE_DEFAULT_OPTIONS, FIELD_DATE_FORMATS, FIELD_FAMILY_LIST } from "./lanes/field-contract.js";
|
|
5
|
+
import { formConditionOptionsError } from "./lanes/datarule-protocol.js";
|
|
6
|
+
import { CAPABILITY_CONTROL_TYPES, capabilityShapeError, deriveControlOptions, deriveFieldOptions } from "./capability-shape.js";
|
|
7
|
+
export { FIELD_FAMILY_LIST, FIELD_CONTROL_CODES };
|
|
8
|
+
export const propertyTypeArg = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.describe(`field family. Accepted: ${FIELD_FAMILY_LIST}, CHILD_TABLE (子表, requires \`subFields\`). Out of coverage (refused with a citation): OCR, ELECTRONIC_SIGN, and nested CHILD_TABLE. Selection families take \`choices\`; relation families take \`relativeCode\` + \`options\`.`);
|
|
12
|
+
export const relativeCodeArg = z.string().optional().describe("relation target model code (required for WORK_SHEET / MULT_WORK_SHEET)");
|
|
13
|
+
/**
|
|
14
|
+
* Ticket 59 (P0): the wire `name_i18n` column must be a *parseable JSON object
|
|
15
|
+
* string*, never a bare display string. The admin designer runs an unguarded
|
|
16
|
+
* `JSON.parse(name_i18n)` (frontend `compatibleOldData`); a bare "报修主题"
|
|
17
|
+
* throws and the whole Display Fields panel fails to render. Every
|
|
18
|
+
* model/field/sheet create path writes through this one helper so the shape
|
|
19
|
+
* can never drift again.
|
|
20
|
+
*/
|
|
21
|
+
export function nameI18nJson(name) {
|
|
22
|
+
return JSON.stringify({ "zh-CN": name, en: name });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ticket 61 — the platform ignores a relation field's `options.displayField`
|
|
26
|
+
* unless `displayFieldType` is also present (it then falls back to the target
|
|
27
|
+
* model's data title). Live evidence (2026-09-13 repair-module incident): the
|
|
28
|
+
* designer wrote `displayFieldType:12` for a RADIO(12) display field and the
|
|
29
|
+
* verified repair wrote `displayFieldType:0` for a SHORT_TEXT(0) one — the
|
|
30
|
+
* value is the display field's BizPropertyType int. Default 0 (SHORT_TEXT,
|
|
31
|
+
* the live-verified pairing); pass an explicit int for a non-text display
|
|
32
|
+
* field. `displayFieldFormat` was "" on both observed faces.
|
|
33
|
+
*/
|
|
34
|
+
export function ensureRelationDisplayFieldOptions(type, options) {
|
|
35
|
+
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
36
|
+
return options;
|
|
37
|
+
if (typeof options.displayField !== "string" || options.displayField.trim() === "")
|
|
38
|
+
return options;
|
|
39
|
+
return {
|
|
40
|
+
...options,
|
|
41
|
+
...(options.displayFieldType === undefined ? { displayFieldType: 0 } : {}),
|
|
42
|
+
...(options.displayFieldFormat === undefined ? { displayFieldFormat: "" } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ticket 61 root cause (source evidence): the runtime list face resolves a
|
|
47
|
+
* relation's displayed value from the field ROW's top-level
|
|
48
|
+
* `relativePropertyCode`, NOT from `options.displayField` —
|
|
49
|
+
* `QueryRuntimeController.java:1214-1226` reads
|
|
50
|
+
* `bizPropertyModel3.getRelativePropertyCode()` and falls back to `"name"`
|
|
51
|
+
* (the target's data title) when it is empty. The designer writes the
|
|
52
|
+
* top-level key explicitly; `BizPropertyController` create/update (:851/:987)
|
|
53
|
+
* never auto-fills it. Every relation create/update must therefore carry it.
|
|
54
|
+
*/
|
|
55
|
+
export function relationDisplayFieldCode(type, options) {
|
|
56
|
+
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
57
|
+
return undefined;
|
|
58
|
+
const displayField = options.displayField;
|
|
59
|
+
return typeof displayField === "string" && displayField.trim() ? displayField.trim() : undefined;
|
|
60
|
+
}
|
|
61
|
+
export const choicesArg = z.array(z.string()).optional().describe('allowed values for text-choice families (RADIO / CHECKBOX / DROPDOWN_BOX / DROPDOWN_MULTI_BOX), e.g. ["正常","异常","待复检"]。注意:SELECTION(5) 是人员/部门混合选择器,不接受 choices,文本单选请用 DROPDOWN_BOX/RADIO');
|
|
62
|
+
function jsonString() {
|
|
63
|
+
return z.string().refine((value) => {
|
|
64
|
+
if (value === "")
|
|
65
|
+
return true;
|
|
66
|
+
try {
|
|
67
|
+
JSON.parse(value);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}, "must be valid JSON");
|
|
74
|
+
}
|
|
75
|
+
export const dictionaryOptionsArg = z.object({
|
|
76
|
+
// Live 2026-09-24 (bench34): the platform resolves this as the `dicId` of
|
|
77
|
+
// `data_dictionary/getEnableRecordsByDictionaryId`, and the designer's
|
|
78
|
+
// a-select option value is the dictionary **id** — NOT its name. Writing the
|
|
79
|
+
// name makes the runtime answer "该字典已被禁用,无数据返回" and the dropdown
|
|
80
|
+
// renders EMPTY (while storage readback looks fine).
|
|
81
|
+
checkedDictionary: z.string().min(1).describe("字典 **id**(不是 name;取 dictionary.names 的 id)。填 name 会让运行时下拉空白"),
|
|
82
|
+
checkedDictionaryCode: z.string().min(1).describe("字典 code(dictionary.names 的 code)"),
|
|
83
|
+
valueText: z.string().optional(),
|
|
84
|
+
dictionariesType: z.number().int().optional(),
|
|
85
|
+
useDictionariesData: z.array(z.object({ code: z.string().min(1), name: z.string().optional(), id: z.string().optional() }).passthrough()).optional(),
|
|
86
|
+
}).passthrough();
|
|
87
|
+
export const businessModelOptionsArg = z.object({
|
|
88
|
+
schemaCode: z.string().min(1),
|
|
89
|
+
sheetDataItem: z.string().min(1),
|
|
90
|
+
modelName: z.string().optional(),
|
|
91
|
+
dataItemType: z.number().int().optional(),
|
|
92
|
+
orderByFields: z.union([z.string(), z.array(z.string())]).optional().describe("下拉项排序字段,只支持**一个字段名**。传数组会触发运行时 10009(平台把它包成单元素数组 → 嵌套数组);CLI 会把 1 元素数组规范化为字符串"),
|
|
93
|
+
orderType: z.number().int().optional(),
|
|
94
|
+
conditions: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
95
|
+
}).passthrough();
|
|
96
|
+
export const optionsSetArg = z.object({
|
|
97
|
+
optionsType: z.enum(["custom", "dictionary", "businessModel"]),
|
|
98
|
+
dictionary: dictionaryOptionsArg.optional(),
|
|
99
|
+
businessModel: businessModelOptionsArg.optional(),
|
|
100
|
+
custom: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
101
|
+
}).passthrough().superRefine((value, ctx) => {
|
|
102
|
+
if (value.optionsType === "dictionary" && !value.dictionary)
|
|
103
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "dictionary source requires optionsSet.dictionary" });
|
|
104
|
+
if (value.optionsType === "businessModel" && !value.businessModel)
|
|
105
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "businessModel source requires optionsSet.businessModel" });
|
|
106
|
+
if (value.optionsType === "custom" && !value.custom)
|
|
107
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "custom source requires optionsSet.custom" });
|
|
108
|
+
});
|
|
109
|
+
export const relationFilterArg = z.object({
|
|
110
|
+
searchFormula: jsonString().optional().describe("JSON string for data restriction range"),
|
|
111
|
+
conditions: jsonString().optional().describe("JSON string for relation query conditions"),
|
|
112
|
+
}).passthrough();
|
|
113
|
+
/**
|
|
114
|
+
* CHILD_TABLE(8) child columns. A child table is one field on the main model
|
|
115
|
+
* whose definition is `subSchema.properties`; each child column is a scalar
|
|
116
|
+
* field (the child's own `schemaCode` is the parent field code, sortKey is the
|
|
117
|
+
* array order). Nested CHILD_TABLE and relation/formula/relevance families are
|
|
118
|
+
* excluded (omc-core `childtable-field-contract.ts` exclusions).
|
|
119
|
+
*/
|
|
120
|
+
export const subFieldArg = z
|
|
121
|
+
.array(z.object({
|
|
122
|
+
code: z.string().min(1),
|
|
123
|
+
name: z.string().min(1),
|
|
124
|
+
propertyType: z.string().min(1).describe("child family (CHILD_TABLE, OCR, ELECTRONIC_SIGN, MULT_WORK_SHEET, RELEVANCE_DATA and FORMULA are refused; WORK_SHEET is allowed with relativeCode+options)"),
|
|
125
|
+
choices: z.array(z.string()).optional().describe("child text-choice families (RADIO / CHECKBOX / DROPDOWN_BOX / DROPDOWN_MULTI_BOX) allowed values"),
|
|
126
|
+
relativeCode: z.string().min(1).optional().describe("WORK_SHEET child column: the target model schemaCode (e.g. customer)"),
|
|
127
|
+
options: z.record(z.string(), z.unknown()).optional().describe("WORK_SHEET child column: relation display options { schemaCode, queryCode, displayField, mappings, ... }"),
|
|
128
|
+
propertyEmpty: z.boolean().optional().describe("child column 是否不可为空;默认 false(可为空)"),
|
|
129
|
+
}))
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("CHILD_TABLE(8) required: the child columns, in display order; each becomes a subSchema property (schemaCode=父字段 code, sortKey=数组位+1)");
|
|
132
|
+
/**
|
|
133
|
+
* Field options. The shape depends on `propertyType`:
|
|
134
|
+
* - relation families WORK_SHEET(9) / MULT_WORK_SHEET(11):
|
|
135
|
+
* { schemaCode:<target model>, queryCode:<target model>, displayField:"name",
|
|
136
|
+
* mappings:'[{"source":<target field code>,"target":<this-model field code>}]' }
|
|
137
|
+
* - RELEVANCE_DATA(16) — a *reference to another field*, NOT a model relation
|
|
138
|
+
* (8.6.26 `RelevanceDataOptions`, admin field serializer):
|
|
139
|
+
* { relevanceField:<local WORK_SHEET field code>,
|
|
140
|
+
* referenceField:<target field code on the related model>,
|
|
141
|
+
* referenceFieldPropertyType:<int BizPropertyType>,
|
|
142
|
+
* referenceFieldControlType?:<int BizFormPropertyType> }
|
|
143
|
+
* - FORMULA: the standalone 「计算公式」 field family (platform type 89). **NOT the
|
|
144
|
+
* default for calculation requirements** — the platform convention is a NORMAL
|
|
145
|
+
* business field (数值 / 单行文本 …) with a separate 「计算规则」 (field-level
|
|
146
|
+
* data rule, `datarule.create` dataRuleType 2). Prefer that; use FORMULA(计算
|
|
147
|
+
* 公式) only when the requirement explicitly wants a dedicated formula column.
|
|
148
|
+
* The old text labelled this family "FORMULA(100)": 100 is actually 单据号
|
|
149
|
+
* (serial number); the OMC-family name maps to the platform's 计算公式 face.
|
|
150
|
+
* { formulaConfig:"<nested JSON string>", persistent:true }
|
|
151
|
+
* where formulaConfig = { resultType:0|2, resultFormat:""|"percentile", formulaType:"EXPRESSION",
|
|
152
|
+
* expression:"<JSON string {formula,editorText,editorMark}>" }. resultFormat "percentile"
|
|
153
|
+
* is evidenced on fwb readback together with an outer readback-only `format` mirror key
|
|
154
|
+
* (field-families-fwb-20260914.md §100); unknown resultFormat values are refused
|
|
155
|
+
* fail-closed. AGGREGATION is structurally valid but blocked (condition-group
|
|
156
|
+
* semantics not contract-grade).
|
|
157
|
+
* The old top-level { resultType, expression } keys are NOT read by the platform and are refused.
|
|
158
|
+
* - selection families: omit options and pass `choices` instead.
|
|
159
|
+
* Any other keys pass through. `nested:true` / `relationLinkage:true` are refused
|
|
160
|
+
* (coverage boundary).
|
|
161
|
+
*/
|
|
162
|
+
export const fieldOptionsArg = z
|
|
163
|
+
.object({
|
|
164
|
+
schemaCode: z.string().optional().describe("relation target model code (WORK_SHEET / MULT_WORK_SHEET)"),
|
|
165
|
+
queryCode: z.string().optional().describe("relation query code (usually the target model code)"),
|
|
166
|
+
displayField: z.string().optional().describe("relation display field (usually 'name')"),
|
|
167
|
+
mappings: z.string().optional().describe('relation carry mappings, e.g. [{"source":"<target field code>","target":"<this-model field code>"}]'),
|
|
168
|
+
searchFormula: jsonString().optional().describe("WORK_SHEET / MULT_WORK_SHEET data restriction range JSON"),
|
|
169
|
+
conditions: jsonString().optional().describe("WORK_SHEET / MULT_WORK_SHEET query conditions JSON"),
|
|
170
|
+
optionsSet: optionsSetArg.optional().describe("selection source: custom, dictionary, or businessModel"),
|
|
171
|
+
formulaConfig: z.string().optional().describe('FORMULA(计算公式)字段 required: nested JSON string. **计算类需求默认不要建本字段类型**,改用普通字段(数值/单行文本)+计算规则(datarule.create dataRuleType 2, 裸 {字段} 引用)。本字段类型用于明确的「计算公式」列。形状 {"resultType":0|2,"resultFormat":""|"percentile","formulaType":"EXPRESSION","expression":"<JSON string {formula,editorText,editorMark}>"};计算由门户前端执行、API 不回填。'),
|
|
172
|
+
persistent: z.boolean().optional().describe("FORMULA(100): must be true; persistent=false is outside the evidenced contract"),
|
|
173
|
+
relevanceField: z.string().optional().describe("RELEVANCE_DATA(16) required: local WORK_SHEET field code the reference resolves through"),
|
|
174
|
+
referenceField: z.string().optional().describe("RELEVANCE_DATA(16) required: referenced field code on the related model"),
|
|
175
|
+
referenceFieldPropertyType: z.number().int().optional().describe("RELEVANCE_DATA(16) required: referenced field propertyType (BizPropertyType int)"),
|
|
176
|
+
referenceFieldControlType: z.number().int().optional().describe("RELEVANCE_DATA(16): referenced field control type (BizFormPropertyType int)"),
|
|
177
|
+
nested: z.boolean().optional().describe("nested child-table metadata — refused (coverage boundary)"),
|
|
178
|
+
relationLinkage: z.boolean().optional().describe("advanced relation linkage — refused (coverage boundary)"),
|
|
179
|
+
})
|
|
180
|
+
.passthrough();
|
|
181
|
+
/** Live-accepted field families: wire propertyType is an int enum. */
|
|
182
|
+
export const FIELD_PROPERTY_TYPES = {
|
|
183
|
+
SHORT_TEXT: 0,
|
|
184
|
+
LONG_TEXT: 1,
|
|
185
|
+
NUMERICAL: 2,
|
|
186
|
+
DATE: 3,
|
|
187
|
+
LOGICAL: 4,
|
|
188
|
+
SELECTION: 5,
|
|
189
|
+
ATTACHMENT: 6,
|
|
190
|
+
CHILD_TABLE: 8,
|
|
191
|
+
WORK_SHEET: 9,
|
|
192
|
+
ADDRESS: 10,
|
|
193
|
+
MULT_WORK_SHEET: 11,
|
|
194
|
+
RADIO: 12,
|
|
195
|
+
CHECKBOX: 13,
|
|
196
|
+
DROPDOWN_BOX: 14,
|
|
197
|
+
DROPDOWN_MULTI_BOX: 15,
|
|
198
|
+
RELEVANCE_DATA: 16,
|
|
199
|
+
HYPER_LINK: 17,
|
|
200
|
+
TIME: 28,
|
|
201
|
+
STAFF_SELECTOR: 50,
|
|
202
|
+
STAFF_MULTI_SELECTOR: 51,
|
|
203
|
+
DEPARTMENT_SELECTOR: 60,
|
|
204
|
+
DEPARTMENT_MULTI_SELECTOR: 61,
|
|
205
|
+
FORMULA: 100,
|
|
206
|
+
};
|
|
207
|
+
export const OUT_OF_COVERAGE_FIELD_TYPES = {
|
|
208
|
+
OCR: "boundary-unsupported (OCR and ELECTRONIC_SIGN field families)",
|
|
209
|
+
ELECTRONIC_SIGN: "boundary-unsupported (OCR and ELECTRONIC_SIGN field families)",
|
|
210
|
+
};
|
|
211
|
+
/** Reverse lookup: wire propertyType int -> family name (for messages). */
|
|
212
|
+
export function familyName(type) {
|
|
213
|
+
return Object.entries(FIELD_PROPERTY_TYPES).find(([, value]) => value === type)?.[0] ?? `wire:${type}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Read-face type label (round 13): the wire carries `propertyType` as an int
|
|
217
|
+
* (0=SHORT_TEXT, 2=NUMERICAL, 50=STAFF_SELECTOR, …). A bare int forces the
|
|
218
|
+
* caller to decode it or pay a ~19 KB `describe field.create --full` to learn
|
|
219
|
+
* the enum. This returns the family name for a numeric wire value (or passes a
|
|
220
|
+
* string through), so a field read is self-describing at ~11 B/field.
|
|
221
|
+
*/
|
|
222
|
+
export function fieldTypeLabel(raw) {
|
|
223
|
+
if (typeof raw === "number" && Number.isFinite(raw))
|
|
224
|
+
return familyName(raw);
|
|
225
|
+
if (typeof raw === "string" && raw.length > 0)
|
|
226
|
+
return raw in FIELD_PROPERTY_TYPES ? raw : (Number.isFinite(Number(raw)) ? familyName(Number(raw)) : raw);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Chinese concept synonym → family, so a caller who names the concept instead of
|
|
231
|
+
* the enum ("关联引用" for a model relation, "明细" for a child table) gets a
|
|
232
|
+
* "did you mean" hint instead of a bare out-of-coverage refusal. Bench18: an
|
|
233
|
+
* agent picking RELEVANCE_DATA for a model relation cost a wasted write.
|
|
234
|
+
*/
|
|
235
|
+
export const FIELD_TYPE_SYNONYMS = Object.freeze({
|
|
236
|
+
关联: "WORK_SHEET(9)(单条;多条用 MULT_WORK_SHEET(11))",
|
|
237
|
+
关联引用: "WORK_SHEET(9)(引用另一模型的一条记录)——注意 RELEVANCE_DATA(16) 是引用本模型已有关联字段里的某个字段,不是模型关联",
|
|
238
|
+
引用: "WORK_SHEET(9) / MULT_WORK_SHEET(11)",
|
|
239
|
+
明细: "CHILD_TABLE(8)(子表)",
|
|
240
|
+
子表: "CHILD_TABLE(8)",
|
|
241
|
+
金额: "NUMERICAL(2)(+ options.scale 小数位数)",
|
|
242
|
+
数值: "NUMERICAL(2)",
|
|
243
|
+
编号: "SHORT_TEXT(0)",
|
|
244
|
+
文本: "SHORT_TEXT(0) / LONG_TEXT(1)",
|
|
245
|
+
单选: "RADIO(12) / DROPDOWN_BOX(14)(字典来源)",
|
|
246
|
+
多选: "CHECKBOX(13) / DROPDOWN_MULTI_BOX(15)",
|
|
247
|
+
下拉: "DROPDOWN_BOX(14)(字典)",
|
|
248
|
+
日期: "DATE(3)",
|
|
249
|
+
时间: "TIME(28)",
|
|
250
|
+
附件: "ATTACHMENT(6)",
|
|
251
|
+
人员: "STAFF_SELECTOR(50) / STAFF_MULTI_SELECTOR(51)",
|
|
252
|
+
部门: "DEPARTMENT_SELECTOR(60) / DEPARTMENT_MULTI_SELECTOR(61)",
|
|
253
|
+
是或否: "LOGICAL(4)",
|
|
254
|
+
布尔: "LOGICAL(4)",
|
|
255
|
+
});
|
|
256
|
+
/** A "did you mean" hint for a family name that is actually a Chinese concept. */
|
|
257
|
+
export function fieldTypeHint(type) {
|
|
258
|
+
for (const [concept, family] of Object.entries(FIELD_TYPE_SYNONYMS))
|
|
259
|
+
if (type.includes(concept) || concept.includes(type))
|
|
260
|
+
return `「${type}」看起来是概念名而非族名;最接近的是 ${family}`;
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
/** wire propertyType (int) -> form controlType (type-matrix.md §3.1). */
|
|
264
|
+
export const FIELD_CONTROL_TYPES = { ...CAPABILITY_CONTROL_TYPES };
|
|
265
|
+
export const SYSTEM_FIELD_CODES = new Set([
|
|
266
|
+
"owner",
|
|
267
|
+
"creater",
|
|
268
|
+
"createdTime",
|
|
269
|
+
"modifiedTime",
|
|
270
|
+
"modifier",
|
|
271
|
+
"sequenceNo",
|
|
272
|
+
"sequenceStatus",
|
|
273
|
+
"id",
|
|
274
|
+
"name",
|
|
275
|
+
"ownerDeptId",
|
|
276
|
+
"ownerDeptQueryCode",
|
|
277
|
+
"createdDeptId",
|
|
278
|
+
"workflowInstanceId",
|
|
279
|
+
]);
|
|
280
|
+
const RELEVANCE_CONTROL_KEYS = ["relevanceField", "referenceField", "referenceFieldPropertyType", "referenceFieldControlType", "originalOptions", "format1"];
|
|
281
|
+
function pickRelevanceControlOptions(fieldOptions) {
|
|
282
|
+
const picked = {};
|
|
283
|
+
for (const key of RELEVANCE_CONTROL_KEYS)
|
|
284
|
+
if (fieldOptions?.[key] !== undefined)
|
|
285
|
+
picked[key] = fieldOptions[key];
|
|
286
|
+
return picked;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Ticket 69 — designer-default options for the org-selection controls
|
|
290
|
+
* (50/51/60/61/62). `deptVisible` + `multi` are the platform's person /
|
|
291
|
+
* department / mixed discriminator; the rest mirror the designer's
|
|
292
|
+
* `buildControlOptions` defaults observed in the admin bundle.
|
|
293
|
+
*/
|
|
294
|
+
function orgSelectorControlOptions(base, deptVisible, multi) {
|
|
295
|
+
return {
|
|
296
|
+
...base,
|
|
297
|
+
dataItemName: "",
|
|
298
|
+
widgetType: "",
|
|
299
|
+
dataItemType: "",
|
|
300
|
+
multi,
|
|
301
|
+
deptVisible,
|
|
302
|
+
defaultValue: [],
|
|
303
|
+
defaultValueType: "",
|
|
304
|
+
orgRoot: [],
|
|
305
|
+
orgRootValueType: "",
|
|
306
|
+
recursive: true,
|
|
307
|
+
isDisplayRoot: true,
|
|
308
|
+
roles: "",
|
|
309
|
+
mappings: "",
|
|
310
|
+
displayType: "tag",
|
|
311
|
+
dataLinkage: "",
|
|
312
|
+
readonlyCondition: "",
|
|
313
|
+
selectMode: "popup",
|
|
314
|
+
verifyFormula: "",
|
|
315
|
+
noRepeat: "",
|
|
316
|
+
...(deptVisible === "user" ? {} : { departmentLevels: {}, limitDeptSelectionRanges: [] }),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The numeric FORM CONTROL's `format`/`format1` display string.
|
|
321
|
+
*
|
|
322
|
+
* Live correction 2026-09-23 (admin designer bundle `Number.format` options):
|
|
323
|
+
* the numeric control does NOT accept `"decimal"` — the field-level word — it
|
|
324
|
+
* takes a display-format constant: none / integer / tenths / percentile /
|
|
325
|
+
* Millimeter / tenThousand / hundredThousand / millionDecimals /
|
|
326
|
+
* tenMillionDecimals / billionDecimals, optionally prefixed `ratio.` (percent),
|
|
327
|
+
* `RMB.` / `dollar.` / `euro.` / `HK.` (currency). Writing `"decimal"` (the
|
|
328
|
+
* previous behaviour) left the control with an unrecognised format, so the
|
|
329
|
+
* portal rendered the raw decimal. Derive the constant from the field's
|
|
330
|
+
* `scale` (0-based decimal places) and `format` family.
|
|
331
|
+
*/
|
|
332
|
+
const NUMERICAL_DECIMALS_FORMAT = Object.freeze({
|
|
333
|
+
0: "integer",
|
|
334
|
+
1: "tenths",
|
|
335
|
+
2: "percentile",
|
|
336
|
+
3: "Millimeter",
|
|
337
|
+
4: "tenThousand",
|
|
338
|
+
5: "hundredThousand",
|
|
339
|
+
6: "millionDecimals",
|
|
340
|
+
7: "tenMillionDecimals",
|
|
341
|
+
8: "billionDecimals",
|
|
342
|
+
});
|
|
343
|
+
function numericalFormatPrefix(format) {
|
|
344
|
+
const value = String(format ?? "").toLowerCase();
|
|
345
|
+
if (value === "currency" || value === "rmb" || value === "¥")
|
|
346
|
+
return "RMB.";
|
|
347
|
+
if (value === "dollar" || value === "$")
|
|
348
|
+
return "dollar.";
|
|
349
|
+
if (value === "euro" || value === "€")
|
|
350
|
+
return "euro.";
|
|
351
|
+
if (value === "hk" || value === "hkd" || value === "hk$")
|
|
352
|
+
return "HK.";
|
|
353
|
+
if (value === "percent" || value === "percentage" || value === "ratio")
|
|
354
|
+
return "ratio.";
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
export function numericalControlFormat(fieldOptions) {
|
|
358
|
+
const scale = fieldOptions?.scale;
|
|
359
|
+
const decimals = isNumericalScale(scale) ? scale : 2;
|
|
360
|
+
return `${numericalFormatPrefix(fieldOptions?.format)}${NUMERICAL_DECIMALS_FORMAT[decimals]}`;
|
|
361
|
+
}
|
|
362
|
+
export function fieldControlOptions(type, name, relativeCode, relationMappings, relationDisplayField = "name", fieldOptions) {
|
|
363
|
+
const base = { name, name_i18n: { "zh-CN": name, en: name }, visible: true, labelVisible: true, span: 24 };
|
|
364
|
+
const declared = deriveControlOptions(type, fieldOptions);
|
|
365
|
+
const merged = { ...base, ...declared };
|
|
366
|
+
switch (type) {
|
|
367
|
+
// The numeric control carries a display-format constant (not the field
|
|
368
|
+
// word "decimal"); derive it from scale + family. See numericalControlFormat.
|
|
369
|
+
case 2: return { ...merged, format: numericalControlFormat(fieldOptions), format1: numericalControlFormat(fieldOptions) };
|
|
370
|
+
// No mapping -> the designer's BLANK sentinel "" (its "选择业务模型" handler
|
|
371
|
+
// writes mappings:""). A stringified "[]" is non-empty, so the control
|
|
372
|
+
// editor's `setted` badge (isNotEmpty) renders 已设置/Set while the modal it
|
|
373
|
+
// opens is empty — the 2026-09-18 report. Only a real mapping is non-blank.
|
|
374
|
+
case 9:
|
|
375
|
+
case 11: return { ...merged, schemaCode: relativeCode ?? "", displayField: relationDisplayField, relativePropertyMatchCode: relationDisplayField, queryCode: relativeCode ?? "", mappings: relationMappings ?? "", ...(typeof fieldOptions?.searchFormula === "string" ? { searchFormula: fieldOptions.searchFormula } : {}), ...(typeof fieldOptions?.conditions === "string" ? { conditions: fieldOptions.conditions } : {}) };
|
|
376
|
+
case 50: return orgSelectorControlOptions({ ...base, ...deriveControlOptions(type) }, "user", "false");
|
|
377
|
+
case 51: return orgSelectorControlOptions({ ...base, ...deriveControlOptions(type) }, "user", "true");
|
|
378
|
+
case 60: return orgSelectorControlOptions({ ...base, ...deriveControlOptions(type) }, "org", "false");
|
|
379
|
+
case 61: return orgSelectorControlOptions({ ...base, ...deriveControlOptions(type) }, "org", "true");
|
|
380
|
+
case 5: return { ...merged, ...(fieldOptions?.optionsSet ? { optionsSet: fieldOptions.optionsSet } : {}), ...orgSelectorControlOptions({ ...base, ...deriveControlOptions(type) }, "all", "true") };
|
|
381
|
+
case 12:
|
|
382
|
+
case 13:
|
|
383
|
+
case 14:
|
|
384
|
+
case 15: return { ...merged, ...(fieldOptions?.optionsSet ? { optionsSet: fieldOptions.optionsSet } : {}) };
|
|
385
|
+
case 16: return { ...merged, ...pickRelevanceControlOptions(fieldOptions) };
|
|
386
|
+
case 100: return { ...merged, ...(typeof fieldOptions?.formulaConfig === "string" ? { formulaConfig: fieldOptions.formulaConfig, persistent: fieldOptions.persistent === true } : {}) };
|
|
387
|
+
default: return merged;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** relation options face decode; policy: string-or-object accepted, garbage → undefined. */
|
|
391
|
+
function relationOptions(options) {
|
|
392
|
+
return decodeJsonObjectColumn(options, { invalid: "undefined", array: "accept", object: "accept" });
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The relation target model of a bizproperty row. Relation families
|
|
396
|
+
* (WORK_SHEET / MULT_WORK_SHEET) carry the target model code both as the
|
|
397
|
+
* row-level `relativeCode` and as the `schemaCode` key inside the options JSON
|
|
398
|
+
* (field.create writes both). RELEVANCE_DATA(16) is deliberately excluded: it
|
|
399
|
+
* references another *field* (relevanceField/referenceField), not a model, so
|
|
400
|
+
* it adds no inbound model reference of its own — the local WORK_SHEET field it
|
|
401
|
+
* resolves through already carries that reference. Either face counts as an
|
|
402
|
+
* inbound reference for the model.delete precheck (ticket 23: the server's
|
|
403
|
+
* deleteFunctions dispatcher refuses asynchronously with
|
|
404
|
+
* "xxx被模型 yyy 引用,不能删除" — the tool surfaces that check at call time).
|
|
405
|
+
*/
|
|
406
|
+
export function relationTargetOf(row) {
|
|
407
|
+
if (typeof row.relativeCode === "string" && row.relativeCode.trim())
|
|
408
|
+
return row.relativeCode.trim();
|
|
409
|
+
const schemaCode = relationOptions(row.options)?.schemaCode;
|
|
410
|
+
return typeof schemaCode === "string" && schemaCode.trim() ? schemaCode.trim() : undefined;
|
|
411
|
+
}
|
|
412
|
+
export function relationDisplayField(options) {
|
|
413
|
+
const displayField = relationOptions(options)?.displayField;
|
|
414
|
+
return typeof displayField === "string" && displayField.trim() ? displayField.trim() : undefined;
|
|
415
|
+
}
|
|
416
|
+
/** Extract a non-empty relation `mappings` JSON string from a field's options. */
|
|
417
|
+
export function relationMappings(options) {
|
|
418
|
+
const mappings = relationOptions(options)?.mappings;
|
|
419
|
+
if (typeof mappings !== "string")
|
|
420
|
+
return undefined;
|
|
421
|
+
const trimmed = mappings.trim();
|
|
422
|
+
if (trimmed === "" || trimmed === "[]")
|
|
423
|
+
return undefined;
|
|
424
|
+
return trimmed;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Designer-standard default field options by family. Only families with
|
|
428
|
+
* live evidence get a shape beyond the bare `{dictionaryData:""}` — DATE(3)
|
|
429
|
+
* requires `format` or the first runtime save 50000s
|
|
430
|
+
* (BizObjectFacadeImpl.java:2949 reads options.format). TIME(28) has no
|
|
431
|
+
* evidence yet and stays on the bare default.
|
|
432
|
+
*/
|
|
433
|
+
export function familyDefaultFieldOptions(raw) {
|
|
434
|
+
const type = typeof raw === "number" && Number.isInteger(raw)
|
|
435
|
+
? raw
|
|
436
|
+
: typeof raw === "string" && raw in FIELD_PROPERTY_TYPES
|
|
437
|
+
? FIELD_PROPERTY_TYPES[raw]
|
|
438
|
+
: undefined;
|
|
439
|
+
if (type === FIELD_PROPERTY_TYPES.DATE)
|
|
440
|
+
return { ...FIELD_DATE_DEFAULT_OPTIONS };
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Live 2026-09-24 (bench34): the businessModel dropdown's runtime builds its
|
|
445
|
+
* sort as `[optionsSet.businessModel.orderByFields]` (frontend 19497), i.e. it
|
|
446
|
+
* wraps a SINGLE value. A stored ARRAY therefore becomes a nested array and
|
|
447
|
+
* `runtime/query/listSkipQueryListV2` answers `10009 Type conversion error`,
|
|
448
|
+
* leaving the dropdown empty. Only one sort field is supported, so normalize a
|
|
449
|
+
* 1-element array to its string element; refuse anything else.
|
|
450
|
+
*/
|
|
451
|
+
function normalizeBusinessModelOrderBy(options) {
|
|
452
|
+
const optionsSet = options.optionsSet;
|
|
453
|
+
const businessModel = optionsSet?.businessModel;
|
|
454
|
+
if (!businessModel)
|
|
455
|
+
return;
|
|
456
|
+
normalizeBusinessModelCondition(businessModel);
|
|
457
|
+
if (businessModel.orderByFields === undefined)
|
|
458
|
+
return;
|
|
459
|
+
const value = businessModel.orderByFields;
|
|
460
|
+
if (typeof value === "string")
|
|
461
|
+
return;
|
|
462
|
+
if (Array.isArray(value) && value.length === 1 && typeof value[0] === "string" && value[0].trim() !== "") {
|
|
463
|
+
businessModel.orderByFields = value[0];
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
refuseFieldOptions("optionsSet.businessModel.orderByFields 必须是单个字段名字符串", `rejected: orderByFields=${JSON.stringify(value)};平台运行时把它包成单元素数组,数组会变成嵌套数组并触发 10009,下拉为空;只支持一个排序字段`);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Live 2026-09-24 (bench34): the businessModel dropdown's runtime reads a
|
|
470
|
+
* SINGULAR `condition` STRING (frontend 19497 parseHistoryConditions), not the
|
|
471
|
+
* `conditions` array this CLI used to document — with only `conditions` the
|
|
472
|
+
* dropdown query ships `queryCondition: []` (no filtering at all, silently).
|
|
473
|
+
* The runtime string grammar, verified live:
|
|
474
|
+
* - `field === literal` → FIXED (compare to the literal value)
|
|
475
|
+
* - `field == otherField` → DYNAMIC (compare to another control's value)
|
|
476
|
+
* conditions joined by ` && `.
|
|
477
|
+
* Translate the caller's `{field, operator, value|valueFrom}` array into it and
|
|
478
|
+
* expose it as `condition` (keeping `conditions` as the human-readable source).
|
|
479
|
+
*/
|
|
480
|
+
function normalizeBusinessModelCondition(businessModel) {
|
|
481
|
+
if (typeof businessModel.condition === "string" && businessModel.condition.trim() !== "")
|
|
482
|
+
return;
|
|
483
|
+
const conditions = businessModel.conditions;
|
|
484
|
+
if (!Array.isArray(conditions) || conditions.length === 0)
|
|
485
|
+
return;
|
|
486
|
+
const parts = [];
|
|
487
|
+
for (const entry of conditions) {
|
|
488
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
489
|
+
continue;
|
|
490
|
+
const record = entry;
|
|
491
|
+
const field = typeof record.field === "string" ? record.field.trim() : "";
|
|
492
|
+
if (!field)
|
|
493
|
+
continue;
|
|
494
|
+
if (typeof record.valueFrom === "string" && record.valueFrom.trim() !== "")
|
|
495
|
+
parts.push(`${field} == ${record.valueFrom.trim()}`);
|
|
496
|
+
else if (record.value !== undefined && record.value !== null)
|
|
497
|
+
parts.push(`${field} === ${String(record.value)}`);
|
|
498
|
+
}
|
|
499
|
+
// Join WITHOUT spaces: the form renderer's subscription parser
|
|
500
|
+
// (portal 30079) does `condition.split("&&").map(e => e.split(" ")[2])`
|
|
501
|
+
// WITHOUT trimming, so a leading space on a non-first segment shifts the
|
|
502
|
+
// indices and it reads `undefined.split` → a load-time console error. The
|
|
503
|
+
// query parser trims, so the spacer-free join is safe for both.
|
|
504
|
+
if (parts.length > 0)
|
|
505
|
+
businessModel.condition = parts.join("&&");
|
|
506
|
+
}
|
|
507
|
+
export function buildFieldOptions(args) {
|
|
508
|
+
const explicit = args.options;
|
|
509
|
+
const raw = args.propertyType;
|
|
510
|
+
const type = typeof raw === "number" && Number.isInteger(raw)
|
|
511
|
+
? raw
|
|
512
|
+
: typeof raw === "string" && raw in FIELD_PROPERTY_TYPES
|
|
513
|
+
? FIELD_PROPERTY_TYPES[raw]
|
|
514
|
+
: undefined;
|
|
515
|
+
if (explicit) {
|
|
516
|
+
if (type !== undefined) {
|
|
517
|
+
const shapeError = capabilityShapeError(type, "field", explicit);
|
|
518
|
+
if (shapeError)
|
|
519
|
+
refuseFieldOptions(shapeError, `rejected: ${shapeError}`);
|
|
520
|
+
const choiceError = choiceOptionsError(type, explicit);
|
|
521
|
+
if (choiceError)
|
|
522
|
+
refuseFieldOptions("selection field options carry no optionsSet source", `rejected: ${choiceError}`);
|
|
523
|
+
}
|
|
524
|
+
normalizeBusinessModelOrderBy(explicit);
|
|
525
|
+
return explicit;
|
|
526
|
+
}
|
|
527
|
+
const choices = args.choices;
|
|
528
|
+
if (Array.isArray(choices) && choices.length > 0) {
|
|
529
|
+
// CHOICE_PROPERTY_TYPES is the text-choice set (RADIO/CHECKBOX/DROPDOWN_BOX/
|
|
530
|
+
// DROPDOWN_MULTI_BOX). SELECTION(5) is the MIXED people/department selector
|
|
531
|
+
// (control type 62) and requires an org-unit array at runtime; writing
|
|
532
|
+
// `choices` to it persisted an optionsSet the runtime ignored, then
|
|
533
|
+
// data.save threw IllegalStateException @ OrgUnitArrayBizField.contentFromClient
|
|
534
|
+
// (live 8.6.26, 2026-09-27). Refuse instead of producing an unstorable field.
|
|
535
|
+
if (type !== undefined && !CHOICE_PROPERTY_TYPES.has(type))
|
|
536
|
+
refuseFieldOptions(`propertyType ${familyName(type)}(${type}) does not accept choices`, `propertyType ${familyName(type)}(${type}) 不接受 choices:它是人员/组织/混合选择器或非选项族,运行时按组织对象存取。文本单选用 RADIO(12)/DROPDOWN_BOX(14),多选用 CHECKBOX(13)/DROPDOWN_MULTI_BOX(15);人员/部门/混合选择器用 STAFF_SELECTOR/DEPARTMENT_SELECTOR/SELECTION 且不要带 choices`);
|
|
537
|
+
// Ticket 108: no dictionaryData on selection rows — designer-produced
|
|
538
|
+
// rows carry none (fwb §12-15) and the platform only accepts the extra
|
|
539
|
+
// key, never requires it (live 8.6.26 write+read 2026-09-15).
|
|
540
|
+
const selection = {
|
|
541
|
+
optionsSet: {
|
|
542
|
+
optionsType: "custom",
|
|
543
|
+
custom: choices.map((value) => ({
|
|
544
|
+
code: value,
|
|
545
|
+
value,
|
|
546
|
+
name_i18n: { "zh-CN": value, en: value },
|
|
547
|
+
published: false,
|
|
548
|
+
})),
|
|
549
|
+
dictionary: {},
|
|
550
|
+
businessModel: { orderType: 1 },
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
return type !== undefined ? deriveFieldOptions(type, selection) : selection;
|
|
554
|
+
}
|
|
555
|
+
return type !== undefined ? deriveFieldOptions(type) : { dictionaryData: "" };
|
|
556
|
+
}
|
|
557
|
+
export function normalizeFieldPropertyType(args) {
|
|
558
|
+
const raw = args.propertyType;
|
|
559
|
+
if (typeof raw === "number" && Number.isInteger(raw)) {
|
|
560
|
+
if (!Object.values(FIELD_PROPERTY_TYPES).includes(raw))
|
|
561
|
+
throw new ToolRefusalError({ status: "refused", blocker: "capability-out-of-coverage", citation: { code: "capability-out-of-coverage", lane: "field-families", capability: "field family full chain", status: "partial", reason: `propertyType ${raw} is not a live-accepted family`, message: `rejected: propertyType ${raw} is outside the live-accepted field families; citing lane field-families` } });
|
|
562
|
+
return raw;
|
|
563
|
+
}
|
|
564
|
+
const type = String(raw ?? "");
|
|
565
|
+
if (!(type in FIELD_PROPERTY_TYPES)) {
|
|
566
|
+
const hint = fieldTypeHint(type);
|
|
567
|
+
throw new ToolRefusalError({
|
|
568
|
+
status: "refused",
|
|
569
|
+
blocker: "capability-out-of-coverage",
|
|
570
|
+
citation: { code: "capability-out-of-coverage", lane: "field-families", capability: "field family full chain", status: "partial", reason: `${type || "missing"} is not a live-accepted family`, message: `rejected: field family ${type || "(missing)"} is outside the live-accepted set; citing lane field-families` },
|
|
571
|
+
...(hint ? { hint } : {}),
|
|
572
|
+
message: `propertyType「${type || "(missing)"}」不是可用的字段族名。${hint ?? ""} 可用族名:${Object.keys(FIELD_PROPERTY_TYPES).join("、")}`,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return FIELD_PROPERTY_TYPES[type];
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Families a CHILD_TABLE(8) column may carry (omc-core
|
|
579
|
+
* `childtable-field-contract.ts` CONTROL_TYPES allow-list). PRODUCTIVE
|
|
580
|
+
* `WORK_SHEET(9)` is included: live 2026-09-20, a `WORK_SHEET` child column with
|
|
581
|
+
* `relativeCode` + relation options is accepted by create/publish AND resolves
|
|
582
|
+
* the relation on runtime readback. Excluded (each with live evidence in the
|
|
583
|
+
* core contract): nested CHILD_TABLE, MULT_WORK_SHEET, RELEVANCE_DATA, FORMULA,
|
|
584
|
+
* and the OCR/ELECTRONIC_SIGN boundary families.
|
|
585
|
+
*/
|
|
586
|
+
export const CHILD_TABLE_SCALAR_PROPERTY_TYPES = new Set([0, 1, 2, 3, 4, 5, 6, 9, 10, 12, 13, 14, 15, 17, 28, 50, 51, 60, 61]);
|
|
587
|
+
/**
|
|
588
|
+
* Build the `subSchema` that defines a CHILD_TABLE(8) field from `subFields`.
|
|
589
|
+
* Live shape (2026-09-08 `childtable-execute-20260908070635135.md`):
|
|
590
|
+
* `{ code:<parent field code>, properties:[{code,name,schemaCode:<parent field
|
|
591
|
+
* code>,propertyType,propertyEmpty,sortKey}] }`. The child's `schemaCode` is
|
|
592
|
+
* the parent field code, not the main model.
|
|
593
|
+
*/
|
|
594
|
+
export function buildChildTableSubSchema(args) {
|
|
595
|
+
const subs = args.subFields;
|
|
596
|
+
if (!Array.isArray(subs) || subs.length === 0)
|
|
597
|
+
refuseFieldOptions("CHILD_TABLE subFields is missing or empty", "rejected: CHILD_TABLE(8) requires subFields(至少 1 个子字段),每项 { code, name, propertyType[, propertyEmpty] }");
|
|
598
|
+
const parentCode = String(args.code ?? "");
|
|
599
|
+
const properties = subs.map((sub, index) => buildChildTableProperty(parentCode, sub, index));
|
|
600
|
+
return { code: parentCode, properties };
|
|
601
|
+
}
|
|
602
|
+
/** One CHILD_TABLE(8) column row (compact create shape). */
|
|
603
|
+
export function buildChildTableProperty(parentCode, sub, index) {
|
|
604
|
+
const type = normalizeFieldPropertyType(sub);
|
|
605
|
+
if (!CHILD_TABLE_SCALAR_PROPERTY_TYPES.has(type))
|
|
606
|
+
refuseFieldOptions(`CHILD_TABLE child ${String(sub.code)} has out-of-coverage propertyType ${type}`, `rejected: 子表列只接受 ${[...CHILD_TABLE_SCALAR_PROPERTY_TYPES].join("/")};CHILD_TABLE/MULT_WORK_SHEET/RELEVANCE_DATA/FORMULA/OCR/ELECTRONIC_SIGN 不在子表覆盖内(omc-core childtable-field-contract.ts)`);
|
|
607
|
+
const name = String(sub.name ?? "");
|
|
608
|
+
const relation = type === FIELD_PROPERTY_TYPES.WORK_SHEET;
|
|
609
|
+
if (relation && !String(sub.relativeCode ?? "").trim())
|
|
610
|
+
refuseFieldOptions(`CHILD_TABLE WORK_SHEET column ${String(sub.code)} requires relativeCode`, "rejected: 子表关联列必须给 relativeCode(目标模型 schemaCode,如 customer)");
|
|
611
|
+
const options = buildFieldOptions(sub);
|
|
612
|
+
assertNumericalScale(type, options);
|
|
613
|
+
if (relation) {
|
|
614
|
+
const shapeError = capabilityShapeError(type, "field", options);
|
|
615
|
+
if (shapeError)
|
|
616
|
+
refuseFieldOptions(shapeError, `rejected: ${shapeError}`);
|
|
617
|
+
}
|
|
618
|
+
const derived = deriveFieldOptions(type, relation ? ensureRelationDisplayFieldOptions(type, options) : options);
|
|
619
|
+
const displayCode = relation ? relationDisplayFieldCode(type, derived) : undefined;
|
|
620
|
+
return {
|
|
621
|
+
code: String(sub.code ?? ""),
|
|
622
|
+
name,
|
|
623
|
+
name_i18n: nameI18nJson(name),
|
|
624
|
+
schemaCode: parentCode,
|
|
625
|
+
propertyType: type,
|
|
626
|
+
propertyEmpty: sub.propertyEmpty === true,
|
|
627
|
+
...(relation ? { relativeCode: String(sub.relativeCode).trim() } : {}),
|
|
628
|
+
...(displayCode ? { relativePropertyCode: displayCode } : {}),
|
|
629
|
+
sortKey: index + 1,
|
|
630
|
+
options: JSON.stringify(derived),
|
|
631
|
+
defaultProperty: false,
|
|
632
|
+
published: false,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
export function assertFieldFamilyAllowed(args) {
|
|
636
|
+
const type = String(args.propertyType ?? "");
|
|
637
|
+
if (OUT_OF_COVERAGE_FIELD_TYPES[type]) {
|
|
638
|
+
const citation = {
|
|
639
|
+
code: "capability-out-of-coverage",
|
|
640
|
+
lane: "boundary-unsupported",
|
|
641
|
+
capability: OUT_OF_COVERAGE_FIELD_TYPES[type],
|
|
642
|
+
status: "unsupported",
|
|
643
|
+
reason: `${type} field family is explicitly unsupported in the M1 coverage manifest`,
|
|
644
|
+
message: `rejected: ${type} fields are unsupported (coverage manifest boundary-unsupported); citing lane boundary-unsupported`,
|
|
645
|
+
};
|
|
646
|
+
throw new ToolRefusalError({ status: "refused", blocker: "capability-out-of-coverage", citation });
|
|
647
|
+
}
|
|
648
|
+
// Validate the family against the allow-list BEFORE the write permission gate so an
|
|
649
|
+
// unsupported type is refused up front instead of after a token is issued
|
|
650
|
+
// (previously the allow-list check lived in execute(), i.e. post-approval).
|
|
651
|
+
const normalized = normalizeFieldPropertyType(args);
|
|
652
|
+
const options = args.options;
|
|
653
|
+
if (options && (options.nested === true || options.relationLinkage === true)) {
|
|
654
|
+
const citation = {
|
|
655
|
+
code: "capability-out-of-coverage",
|
|
656
|
+
lane: "W6-S27-advanced-childtable",
|
|
657
|
+
capability: "W6 S27 nested child-table metadata",
|
|
658
|
+
status: "blocked",
|
|
659
|
+
reason: "design face refused nested CHILD_TABLE with 50000; no recursive payload contract exists",
|
|
660
|
+
message: "rejected: nested child-table configuration is blocked (W6-S27-advanced-childtable); citing coverage manifest entry",
|
|
661
|
+
};
|
|
662
|
+
throw new ToolRefusalError({ status: "refused", blocker: "capability-out-of-coverage", citation });
|
|
663
|
+
}
|
|
664
|
+
// CHILD_TABLE(8): the field row carries no `options`; its definition is the
|
|
665
|
+
// subSchema built from subFields. Validate that shape here (pre-write)
|
|
666
|
+
// instead of running the options-face gate.
|
|
667
|
+
if (normalized === FIELD_PROPERTY_TYPES.CHILD_TABLE) {
|
|
668
|
+
buildChildTableSubSchema(args);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
assertFieldOptionsAllowed(normalized, options);
|
|
672
|
+
}
|
|
673
|
+
function refuseFieldOptions(reason, message) {
|
|
674
|
+
const citation = {
|
|
675
|
+
code: "capability-out-of-coverage",
|
|
676
|
+
lane: "field-families",
|
|
677
|
+
capability: "field family full chain",
|
|
678
|
+
status: "blocked",
|
|
679
|
+
reason,
|
|
680
|
+
message,
|
|
681
|
+
};
|
|
682
|
+
throw new ToolRefusalError({ status: "refused", blocker: "capability-out-of-coverage", citation });
|
|
683
|
+
}
|
|
684
|
+
/** FORMULA formulaConfig decode; policy: string-only face, must parse to a non-array object. */
|
|
685
|
+
function parseOptionsObject(value) {
|
|
686
|
+
return decodeJsonObjectColumn(value, { invalid: "undefined", array: "reject", object: "reject" });
|
|
687
|
+
}
|
|
688
|
+
function isNonEmptyString(value) {
|
|
689
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* The formula expression body inside a FORMULA formulaConfig.expression value.
|
|
693
|
+
* `expression` is itself a JSON string `{formula,editorText,editorMark}`; fall
|
|
694
|
+
* back to the raw string when it does not parse, so the lint still sees a
|
|
695
|
+
* malformed-but-present body.
|
|
696
|
+
*/
|
|
697
|
+
function expressionBody(expression) {
|
|
698
|
+
const parsed = parseOptionsObject(expression);
|
|
699
|
+
const formula = parsed?.formula;
|
|
700
|
+
return typeof formula === "string" ? formula : expression;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Fail-closed options validation for families whose keys the platform reads
|
|
704
|
+
* strictly. FORMULA(100) and RELEVANCE_DATA(16) were previously written with
|
|
705
|
+
* keys the platform does not read (resultType/expression, relation schemaCode/
|
|
706
|
+
* mappings); this refuses such payloads before the write permission gate instead of
|
|
707
|
+
* persisting an ineffective field. Called for field.create pre-write and for
|
|
708
|
+
* field.update after the target family is read from the existing record.
|
|
709
|
+
*/
|
|
710
|
+
/**
|
|
711
|
+
* Round 4 (ADR 0005): NUMERICAL fields require an explicit `options.scale`.
|
|
712
|
+
* Live correction 2026-09-23 (designer bundle + live round-trip): the field
|
|
713
|
+
* `scale` is a 0-based DECIMAL-PLACES index — scale 0 = integer, 1 = 1dp,
|
|
714
|
+
* 2 = 2dp, … 8 = 8dp. `listview.configure` then maps it through the platform's
|
|
715
|
+
* `QueryRuntimeController.parseNumber` table ({0:1,1:2,2:3,3:11,4:12,23:…}),
|
|
716
|
+
* verified live: scale 2 -> displayFormat 3 (2dp), scale 3 -> displayFormat 11
|
|
717
|
+
* (3dp). The earlier enum reading (1=整数,2=1位,3=2位) was the displayFormat
|
|
718
|
+
* enum misread as scale and produced 3-decimal "two-decimal" amounts. The gate
|
|
719
|
+
* accepts 0..8 and tells the caller to count decimal places.
|
|
720
|
+
*/
|
|
721
|
+
const NUMERICAL_MAX_DECIMALS = 8;
|
|
722
|
+
/** True when `scale` is a valid 0-based decimal-places index. */
|
|
723
|
+
export function isNumericalScale(scale) {
|
|
724
|
+
return typeof scale === "number" && Number.isInteger(scale) && scale >= 0 && scale <= NUMERICAL_MAX_DECIMALS;
|
|
725
|
+
}
|
|
726
|
+
export function numericalScaleWarning(type, options) {
|
|
727
|
+
if (type !== FIELD_PROPERTY_TYPES.NUMERICAL)
|
|
728
|
+
return undefined;
|
|
729
|
+
if (options !== null && typeof options === "object" && !Array.isArray(options)) {
|
|
730
|
+
if (isNumericalScale(options.scale))
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|
|
733
|
+
return "NUMERICAL 字段缺合法 options.scale:门户设计器会显示为裸 decimal(BigDecimal 样式)。scale 是小数位数(0=整数, 1=1位, 2=2位, 3=3位 … 8=8位)——「金额两位小数」用 scale:2,不要照搬列格式枚举";
|
|
734
|
+
}
|
|
735
|
+
export function assertNumericalScale(type, options) {
|
|
736
|
+
const warning = numericalScaleWarning(type, options);
|
|
737
|
+
if (warning !== undefined)
|
|
738
|
+
refuseFieldOptions("NUMERICAL requires an explicit options.scale (verified: absent scale renders as raw decimal in the portal designer)", `rejected: ${warning}`);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Round 4 (ADR 0005): form-interaction conditions (required/readonly/display)
|
|
742
|
+
* are FIELD options, reproduced live against the portal form. Validate them
|
|
743
|
+
* here so a malformed condition is refused at write time instead of breaking
|
|
744
|
+
* (or silently not enforcing in) the portal form.
|
|
745
|
+
*/
|
|
746
|
+
export function assertFormConditions(options) {
|
|
747
|
+
if (!options)
|
|
748
|
+
return;
|
|
749
|
+
const error = formConditionOptionsError(options);
|
|
750
|
+
if (error !== null)
|
|
751
|
+
refuseFieldOptions("field form-condition options fail the reproduced protocol", `rejected: ${error}`);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Selection families (RADIO/CHECKBOX/DROPDOWN/DROPDOWN_MULTI) render their
|
|
755
|
+
* choices from `optionsSet` (custom/dictionary/businessModel). The control
|
|
756
|
+
* derives options from `optionsSet[optionsType]` (frontend 19497), so a field
|
|
757
|
+
* written with a bare `{items:[{value,label}]}` — or `custom`/`labels`/`options`
|
|
758
|
+
* without the `optionsSet` wrapper — is silently accepted by the platform but
|
|
759
|
+
* renders an EMPTY control in the portal (dogfood bench35, 2026-09-24: a radio
|
|
760
|
+
* with `options.items` showed no choices). Refuse the misleading shape at write
|
|
761
|
+
* time with a pointer to `choices`/`optionsSet`.
|
|
762
|
+
*/
|
|
763
|
+
const CHOICE_PROPERTY_TYPES = new Set([
|
|
764
|
+
FIELD_PROPERTY_TYPES.RADIO,
|
|
765
|
+
FIELD_PROPERTY_TYPES.CHECKBOX,
|
|
766
|
+
FIELD_PROPERTY_TYPES.DROPDOWN_BOX,
|
|
767
|
+
FIELD_PROPERTY_TYPES.DROPDOWN_MULTI_BOX,
|
|
768
|
+
]);
|
|
769
|
+
function choiceOptionsError(type, options) {
|
|
770
|
+
if (!CHOICE_PROPERTY_TYPES.has(type))
|
|
771
|
+
return null;
|
|
772
|
+
const optionsSet = options.optionsSet;
|
|
773
|
+
if (optionsSet !== undefined) {
|
|
774
|
+
if (optionsSet === null || typeof optionsSet !== "object" || Array.isArray(optionsSet))
|
|
775
|
+
return "optionsSet 必须是对象({optionsType:'custom'|'dictionary'|'businessModel', ...})";
|
|
776
|
+
const optionsType = optionsSet.optionsType;
|
|
777
|
+
if (!["custom", "dictionary", "businessModel"].includes(String(optionsType)))
|
|
778
|
+
return `optionsSet.optionsType「${String(optionsType)}」无效(custom/dictionary/businessModel)`;
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
// No optionsSet: refuse the shapes that LOOK like choices but the platform ignores.
|
|
782
|
+
for (const key of ["items", "labels", "custom", "dictionary", "businessModel", "options", "dictionaryData"]) {
|
|
783
|
+
if (options[key] !== undefined)
|
|
784
|
+
return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet({optionsType:'custom',custom:[{code,value,name_i18n}]} 或 dictionary/businessModel);裸的 options.${key} 平台不消费,门户会渲染成空控件。用 field.create 的 choices 参数,或 options.optionsSet`;
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
export function assertFieldOptionsAllowed(type, options, schemaCode) {
|
|
789
|
+
if (options) {
|
|
790
|
+
const shapeError = capabilityShapeError(type, "field", options);
|
|
791
|
+
if (shapeError)
|
|
792
|
+
refuseFieldOptions(shapeError, `rejected: ${shapeError}`);
|
|
793
|
+
}
|
|
794
|
+
// DATE(3): the runtime compares options.format LITERALLY against a fixed
|
|
795
|
+
// UPPERCASE enum (DateBizField.contentFromClient → `不支持的format:<v>`).
|
|
796
|
+
// The shape check above only requires the key to be present, so a Java-style
|
|
797
|
+
// `yyyy-MM-dd` passes create and then 50000s on the FIRST runtime save
|
|
798
|
+
// (2026-09-30 R5 live). Refuse any value outside the enum.
|
|
799
|
+
if (type === FIELD_PROPERTY_TYPES.DATE && options && options.format !== undefined) {
|
|
800
|
+
if (!FIELD_DATE_FORMATS.includes(String(options.format)))
|
|
801
|
+
refuseFieldOptions(`DATE options.format ${JSON.stringify(options.format)} 不是平台支持的格式`, `rejected: DATE(3) options.format 必须是平台枚举(区分大小写的大写写法):${FIELD_DATE_FORMATS.join("、")}。Java 写法 yyyy-MM-dd 不在其中——平台 DateBizField 会判为 不支持的format,字段能创建但**首次运行时 save 报 50000**(2026-09-30 R5 实测)`);
|
|
802
|
+
}
|
|
803
|
+
if (type === FIELD_PROPERTY_TYPES.FORMULA) {
|
|
804
|
+
if (options && (options.resultType !== undefined || options.expression !== undefined))
|
|
805
|
+
refuseFieldOptions("FORMULA options carried the legacy top-level resultType/expression keys, which the platform does not read", "rejected: FORMULA(100) options must use formulaConfig (nested JSON string) + persistent:true; top-level resultType/expression are not platform keys");
|
|
806
|
+
const formulaConfig = options?.formulaConfig;
|
|
807
|
+
if (!isNonEmptyString(formulaConfig))
|
|
808
|
+
refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression) — see field-contract");
|
|
809
|
+
const config = parseOptionsObject(formulaConfig);
|
|
810
|
+
if (!config)
|
|
811
|
+
refuseFieldOptions("FORMULA formulaConfig is not a parseable JSON object string", "rejected: FORMULA(100) options.formulaConfig must be a JSON object string");
|
|
812
|
+
if (config.formulaType !== undefined && config.formulaType !== "EXPRESSION" && config.formulaType !== "AGGREGATION")
|
|
813
|
+
refuseFieldOptions(`FORMULA formulaType ${String(config.formulaType)} is not EXPRESSION or AGGREGATION`, "rejected: FORMULA(100) formulaType must be EXPRESSION or AGGREGATION (8.6.26 FormulaPropertyOptions.FormulaType)");
|
|
814
|
+
if (config.resultType !== 0 && config.resultType !== 2)
|
|
815
|
+
refuseFieldOptions(`FORMULA resultType ${String(config.resultType)} is not an evidenced result type`, "rejected: FORMULA(100) resultType must be SHORT_TEXT(0) or NUMERICAL(2) (omc-core formula-field-contract v1 boundary)");
|
|
816
|
+
if (config.resultFormat !== undefined && config.resultFormat !== "" && config.resultFormat !== "percentile")
|
|
817
|
+
refuseFieldOptions(`FORMULA resultFormat ${String(config.resultFormat)} is not an evidenced value`, 'rejected: FORMULA(100) resultFormat must be "" or "percentile" (field-families-fwb-20260914.md §100); unknown values are refused fail-closed');
|
|
818
|
+
// Ticket 114: the outer `format` key is a readback MIRROR of the inner
|
|
819
|
+
// resultFormat (fwb §100). Each face being individually valid is not
|
|
820
|
+
// enough — a disagreement would silently persist two contradicting
|
|
821
|
+
// values while which face the runtime reads stays unevidenced. Refuse
|
|
822
|
+
// the mismatch instead of guessing.
|
|
823
|
+
const outerFormat = options?.format;
|
|
824
|
+
if (outerFormat !== undefined && outerFormat !== "" && outerFormat !== "percentile")
|
|
825
|
+
refuseFieldOptions(`FORMULA outer format ${String(outerFormat)} is not an evidenced value`, 'rejected: FORMULA(100) outer options.format must be "" or "percentile" — it mirrors formulaConfig.resultFormat (field-families-fwb-20260914.md §100)');
|
|
826
|
+
if (outerFormat !== undefined && outerFormat !== (config.resultFormat ?? ""))
|
|
827
|
+
refuseFieldOptions(`FORMULA outer format ${String(outerFormat)} disagrees with inner resultFormat ${String(config.resultFormat ?? "")}`, "rejected: formula-format-mismatch — outer options.format must equal formulaConfig.resultFormat (they are two faces of one setting; fwb §100)");
|
|
828
|
+
if (options?.persistent !== true)
|
|
829
|
+
refuseFieldOptions("FORMULA persistent was not true", "rejected: FORMULA(100) requires persistent:true (persistent=false is outside the evidenced contract)");
|
|
830
|
+
// AGGREGATION formulaType IS the platform's supported roll-up (form 控制
|
|
831
|
+
// 「计算」控件 → 公式配置 → 配置方式「聚合计算 Aggregation Calculation」;
|
|
832
|
+
// UI 实测 2026-10-07:文案「跨模型数据聚合计算,如求和/最大值/最小值」,
|
|
833
|
+
// 步骤=选业务模型→选计算方式→定义参与计算的数据范围). FormulaProcessor
|
|
834
|
+
// .executeMainAggregation (cloudpivot-bizobject 8.6.26) JSON-parses
|
|
835
|
+
// `formulaConfig.aggregation` into AggregationFormula {schemaCode,dataItemCode,
|
|
836
|
+
// formula,calculateType,queryConditions}, queries that schema with the rule's
|
|
837
|
+
// queryConditions, and evaluates `formula` over the matched rows' dataItemCode
|
|
838
|
+
// values. Previously this tool BLOCKED the type outright (ticket 24 saw an
|
|
839
|
+
// empty-queryConditions probe evaluate to 0) — a FALSE NEGATIVE (the same
|
|
840
|
+
// 二开-vs-配置 call an agent wrongly made: 表单字段「计算」公式可直接做聚合).
|
|
841
|
+
// Validate the evidenced shape and allow it.
|
|
842
|
+
// NOTE (open): the exact aggregation for a SAME-model child table (子表求和回
|
|
843
|
+
// 主表) still needs a live sample — a guessed child-schemaCode shape hit
|
|
844
|
+
// 305003 at publish (checkCycleOfAggregation in BizPropertyManagerImpl treats
|
|
845
|
+
// the child's parent == the field's own schema as a cycle). The UI save
|
|
846
|
+
// payload is the authoritative shape; do not hand-fabricate it.
|
|
847
|
+
// The EXPRESSION-as-aggregate trap below still applies only to EXPRESSION.
|
|
848
|
+
const isAggregation = config.formulaType === "AGGREGATION";
|
|
849
|
+
if (isAggregation) {
|
|
850
|
+
if (!isNonEmptyString(config.aggregation))
|
|
851
|
+
refuseFieldOptions("AGGREGATION formulaConfig is missing a non-empty aggregation JSON string", "rejected: AGGREGATION requires formulaConfig.aggregation (a nested JSON *string*, not an object)");
|
|
852
|
+
else {
|
|
853
|
+
const agg = parseOptionsObject(config.aggregation);
|
|
854
|
+
if (!agg || !isNonEmptyString(agg.schemaCode) || !isNonEmptyString(agg.dataItemCode) || !isNonEmptyString(agg.formula))
|
|
855
|
+
refuseFieldOptions("AGGREGATION aggregation is missing schemaCode/dataItemCode/formula", 'rejected: aggregation 形状 {schemaCode:"<子表schemaCode>", dataItemCode:"<子表字段code>", formula:"SUM({<子表schemaCode>.<子表字段code>})", calculateType?:.., queryConditions?:[]}(FormulaProcessor.executeMainAggregation + AggregationFormula)');
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else if (!isNonEmptyString(config.expression)) {
|
|
859
|
+
refuseFieldOptions("EXPRESSION formulaConfig is missing a non-empty expression", "rejected: EXPRESSION requires formulaConfig.expression");
|
|
860
|
+
}
|
|
861
|
+
// Aggregation-as-EXPRESSION (e.g. `SUM({child.field})` on a MAIN formula field
|
|
862
|
+
// over its own CHILD table) is the platform's FORM-designer shape for
|
|
863
|
+
// subtable→main roll-up — NOT a trap. UI-verified 2026-10-07: the form
|
|
864
|
+
// Formula control's Expression Calculation mode offers `SUM(v)` ("v is a
|
|
865
|
+
// numeric subform control") and the Data Item tree lists child columns; a
|
|
866
|
+
// configured `SUM(报名明细.金额)` published errcode:0 and computed 350 at
|
|
867
|
+
// runtime via the portal form. The earlier block here was a FALSE NEGATIVE
|
|
868
|
+
// (a round9 probe conflated a NPE from a SEPARATE broken child formula rule
|
|
869
|
+
// with this shape). ALLOWED. NOTE: OMC's `data.save`/`data.update` (API path)
|
|
870
|
+
// can 50000 on models that carry a CHILD formula rule, because the server
|
|
871
|
+
// re-runs CalculateHandler→FormulaProcessor; the portal form path does not.
|
|
872
|
+
// Verify such models via the portal form, not the API.
|
|
873
|
+
void expressionBody(typeof config.expression === "string" ? config.expression : "");
|
|
874
|
+
// Field-reference semantics (live 2026-09-24, bench38/bench40) — documented,
|
|
875
|
+
// NOT gated (the own-schema prefix is right or wrong depending on whether the
|
|
876
|
+
// formula field itself sits on a main or a child schema, which this check
|
|
877
|
+
// cannot see):
|
|
878
|
+
// - BARE `{field}` = the MAIN record's field;
|
|
879
|
+
// - DOTTED `{childSchema.field}` = a CHILD table's field.
|
|
880
|
+
// So a MAIN-table formula references its own fields bare (a dotted own-schema
|
|
881
|
+
// ref is read as a child table and silently computes null — bench38), while a
|
|
882
|
+
// CHILD-table formula MUST reference sibling child fields dotted with the
|
|
883
|
+
// child schema (a bare ref there reads the main record and 50000s — bench40).
|
|
884
|
+
// Both shapes are accepted here; the runtime split is authoritative.
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (type === FIELD_PROPERTY_TYPES.RELEVANCE_DATA) {
|
|
888
|
+
if (options && (options.schemaCode !== undefined || options.mappings !== undefined || options.displayField !== undefined))
|
|
889
|
+
refuseFieldOptions("RELEVANCE_DATA options carried relation keys (schemaCode/mappings/displayField)", "rejected: RELEVANCE_DATA(16) references another field, not a model; use relevanceField/referenceField/referenceFieldPropertyType");
|
|
890
|
+
if (!isNonEmptyString(options?.relevanceField) || !isNonEmptyString(options?.referenceField) || !Number.isInteger(options?.referenceFieldPropertyType))
|
|
891
|
+
refuseFieldOptions("RELEVANCE_DATA options are missing relevanceField/referenceField/referenceFieldPropertyType", "rejected: RELEVANCE_DATA(16) requires options {relevanceField, referenceField, referenceFieldPropertyType} (RelevanceDataOptions / validateRelevanceDataPropertyConfig)");
|
|
892
|
+
if (options?.referenceFieldControlType !== undefined && !Number.isInteger(options.referenceFieldControlType))
|
|
893
|
+
refuseFieldOptions("RELEVANCE_DATA referenceFieldControlType is not an integer control code", "rejected: RELEVANCE_DATA(16) referenceFieldControlType must be an integer BizFormPropertyType code");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
//# sourceMappingURL=field-families.js.map
|