@stalfh233/omc-cli 0.0.0-stage → 0.4.0

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.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. 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