@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,374 @@
1
+ import { z } from "zod";
2
+ import { ENDPOINTS } from "../endpoints.js";
3
+ import { fieldChangeWireBody, fieldUpdateWireBody, prepareFieldChange, prepareFieldUpdate } from "../field-change.js";
4
+ import { choicesArg, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, subFieldArg, SYSTEM_FIELD_CODES, } from "../field-families.js";
5
+ import { capabilityArg, environmentArg } from "../args.js";
6
+ import { governedRead } from "../read.js";
7
+ import { writeTool } from "../write.js";
8
+ import { decodeJsonColumn, decodeJsonObjectColumn, JSON_COLUMN_PASSTHROUGH } from "../json-column.js";
9
+ import { syncFormConditionDataRules } from "./datarule.js";
10
+ import { FORM_CONDITION_OPTION_KEYS } from "./datarule-protocol.js";
11
+ import { defineTool, ToolRefusalError } from "../tool-types.js";
12
+ import { wireCall, wireRows, WireError } from "../wire.js";
13
+ import { FIELD_LANE_CONTRACT } from "./field-contract.js";
14
+ const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
15
+ const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
16
+ /** Parse a raw options JSON string for readback; policy: non-strings and unparseable strings pass through verbatim. */
17
+ function parseFieldOptionsForReadback(options) {
18
+ return decodeJsonColumn(options, JSON_COLUMN_PASSTHROUGH);
19
+ }
20
+ /**
21
+ * `name_i18n` is a JSON locale map. When every locale value equals the display
22
+ * `name` there is no translation to convey, so echoing the map on every field of
23
+ * every `field.list` readback is pure noise (~45 B/field; a 16-field model ≈700 B,
24
+ * ~27% of that read). Omit it then; a real translation (a locale differing from
25
+ * `name`, or a locale-specific value) is preserved verbatim.
26
+ */
27
+ function redundantNameI18n(nameI18n, name) {
28
+ if (typeof nameI18n !== "string" || typeof name !== "string")
29
+ return false;
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(nameI18n);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
38
+ return false;
39
+ const values = Object.values(parsed);
40
+ return values.length > 0 && values.every((value) => value === name);
41
+ }
42
+ /**
43
+ * Ticket 60: a supplied `name_i18n` must be an object, or a string that parses
44
+ * to an object (never an array / bare display string). Runs before the approval
45
+ * gate so a malformed repair never burns a token or reaches the wire.
46
+ */
47
+ function assertNameI18nArg(nameI18n) {
48
+ if (nameI18n === undefined)
49
+ return;
50
+ const refuse = (reason) => {
51
+ throw new ToolRefusalError({ status: "refused", blocker: "field-name-i18n-invalid", message: `field.update 的 name_i18n 必须是对象或可解析为对象的 JSON 串:${reason}` });
52
+ };
53
+ if (typeof nameI18n !== "string") {
54
+ if (typeof nameI18n !== "object" || nameI18n === null || Array.isArray(nameI18n))
55
+ refuse("收到非对象值");
56
+ return;
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(nameI18n);
61
+ }
62
+ catch {
63
+ refuse("字符串不是合法 JSON");
64
+ }
65
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
66
+ refuse("JSON 解析结果不是对象");
67
+ }
68
+ export function registerFieldLane(tools) {
69
+ tools.push(defineTool({
70
+ name: "field.list",
71
+ layer: "low",
72
+ kind: "read",
73
+ lane: FIELD_LANE,
74
+ scope: FIELD_TOOLS["field.list"].scope,
75
+ description: FIELD_TOOLS["field.list"].description,
76
+ inputSchema: { environment: environmentArg, schemaCode: z.string(), published: z.boolean().default(false), code: z.string().optional(), fields: z.array(z.string()).optional(), all: z.boolean().optional().describe("默认只返业务字段(过滤 SYSTEM_FIELD_CODES 的 13 个平台字段:id/name/sequenceNo/createdTime/creater/sequenceStatus/…);true 连系统字段一起返"), capability: capabilityArg },
77
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
78
+ const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: args.published ? "true" : "false" } })).data);
79
+ const mapped = rows.map((row) => {
80
+ const type = Number(row.type ?? row.propertyType);
81
+ const typeLabel = fieldTypeLabel(row.type ?? row.propertyType);
82
+ const options = row.options !== undefined && row.options !== null ? parseFieldOptionsForReadback(row.options) : undefined;
83
+ // Round 4 (ADR 0005): read-side annotation for stored scale-less
84
+ // NUMERICAL landmines (the platform accepted them for years).
85
+ const scaleWarning = numericalScaleWarning(type, options);
86
+ return {
87
+ code: row.code ?? row.propertyCode,
88
+ name: row.name,
89
+ ...(redundantNameI18n(row.name_i18n, row.name) ? {} : typeof row.name_i18n === "string" ? { name_i18n: row.name_i18n } : {}),
90
+ type: row.type ?? row.propertyType,
91
+ ...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
92
+ ...(row.subSchema !== undefined && row.subSchema !== null ? { subSchema: row.subSchema } : {}),
93
+ ...(row.relativeCode ? { relativeCode: row.relativeCode } : {}),
94
+ ...(typeof row.relativePropertyCode === "string" && row.relativePropertyCode ? { relativePropertyCode: row.relativePropertyCode } : {}),
95
+ ...(options !== undefined ? { options } : {}),
96
+ ...(scaleWarning !== undefined ? { shapeWarning: scaleWarning } : {}),
97
+ };
98
+ });
99
+ // Projection: ask for one field (`--code`) or a named set (`--fields`) so a
100
+ // caller can verify one field's `options` without pulling the whole list
101
+ // (which token-diets to {count,sha256} past 4 KB) through an artifact.
102
+ const selector = [args.code, ...(args.fields ?? [])].filter((entry) => typeof entry === "string" && entry.length > 0);
103
+ const selected = selector.length > 0 ? mapped.filter((field) => selector.includes(String(field.code))) : mapped;
104
+ // Round 10: the default face is BUSINESS fields only — 13 of 21 fields on a
105
+ // typical model are platform system fields, so a caller asking "what fields
106
+ // does this model have" paid for noise it then had to filter. `all:true`
107
+ // restores the previous full list (uses the SAME SYSTEM_FIELD_CODES set the
108
+ // list-view/form scaffolds already filter with).
109
+ const visible = args.all === true ? selected : selected.filter((field) => !SYSTEM_FIELD_CODES.has(String(field.code)));
110
+ return { schemaCode: args.schemaCode, published: args.published === true, ...(selector.length > 0 ? { selected: selector } : {}), ...(args.all !== true ? { businessOnly: true } : {}), fields: visible };
111
+ }, {
112
+ // Round 4: the default diet reduced the whole list to a count, so agents
113
+ // fetched the 28 KB artifact just to read field codes (benchmark: 4×28 KB).
114
+ // The useful compact form is the inventory: code/type/name (+ any shape
115
+ // warning). `options` stays out — use code/fields selector for that.
116
+ target: `field.list:${args.schemaCode}:${args.published === true ? "published" : "draft"}`,
117
+ compact: (result) => {
118
+ const fields = Array.isArray(result.fields) ? result.fields : [];
119
+ return {
120
+ schemaCode: result.schemaCode,
121
+ published: result.published,
122
+ ...(result.businessOnly === true ? { businessOnly: true } : {}),
123
+ ...(result.selected !== undefined ? { selected: result.selected } : {}),
124
+ fields: fields.map((field) => {
125
+ // Round 4 (live bench26): the diet dropped `options` entirely, so an
126
+ // agent could not tell an authored form condition existed and either
127
+ // re-added it or paid an `--full`/artifact read to check. Signal just
128
+ // the non-empty condition KEY NAMES (a few bytes), values stay out.
129
+ const options = (field.options ?? {});
130
+ const conditions = Object.keys(FORM_CONDITION_OPTION_KEYS).filter((key) => {
131
+ const value = options[key];
132
+ return key === "readonlyFormula" ? value === true : typeof value === "string" && value.trim() !== "";
133
+ });
134
+ return {
135
+ code: field.code,
136
+ type: field.type,
137
+ ...(field.typeName !== undefined ? { typeName: field.typeName } : {}),
138
+ ...(field.name !== undefined ? { name: field.name } : {}),
139
+ ...(conditions.length > 0 ? { conditions } : {}),
140
+ ...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
141
+ };
142
+ }),
143
+ hint: "字段详情(options/subSchema)用 omc call field.list --args '{\"schemaCode\":\"...\",\"published\":true,\"code\":\"<field>\"}'",
144
+ };
145
+ },
146
+ }),
147
+ }));
148
+ writeTool(tools, "field.create", FIELD_LANE, FIELD_TOOLS["field.create"].scope, FIELD_TOOLS["field.create"].description, {
149
+ schemaCode: z.string().min(1),
150
+ code: z.string().min(1),
151
+ name: z.string().min(1),
152
+ propertyType: propertyTypeArg,
153
+ options: fieldOptionsArg.optional(),
154
+ relativeCode: relativeCodeArg,
155
+ choices: choicesArg,
156
+ subFields: subFieldArg,
157
+ propertyEmpty: z.boolean().optional().describe("数据模型层是否声明该字段不可为空;默认 false(可为空)。必填/只读请在表单设计与流程节点权限配置,不要在模型层设置——平台会据此把流程各节点强制成可见+可编辑+必填"),
158
+ }, async (session, args) => {
159
+ const draft = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data);
160
+ const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
161
+ if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code))
162
+ throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST` });
163
+ // One seam owns field-change normalization, every reproduced gate, and the
164
+ // wire body (src/field-change.ts), so configure_model and intent share it.
165
+ const prepared = prepareFieldChange(args);
166
+ const performed = await wireCall(session, { path: ENDPOINTS.fieldCreate, method: "POST", body: fieldChangeWireBody(args, prepared) });
167
+ const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
168
+ const rows = wireRows(readback.data);
169
+ return {
170
+ performed: { errcode: performed.errcode },
171
+ readback: { draftFieldPresent: rows.some((row) => (row.code ?? row.propertyCode) === args.code) },
172
+ };
173
+ });
174
+ writeTool(tools, "field.update", FIELD_LANE, FIELD_TOOLS["field.update"].scope, FIELD_TOOLS["field.update"].description, {
175
+ schemaCode: z.string().min(1),
176
+ code: z.string().min(1),
177
+ name: z.string().min(1).optional().describe("new display name; also rewrites name_i18n to a valid JSON object string"),
178
+ name_i18n: z.union([z.record(z.string(), z.unknown()), z.string()]).optional().describe("explicit i18n object (or JSON object string) to store verbatim; must parse to an object"),
179
+ options: fieldOptionsArg.optional(),
180
+ choices: choicesArg,
181
+ relativeCode: relativeCodeArg,
182
+ subFields: subFieldArg,
183
+ propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)"),
184
+ }, async (session, args) => {
185
+ const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
186
+ let rows = wireRows(listed.data);
187
+ if (!rows.some((row) => (row.code ?? row.propertyCode) === args.code)) {
188
+ const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
189
+ rows = wireRows(published.data);
190
+ }
191
+ const record = rows.find((row) => (row.code ?? row.propertyCode) === args.code);
192
+ if (!record)
193
+ throw new WireError("business", `field ${args.code} not found on ${args.schemaCode}`, 301001);
194
+ const update = {
195
+ schemaCode: args.schemaCode,
196
+ code: args.code,
197
+ base: record,
198
+ ...(args.name !== undefined ? { name: args.name } : {}),
199
+ ...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
200
+ ...(args.options !== undefined ? { options: args.options } : {}),
201
+ ...(args.choices !== undefined ? { choices: args.choices } : {}),
202
+ ...(args.relativeCode !== undefined ? { relativeCode: args.relativeCode } : {}),
203
+ ...(args.subFields !== undefined ? { subFields: args.subFields } : {}),
204
+ ...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
205
+ };
206
+ const prepared = prepareFieldUpdate(update);
207
+ const performed = await wireCall(session, { path: ENDPOINTS.fieldUpdate, method: "POST", body: fieldUpdateWireBody(update, prepared) });
208
+ const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
209
+ const after = wireRows(readback.data).find((row) => (row.code ?? row.propertyCode) === args.code);
210
+ // Round 4 (B): a form-interaction condition must exist on BOTH faces to be
211
+ // complete — field options (portal runtime) AND a bizdatarule type3/4/5
212
+ // row (designer design-time Set/Unset). field.update historically wrote
213
+ // only options, so CLI-authored conditions showed "Unset" in the designer.
214
+ // Pair the changed condition key(s) into the rule projection; a sync
215
+ // failure never hides the field write (it is surfaced as formConditionSync.error).
216
+ let formConditionSync;
217
+ let formConditionSyncError;
218
+ if (args.options !== undefined && !prepared.isChildTable) {
219
+ const previous = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
220
+ try {
221
+ const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, previous, prepared.options);
222
+ if (sync.created.length + sync.updated.length + sync.deleted.length + sync.skipped.length > 0)
223
+ formConditionSync = sync;
224
+ }
225
+ catch (error) {
226
+ formConditionSyncError = error instanceof Error ? error.message : String(error);
227
+ }
228
+ }
229
+ // Round 4: echo the EFFECTIVE stored options (parsed), not just presence.
230
+ // An agent setting readonlyFormula/requiredFormula/scale had no way to
231
+ // confirm the write landed — the readback only named the field. Field
232
+ // options are the form-interaction contract (ADR 0005), so read them back.
233
+ const storedOptions = after !== undefined ? parseFieldOptionsForReadback(after.options) : undefined;
234
+ return {
235
+ performed: { errcode: performed.errcode },
236
+ readback: {
237
+ fieldPresent: after !== undefined,
238
+ ...(after?.name !== undefined ? { name: after.name } : {}),
239
+ ...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
240
+ ...(after?.type !== undefined || after?.propertyType !== undefined ? { type: after?.type ?? after?.propertyType } : {}),
241
+ ...(storedOptions !== undefined && storedOptions !== null ? { options: storedOptions } : {}),
242
+ ...(formConditionSync !== undefined ? { formConditionSync } : {}),
243
+ ...(formConditionSyncError !== undefined ? { formConditionSync: { error: formConditionSyncError } } : {}),
244
+ },
245
+ };
246
+ }, (args) => assertNameI18nArg(args.name_i18n));
247
+ writeTool(tools, "field.publish", FIELD_LANE, FIELD_TOOLS["field.publish"].scope, FIELD_TOOLS["field.publish"].description, {
248
+ schemaCode: z.string().min(1),
249
+ code: z.string().min(1).optional().describe("optional single field to verify on the published face"),
250
+ codes: z.array(z.string().min(1)).optional().describe("optional list of fields to verify; publishing is model-scoped, do NOT call once per field"),
251
+ }, async (session, args) => {
252
+ // bizproperty/publish is MODEL-SCOPED: one call publishes every draft
253
+ // field of the schema. `code`/`codes` only select which fields to verify
254
+ // on the published face after the single publish; omit them to publish
255
+ // and read back all codes (the batch path).
256
+ //
257
+ // Ticket 81 idempotent retry: with explicit code/codes, read the
258
+ // published face FIRST (a retry after a timeout hits the wire read
259
+ // cache — zero extra wires) and skip the publish POST entirely when
260
+ // every requested field is already published. A genuinely missing
261
+ // field still drives the publish.
262
+ const requestedCodes = [...(args.code ? [args.code] : []), ...(args.codes ?? [])];
263
+ const codeOf = (row) => String(row.code ?? row.propertyCode ?? "");
264
+ // cpdocs base3.0 parity (cp-bizproperty verifyPlan): presence on the
265
+ // published LIST face is proven later, against a map of each field's
266
+ // pre-publish DRAFT shape (name + type). The draft face must be
267
+ // snapshotted BEFORE the publish because a model-scoped publish can leave
268
+ // it empty; the draft row is the caller-visible expectation.
269
+ const draftSnapshot = new Map(wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data)
270
+ .map((row) => [codeOf(row), { name: String(row.name ?? ""), propertyType: Number(row.type ?? row.propertyType) }]));
271
+ const verifyFieldDetails = async (targets) => {
272
+ const bounded = targets.slice(0, 25);
273
+ const mismatches = [];
274
+ for (const code of bounded) {
275
+ const expected = draftSnapshot.get(code);
276
+ if (!expected)
277
+ continue;
278
+ let detail;
279
+ try {
280
+ detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode: args.schemaCode, propertyCode: code } })).data;
281
+ }
282
+ catch {
283
+ // The detail endpoint is optional across builds: report the
284
+ // verification as unavailable rather than failing the publish.
285
+ return { mode: "unavailable", verified: false, note: "bizproperty/get 不可用,字段详情未核对(仅列表面存在性)" };
286
+ }
287
+ const actual = { name: String(detail.name ?? ""), propertyType: Number(detail.propertyType ?? detail.type) };
288
+ if (actual.name !== expected.name || actual.propertyType !== expected.propertyType)
289
+ mismatches.push({ code, expected, actual });
290
+ }
291
+ return { mode: "detail", verified: true, checked: bounded.length, ok: mismatches.length === 0, ...(mismatches.length > 0 ? { mismatches } : {}) };
292
+ };
293
+ const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
294
+ const prePublished = wireRows(preRead.data).map(codeOf).filter((code) => code.length > 0);
295
+ if (requestedCodes.length > 0 && requestedCodes.every((code) => prePublished.includes(code))) {
296
+ return {
297
+ performed: { skipped: "already-published", scope: "model" },
298
+ readback: {
299
+ publishedCodes: prePublished,
300
+ ...(args.code ? { publishedFieldPresent: true } : {}),
301
+ ...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, true])) } : {}),
302
+ allRequestedPresent: true,
303
+ verification: await verifyFieldDetails(requestedCodes),
304
+ },
305
+ };
306
+ }
307
+ // Ticket 79 live-fidelity (docker log evidence, 2026-09-13 21:43-21:45,
308
+ // cloud8_launcher log_error.log + nginx access timings): under DB
309
+ // connection-pool contention a model publish can fail with a transient
310
+ // `表单:<t>_<schema>不存在列:<col>` 50000 — the addIndexes/updateColumns
311
+ // assert reads the serializer view while the failed attempt's DDL
312
+ // auto-commits, so the state SELF-HEALS across retries (live: three
313
+ // consecutive attempts each healed one column, the fourth succeeded).
314
+ // Retry that exact signature up to 3 attempts; anything else rethrows.
315
+ let performed;
316
+ // Backoff matches the observed healing cadence on the small env (live:
317
+ // attempts ~25s/73s apart each reconciled one column).
318
+ const backoffMs = [2000, 5000];
319
+ for (let attempt = 1; attempt <= 3; attempt++) {
320
+ try {
321
+ performed = await wireCall(session, {
322
+ path: ENDPOINTS.fieldPublish,
323
+ method: "POST",
324
+ body: { bizSchemaCode: args.schemaCode, sendMessage: false },
325
+ });
326
+ break;
327
+ }
328
+ catch (error) {
329
+ const transientColumnSync = error instanceof WireError && error.kind === "business" && error.errcode === 50000 && error.message.includes("不存在列");
330
+ if (!(transientColumnSync && attempt < 3))
331
+ throw error;
332
+ await new Promise((resolve) => setTimeout(resolve, backoffMs[attempt - 1] ?? 5000));
333
+ }
334
+ }
335
+ const publishOutcome = performed;
336
+ const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
337
+ const publishedCodes = wireRows(readback.data).map(codeOf).filter((code) => code.length > 0);
338
+ const requested = [...(args.code ? [args.code] : []), ...(args.codes ?? [])];
339
+ const present = requested.filter((code) => publishedCodes.includes(code));
340
+ const targets = (requested.length > 0 ? requested : [...draftSnapshot.keys()]).filter((code) => publishedCodes.includes(code));
341
+ const verification = await verifyFieldDetails(targets);
342
+ const mismatches = verification.mismatches;
343
+ if (mismatches !== undefined)
344
+ throw new ToolRefusalError({
345
+ status: "refused",
346
+ blocker: "field-publish-readback-mismatch",
347
+ performed: { errcode: publishOutcome.errcode, scope: "model" },
348
+ mismatches,
349
+ message: `字段已发布,但发布后详情回读与草稿面不一致(${mismatches.map((item) => String(item.code)).join("、")}):这是元数据与预期形状相异的信号,先核对再决定后续;不要当作成功`,
350
+ });
351
+ return {
352
+ performed: { errcode: publishOutcome.errcode, scope: "model" },
353
+ readback: {
354
+ publishedCodes,
355
+ ...(args.code ? { publishedFieldPresent: publishedCodes.includes(args.code) } : {}),
356
+ ...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, publishedCodes.includes(code)])) } : {}),
357
+ ...(requested.length > 0 ? { allRequestedPresent: present.length === requested.length } : {}),
358
+ verification,
359
+ },
360
+ };
361
+ });
362
+ writeTool(tools, "field.delete", FIELD_LANE, FIELD_TOOLS["field.delete"].scope, FIELD_TOOLS["field.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
363
+ const performed = await wireCall(session, {
364
+ path: ENDPOINTS.fieldForceDelete,
365
+ method: "POST",
366
+ query: { bizSchemaCode: args.schemaCode, bizPropertyCode: args.code },
367
+ });
368
+ const draft = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
369
+ const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
370
+ const gone = (value) => !wireRows(value).some((row) => (row.code ?? row.propertyCode) === args.code);
371
+ return { performed: { errcode: performed.errcode }, readback: { absentDraft: gone(draft.data), absentPublished: gone(published.data) } };
372
+ });
373
+ }
374
+ //# sourceMappingURL=field.js.map
@@ -0,0 +1,97 @@
1
+ import { assertDescriptionCarries, assertLaneContract, behaviorBinding, CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION } from "./contract-support.js";
2
+ import { LANES } from "./lane-ids.js";
3
+ /**
4
+ * form-publish-chain wire facts (live-proven). The draft→publish promotion,
5
+ * the empty-form scaffold, and the relation-mapping carry are the single
6
+ * source for the handler descriptions and the coverage declarations.
7
+ */
8
+ export const FORM_WIRE = Object.freeze({
9
+ draftBody: "bizsheet/update: reads the current sheet, merges explicit draftViewJson/draftAttributesJson or preserves the existing draft; an empty fresh default form is scaffolded from the model's published fields",
10
+ scaffold: "designer-standard default-form scaffold: Title(203, useDataTitle) + system row creater(101)/createdTime(104)/sequenceNo(100) + group(200) + one mapped control per published business field",
11
+ relationMappings: "a relation field's options.mappings is the source of truth and is propagated into the matching form control's options.mappings",
12
+ redundantFieldVisibility: "ticket 66: a redundant field is a relation-mapping carry-through target (options.mappings[{source,target}]) — it stays BOUND on the form; when the model has no workflow (GET workflow/isworkflow?schemaCode false, WorkflowTemplateController.java:1348) the scaffold sets its control options.visible=false (form-design hidden, option 1); with a workflow it stays visible and per-node visibility is configured via each activity's propertyPermissions (ParticipantActivity.java:52, PropertyPermission.visible/editable/required — option 2, preferred), carried through the workflow template",
13
+ missingFieldsErrcode: 302020,
14
+ publishBody: "bizsheet/publish promotes draft*Json → published*Json verbatim (including draftHtmlJson → publishedHtmlJson); a saved draft (draftViewJson/draftAttributesJson) is required",
15
+ publishAlreadyPublished: "published:true with force!==true skips as already-published",
16
+ forceRepublish: "onlinejs.compose passes force:true so it can re-promote after a prior publish",
17
+ emptyLayoutGuard: "ticket 74: the platform designer's Save is NOT lossless — it clears draftViewJson to \"[]\" and draftAttributesJson to \"{}\"; form.draft refuses CALLER-PROVIDED empty layouts (blocker form-empty-layout, zero wire, escape allowEmptyLayout:true) and form.publish refuses to promote a draft with fewer controls than the published face (blocker form-publish-degrade, escape allowDegrade:true); the recovery path is the form version history (/api/api/app/bizsheet/history/list) — fetch the intact version's view/attributes originals and rewrite the draft before publishing",
18
+ actionsColumns: "ticket 100: draftActionsJson/publishedActionsJson are REAL bizsheet columns (BizForm.java:95-100 @SerializableField; BizFormModel.java:59-62 serialized by bizsheet/get) — the designer writes draftActionsJson on save and both faces on publish (frontend-dist/admin/js/4280.cfa75764.js offsets 193538/185347); form.get full:true surfaces them; publish promotes draftActionsJson → publishedActionsJson",
19
+ });
20
+ const FORM_GET_DESCRIPTION = '读取默认表单(bizsheet/get)的发布状态与控件清单(紧凑默认)。**发布态以返回的 `published` / `existDraft` 布尔为准**(`published:true`=已发布;`published:false,existDraft:true`=只有草稿,**未发布**)——不要用 `publishedControlCodes` 是否非空来推断(草稿字段会让它非空,误判为已发布)。**每个模型交付前其默认表单必须 `published:true`**:用 `configure_form` 或 `form.draft(rebuild:true)`+`form.publish` 发布,并逐模型 `form.get` 核对。传 `controls:["code",…]` 只取指定控件的 options(如关联控件的 mappings);传 `full:true` 才返回 draft/published 的完整 view/attributes/html/actions JSON(actions 列为设计器保存/发布的表单级按钮动作)。体积大,仅在需要整体布局时用。';
21
+ const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list).";
22
+ const FORM_PUBLISH_DESCRIPTION = "Publish the default form (bizsheet/publish, proven shape: draft*Json promoted to published*Json verbatim) with published-face readback. Degrade guard: refuses to promote a draft carrying FEWER controls than the published face (form-publish-degrade) — the designer's Save clears drafts non-losslessly; recover via the form version history (/api/api/app/bizsheet/history/list) and rewrite the draft first, or pass allowDegrade:true to force the reduction.";
23
+ const CONFIGURE_FORM_DESCRIPTION = "High-level: bind fields into the default form and publish it (form.draft → form.publish), subject to the write permission gate. Delegates to the proven form.draft scaffold + form.publish paths so the real empty-form layout (and any caller-supplied draftHtmlJson) is persisted and promoted — never bypasses with a shortcut body. `sheetCode` is optional: omitted means the model's default sheet (sheetCode = schemaCode).";
24
+ export const FORM_LANE_CONTRACT = Object.freeze({
25
+ lane: LANES.form,
26
+ family: CONTRACT_FAMILY,
27
+ referenceVersion: CONTRACT_REFERENCE_VERSION,
28
+ endpoints: Object.freeze({
29
+ sheetGet: "sheetGet",
30
+ sheetUpdate: "sheetUpdate",
31
+ sheetPublish: "sheetPublish",
32
+ fieldList: "fieldList",
33
+ }),
34
+ wire: Object.freeze({
35
+ draftBody: FORM_WIRE.draftBody,
36
+ scaffold: FORM_WIRE.scaffold,
37
+ relationMappings: FORM_WIRE.relationMappings,
38
+ redundantFieldVisibility: FORM_WIRE.redundantFieldVisibility,
39
+ missingFieldsErrcode: FORM_WIRE.missingFieldsErrcode,
40
+ publishBody: FORM_WIRE.publishBody,
41
+ publishAlreadyPublished: FORM_WIRE.publishAlreadyPublished,
42
+ forceRepublish: FORM_WIRE.forceRepublish,
43
+ emptyLayoutGuard: FORM_WIRE.emptyLayoutGuard,
44
+ actionsColumns: FORM_WIRE.actionsColumns,
45
+ }),
46
+ wireBindings: Object.freeze({
47
+ draftBody: Object.freeze([behaviorBinding("test/form-scaffold.test.ts#form.draft scaffolds an empty default form from published fields", assertFormContract)]),
48
+ scaffold: Object.freeze([behaviorBinding("test/form-scaffold.test.ts#form.draft scaffolds an empty default form from published fields", assertFormContract)]),
49
+ relationMappings: Object.freeze([behaviorBinding("test/form-scaffold.test.ts#form.draft propagates a relation field's mappings into its form control", assertFormContract)]),
50
+ redundantFieldVisibility: Object.freeze([behaviorBinding("test/redundant-field-form.test.ts#form.draft binds a redundant field but hides it when the model has no workflow", assertFormContract)]),
51
+ missingFieldsErrcode: Object.freeze([behaviorBinding("test/form-lifecycle.test.ts#ticket 42: a fresh model has no form", assertFormContract)]),
52
+ publishBody: Object.freeze([behaviorBinding("test/form-lifecycle.test.ts#ticket 42: form.publish alone creates + scaffolds + publishes a formless model", assertFormContract)]),
53
+ publishAlreadyPublished: Object.freeze([behaviorBinding("test/human-summaries.test.ts#configure_model and apply_changes carry whole-package summaries", assertFormContract)]),
54
+ forceRepublish: Object.freeze([behaviorBinding("test/issue-11-online-js.test.ts#onlinejs.compose merges customScript into the existing draftHtmlJson layout", assertFormContract)]),
55
+ emptyLayoutGuard: Object.freeze([behaviorBinding("test/ticket-74-form-empty-layout.test.ts#ticket 74: form.draft refuses an explicitly empty draftViewJson", assertFormContract)]),
56
+ actionsColumns: Object.freeze([behaviorBinding("test/ticket-100-form-actions-columns.test.ts#form.get full:true surfaces draftActionsJson/publishedActionsJson", assertFormContract)]),
57
+ }),
58
+ evidence: Object.freeze([
59
+ ".scratch/omc-route-wayfinder/leg2-closeout.md",
60
+ "packages/omc-cli/src/form-support.ts (shared draft/publish write bodies)",
61
+ "packages/omc-cli/test/form-scaffold.test.ts",
62
+ ]),
63
+ tools: Object.freeze({
64
+ "form.get": Object.freeze({ scope: "publishedHtmlJson authoritative readback", description: FORM_GET_DESCRIPTION }),
65
+ "form.draft": Object.freeze({ scope: "draft publish publishedHtmlJson readback", description: FORM_DRAFT_DESCRIPTION }),
66
+ "form.publish": Object.freeze({ scope: "draft publish publishedHtmlJson readback", description: FORM_PUBLISH_DESCRIPTION }),
67
+ configure_form: Object.freeze({ scope: "draft publish publishedHtmlJson readback", description: CONFIGURE_FORM_DESCRIPTION }),
68
+ }),
69
+ });
70
+ export function assertFormContract() {
71
+ assertLaneContract(FORM_LANE_CONTRACT);
72
+ if (FORM_WIRE.missingFieldsErrcode !== 302020)
73
+ throw new Error("form contract pins the empty-form scaffold errcode to 302020");
74
+ assertDescriptionCarries(FORM_LANE_CONTRACT, "form.get", ["bizsheet/get", "draft/published", "mappings", "`full:true`", "actions"]);
75
+ if (!FORM_WIRE.actionsColumns.includes("BizForm.java:95-100"))
76
+ throw new Error("form contract must pin the actions-columns evidence (ticket 100)");
77
+ assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["bizsheet/update", "draftViewJson", "draftAttributesJson", "scaffold", "draftHtmlJson", "form-empty-layout"]);
78
+ if (!FORM_WIRE.redundantFieldVisibility.includes("propertyPermissions"))
79
+ throw new Error("form contract must carry the redundant-field visibility (propertyPermissions) fact");
80
+ assertDescriptionCarries(FORM_LANE_CONTRACT, "form.publish", ["bizsheet/publish", "draft*Json", "published*Json", "verbatim", "form-publish-degrade", "history/list"]);
81
+ assertDescriptionCarries(FORM_LANE_CONTRACT, "configure_form", ["form.draft", "form.publish", "scaffold", "draftHtmlJson"]);
82
+ if (!FORM_WIRE.draftBody.includes("bizsheet/update"))
83
+ throw new Error("form contract must pin the draft update endpoint");
84
+ if (!FORM_WIRE.scaffold.includes("Title(203") || !FORM_WIRE.scaffold.includes("sequenceNo(100)"))
85
+ throw new Error("form contract must pin the designer scaffold shape");
86
+ if (!FORM_WIRE.relationMappings.includes("options.mappings"))
87
+ throw new Error("form contract must pin the relation-mapping carry");
88
+ if (!FORM_WIRE.publishBody.includes("bizsheet/publish") || !FORM_WIRE.publishBody.includes("draftHtmlJson"))
89
+ throw new Error("form contract must pin the publish promotion");
90
+ if (!FORM_WIRE.publishAlreadyPublished.includes("already-published"))
91
+ throw new Error("form contract must pin the already-published skip");
92
+ if (!FORM_WIRE.forceRepublish.includes("force:true"))
93
+ throw new Error("form contract must pin the force-republish override");
94
+ if (!FORM_WIRE.emptyLayoutGuard.includes("form-empty-layout") || !FORM_WIRE.emptyLayoutGuard.includes("form-publish-degrade"))
95
+ throw new Error("form contract must pin the empty-layout/degrade guards");
96
+ }
97
+ //# sourceMappingURL=form-contract.js.map
@@ -0,0 +1,16 @@
1
+ import { claim, declareLaneCoverage } from "./coverage-declaration.js";
2
+ import { FORM_LANE_CONTRACT } from "./form-contract.js";
3
+ const T = FORM_LANE_CONTRACT.tools;
4
+ export const FORM_COVERAGE = declareLaneCoverage(FORM_LANE_CONTRACT.lane, [
5
+ {
6
+ capability: "Form publish chain (draft-first → publish → publishedHtmlJson readback)",
7
+ evidence: [".scratch/omc-route-wayfinder/leg2-closeout.md", "packages/omc-cli/src/lanes/form.ts"],
8
+ tools: [
9
+ claim("form.get", T["form.get"].scope),
10
+ claim("form.draft", T["form.draft"].scope),
11
+ claim("form.publish", T["form.publish"].scope),
12
+ claim("configure_form", T["configure_form"].scope),
13
+ ],
14
+ },
15
+ ]);
16
+ //# sourceMappingURL=form-coverage.js.map