@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,578 @@
1
+ import { z } from "zod";
2
+ import { ENDPOINTS } from "../endpoints.js";
3
+ import { capabilityArg, environmentArg } from "../args.js";
4
+ import { governedRead } from "../read.js";
5
+ import { writeTool } from "../write.js";
6
+ import { encodeJsonColumn } from "../json-column.js";
7
+ import { defineTool, ToolRefusalError } from "../tool-types.js";
8
+ import { wireCall, wireRows, WireError } from "../wire.js";
9
+ import { resolveModelTreeNode } from "./model.js";
10
+ import { SYSTEM_FIELD_CODES } from "../field-families.js";
11
+ import { LIST_VIEW_LANE_CONTRACT } from "./list-view-contract.js";
12
+ import { assertListViewConfigureShape, decodeActionRow, decodeButtonRow, decodeConditionRow, decodeGantt, decodePresentation, decodeSortRow, listViewActionArg, listViewButtonArg, listViewConditionArg, listViewGanttArg, listViewMultiTableHeaderArg, listViewOptionsArg, listViewPresentationArg, listViewQueryLayerArg, listViewSortArg, mergeActions, PLATFORM_DEFAULT_LIST_ACTIONS, normalizeActions, normalizeButtons, normalizeConditions, normalizeGantt, normalizePresentation, normalizeSorts, parseConditionOptions, presentationTypeArg, readArray, resolveFieldRef, buttonsRenderCompat, columnsRenderCompat, } from "./list-view-shapes.js";
13
+ // Ticket 53: every shape (enum/zod/encode/decode/lint) lives once in
14
+ // ./list-view-shapes.ts and is re-exported for existing import paths.
15
+ export { assertListViewConfigureShape } from "./list-view-shapes.js";
16
+ const LIST_VIEW_LANE = LIST_VIEW_LANE_CONTRACT.lane;
17
+ const LIST_VIEW_TOOLS = LIST_VIEW_LANE_CONTRACT.tools;
18
+ function fieldOptions(options) {
19
+ if (typeof options === "string") {
20
+ try {
21
+ return JSON.parse(options);
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ return options && typeof options === "object" ? options : {};
28
+ }
29
+ /**
30
+ * QueryRuntimeController.parseNumber keys its formatting off displayFormat with
31
+ * no case 0 (valid values start at 1): 1=integer, 2=1dp, 3=2dp, 11=3dp,
32
+ * 12=4dp, 27..30=5..8dp; currency 7/15/16/31..34; percent 4/5/6/13/14.
33
+ */
34
+ export function numericDisplayFormat(options) {
35
+ const opts = fieldOptions(options);
36
+ const format = String(opts.format ?? "decimal");
37
+ const scale = Number(opts.scale ?? 0) || 0;
38
+ const decimalByScale = { 0: 1, 1: 2, 2: 3, 3: 11, 4: 12, 5: 27, 6: 28, 7: 29, 8: 30 };
39
+ const currencyByScale = { 2: 7, 3: 15, 4: 16, 5: 31, 6: 32, 7: 33, 8: 34 };
40
+ const percentByScale = { 0: 4, 1: 5, 2: 6, 3: 13, 4: 14 };
41
+ if (format === "currency" || format === "rmb" || format === "¥")
42
+ return currencyByScale[scale] ?? 7;
43
+ if (format === "percent" || format === "percentage")
44
+ return percentByScale[scale] ?? 6;
45
+ if (format === "dollar" || format === "$")
46
+ return 8;
47
+ return decimalByScale[scale] ?? 3;
48
+ }
49
+ /** QueryRuntimeController.parseDate: 1=yyyy-MM-dd, 2=datetime, 3=minute, 4=yyyy-MM. */
50
+ export function dateDisplayFormat(options) {
51
+ const format = String(fieldOptions(options).format ?? "YYYY-MM-DD");
52
+ if (/HH:mm:ss/i.test(format))
53
+ return 2;
54
+ if (/HH:mm$/i.test(format))
55
+ return 3;
56
+ if (/^YYYY-MM$/i.test(format))
57
+ return 4;
58
+ return 1;
59
+ }
60
+ function columnDisplayFormat(type, options) {
61
+ if (type === 2)
62
+ return numericDisplayFormat(options);
63
+ if (type === 3)
64
+ return dateDisplayFormat(options);
65
+ return null;
66
+ }
67
+ /**
68
+ * Ticket 75 — expand high-level button params (targetModel/targetForm/
69
+ * configureList) into the authoritative actionConfig shape from readback:
70
+ * - schemaCodeVal: the target model object (app-tree node id, app metadata,
71
+ * published derived from the model having published fields — field.publish
72
+ * publishes the whole schema);
73
+ * - formListData: the target default form (bizsheet/get; only sheetType 0
74
+ * 默认表单 binding has live evidence);
75
+ * - configureList: data assignments with the inline field-object copies
76
+ * (array-wrapped, as captured) and the evidenced literals
77
+ * isConst:"false" / OptionType:"Eq".
78
+ * Fails closed (listview-button-target-unresolved) BEFORE the query/update
79
+ * write when the model node, form, or any assignment field is not found.
80
+ */
81
+ async function expandButtonActionConfigs(session, viewSchemaCode, buttons) {
82
+ const appRows = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data);
83
+ const appNameOf = (appCode) => {
84
+ const row = appRows.find((candidate) => String(candidate.appCode ?? candidate.code ?? "") === appCode);
85
+ return typeof row?.appName === "string" ? row.appName : "";
86
+ };
87
+ const publishedFieldsOf = async (schemaCode) => wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
88
+ const expanded = [];
89
+ for (const button of buttons) {
90
+ // FLOW: the list runtime reads actionConfig.schemaCodeVal.code and
91
+ // actionConfig.WorkflowListData[0].workflowCode and ITERATES
92
+ // actionConfig.configureList (decompiled 8.6.34 7926.js case 17). Expand
93
+ // the high-level targetCode (workflow code) into that shape.
94
+ if (button.bindAction === "FLOW" && button.actionConfig === undefined && typeof button.targetCode === "string" && button.targetCode) {
95
+ expanded.push({
96
+ ...button,
97
+ actionConfig: {
98
+ schemaCodeVal: { code: viewSchemaCode, value: viewSchemaCode },
99
+ WorkflowListData: [{ workflowCode: button.targetCode }],
100
+ configureList: [],
101
+ },
102
+ });
103
+ continue;
104
+ }
105
+ if (button.targetModel === undefined && button.targetForm === undefined && button.configureList === undefined) {
106
+ expanded.push(button);
107
+ continue;
108
+ }
109
+ const targetModel = button.targetModel ?? viewSchemaCode;
110
+ const unresolved = (what) => {
111
+ throw new ToolRefusalError({
112
+ status: "refused",
113
+ blocker: "listview-button-target-unresolved",
114
+ schemaCode: viewSchemaCode,
115
+ targetModel,
116
+ message: `按钮目标解析失败(零副作用):${what}。用 discover_environment / form.get 确认目标模型与默认表单存在并已发布后再配置。`,
117
+ });
118
+ };
119
+ // 1. Target model node (AppFunction id via the app trees — ticket 68 fact).
120
+ const resolvedNode = await resolveModelTreeNode(session, targetModel);
121
+ if (!resolvedNode.node)
122
+ unresolved(`模型 ${targetModel} 在任何应用树中都找不到节点`);
123
+ const node = resolvedNode.node;
124
+ const appCode = resolvedNode.applicationCode;
125
+ // 2. Published face of the target model (drives schemaCodeVal.published
126
+ // and the configureList target-field inline copies).
127
+ const targetFieldRows = await publishedFieldsOf(targetModel);
128
+ if (targetFieldRows.length === 0)
129
+ unresolved(`模型 ${targetModel} 没有已发布字段(schemaCodeVal.published 无法为真)`);
130
+ const schemaCodeVal = {
131
+ code: targetModel,
132
+ id: node.id ?? null,
133
+ name: typeof node.name === "string" && node.name ? node.name : targetModel,
134
+ type: "BizModel",
135
+ appCode,
136
+ appName: appNameOf(appCode),
137
+ published: true,
138
+ };
139
+ // 3. Target default form (formListData; sheetType 0 默认表单 only — the
140
+ // sole live-evidenced binding).
141
+ const formCode = button.targetForm ?? targetModel;
142
+ let sheet = null;
143
+ try {
144
+ sheet = ((await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: targetModel, sheetCode: formCode } })).data ?? null);
145
+ }
146
+ catch (error) {
147
+ if (!(error instanceof WireError && error.errcode === 302024))
148
+ throw error;
149
+ }
150
+ if (!sheet || !sheet.id)
151
+ unresolved(`模型 ${targetModel} 的表单 ${formCode} 不存在(formListData 无从展开)`);
152
+ const resolvedSheet = sheet;
153
+ if (resolvedSheet.sheetType !== undefined && resolvedSheet.sheetType !== null && Number(resolvedSheet.sheetType) !== 0)
154
+ unresolved(`表单 ${formCode} 的 sheetType=${String(resolvedSheet.sheetType)} 非默认表单——仅 sheetType 0(默认表单)绑定有 live 取证`);
155
+ const formListData = [
156
+ {
157
+ code: resolvedSheet.code ?? formCode,
158
+ schemaCode: targetModel,
159
+ id: resolvedSheet.id,
160
+ name: resolvedSheet.name ?? formCode,
161
+ sheetType: 0,
162
+ sheetTypeName: "默认表单",
163
+ published: resolvedSheet.published === true,
164
+ },
165
+ ];
166
+ // 4. configureList — inline field copies array-wrapped (captured shape).
167
+ const configureList = [];
168
+ if (button.configureList !== undefined) {
169
+ const sourceFieldRows = await publishedFieldsOf(viewSchemaCode);
170
+ for (const assignment of button.configureList) {
171
+ const sourceRow = sourceFieldRows.find((row) => String(row.code ?? row.propertyCode ?? "") === assignment.currentDataItemId);
172
+ if (!sourceRow)
173
+ unresolved(`赋值源字段 ${viewSchemaCode}.${assignment.currentDataItemId} 不在发布面上`);
174
+ const targetRow = targetFieldRows.find((row) => String(row.code ?? row.propertyCode ?? "") === assignment.targetDataItemId);
175
+ if (!targetRow)
176
+ unresolved(`赋值目标字段 ${targetModel}.${assignment.targetDataItemId} 不在发布面上`);
177
+ configureList.push({
178
+ currentDataItemId: assignment.currentDataItemId,
179
+ targetDataItemId: assignment.targetDataItemId,
180
+ targetDataItem: [{ ...targetRow, schemaCode: targetModel }],
181
+ currentDataItem: [{ ...sourceRow, schemaCode: viewSchemaCode }],
182
+ isConst: "false",
183
+ OptionType: "Eq",
184
+ });
185
+ }
186
+ }
187
+ const { targetModel: _tm, targetForm: _tf, configureList: _cl, ...rest } = button;
188
+ expanded.push({ ...rest, actionConfig: { schemaCodeVal, formListData, configureList } });
189
+ }
190
+ return expanded;
191
+ }
192
+ export function filterDefaultSystemColumns(columns, hideDefaultSystemFields) {
193
+ if (!hideDefaultSystemFields)
194
+ return [...columns];
195
+ return columns.filter((column) => typeof column.propertyCode !== "string" || !SYSTEM_FIELD_CODES.has(column.propertyCode));
196
+ }
197
+ export function registerListViewLane(tools) {
198
+ tools.push(defineTool({
199
+ name: "listview.get",
200
+ layer: "low",
201
+ kind: "read",
202
+ lane: LIST_VIEW_LANE,
203
+ scope: LIST_VIEW_TOOLS["listview.get"].scope,
204
+ description: LIST_VIEW_TOOLS["listview.get"].description,
205
+ inputSchema: {
206
+ environment: environmentArg,
207
+ schemaCode: z.string().min(1),
208
+ code: z.string().optional().describe("视图 code;省略时用默认视图(=schemaCode)"),
209
+ full: z.boolean().optional().describe("返回 query/get 原始报文(大体积);默认返回列/排序/条件/动作/按钮的紧凑清单"),
210
+ capability: capabilityArg,
211
+ },
212
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
213
+ const code = args.code ?? args.schemaCode;
214
+ const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
215
+ const model = (got.data ?? null);
216
+ if (model === null || !model.id)
217
+ return { schemaCode: args.schemaCode, code, present: false, note: "视图不存在(query/get 空或 302025);先 listview.create 或 listview.configure" };
218
+ if (args.full === true)
219
+ return { schemaCode: args.schemaCode, code, present: true, data: model };
220
+ const columns = readArray(model, "queryColumns").map((column) => ({
221
+ propertyCode: column.propertyCode,
222
+ propertyType: column.propertyType,
223
+ displayFormat: column.displayFormat,
224
+ visible: column.visible,
225
+ }));
226
+ // Direction is decoded to the enum NAME (ASC/DESC) — the same shape
227
+ // `listview.configure` reads back, so a get→configure round-trip does not
228
+ // surprise the caller with a raw numeric index (dogfood R2 round17b).
229
+ const sorts = readArray(model, "querySorts")
230
+ .map(decodeSortRow)
231
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
232
+ // Round 16 (bench67 feedback): `conditionCount` alone cannot verify a
233
+ // declared filter (e.g. "status != 已归档") — expose the decoded rows
234
+ // (same projector listview.configure reads back) so acceptance can see
235
+ // the property/operator/default the view actually carries.
236
+ const conditions = readArray(model, "queryConditions")
237
+ .map(decodeConditionRow)
238
+ .sort((a, b) => String(a.propertyCode ?? "").localeCompare(String(b.propertyCode ?? "")));
239
+ return {
240
+ schemaCode: args.schemaCode,
241
+ code,
242
+ present: true,
243
+ name: model.name ?? null,
244
+ published: model.published === true,
245
+ presentationType: model.queryPresentationType ?? null,
246
+ columnCount: columns.length,
247
+ columns,
248
+ sorts,
249
+ conditionCount: conditions.length,
250
+ conditions,
251
+ actionCodes: readArray(model, "queryActions").map((action) => action.actionCode).filter((value) => value !== undefined),
252
+ buttonCount: readArray(model, "customButtons").length,
253
+ options: model.options !== undefined && model.options !== null ? parseConditionOptions(model.options) : null,
254
+ // Round 16 (bench65 feedback): the query row's `published=false` is NOT
255
+ // an "inactive view" signal — a list view has no draft/publish
256
+ // lifecycle; this column is a design-time placeholder. The view is
257
+ // live as soon as it is saved. Say so, so the flag is not misread.
258
+ hint: "published 是设计态占位(列表视图无草稿/发布生命周期),false 不代表视图未生效;列/排序/条件/动作/按钮是否落成以 columns/sorts/conditionCount/actionCodes 与 field.list 为准。完整报文加 full:true。NUMERICAL 列的 displayFormat 是平台数值格式枚举(decimal scale2→3、currency→7、percent→6…),不是字段 scale;列的小数位以字段 options.scale 为准(用 field.list 读回)",
259
+ };
260
+ }),
261
+ }));
262
+ tools.push(defineTool({
263
+ name: "listview.list",
264
+ layer: "low",
265
+ kind: "read",
266
+ lane: LIST_VIEW_LANE,
267
+ scope: LIST_VIEW_TOOLS["listview.list"].scope,
268
+ description: LIST_VIEW_TOOLS["listview.list"].description,
269
+ inputSchema: { environment: environmentArg, schemaCode: z.string().min(1), capability: capabilityArg },
270
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
271
+ const rows = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode: args.schemaCode, clientType: "PC" } })).data);
272
+ return {
273
+ schemaCode: args.schemaCode,
274
+ count: rows.length,
275
+ views: rows.map((row) => ({ code: row.code ?? null, name: row.name ?? null, type: row.type ?? null, published: row.published === true })),
276
+ hint: "默认视图 code = schemaCode;读单个视图用 listview.get",
277
+ };
278
+ }),
279
+ }));
280
+ writeTool(tools, "listview.create", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.create"].scope, LIST_VIEW_TOOLS["listview.create"].description, { schemaCode: z.string().min(1), name: z.string().min(1), code: z.string().optional() }, async (session, args) => {
281
+ // Ticket 81: read the header FIRST — a same-code retry short-circuits
282
+ // with the existing view and sends ZERO create wires (the live
283
+ // post-timeout retry otherwise answers 303002 编码重复).
284
+ const existing = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
285
+ if (existing.data != null) {
286
+ return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existing.data } };
287
+ }
288
+ try {
289
+ const performed = await wireCall(session, {
290
+ path: ENDPOINTS.listQueryCreate,
291
+ method: "POST",
292
+ body: { schemaCode: args.schemaCode, name: args.name, code: args.code, type: "LIST", showOnPc: false, published: false },
293
+ });
294
+ const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
295
+ return { performed: { errcode: performed.errcode }, readback: { header: readback.data } };
296
+ }
297
+ catch (error) {
298
+ // Race path: another writer created the view between the pre-check and
299
+ // the create — the platform's 303002 is absence of work, not failure.
300
+ if (!(error instanceof WireError && error.kind === "business" && error.errcode === 303002))
301
+ throw error;
302
+ const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
303
+ if (!header.data)
304
+ throw error;
305
+ return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: header.data } };
306
+ }
307
+ });
308
+ writeTool(tools, "listview.configure", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.configure"].scope, LIST_VIEW_TOOLS["listview.configure"].description, {
309
+ schemaCode: z.string().min(1),
310
+ code: z.string().optional(),
311
+ fieldCodes: z.array(z.string()).min(1).describe("要展示的字段:可给 propertyCode 或字段显示名(唯一匹配时自动解析,歧义或不存在则拒绝,不静默跳过)"),
312
+ hideDefaultSystemFields: z.boolean().optional().describe("是否隐藏平台默认系统列(createdTime/creater/name/sequenceNo 等);平台 update 按提交列集合决定,不能靠 visible:false"),
313
+ replaceColumns: z.boolean().optional().describe("把 fieldCodes 当作列集合的权威全集:未列出的已有列(含系统列)全部移除,最终列 = fieldCodes(按给定顺序)。默认 false 为增量(保留已有列)。系统列与业务列一视同仁——想要 createdTime/sequenceNo 就把它写进 fieldCodes"),
314
+ sorts: z.array(listViewSortArg).optional().describe("querySorts; omitted leaves the existing sorts untouched, provided replaces them"),
315
+ conditions: z.array(listViewConditionArg).optional().describe("queryConditions; omitted leaves the existing conditions untouched, provided replaces them"),
316
+ actions: z.array(listViewActionArg).optional().describe("queryActions; omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (existing platform defaults like 新增/删除/导入/导出 are preserved, a matching actionCode is updated). Use replaceActions:true for the authoritative full set"),
317
+ replaceActions: z.boolean().optional().describe("把 actions 当作权威全集:未列出的已有动作(含平台内置的 add/删除/导入/导出/修改拥有者)全部移除。默认 false 为合并(保留内置动作,按 actionCode 更新/追加)"),
318
+ defaultActions: z.boolean().optional().describe("先并入平台对 PC LIST 视图的默认动作集(add/delete/batch_update/batch_submit/import/export/batch_print/qr_code/editowner/import_attachment/export_attachment,常用于把被替换掉的内置按钮找回),再叠加 actions"),
319
+ presentation: listViewPresentationArg.optional(),
320
+ buttons: z
321
+ .array(listViewButtonArg)
322
+ .optional()
323
+ .describe("customButtons; sent as the customButtonJson JSON array string; omitted leaves the existing buttons untouched, provided replaces every button under the QUERY trigger (schema + child tables)"),
324
+ options: listViewOptionsArg.optional(),
325
+ presentationType: presentationTypeArg.optional(),
326
+ gantt: listViewGanttArg.optional(),
327
+ queryLayer: listViewQueryLayerArg.optional(),
328
+ multiTableHeaderConfig: listViewMultiTableHeaderArg.optional(),
329
+ }, async (session, args) => {
330
+ const code = args.code ?? args.schemaCode;
331
+ const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
332
+ const model = (got.data ?? {});
333
+ if (!model.id)
334
+ throw new WireError("business", `list view ${code} not found on ${args.schemaCode}`, 302025);
335
+ const columns = Array.isArray(model.queryColumns) ? ([...model.queryColumns]) : [];
336
+ const fieldRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
337
+ let sortKey = columns.reduce((max, c) => Math.max(max, Number(c.sortKey) || 0), 0);
338
+ const added = [];
339
+ const updated = [];
340
+ // A `fieldCodes` entry may be a propertyCode OR a display name (cpdocs
341
+ // base3.0 parity); an unresolvable reference is REFUSED, never silently
342
+ // skipped — a silently dropped column reported as success is the
343
+ // "declared a column, nothing appeared" trap.
344
+ const unknownRefs = args.fieldCodes.filter((ref) => resolveFieldRef(fieldRows, ref) === undefined);
345
+ if (unknownRefs.length > 0)
346
+ throw new ToolRefusalError({
347
+ status: "refused",
348
+ blocker: "listview-columns-unknown-field",
349
+ unknown: unknownRefs,
350
+ message: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段(可给 propertyCode 或字段名):${unknownRefs.join("、")};先 field.create + field.publish,或修正引用`,
351
+ });
352
+ // replaceColumns: the caller's fieldCodes ARE the view. Start from an
353
+ // empty set so system columns fall away unless listed — a system field
354
+ // can still be shown by naming its code in fieldCodes (createdTime etc.
355
+ // are published fields too), so the caller controls the exact set.
356
+ if (args.replaceColumns === true)
357
+ columns.length = 0;
358
+ for (const ref of args.fieldCodes) {
359
+ const fieldCode = resolveFieldRef(fieldRows, ref).code;
360
+ const field = fieldRows.find((row) => (row.code ?? row.propertyCode) === fieldCode);
361
+ const type = Number(field.type ?? field.propertyType);
362
+ const displayFormat = columnDisplayFormat(type, field.options);
363
+ const existing = columns.find((c) => c.propertyCode === fieldCode);
364
+ if (existing) {
365
+ existing.name = field.name;
366
+ existing.propertyType = type;
367
+ existing.displayFormat = displayFormat;
368
+ existing.visible = true;
369
+ updated.push(fieldCode);
370
+ continue;
371
+ }
372
+ sortKey += 1;
373
+ columns.push({ name: field.name, propertyCode: fieldCode, propertyType: type, isDefaultProperty: false, displayFormat, width: "160", sortKey, sumType: null, relativeSchemaCode: null, childColumns: null, clientType: "PC", schemaAlias: "a_1", propertyAlias: fieldCode, syncDefaultFormat: false, displayFields: null, queryLevel: 1, options: null, visible: true, virtualField: false, enableEncrypt: false, autoFilled: false });
374
+ added.push(fieldCode);
375
+ }
376
+ // replaceColumns already reduced the set to fieldCodes; hideDefaultSystemFields
377
+ // additionally drops any system column that slipped in (an echoed system
378
+ // column in incremental mode, or one the caller listed only to name its
379
+ // display). Both act by OMITTING columns from the submitted PC collection:
380
+ // the platform forces every submitted column visible=true
381
+ // (BizQueryController.reSetQueryColumns:1000-1027), so visible:false is
382
+ // not an option.
383
+ const configuredColumns = filterDefaultSystemColumns(columns, args.hideDefaultSystemFields === true);
384
+ const body = { ...model, queryColumns: configuredColumns };
385
+ if (args.presentation !== undefined)
386
+ body.queryPresentation = normalizePresentation(args.presentation);
387
+ if (args.sorts !== undefined)
388
+ body.querySorts = normalizeSorts(args.sorts, fieldRows, args.schemaCode);
389
+ // Keep the normalized requested conditions so the post-write readback can
390
+ // compare what was ASKED for against what the platform stored, rather than
391
+ // reporting a write-side `ok` that silently lost fields (dogfood R2).
392
+ let requestedConditionKeys;
393
+ let requestedConditionSpec;
394
+ if (args.conditions !== undefined) {
395
+ const normalizedConditions = normalizeConditions(args.conditions, fieldRows, args.schemaCode);
396
+ body.queryConditions = normalizedConditions;
397
+ requestedConditionKeys = normalizedConditions.map((row) => String(row.propertyCode ?? ""));
398
+ requestedConditionSpec = normalizedConditions.map((row) => ({ propertyCode: String(row.propertyCode ?? ""), filterType: row.filterType }));
399
+ }
400
+ // saveBizQueryDetail removes every action whose clientType matches the
401
+ // update and re-adds the submitted list, so an omitted `actions` must echo
402
+ // the read-face effective set (stored + platform-injected) rather than
403
+ // sending an empty list, which would delete the view's actions.
404
+ //
405
+ // Live 2026-09-17: because it is replace-then-re-add, passing ONE action
406
+ // used to delete the built-in 新增/删除/导入/导出 and the list rendered
407
+ // with no Add button. `actions` now MERGES into the effective set by
408
+ // actionCode; replaceActions:true restores the authoritative-set
409
+ // behaviour, and defaultActions:true re-seeds the platform's own default
410
+ // set (the repair path for a view that already lost its buttons).
411
+ if (args.actions === undefined) {
412
+ body.queryActions = readArray(model, "queryActions");
413
+ }
414
+ else {
415
+ const normalized = normalizeActions(args.actions, args.schemaCode);
416
+ if (args.replaceActions === true) {
417
+ body.queryActions = normalized;
418
+ }
419
+ else {
420
+ const seeded = args.defaultActions === true
421
+ ? normalizeActions(PLATFORM_DEFAULT_LIST_ACTIONS.map((action) => ({ ...action })), args.schemaCode)
422
+ : [];
423
+ body.queryActions = mergeActions([...seeded, ...readArray(model, "queryActions")], normalized);
424
+ }
425
+ }
426
+ // BizButtonFacadeImpl.addBizButtonList runs only when the update body
427
+ // carries a JSON array in customButtonJson; it then deletes every button
428
+ // under the QUERY trigger for the schema (+ its child tables) and re-adds
429
+ // the submitted list. An omitted `buttons` therefore leaves the stored
430
+ // buttons untouched (no customButtonJson key), while `buttons: []` clears.
431
+ // Ticket 75: high-level targetModel/targetForm/configureList params are
432
+ // expanded into the authoritative actionConfig shape from readback
433
+ // BEFORE normalizeButtons (targetCode/targetObjCode never emitted for
434
+ // ADD_DATA/FORM — measured ineffective, see dry-lint).
435
+ if (args.buttons !== undefined) {
436
+ const expandedButtons = await expandButtonActionConfigs(session, args.schemaCode, args.buttons);
437
+ body.customButtonJson = JSON.stringify(normalizeButtons(expandedButtons, args.schemaCode, code));
438
+ }
439
+ else {
440
+ delete body.customButtonJson;
441
+ }
442
+ // BizQueryModel.options is a single String column. BizQueryController.update
443
+ // sets publish=TRUE before updateBizQuery, so after the first configure the
444
+ // store's publish is TRUE and only the name/options-diff branch
445
+ // (ModelManagementFacadeImpl:1966-1973) persists options — an omitted body
446
+ // key (null) would overwrite a non-null stored value. `{...model}` already
447
+ // echoes the read-face options, so only a provided value replaces it.
448
+ if (args.options !== undefined)
449
+ body.options = encodeJsonColumn(args.options);
450
+ // QueryPresentationType is a header enum always assignable by
451
+ // updateBizQuery (:1955, not gated), so an omitted body key (null) would
452
+ // overwrite a stored type; `{...model}` echoes the read-face value. GANTT
453
+ // requires queryGanttModel (controller validateNotEmptyGantt :1121-1136).
454
+ if (args.presentationType !== undefined)
455
+ body.queryPresentationType = args.presentationType;
456
+ // saveBizQueryDetail only swaps queryGantt when non-null (:178-183), so an
457
+ // omitted gantt leaves the stored one untouched.
458
+ if (args.gantt !== undefined)
459
+ body.queryGanttModel = normalizeGantt(args.gantt, args.schemaCode);
460
+ // saveBizQueryDetail assigns queryLayer and multiTableHeaderConfig
461
+ // unconditionally (:184-186); the read face always carries them
462
+ // (BizQueryFacadeImpl:344-345), so an omitted arg relies on the
463
+ // `{...model}` echo while a provided value replaces it.
464
+ if (args.queryLayer !== undefined)
465
+ body.queryLayer = args.queryLayer;
466
+ if (args.multiTableHeaderConfig !== undefined) {
467
+ body.multiTableHeaderConfig = encodeJsonColumn(args.multiTableHeaderConfig);
468
+ }
469
+ const performed = await wireCall(session, { path: ENDPOINTS.listQueryUpdate, method: "POST", body });
470
+ // BUG-11 (live, docker+source verified): the controller's /query/update
471
+ // forces publish=TRUE and the engine's updateBizQuery SKIPS the entity
472
+ // update block once the STORED view is published
473
+ // (ModelManagementFacadeImpl:1998-2010), so a body-driven
474
+ // queryPresentationType is silently dropped for a published view (the
475
+ // default view is published). The designer switches view type through
476
+ // /query/update_header (a plain header update, no publish gate) — drive
477
+ // the same path whenever presentationType is provided.
478
+ if (args.presentationType !== undefined) {
479
+ const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
480
+ const headerModel = (header.data ?? {});
481
+ await wireCall(session, {
482
+ path: ENDPOINTS.listQueryUpdateHeader,
483
+ method: "POST",
484
+ body: { ...headerModel, code, schemaCode: args.schemaCode, queryPresentationType: args.presentationType },
485
+ });
486
+ }
487
+ const after = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
488
+ const afterModel = (after.data ?? {});
489
+ const afterCols = readArray(afterModel, "queryColumns").map((c) => ({
490
+ propertyCode: c.propertyCode,
491
+ propertyType: c.propertyType,
492
+ displayFormat: c.displayFormat ?? null,
493
+ visible: c.visible ?? null,
494
+ sumType: c.sumType ?? null,
495
+ }));
496
+ const sorts = readArray(afterModel, "querySorts")
497
+ .map(decodeSortRow)
498
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
499
+ const conditions = readArray(afterModel, "queryConditions")
500
+ .map(decodeConditionRow)
501
+ .sort((a, b) => String(a.propertyCode ?? "").localeCompare(String(b.propertyCode ?? "")));
502
+ const readback = { code, added, updated, columns: afterCols, sorts, conditions };
503
+ // Post-write semantic readback (dogfood R2): the platform MODELS a list
504
+ // condition as a filter CONTROL definition, so a requested condition whose
505
+ // propertyCode does not survive the round-trip was lost. Surface that as
506
+ // `conditionReadbackStatus` (not a business `ok`) so a caller never
507
+ // mistakes the success envelope for the filter having landed.
508
+ if (requestedConditionKeys !== undefined) {
509
+ const storedKeys = new Set(conditions.map((condition) => String(condition.propertyCode ?? "")));
510
+ const missingConditions = requestedConditionKeys.filter((key) => !storedKeys.has(key));
511
+ // Also verify the OPERATOR (filterType) survived — a propertyCode that
512
+ // lands with the WRONG comparison is just as wrong as one that is
513
+ // dropped (dogfood R2 follow-up).
514
+ const storedByCode = new Map(conditions.map((condition) => [String(condition.propertyCode ?? ""), condition]));
515
+ const filterTypeMismatches = (requestedConditionSpec ?? [])
516
+ .filter((spec) => storedByCode.has(spec.propertyCode))
517
+ .filter((spec) => String(storedByCode.get(spec.propertyCode)?.filterType ?? "") !== String(spec.filterType ?? ""))
518
+ .map((spec) => ({ propertyCode: spec.propertyCode, requested: spec.filterType ?? null, stored: storedByCode.get(spec.propertyCode)?.filterType ?? null }));
519
+ const equivalent = missingConditions.length === 0 && filterTypeMismatches.length === 0;
520
+ readback.conditionReadback = {
521
+ status: equivalent ? "equivalent" : "listview-condition-readback-mismatch",
522
+ requested: requestedConditionKeys,
523
+ stored: [...storedKeys],
524
+ // Operator-level summary so "operator + value" acceptance needs no
525
+ // `--full`/artifact fetch (dogfood R2 round17b).
526
+ operators: (requestedConditionSpec ?? []).map((spec) => ({
527
+ propertyCode: spec.propertyCode,
528
+ requested: spec.filterType ?? null,
529
+ stored: storedByCode.get(spec.propertyCode)?.filterType ?? null,
530
+ defaultValue: storedByCode.get(spec.propertyCode)?.defaultValue ?? null,
531
+ })),
532
+ ...(missingConditions.length > 0 ? { missing: missingConditions } : {}),
533
+ ...(filterTypeMismatches.length > 0 ? { filterTypeMismatches } : {}),
534
+ };
535
+ }
536
+ readback.columnsRenderCompat = columnsRenderCompat(readArray(afterModel, "queryColumns"));
537
+ if (args.actions !== undefined) {
538
+ readback.actions = readArray(afterModel, "queryActions")
539
+ .map(decodeActionRow)
540
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
541
+ }
542
+ if (afterModel.queryPresentation && typeof afterModel.queryPresentation === "object") {
543
+ readback.presentation = decodePresentation(afterModel.queryPresentation);
544
+ }
545
+ if (Array.isArray(afterModel.customButtons)) {
546
+ const buttons = readArray(afterModel, "customButtons");
547
+ readback.buttons = buttons
548
+ .map(decodeButtonRow)
549
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
550
+ readback.buttonsRenderCompat = buttonsRenderCompat(buttons);
551
+ }
552
+ if (afterModel.options !== undefined && afterModel.options !== null) {
553
+ readback.options = parseConditionOptions(afterModel.options);
554
+ }
555
+ if (afterModel.queryPresentationType !== undefined && afterModel.queryPresentationType !== null) {
556
+ readback.presentationType = afterModel.queryPresentationType;
557
+ }
558
+ if (afterModel.queryGanttModel && typeof afterModel.queryGanttModel === "object") {
559
+ readback.gantt = decodeGantt(afterModel.queryGanttModel);
560
+ }
561
+ if (afterModel.queryLayer !== undefined && afterModel.queryLayer !== null) {
562
+ readback.queryLayer = afterModel.queryLayer;
563
+ }
564
+ if (afterModel.multiTableHeaderConfig !== undefined && afterModel.multiTableHeaderConfig !== null && afterModel.multiTableHeaderConfig !== "") {
565
+ readback.multiTableHeaderConfig = parseConditionOptions(afterModel.multiTableHeaderConfig);
566
+ }
567
+ return { performed: { errcode: performed.errcode }, readback };
568
+ }, assertListViewConfigureShape);
569
+ writeTool(tools, "listview.delete", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.delete"].scope, LIST_VIEW_TOOLS["listview.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
570
+ const before = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
571
+ if (before.data == null)
572
+ return { performed: { skipped: "already-absent" }, readback: { absent: true, header: null } };
573
+ const performed = await wireCall(session, { path: ENDPOINTS.listQueryDelete, method: "POST", query: { schemaCode: args.schemaCode, code: args.code } });
574
+ const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
575
+ return { performed: { errcode: performed.errcode }, readback: { absent: readback.data == null, header: readback.data ?? null } };
576
+ });
577
+ }
578
+ //# sourceMappingURL=list-view.js.map