@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,1207 @@
1
+ import { z } from "zod";
2
+ import { ToolRefusalError } from "../tool-types.js";
3
+ import { decodeJsonColumn, encodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "../json-column.js";
4
+ import { FIELD_PROPERTY_TYPES } from "../field-families.js";
5
+ function wireEnum(key, index) {
6
+ return {
7
+ key,
8
+ names: Object.keys(index),
9
+ index,
10
+ name: Object.fromEntries(Object.entries(index).map(([name, value]) => [value, name])),
11
+ };
12
+ }
13
+ /** Encode an enum name to its wire numeric index (undefined when unknown). */
14
+ export function encodeEnumName(descriptor, name) {
15
+ return typeof name === "string" ? descriptor.index[name] : undefined;
16
+ }
17
+ /** Decode a wire numeric index back to its enum name (unknown numbers pass through). */
18
+ export function decodeWireEnum(descriptor, value) {
19
+ return typeof value === "number" ? descriptor.name[value] ?? value : value ?? null;
20
+ }
21
+ /**
22
+ * `QueryFilterType` (QueryFilterType.java) is the list-condition OPERATOR
23
+ * (Eq/NotEq/Gt/Lt/Gte/Lte/Like/NotLike/IsNull/IsNotNull/In/NotIn/Between/
24
+ * NotBetween/Of/NotOf/ContainsAll/…). Unlike SortDirectionType/DisplayType it
25
+ * carries NO `@JsonValue`; as an `IStringEnum` it serializes as its NAME STRING
26
+ * on the wire (live readback of `h_biz_query_detail.queryConfigJson`
27
+ * filterType:"Like"). So the tool speaks/reads the enum name verbatim — no
28
+ * numeric index conversion.
29
+ */
30
+ export const QUERY_FILTER_TYPE = [
31
+ "Eq", "NotEq", "Gt", "Lt", "Gte", "Lte", "Like", "NotLike",
32
+ "IsNull", "IsNotNull", "Of", "NotOf", "In", "NotIn", "Between", "NotBetween",
33
+ "ContainsAll", "NotContainsAll", "ContainsAny", "NotContainsAny", "EqAny", "NotEqAny",
34
+ "MonthTimeBetween", "DayTimeBetween", "TimeBetween", "TimeNotBetween",
35
+ "TimeGt", "TimeGte", "TimeLt", "TimeLte",
36
+ ];
37
+ const QUERY_FILTER_TYPES = new Set(QUERY_FILTER_TYPE);
38
+ /**
39
+ * The platform's own default operator per property type
40
+ * (`QueryFilterType.getDefaultQueryFilterType`, QueryFilterType.java) — used
41
+ * when a condition omits `filterType`, so the stored row names its comparison.
42
+ */
43
+ function defaultQueryFilterType(propertyType) {
44
+ switch (propertyType) {
45
+ case FIELD_PROPERTY_TYPES.LOGICAL:
46
+ case FIELD_PROPERTY_TYPES.WORK_SHEET:
47
+ case FIELD_PROPERTY_TYPES.MULT_WORK_SHEET:
48
+ case FIELD_PROPERTY_TYPES.STAFF_SELECTOR:
49
+ case FIELD_PROPERTY_TYPES.ADDRESS:
50
+ case FIELD_PROPERTY_TYPES.RADIO:
51
+ case FIELD_PROPERTY_TYPES.DROPDOWN_BOX:
52
+ return "Eq";
53
+ case FIELD_PROPERTY_TYPES.DEPARTMENT_SELECTOR:
54
+ return "Eq";
55
+ case FIELD_PROPERTY_TYPES.DATE:
56
+ case FIELD_PROPERTY_TYPES.NUMERICAL:
57
+ return "Between";
58
+ case FIELD_PROPERTY_TYPES.ATTACHMENT:
59
+ return "IsNull";
60
+ default:
61
+ return "Like";
62
+ }
63
+ }
64
+ /**
65
+ * Wire enum maps pinned to the 8.6.26 query DTOs. The enums carry
66
+ * `@JsonValue getIndex()` (BizQuerySortModel/BizQueryConditionModel), so the
67
+ * HTTP body uses the numeric index while the tool speaks the enum name and
68
+ * decodes it back on readback.
69
+ */
70
+ export const SORT_DIRECTION = wireEnum("SortDirectionType", { ASC: 1, DESC: 2 });
71
+ export const DISPLAY_TYPE = wireEnum("DisplayType", { INPUT: 0, RADIO: 1, CHECKBOX: 2, SELECT: 3, RANGE: 4 });
72
+ export const CHOICE_TYPE = wireEnum("ChoiceType", { SELECT: 1, POPUP: 2, TREE_LEFT: 3 });
73
+ /**
74
+ * BizQueryActionModel enums. `QueryActionType` (QueryActionType.java:13-35) and
75
+ * `QueryActionRelativeType` (BIZSHEET 0/WORKFLOW 1) both carry
76
+ * `@JsonValue getIndex()` so the wire body is numeric; the tool speaks the enum
77
+ * name and decodes on readback. `associationType`/`associationCode` are the
78
+ * JSON names of the DTO fields `queryActionRelativeType`/`relativeCode`
79
+ * (`@JsonProperty`, BizQueryActionModel.java:31-35).
80
+ */
81
+ export const QUERY_ACTION_TYPE = wireEnum("QueryActionType", {
82
+ QUERY: 1,
83
+ ADD: 2,
84
+ EDIT: 3,
85
+ DELETE: 4,
86
+ IMPORT: 5,
87
+ EXPORT: 6,
88
+ CUSTOM: 7,
89
+ QR_CODE: 8,
90
+ EDITOWNER: 9,
91
+ BATCH_PRINT: 10,
92
+ BATCH_UPDATE: 11,
93
+ CUSTOMIZE_BTN: 12,
94
+ PRINT: 13,
95
+ COPY: 14,
96
+ TEMPORARY: 15,
97
+ SUBMIT: 16,
98
+ CLEAR_ROW: 17,
99
+ ADD_BEFORE: 18,
100
+ ADD_AFTER: 19,
101
+ MANUAL_SYNC: 20,
102
+ IMPORT_ATTACHMENT: 21,
103
+ EXPORT_ATTACHMENT: 22,
104
+ BATCH_SUBMIT: 23,
105
+ });
106
+ export const QUERY_ACTION_RELATIVE = wireEnum("QueryActionRelativeType", { BIZSHEET: 0, WORKFLOW: 1 });
107
+ /**
108
+ * `BizButtonModel` enums (listview custom buttons). Unlike the query DTOs
109
+ * above, these implement `IStringEnum` with no `@JsonValue`, so Hutool parses
110
+ * and Jackson serializes them by enum name (the desensitized customButtonJson
111
+ * carries `bindAction:"SCRIPT"` etc.). `triggerType`/`triggerCode` are forced by
112
+ * `BizButtonFacadeImpl.addBizButtonList(String,...)` (:96-101), so they are only
113
+ * decoded back, never authored. `sortKey` is overwritten with the array index.
114
+ */
115
+ export const BUTTON_TRIGGER_TYPE = wireEnum("ButtonTriggerType", { QUERY: 1, FORM: 2 });
116
+ export const BUTTON_BIND_ACTION = wireEnum("ButtonBindAction", {
117
+ LIST: 1,
118
+ FORM: 2,
119
+ ADD_DATA: 3,
120
+ FLOW: 4,
121
+ BUSINESS_RULE: 5,
122
+ SCRIPT: 6,
123
+ CUSTOM_PAGE: 7,
124
+ WORKFLOW_CENTER_PAGE: 8,
125
+ FRONT_EVENT: 9,
126
+ AGENT: 10,
127
+ });
128
+ export const BUTTON_USE_LOCATION = wireEnum("ButtonUseLocation", { LIST: 1, FORM: 2, ROW: 3 });
129
+ export const BUTTON_OPERATE_TYPE = wireEnum("ButtonOperateType", { WITH_VIEW: 1, WITH_OUT_VIEW: 2 });
130
+ export const BUTTON_SHOW_PERM_TYPE = wireEnum("ButtonShowPermType", {
131
+ BIND_SYS_BUTTON: 1,
132
+ BIND_BUSINESS_RULE: 2,
133
+ BIND_FRONT_EVENT_BUTTON: 3,
134
+ });
135
+ /**
136
+ * `BizButtonFacadeImpl.validateParamMessage` rejects these codes (uppercased)
137
+ * with `INVALID_SYSTEM_CODE_USED` (:243-245), so the dry-lint refuses them too.
138
+ */
139
+ export const BUTTON_SYSTEM_CODES = new Set(["ADD", "DELETE", "IMPORT", "EXPORT", "QR_CODE", "EDITOWNER", "BATCH_PRINT", "BATCH_UPDATE"]);
140
+ /**
141
+ * `QueryPresentationType` (8.6.26) is an `IStringEnum` with `@JsonValue
142
+ * getIndex()` whose index equals the name, so the wire carries the name
143
+ * LIST/BOARD/CALENDAR/GANTT; `@JsonCreator get(String)` parses it back.
144
+ * (No numeric index — the wire value IS the name.)
145
+ */
146
+ export const QUERY_PRESENTATION_TYPE = {
147
+ key: "QueryPresentationType",
148
+ names: ["LIST", "BOARD", "CALENDAR", "GANTT"],
149
+ };
150
+ /**
151
+ * BizQueryPresentModel payload fields. On the wire they are all JSON strings
152
+ * (BizQueryPresent.java declares htmlJson/actionsJson/columnsJson/
153
+ * schemaRelations/options as TEXT and queryViewDataSource as LONGTEXT);
154
+ * `clientType` is a ClientType string enum (PC/APP/OTHER) forced to PC by
155
+ * query/update. `schemaRelations` is platform-derived by
156
+ * ModelManagementFacadeImpl.resolveRelations and overwrites any input.
157
+ */
158
+ export const PRESENTATION_FIELDS = ["htmlJson", "actionsJson", "columnsJson", "queryViewDataSource", "schemaRelations", "options"];
159
+ export const directionArg = z.enum(["ASC", "DESC"]).describe("SortDirectionType; sent numeric ASC(1)/DESC(2)");
160
+ export const displayTypeArg = z
161
+ .enum(["INPUT", "RADIO", "CHECKBOX", "SELECT", "RANGE"])
162
+ .describe("DisplayType; sent numeric INPUT(0)/RADIO(1)/CHECKBOX(2)/SELECT(3)/RANGE(4)");
163
+ export const choiceTypeArg = z.enum(["SELECT", "POPUP", "TREE_LEFT"]).describe("ChoiceType; sent numeric SELECT(1)/POPUP(2)/TREE_LEFT(3)");
164
+ export const conditionOptionsArg = z
165
+ .union([z.string(), z.record(z.string(), z.unknown())])
166
+ .describe("queryCondition options; an object is JSON-stringified for the wire and decoded on readback");
167
+ export const listViewSortArg = z.object({
168
+ propertyCode: z.string().min(1).describe("目标字段:propertyCode 或字段显示名(唯一匹配时自动解析,歧义拒绝)"),
169
+ propertyType: z.number().int().optional().describe("BizPropertyType numeric; derived from published field metadata when omitted"),
170
+ direction: directionArg,
171
+ sortKey: z.number().int().optional().describe("sort order; defaults to the array position"),
172
+ });
173
+ export const listViewConditionArg = z
174
+ .object({
175
+ propertyCode: z.string().min(1).describe("目标字段:propertyCode 或字段显示名(唯一匹配时自动解析,歧义拒绝)"),
176
+ propertyType: z.number().int().optional().describe("BizPropertyType numeric; derived from published field metadata when omitted"),
177
+ displayType: displayTypeArg.optional(),
178
+ defaultValue: z.string().optional(),
179
+ options: conditionOptionsArg.optional(),
180
+ startValue: z.string().optional(),
181
+ endValue: z.string().optional(),
182
+ shortcutFilter: z.boolean().optional(),
183
+ choiceType: choiceTypeArg.optional(),
184
+ dateType: z.number().int().optional(),
185
+ accurateSearch: z.boolean().optional(),
186
+ showNull: z.boolean().optional(),
187
+ showNotNull: z.boolean().optional(),
188
+ includeSubData: z.boolean().optional(),
189
+ buildTreeBy: z.string().optional(),
190
+ visible: z.boolean().optional(),
191
+ filterType: z.string().optional().describe(`QueryFilterType 比较符(平台字符串枚举):${QUERY_FILTER_TYPE.join("/")};省略时按 propertyType 取平台默认`),
192
+ operator: z.string().optional().describe("filterType 的别名(同一比较符)"),
193
+ op: z.string().optional().describe("filterType 的别名(同一比较符)"),
194
+ })
195
+ // Keep unknown keys so `assertListViewConfigureShape` can REFUSE them with a
196
+ // helpful message. A default `z.object` strips them, so `{operator, value}`
197
+ // silently disappeared and the write looked like it landed (dogfood R2).
198
+ .passthrough();
199
+ export const queryActionTypeArg = z
200
+ .enum(Object.keys(QUERY_ACTION_TYPE.index))
201
+ .describe("QueryActionType; sent numeric QUERY(1)/ADD(2)/EDIT(3)/DELETE(4)/IMPORT(5)/EXPORT(6)/CUSTOM(7)/QR_CODE(8)/EDITOWNER(9)/BATCH_PRINT(10)/BATCH_UPDATE(11)/CUSTOMIZE_BTN(12)/PRINT(13)/COPY(14)/TEMPORARY(15)/SUBMIT(16)/CLEAR_ROW(17)/ADD_BEFORE(18)/ADD_AFTER(19)/MANUAL_SYNC(20)/IMPORT_ATTACHMENT(21)/EXPORT_ATTACHMENT(22)/BATCH_SUBMIT(23)");
202
+ export const associationTypeArg = z.enum(["BIZSHEET", "WORKFLOW"]).describe("QueryActionRelativeType; sent numeric BIZSHEET(0)/WORKFLOW(1)");
203
+ export const listViewActionArg = z.object({
204
+ actionCode: z.string().min(1),
205
+ name: z.string().optional(),
206
+ icon: z.string().optional(),
207
+ queryActionType: queryActionTypeArg,
208
+ systemAction: z.boolean().optional(),
209
+ serviceCode: z.string().optional(),
210
+ serviceMethod: z.string().optional(),
211
+ customService: z.boolean().optional(),
212
+ associationCode: z.string().optional().describe("relativeCode JSON name; the associated form/workflow code"),
213
+ associationType: associationTypeArg.optional(),
214
+ sortKey: z.number().int().optional().describe("sort order; query/update resets it to the array position i+1"),
215
+ });
216
+ export const buttonBindActionArg = z
217
+ .enum(Object.keys(BUTTON_BIND_ACTION.index))
218
+ .describe("ButtonBindAction; sent as the enum name LIST(1)/FORM(2)/ADD_DATA(3)/FLOW(4)/BUSINESS_RULE(5)/SCRIPT(6)/CUSTOM_PAGE(7)/WORKFLOW_CENTER_PAGE(8)/FRONT_EVENT(9)/AGENT(10)");
219
+ export const buttonUseLocationArg = z
220
+ .enum(["LIST", "FORM", "ROW"])
221
+ .describe("ButtonUseLocation; sent as the enum name LIST(1)/FORM(2)/ROW(3)");
222
+ export const buttonOperateTypeArg = z
223
+ .enum(["WITH_VIEW", "WITH_OUT_VIEW"])
224
+ .describe("ButtonOperateType; sent as the enum name WITH_VIEW(1)/WITH_OUT_VIEW(2)");
225
+ export const buttonShowPermTypeArg = z
226
+ .enum(["BIND_SYS_BUTTON", "BIND_BUSINESS_RULE", "BIND_FRONT_EVENT_BUTTON"])
227
+ .describe("ButtonShowPermType; sent as the enum name BIND_SYS_BUTTON(1)/BIND_BUSINESS_RULE(2)/BIND_FRONT_EVENT_BUTTON(3)");
228
+ const buttonConfigureListArg = z
229
+ .object({
230
+ currentDataItemId: z.string().min(1).optional().describe("源字段 code(当前视图模型上的字段);缺失由 dry-lint 拒绝(listview-buttons-invalid)"),
231
+ targetDataItemId: z.string().min(1).optional().describe("目标字段 code(targetModel 上的字段);缺失由 dry-lint 拒绝(listview-buttons-invalid)"),
232
+ isConst: z.string().optional().describe('常量开关是字符串 "true"/"false";仅 "false" 已取证(常量赋值键名未取证,未实现)'),
233
+ OptionType: z.string().optional().describe("赋值算子(注意大写 O);仅 Eq 已取证"),
234
+ })
235
+ .describe("actionConfig.configureList[] 数据赋值条目;字段对象内联副本由工具从发布面读回展开");
236
+ export const listViewButtonArg = z
237
+ .object({
238
+ code: z.string().min(1),
239
+ name: z.string().min(1),
240
+ useLocation: buttonUseLocationArg,
241
+ bindAction: buttonBindActionArg,
242
+ operateType: buttonOperateTypeArg,
243
+ showPermCode: z.string().optional(),
244
+ showPermType: buttonShowPermTypeArg.optional(),
245
+ hint: z.string().optional(),
246
+ targetCode: z.string().optional(),
247
+ targetObjCode: z.string().optional(),
248
+ description: z.string().optional(),
249
+ targetModel: z.string().min(1).optional().describe("ADD_DATA/FORM 目标模型 code;工具读回应用树节点内联展开成 actionConfig.schemaCodeVal(targetCode/targetObjCode 对这两类按钮无效——实测恒 null 不参与)"),
250
+ targetForm: z.string().min(1).optional().describe("ADD_DATA/FORM 目标表单 code;缺省用目标模型的默认表单;展开成 actionConfig.formListData"),
251
+ configureList: z.array(buttonConfigureListArg).optional().describe("ADD_DATA 数据赋值;展开成 actionConfig.configureList(含 isConst:\"false\"、OptionType:\"Eq\" 与字段内联副本)"),
252
+ actionConfig: z
253
+ .union([z.string(), z.record(z.string(), z.unknown())])
254
+ .optional()
255
+ .describe("BizButtonModel.actionConfig 原样透传(与 targetModel/targetForm/configureList 互斥);ADD_DATA/FORM 只接受 schemaCodeVal/formListData/configureList 子形状"),
256
+ })
257
+ .describe("customButtons[] entry (BizButtonModel); name/code/useLocation/bindAction/operateType are required; triggerType/triggerCode are forced to QUERY/view code and sortKey to the array position");
258
+ /**
259
+ * `BizQueryModel.options` (a String column on `h_biz_query`) parsed by the
260
+ * platform as `BizQueryOption` (8.6.26): only `pagingSetting`
261
+ * (BizQueryPaging{pageSize,showTotal}) and `listViewModeSetting`
262
+ * (ListViewModeSetting{mode,canSwitch}) have class evidence. `defaultOption()`
263
+ * is pageSize 20 / showTotal true / mode "spreadsheet" / canSwitch false.
264
+ * `rowColorSetting` exists only in newer yst/fwb samples with no 8.6.26 class,
265
+ * so the object form refuses it (a raw string stays the escape hatch).
266
+ */
267
+ export const listViewPagingSettingArg = z
268
+ .object({
269
+ pageSize: z.number().int().positive().optional().describe("BizQueryPaging.pageSize (8.6.26 default 20)"),
270
+ showTotal: z.boolean().optional().describe("BizQueryPaging.showTotal (8.6.26 default true)"),
271
+ })
272
+ .strict();
273
+ export const listViewModeSettingArg = z
274
+ .object({
275
+ mode: z.string().min(1).optional().describe("ListViewModeSetting.mode; 8.6.26 SPREAD_SHEET default \"spreadsheet\" (a plain String, no enum)"),
276
+ canSwitch: z.boolean().optional().describe("ListViewModeSetting.canSwitch (8.6.26 default false)"),
277
+ })
278
+ .strict();
279
+ export const listViewOptionsArg = z
280
+ .union([
281
+ z.string(),
282
+ z
283
+ .object({
284
+ pagingSetting: listViewPagingSettingArg.optional(),
285
+ listViewModeSetting: listViewModeSettingArg.optional(),
286
+ })
287
+ .strict(),
288
+ ])
289
+ .describe("view header options (BizQueryModel.options, a BizQueryOption JSON string); a raw string is sent verbatim, an object is JSON-stringified; object keys are limited to pagingSetting{pageSize,showTotal} + listViewModeSetting{mode,canSwitch} (rowColorSetting has no 8.6.26 class and is refused)");
290
+ /**
291
+ * `queryGanttModel` (BizQueryGanttModel) is the GANTT config; when
292
+ * presentationType is GANTT the controller requires it with non-null
293
+ * startTimePropertyCode/endTimePropertyCode/titlePropertyCode
294
+ * (BizQueryController.validateNotEmptyGantt :1121-1136).
295
+ */
296
+ export const presentationTypeArg = z.enum(QUERY_PRESENTATION_TYPE.names).describe("QueryPresentationType; sent as the name LIST/BOARD/CALENDAR/GANTT (@JsonValue index == name)");
297
+ export const listViewGanttArg = z
298
+ .object({
299
+ startTimePropertyCode: z.string().min(1).optional(),
300
+ endTimePropertyCode: z.string().min(1).optional(),
301
+ progressPropertyCode: z.string().optional(),
302
+ titlePropertyCode: z.string().min(1).optional(),
303
+ preDependencyPropertyCode: z.string().optional(),
304
+ endDependencyPropertyCode: z.string().optional(),
305
+ levelPropertyCode: z.string().optional().describe("hierarchy field; platform forces parentId for TREE models"),
306
+ milepostCode: z.string().optional(),
307
+ sortKey: z.string().optional().describe("BizQueryGantt.sortKey is a String column"),
308
+ configJson: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe("extension JSON; an object is JSON-stringified"),
309
+ liableManCode: z.string().optional(),
310
+ statusCode: z.string().optional(),
311
+ defaultPrecision: z.string().optional(),
312
+ })
313
+ .strict()
314
+ .describe("BizQueryGanttModel (queryGanttModel); queryId/schemaCode are platform/tool supplied; required (startTime/endTime/title) only when presentationType is GANTT");
315
+ /**
316
+ * `queryLayer` (BizQueryLayer[]) is a real JSON array in the DTO (not a String
317
+ * column), so it is sent as an array. `multiTableHeaderConfig` is a String
318
+ * column holding a HeaderConfig[] JSON (type/name/field/propertyType/children),
319
+ * parsed by EasyExcelUtils/DefaultDataIEFileOperateServiceImpl via
320
+ * JSONObject.parseArray. saveBizQueryDetail assigns both unconditionally
321
+ * (:184-186), so omitted keys would overwrite stored values; the read face
322
+ * always carries them (BizQueryFacadeImpl:344-345), so the tool echoes them.
323
+ */
324
+ export const listViewQueryLayerArg = z
325
+ .array(z.record(z.string(), z.unknown()))
326
+ .describe("queryLayer (BizQueryLayer[]); array of layer objects, passed through and read back");
327
+ export const listViewMultiTableHeaderArg = z
328
+ .union([z.string(), z.array(z.record(z.string(), z.unknown()))])
329
+ .describe("multiTableHeaderConfig (HeaderConfig[] JSON); a raw JSON string is sent verbatim, an array is JSON-stringified; decoded on readback when valid JSON");
330
+ export const presentationJsonArg = z
331
+ .union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())])
332
+ .describe("queryPresentation field; a JSON string is sent as-is, an object/array is JSON-stringified; decoded on readback when valid JSON");
333
+ export const listViewPresentationArg = z
334
+ .object({
335
+ htmlJson: presentationJsonArg.optional(),
336
+ actionsJson: presentationJsonArg.optional(),
337
+ columnsJson: presentationJsonArg.optional(),
338
+ queryViewDataSource: presentationJsonArg.optional(),
339
+ schemaRelations: presentationJsonArg.optional(),
340
+ options: presentationJsonArg.optional(),
341
+ })
342
+ .describe("queryPresentation {clientType,htmlJson,actionsJson,columnsJson,queryViewDataSource,schemaRelations,options}; omitted leaves the existing presentation untouched, provided replaces it (clientType is forced to PC; schemaRelations is platform-derived)");
343
+ function refusePresentation(message) {
344
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-presentation-render-invalid", message });
345
+ }
346
+ function decodePresentationForValidation(value, key) {
347
+ if (typeof value !== "string")
348
+ return value;
349
+ try {
350
+ return JSON.parse(value);
351
+ }
352
+ catch (error) {
353
+ refusePresentation(`presentation.${key} must be valid JSON: ${error.message}`);
354
+ }
355
+ }
356
+ function presentationShape(value, key) {
357
+ const decoded = decodePresentationForValidation(value, key);
358
+ if (key === "htmlJson") {
359
+ if (!Array.isArray(decoded))
360
+ refusePresentation("presentation.htmlJson must decode to an array");
361
+ for (const [index, element] of decoded.entries()) {
362
+ if (!element || typeof element !== "object" || Array.isArray(element))
363
+ refusePresentation(`presentation.htmlJson[${index}] must be an object`);
364
+ const row = element;
365
+ if (typeof row.tagName !== "string" || row.tagName.length === 0 || !Array.isArray(row.attrs))
366
+ refusePresentation(`presentation.htmlJson[${index}] requires non-empty tagName and attrs array`);
367
+ }
368
+ return decoded;
369
+ }
370
+ if (key === "actionsJson" || key === "columnsJson") {
371
+ if (!Array.isArray(decoded))
372
+ refusePresentation(`presentation.${key} must decode to an array`);
373
+ for (const [index, element] of decoded.entries()) {
374
+ if (!element || typeof element !== "object" || Array.isArray(element))
375
+ refusePresentation(`presentation.${key}[${index}] must be an object`);
376
+ const row = element;
377
+ const required = key === "actionsJson" ? ["actionCode", "name", "icon"] : ["propertyCode", "propertyType"];
378
+ for (const field of required) {
379
+ const valid = field === "propertyType" ? typeof row[field] === "number" : typeof row[field] === "string" && row[field].length > 0;
380
+ if (!valid)
381
+ refusePresentation(`presentation.${key}[${index}] requires ${field}`);
382
+ }
383
+ }
384
+ return decoded;
385
+ }
386
+ return decoded;
387
+ }
388
+ export function validatePresentationRenderShape(presentation) {
389
+ for (const key of ["htmlJson", "actionsJson", "columnsJson"]) {
390
+ if (presentation[key] !== undefined)
391
+ presentationShape(presentation[key], key);
392
+ }
393
+ }
394
+ function decodeButtonActionConfig(value, code) {
395
+ if (value === undefined)
396
+ return undefined;
397
+ if (typeof value === "string") {
398
+ try {
399
+ value = JSON.parse(value);
400
+ }
401
+ catch (error) {
402
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: actionConfig must be valid JSON (${error.message})` });
403
+ }
404
+ }
405
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
406
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: actionConfig must decode to an object` });
407
+ }
408
+ return value;
409
+ }
410
+ function requireButtonConfig(config, code, bindAction) {
411
+ const required = bindAction === "SCRIPT" ? ["scriptData"] : bindAction === "BUSINESS_RULE" || bindAction === "FLOW" ? ["targetCode"] : bindAction === "ADD_DATA" || bindAction === "FORM" ? ["schemaCodeVal", "formListData", "configureList"] : [];
412
+ for (const key of required) {
413
+ if (config?.[key] === undefined || config[key] === null) {
414
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: ${bindAction} actionConfig requires ${key}` });
415
+ }
416
+ }
417
+ if (bindAction === "SCRIPT" && typeof config?.scriptData !== "string") {
418
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: SCRIPT actionConfig.scriptData must be a string` });
419
+ }
420
+ if ((bindAction === "ADD_DATA" || bindAction === "FORM") && (typeof config?.schemaCodeVal !== "object" || config.schemaCodeVal === null || Array.isArray(config.schemaCodeVal) || !Array.isArray(config.formListData) || !Array.isArray(config.configureList))) {
421
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: ${bindAction} actionConfig requires object schemaCodeVal and array formListData/configureList` });
422
+ }
423
+ }
424
+ export function validateButtonActionConfig(entry) {
425
+ const bindAction = typeof entry.bindAction === "string" ? entry.bindAction : "";
426
+ const code = String(entry.code ?? "");
427
+ const config = decodeButtonActionConfig(entry.actionConfig, code);
428
+ if (bindAction === "BUSINESS_RULE") {
429
+ if (typeof entry.targetCode !== "string" || entry.targetCode.length === 0) {
430
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: BUSINESS_RULE requires targetCode` });
431
+ }
432
+ return;
433
+ }
434
+ if (bindAction === "FLOW") {
435
+ if (typeof entry.targetCode !== "string" || entry.targetCode.length === 0) {
436
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: FLOW requires targetCode (workflow code)` });
437
+ }
438
+ // A caller-supplied FLOW actionConfig must carry WorkflowListData; the
439
+ // runtime indexes [0].workflowCode and iterates configureList. Omitted is
440
+ // fine — expandButtonActionConfigs derives both from targetCode.
441
+ if (config !== undefined && (typeof config !== "object" || config === null || !Array.isArray(config.WorkflowListData))) {
442
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: FLOW actionConfig requires a WorkflowListData array (or omit actionConfig and the tool derives it from targetCode)` });
443
+ }
444
+ return;
445
+ }
446
+ if ((bindAction === "ADD_DATA" || bindAction === "FORM") && (entry.targetModel !== undefined || entry.targetForm !== undefined || entry.configureList !== undefined) && entry.actionConfig === undefined)
447
+ return;
448
+ requireButtonConfig(config, code, bindAction);
449
+ }
450
+ export function columnsRenderCompat(columns) {
451
+ const violations = [];
452
+ for (const [index, column] of columns.entries()) {
453
+ if (typeof column.propertyCode !== "string" || column.propertyCode.length === 0)
454
+ violations.push(`columns[${index}].propertyCode`);
455
+ if (typeof column.propertyType !== "number")
456
+ violations.push(`columns[${index}].propertyType`);
457
+ if (typeof column.name !== "string" || column.name.length === 0)
458
+ violations.push(`columns[${index}].name`);
459
+ if (typeof column.visible !== "boolean")
460
+ violations.push(`columns[${index}].visible`);
461
+ if (typeof column.width !== "string" && typeof column.width !== "number")
462
+ violations.push(`columns[${index}].width`);
463
+ if (typeof column.sortKey !== "number")
464
+ violations.push(`columns[${index}].sortKey`);
465
+ }
466
+ return violations.length ? { ok: false, violations } : { ok: true };
467
+ }
468
+ export function buttonsRenderCompat(buttons) {
469
+ const violations = [];
470
+ for (const [index, button] of buttons.entries()) {
471
+ try {
472
+ validateButtonActionConfig(button);
473
+ }
474
+ catch {
475
+ violations.push(`buttons[${index}].actionConfig`);
476
+ }
477
+ }
478
+ return violations.length ? { ok: false, violations } : { ok: true };
479
+ }
480
+ /**
481
+ * The keys a list-view condition (a filter CONTROL definition) accepts. Mirrors
482
+ * `com.authine.cloudpivot.metadata.bizquery.BizQueryConditionModel` (8.6.26) at
483
+ * the field level, minus server-owned keys (queryId/schemaCode/clientType). A
484
+ * condition is a filter control whose `filterType` (QueryFilterType) declares
485
+ * the comparison operator (`Eq`/`NotEq`/…), with an optional value carrier
486
+ * (`defaultValue`/`options`/`startValue`/`endValue`/`showNull`/`showNotNull`).
487
+ */
488
+ const LISTVIEW_CONDITION_KEYS = new Set([
489
+ "propertyCode",
490
+ "propertyType",
491
+ "name",
492
+ "displayType",
493
+ "defaultValue",
494
+ "options",
495
+ "startValue",
496
+ "endValue",
497
+ "shortcutFilter",
498
+ "choiceType",
499
+ "dateType",
500
+ "accurateSearch",
501
+ "showNull",
502
+ "showNotNull",
503
+ "includeSubData",
504
+ "buildTreeBy",
505
+ "visible",
506
+ "filterType",
507
+ ]);
508
+ /**
509
+ * Out-the-door dry lint for `sorts`/`conditions`/`actions`/`presentation`/
510
+ * `buttons`/`options`. `omc call` bypasses zod, so this refuses a malformed list
511
+ * or options object before the write permission gate and before any write. Duplicates
512
+ * are refused because the provided list replaces the stored one; `options`
513
+ * objects are limited to the 8.6.26-evidenced `pagingSetting`/
514
+ * `listViewModeSetting` (rowColorSetting has no class).
515
+ */
516
+ export function assertListViewConfigureShape(args) {
517
+ const lists = [
518
+ ["sorts", args.sorts],
519
+ ["conditions", args.conditions],
520
+ ];
521
+ for (const [kind, value] of lists) {
522
+ if (value === undefined)
523
+ continue;
524
+ if (!Array.isArray(value)) {
525
+ throw new ToolRefusalError({ status: "refused", blocker: `listview-${kind}-invalid`, message: `${kind} must be an array` });
526
+ }
527
+ const seen = new Set();
528
+ for (const entry of value) {
529
+ const propertyCode = entry?.propertyCode;
530
+ if (typeof propertyCode !== "string" || propertyCode.length === 0) {
531
+ throw new ToolRefusalError({ status: "refused", blocker: `listview-${kind}-invalid`, message: `${kind} entries require a non-empty propertyCode` });
532
+ }
533
+ if (seen.has(propertyCode)) {
534
+ throw new ToolRefusalError({ status: "refused", blocker: `listview-${kind}-duplicate`, message: `${kind} has duplicate propertyCode ${propertyCode}` });
535
+ }
536
+ seen.add(propertyCode);
537
+ if (kind === "sorts" && entry.direction !== "ASC" && entry.direction !== "DESC") {
538
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-sorts-invalid", message: `sorts direction must be ASC or DESC (got ${String(entry.direction)})` });
539
+ }
540
+ // A list-view filter is a filter CONTROL whose OPERATOR is `filterType`
541
+ // (QueryFilterType). `operator`/`op` are accepted aliases for it; a bare
542
+ // `value` is not a platform key (the value carrier is defaultValue/
543
+ // options/startValue/endValue). An unknown key is refused rather than
544
+ // silently dropped, so `ok:true` cannot hide a filter that never landed.
545
+ if (kind === "conditions") {
546
+ const ALIASES = new Set(["operator", "op"]);
547
+ for (const key of Object.keys(entry)) {
548
+ if (LISTVIEW_CONDITION_KEYS.has(key) || ALIASES.has(key))
549
+ continue;
550
+ const operatorish = key === "operator" || key === "value";
551
+ throw new ToolRefusalError({
552
+ status: "refused",
553
+ blocker: "listview-condition-unknown-key",
554
+ message: operatorish
555
+ ? `conditions 的 "${key}" 不是平台字段:比较符是 filterType(QueryFilterType:Eq/NotEq/…,operator/op 是其别名),值用 defaultValue/options/startValue/endValue 承载。可选键:${[...LISTVIEW_CONDITION_KEYS].join(" / ")}。要"状态不等于已归档"用 {propertyCode:"status", filterType:"NotEq", defaultValue:"已归档"}`
556
+ : `conditions 不支持键 "${key}";可选键:${[...LISTVIEW_CONDITION_KEYS].join(" / ")} + 别名 operator/op`,
557
+ key,
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ const actions = args.actions;
564
+ if (actions !== undefined) {
565
+ if (!Array.isArray(actions)) {
566
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-invalid", message: "actions must be an array" });
567
+ }
568
+ const seen = new Set();
569
+ for (const entry of actions) {
570
+ const actionCode = entry?.actionCode;
571
+ if (typeof actionCode !== "string" || actionCode.length === 0) {
572
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-invalid", message: "actions entries require a non-empty actionCode" });
573
+ }
574
+ if (seen.has(actionCode)) {
575
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-duplicate", message: `actions has duplicate actionCode ${actionCode}` });
576
+ }
577
+ seen.add(actionCode);
578
+ if (typeof entry.queryActionType !== "string" || !(entry.queryActionType in QUERY_ACTION_TYPE.index)) {
579
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-invalid", message: `actions queryActionType must be a QueryActionType name (got ${String(entry.queryActionType)})` });
580
+ }
581
+ const defaults = ACTION_RENDER_DEFAULTS[entry.queryActionType];
582
+ if (!defaults && (typeof entry.name !== "string" || entry.name.length === 0 || typeof entry.icon !== "string" || entry.icon.length === 0)) {
583
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-invalid", message: `actions ${actionCode} requires explicit name and icon for rendering` });
584
+ }
585
+ if (entry.associationType !== undefined && (typeof entry.associationType !== "string" || !(entry.associationType in QUERY_ACTION_RELATIVE.index))) {
586
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-invalid", message: `actions associationType must be BIZSHEET or WORKFLOW (got ${String(entry.associationType)})` });
587
+ }
588
+ }
589
+ }
590
+ if (args.presentation !== undefined) {
591
+ const presentation = args.presentation;
592
+ if (typeof presentation !== "object" || presentation === null || Array.isArray(presentation)) {
593
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-presentation-invalid", message: "presentation must be an object" });
594
+ }
595
+ validatePresentationRenderShape(presentation);
596
+ for (const key of PRESENTATION_FIELDS) {
597
+ const value = presentation[key];
598
+ if (value === undefined || typeof value === "string")
599
+ continue;
600
+ if (typeof value === "object" && value !== null)
601
+ continue;
602
+ throw new ToolRefusalError({
603
+ status: "refused",
604
+ blocker: "listview-presentation-invalid",
605
+ message: `presentation.${key} must be a JSON string, object, or array (got ${typeof value})`,
606
+ });
607
+ }
608
+ }
609
+ const buttons = args.buttons;
610
+ if (buttons !== undefined) {
611
+ if (!Array.isArray(buttons)) {
612
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: "buttons must be an array" });
613
+ }
614
+ const seen = new Set();
615
+ for (const entry of buttons) {
616
+ const code = entry?.code;
617
+ const name = entry?.name;
618
+ if (typeof code !== "string" || code.length === 0) {
619
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: "buttons entries require a non-empty code" });
620
+ }
621
+ if (typeof name !== "string" || name.length === 0) {
622
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: "buttons entries require a non-empty name" });
623
+ }
624
+ if (seen.has(code)) {
625
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-duplicate", message: `buttons has duplicate code ${code}` });
626
+ }
627
+ seen.add(code);
628
+ if (BUTTON_SYSTEM_CODES.has(code.toUpperCase())) {
629
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-system-code", message: `buttons code ${code} is a reserved system code` });
630
+ }
631
+ if (typeof entry.useLocation !== "string" || !(entry.useLocation in BUTTON_USE_LOCATION.index)) {
632
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons useLocation must be LIST, FORM, or ROW (got ${String(entry.useLocation)})` });
633
+ }
634
+ if (typeof entry.bindAction !== "string" || !(entry.bindAction in BUTTON_BIND_ACTION.index)) {
635
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons bindAction must be a ButtonBindAction name (got ${String(entry.bindAction)})` });
636
+ }
637
+ if (typeof entry.operateType !== "string" || !(entry.operateType in BUTTON_OPERATE_TYPE.index)) {
638
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons operateType must be WITH_VIEW or WITH_OUT_VIEW (got ${String(entry.operateType)})` });
639
+ }
640
+ if (entry.showPermType !== undefined && (typeof entry.showPermType !== "string" || !(entry.showPermType in BUTTON_SHOW_PERM_TYPE.index))) {
641
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons showPermType must be a ButtonShowPermType name (got ${String(entry.showPermType)})` });
642
+ }
643
+ if (entry.actionConfig !== undefined && typeof entry.actionConfig !== "string" && (typeof entry.actionConfig !== "object" || entry.actionConfig === null || Array.isArray(entry.actionConfig))) {
644
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: "buttons actionConfig must be a JSON string or object" });
645
+ }
646
+ // Ticket 75 — the ADD_DATA/FORM target binding lives in actionConfig
647
+ // (schemaCodeVal + formListData + configureList); targetCode/targetObjCode
648
+ // are measured always-null and NOT consumed for these bind actions.
649
+ if (entry.bindAction === "ADD_DATA" || entry.bindAction === "FORM") {
650
+ if (entry.targetCode !== undefined || entry.targetObjCode !== undefined) {
651
+ throw new ToolRefusalError({
652
+ status: "refused",
653
+ blocker: "listview-button-target-ineffective",
654
+ message: `buttons ${String(entry.code)}:ADD_DATA/FORM 的目标绑定不走 targetCode/targetObjCode(实测恒 null、平台不消费)——它们只认 actionConfig(schemaCodeVal 目标模型对象 + formListData 目标表单 + configureList 数据赋值)。用高层参数 targetModel/targetForm/configureList 让工具展开,或直接给 actionConfig`,
655
+ });
656
+ }
657
+ if (entry.actionConfig !== undefined) {
658
+ // A JSON string is a valid wire form; for ADD_DATA/FORM it must
659
+ // still decode to the authoritative sub-shape (review finding:
660
+ // string actionConfig previously bypassed the key whitelist).
661
+ let actionConfigObject;
662
+ if (typeof entry.actionConfig === "object" && entry.actionConfig !== null) {
663
+ actionConfigObject = entry.actionConfig;
664
+ }
665
+ else if (typeof entry.actionConfig === "string") {
666
+ try {
667
+ const parsed = JSON.parse(entry.actionConfig);
668
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
669
+ actionConfigObject = parsed;
670
+ else
671
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${String(entry.code)}:ADD_DATA/FORM 的 actionConfig 字符串必须解析为对象(schemaCodeVal/formListData/configureList)` });
672
+ }
673
+ catch (error) {
674
+ if (error instanceof ToolRefusalError)
675
+ throw error;
676
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${String(entry.code)}:ADD_DATA/FORM 的 actionConfig 不是合法 JSON——解析失败(${error.message})` });
677
+ }
678
+ }
679
+ if (actionConfigObject) {
680
+ const unknownKeys = Object.keys(actionConfigObject).filter((key) => key !== "schemaCodeVal" && key !== "formListData" && key !== "configureList");
681
+ if (unknownKeys.length > 0) {
682
+ throw new ToolRefusalError({
683
+ status: "refused",
684
+ blocker: "listview-buttons-invalid",
685
+ message: `buttons ${String(entry.code)}:ADD_DATA/FORM 的 actionConfig 子形状只含 schemaCodeVal/formListData/configureList(live 抓包取证);未知键:${unknownKeys.join("、")}`,
686
+ });
687
+ }
688
+ }
689
+ }
690
+ }
691
+ validateButtonActionConfig(entry);
692
+ const usesExpansion = entry.targetModel !== undefined || entry.targetForm !== undefined || entry.configureList !== undefined;
693
+ if (usesExpansion && entry.actionConfig !== undefined) {
694
+ throw new ToolRefusalError({
695
+ status: "refused",
696
+ blocker: "listview-buttons-invalid",
697
+ message: `buttons ${String(entry.code)}:actionConfig 与 targetModel/targetForm/configureList 互斥——要么原样透传,要么让工具从读回元数据展开`,
698
+ });
699
+ }
700
+ if (entry.configureList !== undefined) {
701
+ for (const raw of entry.configureList) {
702
+ const assignment = (raw && typeof raw === "object" ? raw : {});
703
+ if (typeof assignment.currentDataItemId !== "string" || assignment.currentDataItemId.length === 0 || typeof assignment.targetDataItemId !== "string" || assignment.targetDataItemId.length === 0) {
704
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${String(entry.code)}:configureList 每条需要 currentDataItemId + targetDataItemId(字段 code)` });
705
+ }
706
+ if (assignment.isConst !== undefined && assignment.isConst !== "false") {
707
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${String(entry.code)}:configureList.isConst 仅 "false" 已取证(字符串);常量赋值的值键名未取证,未实现` });
708
+ }
709
+ if (assignment.OptionType !== undefined && assignment.OptionType !== "Eq") {
710
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${String(entry.code)}:configureList.OptionType(注意大写 O)仅 "Eq" 已取证` });
711
+ }
712
+ }
713
+ }
714
+ }
715
+ }
716
+ const options = args.options;
717
+ if (options !== undefined && typeof options !== "string") {
718
+ const refuseOptions = (message, unknown) => {
719
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-options-invalid", message, ...(unknown ? { unknown } : {}) });
720
+ };
721
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
722
+ refuseOptions("options must be a BizQueryOption JSON string or object");
723
+ }
724
+ const record = options;
725
+ const unknown = Object.keys(record).filter((key) => key !== "pagingSetting" && key !== "listViewModeSetting");
726
+ if (unknown.length > 0) {
727
+ refuseOptions(`options only supports pagingSetting and listViewModeSetting (rowColorSetting has no 8.6.26 class evidence); unknown keys: ${unknown.join(", ")}`, unknown);
728
+ }
729
+ const paging = record.pagingSetting;
730
+ if (paging !== undefined) {
731
+ if (typeof paging !== "object" || paging === null || Array.isArray(paging))
732
+ refuseOptions("options.pagingSetting must be an object");
733
+ const pagingRecord = paging;
734
+ const pagingUnknown = Object.keys(pagingRecord).filter((key) => key !== "pageSize" && key !== "showTotal");
735
+ if (pagingUnknown.length > 0)
736
+ refuseOptions(`options.pagingSetting only supports pageSize/showTotal; unknown keys: ${pagingUnknown.join(", ")}`, pagingUnknown);
737
+ const pageSize = pagingRecord.pageSize;
738
+ if (pageSize !== undefined && (typeof pageSize !== "number" || !Number.isInteger(pageSize) || pageSize <= 0))
739
+ refuseOptions("options.pagingSetting.pageSize must be a positive integer");
740
+ if (pagingRecord.showTotal !== undefined && typeof pagingRecord.showTotal !== "boolean")
741
+ refuseOptions("options.pagingSetting.showTotal must be a boolean");
742
+ }
743
+ const modeSetting = record.listViewModeSetting;
744
+ if (modeSetting !== undefined) {
745
+ if (typeof modeSetting !== "object" || modeSetting === null || Array.isArray(modeSetting))
746
+ refuseOptions("options.listViewModeSetting must be an object");
747
+ const modeRecord = modeSetting;
748
+ const modeUnknown = Object.keys(modeRecord).filter((key) => key !== "mode" && key !== "canSwitch");
749
+ if (modeUnknown.length > 0)
750
+ refuseOptions(`options.listViewModeSetting only supports mode/canSwitch; unknown keys: ${modeUnknown.join(", ")}`, modeUnknown);
751
+ if (modeRecord.mode !== undefined && (typeof modeRecord.mode !== "string" || modeRecord.mode.length === 0))
752
+ refuseOptions("options.listViewModeSetting.mode must be a non-empty string");
753
+ if (modeRecord.canSwitch !== undefined && typeof modeRecord.canSwitch !== "boolean")
754
+ refuseOptions("options.listViewModeSetting.canSwitch must be a boolean");
755
+ }
756
+ }
757
+ const presentationType = args.presentationType;
758
+ if (presentationType !== undefined && (typeof presentationType !== "string" || !QUERY_PRESENTATION_TYPE.names.includes(presentationType))) {
759
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-presentation-type-invalid", message: `presentationType must be a QueryPresentationType name (LIST/BOARD/CALENDAR/GANTT), got ${String(presentationType)}` });
760
+ }
761
+ const gantt = args.gantt;
762
+ if (gantt !== undefined) {
763
+ if (typeof gantt !== "object" || gantt === null || Array.isArray(gantt)) {
764
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-gantt-invalid", message: "gantt must be a BizQueryGanttModel object" });
765
+ }
766
+ const ganttRecord = gantt;
767
+ const stringKeys = ["startTimePropertyCode", "endTimePropertyCode", "progressPropertyCode", "titlePropertyCode", "preDependencyPropertyCode", "endDependencyPropertyCode", "levelPropertyCode", "milepostCode", "sortKey", "liableManCode", "statusCode", "defaultPrecision"];
768
+ for (const key of stringKeys) {
769
+ if (ganttRecord[key] !== undefined && typeof ganttRecord[key] !== "string") {
770
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-gantt-invalid", message: `gantt.${key} must be a string` });
771
+ }
772
+ }
773
+ if (ganttRecord.configJson !== undefined && typeof ganttRecord.configJson !== "string" && (typeof ganttRecord.configJson !== "object" || ganttRecord.configJson === null || Array.isArray(ganttRecord.configJson))) {
774
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-gantt-invalid", message: "gantt.configJson must be a JSON string or object" });
775
+ }
776
+ }
777
+ // BizQueryController.validateNotEmptyGantt (:1121-1136): GANTT requires the
778
+ // gantt model with startTime/endTime/title codes non-null.
779
+ if (presentationType === "GANTT") {
780
+ if (gantt === undefined || typeof gantt !== "object" || gantt === null) {
781
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-gantt-required", message: "presentationType GANTT requires a gantt model" });
782
+ }
783
+ const ganttRecord = gantt;
784
+ for (const key of ["startTimePropertyCode", "endTimePropertyCode", "titlePropertyCode"]) {
785
+ if (typeof ganttRecord[key] !== "string" || ganttRecord[key].length === 0) {
786
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-gantt-required", message: `presentationType GANTT requires gantt.${key}` });
787
+ }
788
+ }
789
+ }
790
+ const queryLayer = args.queryLayer;
791
+ if (queryLayer !== undefined) {
792
+ if (!Array.isArray(queryLayer)) {
793
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-query-layer-invalid", message: "queryLayer must be an array of BizQueryLayer objects" });
794
+ }
795
+ for (const entry of queryLayer) {
796
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
797
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-query-layer-invalid", message: "queryLayer entries must be objects" });
798
+ }
799
+ }
800
+ }
801
+ const multiTableHeaderConfig = args.multiTableHeaderConfig;
802
+ if (multiTableHeaderConfig !== undefined && typeof multiTableHeaderConfig !== "string") {
803
+ if (!Array.isArray(multiTableHeaderConfig)) {
804
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-multi-table-header-invalid", message: "multiTableHeaderConfig must be a JSON string or an array of HeaderConfig objects" });
805
+ }
806
+ for (const entry of multiTableHeaderConfig) {
807
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
808
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-multi-table-header-invalid", message: "multiTableHeaderConfig entries must be objects" });
809
+ }
810
+ }
811
+ }
812
+ }
813
+ export function findField(fieldRows, propertyCode) {
814
+ return fieldRows.find((row) => (row.code ?? row.propertyCode) === propertyCode);
815
+ }
816
+ /**
817
+ * cpdocs base3.0 parity (cp-query-view resolveField): a field reference may be
818
+ * either its propertyCode or its display name, so a caller can say「订单号」
819
+ * without first looking up `eq_ro_subject`. Code match wins; a name matching
820
+ * exactly one field resolves; a name matching several is ambiguous and refused;
821
+ * no match returns undefined (the caller then reports it as unknown).
822
+ */
823
+ export function resolveFieldRef(fieldRows, ref) {
824
+ const byCode = fieldRows.find((row) => (row.code ?? row.propertyCode) === ref);
825
+ if (byCode)
826
+ return { code: String(byCode.code ?? byCode.propertyCode), field: byCode };
827
+ const byName = fieldRows.filter((row) => String(row.name ?? "") === ref);
828
+ if (byName.length > 1)
829
+ throw new ToolRefusalError({
830
+ status: "refused",
831
+ blocker: "listview-field-ambiguous",
832
+ reference: ref,
833
+ candidates: byName.map((row) => String(row.code ?? row.propertyCode)),
834
+ message: `字段引用「${ref}」在该模型上匹配到多个字段(${byName.map((row) => String(row.code ?? row.propertyCode)).join("、")});请改用 propertyCode`,
835
+ });
836
+ if (byName.length === 1)
837
+ return { code: String(byName[0].code ?? byName[0].propertyCode), field: byName[0] };
838
+ return undefined;
839
+ }
840
+ export function fieldPropertyType(field) {
841
+ const raw = field?.type ?? field?.propertyType;
842
+ if (raw === undefined)
843
+ return undefined;
844
+ const parsed = Number(raw);
845
+ return Number.isNaN(parsed) ? undefined : parsed;
846
+ }
847
+ function collectUnknown(rows, kind) {
848
+ const unknown = rows.flatMap((row) => row.unknown);
849
+ if (unknown.length > 0) {
850
+ throw new ToolRefusalError({
851
+ status: "refused",
852
+ blocker: `listview-${kind}-unknown-field`,
853
+ message: `${kind} reference fields that are not published on this schema; pass propertyType to target a field outside the published metadata`,
854
+ unknown,
855
+ });
856
+ }
857
+ }
858
+ export function normalizeSorts(sorts, fieldRows, schemaCode) {
859
+ const built = sorts.map((sort, index) => {
860
+ const resolved = resolveFieldRef(fieldRows, sort.propertyCode);
861
+ const field = resolved?.field;
862
+ const propertyCode = resolved?.code ?? sort.propertyCode;
863
+ const propertyType = sort.propertyType ?? fieldPropertyType(field);
864
+ return {
865
+ unknown: propertyType === undefined ? [sort.propertyCode] : [],
866
+ row: {
867
+ name: field?.name ?? null,
868
+ propertyCode,
869
+ propertyType: propertyType ?? null,
870
+ schemaCode,
871
+ direction: SORT_DIRECTION.index[sort.direction] ?? null,
872
+ sortKey: sort.sortKey ?? index + 1,
873
+ clientType: "PC",
874
+ },
875
+ };
876
+ });
877
+ collectUnknown(built, "sorts");
878
+ return built.map((entry) => entry.row);
879
+ }
880
+ export function normalizeConditions(conditions, fieldRows, schemaCode) {
881
+ const built = conditions.map((condition) => {
882
+ const resolved = resolveFieldRef(fieldRows, condition.propertyCode);
883
+ const field = resolved?.field;
884
+ const propertyType = condition.propertyType ?? fieldPropertyType(field);
885
+ const row = {
886
+ name: field?.name ?? null,
887
+ propertyCode: resolved?.code ?? condition.propertyCode,
888
+ propertyType: propertyType ?? null,
889
+ schemaCode,
890
+ clientType: "PC",
891
+ };
892
+ // filterType is the condition's OPERATOR (QueryFilterType). Accept the
893
+ // caller's `operator`/`op` as aliases; validate the value fail-closed (the
894
+ // platform parse throws on an unknown name). Default it from the same table
895
+ // the platform uses when omitted, so a bare control row still names its
896
+ // comparison instead of silently reading back as "Like".
897
+ const requestedFilterType = condition.filterType ?? condition.operator ?? condition.op;
898
+ if (requestedFilterType !== undefined && !QUERY_FILTER_TYPES.has(String(requestedFilterType)))
899
+ throw new ToolRefusalError({
900
+ status: "refused",
901
+ blocker: "listview-condition-filtertype-invalid",
902
+ propertyCode: row.propertyCode,
903
+ value: requestedFilterType,
904
+ message: `conditions 的 filterType "${String(requestedFilterType)}" 不是平台 QueryFilterType;可选:${QUERY_FILTER_TYPE.join(" / ")}`,
905
+ });
906
+ row.filterType = requestedFilterType !== undefined ? String(requestedFilterType) : defaultQueryFilterType(propertyType);
907
+ if (condition.displayType !== undefined)
908
+ row.displayType = DISPLAY_TYPE.index[condition.displayType] ?? null;
909
+ if (condition.defaultValue !== undefined)
910
+ row.defaultValue = condition.defaultValue;
911
+ if (condition.options !== undefined)
912
+ row.options = encodeJsonColumn(condition.options);
913
+ if (condition.startValue !== undefined)
914
+ row.startValue = condition.startValue;
915
+ if (condition.endValue !== undefined)
916
+ row.endValue = condition.endValue;
917
+ if (condition.shortcutFilter !== undefined)
918
+ row.shortcutFilter = condition.shortcutFilter;
919
+ if (condition.choiceType !== undefined)
920
+ row.choiceType = CHOICE_TYPE.index[condition.choiceType] ?? null;
921
+ if (condition.dateType !== undefined)
922
+ row.dateType = condition.dateType;
923
+ if (condition.accurateSearch !== undefined)
924
+ row.accurateSearch = condition.accurateSearch;
925
+ if (condition.showNull !== undefined)
926
+ row.showNull = condition.showNull;
927
+ if (condition.includeSubData !== undefined)
928
+ row.includeSubData = condition.includeSubData;
929
+ if (condition.buildTreeBy !== undefined)
930
+ row.buildTreeBy = condition.buildTreeBy;
931
+ // A query condition IS a filter the list shows; the platform defaults an
932
+ // omitted `visible` to false, which hides every filter the caller asked
933
+ // for. Offer it shown unless the caller explicitly opts out.
934
+ row.visible = condition.visible ?? true;
935
+ return { unknown: propertyType === undefined ? [condition.propertyCode] : [], row };
936
+ });
937
+ collectUnknown(built, "conditions");
938
+ return built.map((entry) => entry.row);
939
+ }
940
+ /**
941
+ * Ticket 116 — designer-default icon/name per queryActionType. Evidence:
942
+ * native rows (h_biz_query_detail.queryConfigJson, e.g. dev_repair_record)
943
+ * carry an icon on EVERY action; a written row with icon:null persists green
944
+ * but renders the button without its icon — the same "data ok, render
945
+ * broken" class as tickets 109-112. Defaults apply ONLY when the caller
946
+ * omits the field; explicit values win.
947
+ */
948
+ const ACTION_RENDER_DEFAULTS = Object.freeze({
949
+ ADD: { icon: "plus", name: "新增" },
950
+ EDIT: { icon: "edit", name: "编辑" },
951
+ DELETE: { icon: "delete", name: "删除" },
952
+ IMPORT: { icon: "upload", name: "导入" },
953
+ EXPORT: { icon: "download", name: "导出" },
954
+ BATCH_PRINT: { icon: "plus", name: "批量打印" },
955
+ QR_CODE: { icon: "plus", name: "打印二维码" },
956
+ EDITOWNER: { icon: "plus", name: "修改拥有者" },
957
+ IMPORT_ATTACHMENT: { icon: "upload", name: "导入附件" },
958
+ EXPORT_ATTACHMENT: { icon: "download", name: "导出附件" },
959
+ BATCH_UPDATE: { icon: "plus", name: "批量修改" },
960
+ BATCH_SUBMIT: { icon: "plus", name: "批量提交" },
961
+ COPY: { icon: "copy", name: "复制" },
962
+ PRINT: { icon: "print", name: "打印" },
963
+ });
964
+ /**
965
+ * The platform's own default action set for a PC LIST view, measured live on
966
+ * 2026-09-17 from two un-configured views (eq_repair_order, a fresh model's
967
+ * default view) — identical 11 rows in both. `defaultActions:true` seeds a
968
+ * merge with this set so a view that already lost its buttons (by an earlier
969
+ * `actions` replace) gets 新增/导入/导出 back.
970
+ */
971
+ export const PLATFORM_DEFAULT_LIST_ACTIONS = Object.freeze([
972
+ { actionCode: "add", queryActionType: "ADD" },
973
+ { actionCode: "delete", queryActionType: "DELETE" },
974
+ { actionCode: "batch_update", queryActionType: "BATCH_UPDATE" },
975
+ { actionCode: "batch_submit", queryActionType: "BATCH_SUBMIT" },
976
+ { actionCode: "import", queryActionType: "IMPORT" },
977
+ { actionCode: "export", queryActionType: "EXPORT" },
978
+ { actionCode: "batch_print", queryActionType: "BATCH_PRINT" },
979
+ { actionCode: "qr_code", queryActionType: "QR_CODE" },
980
+ { actionCode: "editowner", queryActionType: "EDITOWNER" },
981
+ { actionCode: "import_attachment", queryActionType: "IMPORT_ATTACHMENT" },
982
+ { actionCode: "export_attachment", queryActionType: "EXPORT_ATTACHMENT" },
983
+ ]);
984
+ /**
985
+ * Merge caller actions into a base set by actionCode (existing rows keep their
986
+ * platform fields, caller fields win) and append the rest. Live 2026-09-17:
987
+ * `queryActions` is REPLACE, not merge — the platform deletes every stored
988
+ * action and re-adds the submitted list, so passing one action used to drop
989
+ * the built-in 新增/删除/导入/导出 and the form rendered with no Add button.
990
+ * Order = base order then appended; the platform resets sortKey to i+1.
991
+ */
992
+ export function mergeActions(base, incoming) {
993
+ const merged = [];
994
+ const indexOf = new Map();
995
+ for (const row of base) {
996
+ const key = String(row.actionCode ?? "");
997
+ if (key === "" || indexOf.has(key))
998
+ continue;
999
+ indexOf.set(key, merged.length);
1000
+ merged.push({ ...row });
1001
+ }
1002
+ for (const action of incoming) {
1003
+ const key = String(action.actionCode ?? "");
1004
+ const at = indexOf.get(key);
1005
+ if (at !== undefined)
1006
+ merged[at] = { ...merged[at], ...action };
1007
+ else {
1008
+ indexOf.set(key, merged.length);
1009
+ merged.push({ ...action });
1010
+ }
1011
+ }
1012
+ return merged;
1013
+ }
1014
+ export function normalizeActions(actions, schemaCode) {
1015
+ return actions.map((action, index) => {
1016
+ // Ticket 116: render-fidelity defaults for the evidenced action families.
1017
+ const renderDefault = ACTION_RENDER_DEFAULTS[action.queryActionType];
1018
+ const row = {
1019
+ actionCode: action.actionCode,
1020
+ queryActionType: QUERY_ACTION_TYPE.index[action.queryActionType] ?? null,
1021
+ sortKey: action.sortKey ?? index + 1,
1022
+ clientType: "PC",
1023
+ schemaCode,
1024
+ };
1025
+ row.name = action.name ?? renderDefault?.name;
1026
+ row.icon = action.icon ?? renderDefault?.icon;
1027
+ if (row.name === undefined || row.icon === undefined) {
1028
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-actions-invalid", message: `action ${action.actionCode} (${action.queryActionType}) requires name and icon for rendering` });
1029
+ }
1030
+ if (action.systemAction !== undefined)
1031
+ row.systemAction = action.systemAction;
1032
+ if (action.serviceCode !== undefined)
1033
+ row.serviceCode = action.serviceCode;
1034
+ if (action.serviceMethod !== undefined)
1035
+ row.serviceMethod = action.serviceMethod;
1036
+ if (action.customService !== undefined)
1037
+ row.customService = action.customService;
1038
+ if (action.associationCode !== undefined)
1039
+ row.associationCode = action.associationCode;
1040
+ if (action.associationType !== undefined)
1041
+ row.associationType = QUERY_ACTION_RELATIVE.index[action.associationType] ?? null;
1042
+ return row;
1043
+ });
1044
+ }
1045
+ /**
1046
+ * `buttons` -> the `customButtonJson` update-body field (a JSON array string).
1047
+ * `BizButtonFacadeImpl.addBizButtonList(String,...)` (:112-137) forces
1048
+ * schemaCode/triggerType(QUERY)/triggerCode(view code), sets sortKey to the
1049
+ * array index, rejects duplicate `schemaCode.code` and reserved system codes.
1050
+ * Enum fields are sent as their `IStringEnum` names (no `@JsonValue`).
1051
+ */
1052
+ export function normalizeButtons(buttons, schemaCode, triggerCode) {
1053
+ return buttons.map((button, index) => {
1054
+ const row = {
1055
+ schemaCode,
1056
+ name: button.name,
1057
+ code: button.code,
1058
+ triggerType: "QUERY",
1059
+ triggerCode,
1060
+ useLocation: button.useLocation,
1061
+ bindAction: button.bindAction,
1062
+ operateType: button.operateType,
1063
+ sortKey: index,
1064
+ };
1065
+ if (button.showPermCode !== undefined)
1066
+ row.showPermCode = button.showPermCode;
1067
+ if (button.showPermType !== undefined)
1068
+ row.showPermType = button.showPermType;
1069
+ if (button.hint !== undefined)
1070
+ row.hint = button.hint;
1071
+ if (button.targetCode !== undefined)
1072
+ row.targetCode = button.targetCode;
1073
+ if (button.targetObjCode !== undefined)
1074
+ row.targetObjCode = button.targetObjCode;
1075
+ if (button.description !== undefined)
1076
+ row.description = button.description;
1077
+ // BizButtonModel.actionConfig is a non-null String on the render path. The
1078
+ // list runtime does `"string"==typeof a ? JSON.parse(a) : a`, then for
1079
+ // FORM/ADD_DATA/FLOW it ITERATES `a.configureList` — so both a null
1080
+ // (configureList on null) and a config without the array
1081
+ // ("undefined is not iterable") blank the click. Emit an empty
1082
+ // configureList for those three bind actions and `{}` otherwise.
1083
+ const needsConfigureList = button.bindAction === "FLOW" || button.bindAction === "ADD_DATA" || button.bindAction === "FORM";
1084
+ row.actionConfig = encodeJsonColumn(button.actionConfig ?? (needsConfigureList ? { configureList: [] } : {}));
1085
+ return row;
1086
+ });
1087
+ }
1088
+ /** condition options / presentation / gantt configJson decode; policy: non-strings null-coalesce, unparseable passes through. */
1089
+ export function parseConditionOptions(value) {
1090
+ return decodeJsonColumn(value, JSON_COLUMN_NULL_COALESCE);
1091
+ }
1092
+ /**
1093
+ * `presentation` replaces the whole queryPresentation (saveBizQueryDetail
1094
+ * removes every presentation whose clientType matches before re-adding), so a
1095
+ * provided object sends exactly its fields; clientType is forced to PC (the
1096
+ * controller overwrites it). JSON-ish fields are stringified for the wire.
1097
+ */
1098
+ export function normalizePresentation(presentation) {
1099
+ const row = { clientType: "PC" };
1100
+ for (const key of PRESENTATION_FIELDS) {
1101
+ const value = presentation[key];
1102
+ if (value !== undefined)
1103
+ row[key] = encodeJsonColumn(value);
1104
+ }
1105
+ return row;
1106
+ }
1107
+ export function decodePresentation(row) {
1108
+ const out = { clientType: row.clientType ?? null };
1109
+ for (const key of PRESENTATION_FIELDS)
1110
+ out[key] = parseConditionOptions(row[key]);
1111
+ return out;
1112
+ }
1113
+ export const GANTT_FIELDS = ["startTimePropertyCode", "endTimePropertyCode", "progressPropertyCode", "titlePropertyCode", "preDependencyPropertyCode", "endDependencyPropertyCode", "levelPropertyCode", "milepostCode", "sortKey", "configJson", "liableManCode", "statusCode", "defaultPrecision"];
1114
+ /**
1115
+ * `gantt` -> the query/update body `queryGanttModel`. The tool supplies
1116
+ * schemaCode; queryId is set by BizQueryDetailServiceImpl.saveBizQueryDetail
1117
+ * (:179-181). configJson is a JSON string field (object stringified).
1118
+ */
1119
+ export function normalizeGantt(gantt, schemaCode) {
1120
+ const row = { schemaCode };
1121
+ for (const key of GANTT_FIELDS) {
1122
+ const value = gantt[key];
1123
+ if (value !== undefined)
1124
+ row[key] = key === "configJson" ? encodeJsonColumn(value) : value;
1125
+ }
1126
+ return row;
1127
+ }
1128
+ export function decodeGantt(row) {
1129
+ const out = { queryId: row.queryId ?? null, schemaCode: row.schemaCode ?? null };
1130
+ for (const key of GANTT_FIELDS)
1131
+ out[key] = key === "configJson" ? parseConditionOptions(row[key]) : row[key] ?? null;
1132
+ return out;
1133
+ }
1134
+ export function decodeSortRow(row) {
1135
+ return {
1136
+ propertyCode: row.propertyCode ?? null,
1137
+ propertyType: row.propertyType ?? null,
1138
+ direction: decodeWireEnum(SORT_DIRECTION, row.direction),
1139
+ sortKey: row.sortKey ?? null,
1140
+ clientType: row.clientType ?? null,
1141
+ };
1142
+ }
1143
+ export function decodeConditionRow(row) {
1144
+ return {
1145
+ propertyCode: row.propertyCode ?? null,
1146
+ propertyType: row.propertyType ?? null,
1147
+ displayType: decodeWireEnum(DISPLAY_TYPE, row.displayType),
1148
+ // The condition's operator (QueryFilterType); it serializes as a NAME STRING
1149
+ // (no @JsonValue), so pass it through verbatim (live proof: filterType:"Like").
1150
+ filterType: row.filterType ?? null,
1151
+ defaultValue: row.defaultValue ?? null,
1152
+ options: parseConditionOptions(row.options),
1153
+ startValue: row.startValue ?? null,
1154
+ endValue: row.endValue ?? null,
1155
+ shortcutFilter: row.shortcutFilter ?? null,
1156
+ choiceType: decodeWireEnum(CHOICE_TYPE, row.choiceType),
1157
+ dateType: row.dateType ?? null,
1158
+ accurateSearch: row.accurateSearch ?? null,
1159
+ showNull: row.showNull ?? null,
1160
+ includeSubData: row.includeSubData ?? null,
1161
+ buildTreeBy: row.buildTreeBy ?? null,
1162
+ visible: row.visible ?? null,
1163
+ };
1164
+ }
1165
+ export function readArray(model, key) {
1166
+ const value = model[key];
1167
+ return Array.isArray(value) ? value : [];
1168
+ }
1169
+ export function decodeActionRow(row) {
1170
+ const type = row.queryActionType;
1171
+ const association = row.associationType ?? row.queryActionRelativeType;
1172
+ return {
1173
+ actionCode: row.actionCode ?? null,
1174
+ name: row.name ?? null,
1175
+ icon: row.icon ?? null,
1176
+ queryActionType: decodeWireEnum(QUERY_ACTION_TYPE, type),
1177
+ systemAction: row.systemAction ?? null,
1178
+ serviceCode: row.serviceCode ?? null,
1179
+ serviceMethod: row.serviceMethod ?? null,
1180
+ customService: row.customService ?? null,
1181
+ associationCode: row.associationCode ?? row.relativeCode ?? null,
1182
+ associationType: decodeWireEnum(QUERY_ACTION_RELATIVE, association),
1183
+ sortKey: row.sortKey ?? null,
1184
+ clientType: row.clientType ?? null,
1185
+ };
1186
+ }
1187
+ export function decodeButtonRow(row) {
1188
+ return {
1189
+ schemaCode: row.schemaCode ?? null,
1190
+ name: row.name ?? null,
1191
+ code: row.code ?? null,
1192
+ triggerCode: row.triggerCode ?? null,
1193
+ triggerType: decodeWireEnum(BUTTON_TRIGGER_TYPE, row.triggerType),
1194
+ showPermCode: row.showPermCode ?? null,
1195
+ showPermType: decodeWireEnum(BUTTON_SHOW_PERM_TYPE, row.showPermType),
1196
+ hint: row.hint ?? null,
1197
+ targetCode: row.targetCode ?? null,
1198
+ targetObjCode: row.targetObjCode ?? null,
1199
+ useLocation: decodeWireEnum(BUTTON_USE_LOCATION, row.useLocation),
1200
+ bindAction: decodeWireEnum(BUTTON_BIND_ACTION, row.bindAction),
1201
+ operateType: decodeWireEnum(BUTTON_OPERATE_TYPE, row.operateType),
1202
+ description: row.description ?? null,
1203
+ actionConfig: parseConditionOptions(row.actionConfig),
1204
+ sortKey: row.sortKey ?? null,
1205
+ };
1206
+ }
1207
+ //# sourceMappingURL=list-view-shapes.js.map