@stalfh233/omc-cli 0.4.14 → 0.4.16

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 (184) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +297 -297
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -1277
  4. package/dist/args.js +0 -0
  5. package/dist/artifacts.js +0 -0
  6. package/dist/bench/call-face-eval.js +0 -0
  7. package/dist/bench/context-attribution.js +0 -0
  8. package/dist/bench/discovery-cost-eval.js +0 -0
  9. package/dist/bench/driver.js +0 -0
  10. package/dist/bench/evals.js +0 -0
  11. package/dist/bench/fake-http-server.js +0 -0
  12. package/dist/bench/instrument.js +0 -0
  13. package/dist/bench/intent-face-eval.js +40 -40
  14. package/dist/bench/run.js +0 -0
  15. package/dist/bench/scenario.js +0 -0
  16. package/dist/bench/types.js +0 -0
  17. package/dist/bi-wire.js +0 -0
  18. package/dist/bizservice-config.js +38 -13
  19. package/dist/call.js +0 -0
  20. package/dist/capability-absences.js +0 -0
  21. package/dist/capability-overview.js +2 -2
  22. package/dist/capability-shape.js +0 -0
  23. package/dist/cli-contract.js +0 -0
  24. package/dist/cli-output.js +41 -15
  25. package/dist/cli.js +42 -3
  26. package/dist/code-rules.js +0 -0
  27. package/dist/command-transport.js +0 -0
  28. package/dist/config-store.js +0 -0
  29. package/dist/context.js +0 -0
  30. package/dist/contract-consistency.js +0 -0
  31. package/dist/contract-resources.js +0 -0
  32. package/dist/coverage-consistency.js +0 -0
  33. package/dist/coverage-registry.js +0 -0
  34. package/dist/coverage.js +0 -0
  35. package/dist/data-list-filter.js +0 -0
  36. package/dist/discovery.js +0 -0
  37. package/dist/endpoints.js +2 -0
  38. package/dist/environment-policy.js +0 -0
  39. package/dist/execution-metadata.js +4 -4
  40. package/dist/fake/app.js +0 -0
  41. package/dist/fake/b2-registration.js +26 -0
  42. package/dist/fake/businessrule.js +0 -0
  43. package/dist/fake/datarule.js +0 -0
  44. package/dist/fake/dictionary.js +0 -0
  45. package/dist/fake/environment.js +0 -0
  46. package/dist/fake/field.js +0 -0
  47. package/dist/fake/form.js +0 -0
  48. package/dist/fake/index.js +0 -0
  49. package/dist/fake/list-view.js +0 -0
  50. package/dist/fake/model.js +0 -0
  51. package/dist/fake/online-js.js +0 -0
  52. package/dist/fake/report.js +0 -0
  53. package/dist/fake/routes.js +0 -0
  54. package/dist/fake/rule-lifecycle.js +0 -0
  55. package/dist/fake/runtime-data.js +0 -0
  56. package/dist/fake/state.js +0 -0
  57. package/dist/fake/workflow.js +0 -0
  58. package/dist/field-change.js +0 -0
  59. package/dist/field-families.js +0 -0
  60. package/dist/form-layout.js +0 -0
  61. package/dist/form-support.js +12 -2
  62. package/dist/goal-routes.js +0 -0
  63. package/dist/governed-execution.js +0 -0
  64. package/dist/human-summary.js +0 -0
  65. package/dist/identity.js +0 -0
  66. package/dist/intent/baseline.js +0 -0
  67. package/dist/intent/capabilities/bizservice.js +0 -0
  68. package/dist/intent/capabilities/businessrule.js +0 -0
  69. package/dist/intent/capabilities/datarule.js +0 -0
  70. package/dist/intent/capabilities/field.js +0 -0
  71. package/dist/intent/capabilities/form.js +0 -0
  72. package/dist/intent/capabilities/index.js +0 -0
  73. package/dist/intent/capabilities/listview.js +0 -0
  74. package/dist/intent/capabilities/model.js +0 -0
  75. package/dist/intent/capabilities/onlinejs.js +0 -0
  76. package/dist/intent/capabilities/report.js +0 -0
  77. package/dist/intent/capabilities/workflow.js +0 -0
  78. package/dist/intent/capability.js +0 -0
  79. package/dist/intent/cli.js +0 -0
  80. package/dist/intent/compare.js +0 -0
  81. package/dist/intent/compiler.js +0 -0
  82. package/dist/intent/dsl.js +0 -0
  83. package/dist/intent/plan-file.js +0 -0
  84. package/dist/intent/readback.js +0 -0
  85. package/dist/intent/schema.js +0 -0
  86. package/dist/intent/validation.js +0 -0
  87. package/dist/intent/yaml.js +0 -0
  88. package/dist/json-column.js +0 -0
  89. package/dist/lanes/app-contract.js +0 -0
  90. package/dist/lanes/app-coverage.js +0 -0
  91. package/dist/lanes/app.js +0 -0
  92. package/dist/lanes/apply-changes.js +14 -1
  93. package/dist/lanes/b2-registration-contract.js +5 -0
  94. package/dist/lanes/b2-registration-coverage.js +1 -0
  95. package/dist/lanes/b2-registration.js +55 -6
  96. package/dist/lanes/businessrule-contract.js +0 -0
  97. package/dist/lanes/businessrule-coverage.js +0 -0
  98. package/dist/lanes/businessrule.js +0 -0
  99. package/dist/lanes/contract-support.js +0 -0
  100. package/dist/lanes/coverage-declaration.js +0 -0
  101. package/dist/lanes/datarule-contract.js +0 -0
  102. package/dist/lanes/datarule-coverage.js +0 -0
  103. package/dist/lanes/datarule-protocol.js +0 -0
  104. package/dist/lanes/datarule.js +0 -0
  105. package/dist/lanes/dictionary-contract.js +0 -0
  106. package/dist/lanes/dictionary-coverage.js +0 -0
  107. package/dist/lanes/dictionary.js +0 -0
  108. package/dist/lanes/environment-contract.js +0 -0
  109. package/dist/lanes/environment-coverage.js +0 -0
  110. package/dist/lanes/environment.js +0 -0
  111. package/dist/lanes/field-contract.js +3 -2
  112. package/dist/lanes/field-coverage.js +0 -0
  113. package/dist/lanes/field.js +268 -172
  114. package/dist/lanes/form-contract.js +2 -2
  115. package/dist/lanes/form-coverage.js +0 -0
  116. package/dist/lanes/form.js +4 -0
  117. package/dist/lanes/lane-ids.js +0 -0
  118. package/dist/lanes/list-view-contract.js +0 -0
  119. package/dist/lanes/list-view-coverage.js +0 -0
  120. package/dist/lanes/list-view-shapes.js +0 -0
  121. package/dist/lanes/list-view.js +0 -0
  122. package/dist/lanes/meta-contract.js +0 -0
  123. package/dist/lanes/meta.js +11 -1
  124. package/dist/lanes/model-contract.js +0 -0
  125. package/dist/lanes/model-coverage.js +0 -0
  126. package/dist/lanes/model.js +1 -1
  127. package/dist/lanes/online-js-contract.js +0 -0
  128. package/dist/lanes/online-js-coverage.js +0 -0
  129. package/dist/lanes/online-js.js +0 -0
  130. package/dist/lanes/report-contract.js +0 -0
  131. package/dist/lanes/report-coverage.js +0 -0
  132. package/dist/lanes/report.js +0 -0
  133. package/dist/lanes/rule-graph.js +0 -0
  134. package/dist/lanes/rule-lifecycle-contract.js +0 -0
  135. package/dist/lanes/rule-lifecycle-coverage.js +0 -0
  136. package/dist/lanes/rule-lifecycle.js +0 -0
  137. package/dist/lanes/runtime-data-contract.js +0 -0
  138. package/dist/lanes/runtime-data-coverage.js +0 -0
  139. package/dist/lanes/runtime-data.js +0 -0
  140. package/dist/lanes/workflow-contract.js +0 -0
  141. package/dist/lanes/workflow-coverage.js +0 -0
  142. package/dist/lanes/workflow.js +2 -0
  143. package/dist/omc-core-lazy.js +6 -0
  144. package/dist/online-js-layout.js +0 -0
  145. package/dist/online-js-source.js +28 -28
  146. package/dist/package-tool.js +0 -0
  147. package/dist/package.js +0 -0
  148. package/dist/plan.js +11 -1
  149. package/dist/read.js +0 -0
  150. package/dist/redact.js +0 -0
  151. package/dist/rule-support.js +0 -0
  152. package/dist/runtime-support.js +0 -0
  153. package/dist/service-doctor.js +28 -0
  154. package/dist/session-manager.js +0 -0
  155. package/dist/session.js +38 -12
  156. package/dist/skills.js +0 -0
  157. package/dist/sqlite-warning-filter.js +27 -0
  158. package/dist/support.js +0 -0
  159. package/dist/tool-types.js +0 -0
  160. package/dist/tools.js +0 -0
  161. package/dist/usage-log.js +4 -0
  162. package/dist/wire.js +0 -0
  163. package/dist/workflow-support.js +0 -0
  164. package/dist/write-lease.js +0 -0
  165. package/dist/write-lock.js +0 -0
  166. package/dist/write.js +24 -2
  167. package/dist/zip.js +0 -0
  168. package/docs/tool-surface-map.md +42 -42
  169. package/package.json +71 -71
  170. package/skills/omc-acceptance-criteria.md +47 -47
  171. package/skills/omc-business-configuration.md +258 -257
  172. package/skills/omc-capabilities.md +197 -197
  173. package/skills/omc-capability-scouting.md +55 -55
  174. package/skills/omc-config-draft-review.md +171 -171
  175. package/skills/omc-five-piece-flow.md +35 -35
  176. package/skills/omc-glossary.md +86 -86
  177. package/skills/omc-refusals.md +110 -110
  178. package/skills/omc-requirement-analysis.md +161 -161
  179. package/skills/omc-requirement-vocabulary.md +51 -51
  180. package/skills/omc-start-here.md +82 -82
  181. package/skills/omc-tool-selection.md +119 -119
  182. package/skills/omc-write-hazards.md +88 -87
  183. package/dist/approval-token.js +0 -102
  184. package/dist/server.js +0 -92
@@ -10,6 +10,7 @@ import { syncFormConditionDataRules } from "./datarule.js";
10
10
  import { FORM_CONDITION_OPTION_KEYS } from "./datarule-protocol.js";
11
11
  import { defineTool, ToolRefusalError } from "../tool-types.js";
12
12
  import { wireCall, wireRows, WireError } from "../wire.js";
13
+ import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
13
14
  import { FIELD_LANE_CONTRACT } from "./field-contract.js";
14
15
  const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
15
16
  const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
@@ -181,184 +182,21 @@ export function registerFieldLane(tools) {
181
182
  relativeCode: relativeCodeArg,
182
183
  subFields: subFieldArg,
183
184
  propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)"),
185
+ cascadeRepublish: z
186
+ .boolean()
187
+ .optional()
188
+ .describe("true 时一调级联生效:field.update → field.publish → form.draft(rebuild) → form.publish(force)。表单按 published fields 全量重建默认表单脚手架(保留 non-structural 控件 options);无跨端点事务,中途失败按 partial steps(failedStep+envelope)如实报告,默认不级联"),
184
189
  }, 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
- };
190
+ const outcome = await runFieldUpdate(session, args);
191
+ if (args.cascadeRepublish !== true)
192
+ return outcome;
193
+ return runFieldUpdateCascade(session, args, outcome);
246
194
  }, (args) => assertNameI18nArg(args.name_i18n));
247
195
  writeTool(tools, "field.publish", FIELD_LANE, FIELD_TOOLS["field.publish"].scope, FIELD_TOOLS["field.publish"].description, {
248
196
  schemaCode: z.string().min(1),
249
197
  code: z.string().min(1).optional().describe("optional single field to verify on the published face"),
250
198
  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
- });
199
+ }, async (session, args) => executeFieldPublish(session, args));
362
200
  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
201
  const performed = await wireCall(session, {
364
202
  path: ENDPOINTS.fieldForceDelete,
@@ -371,4 +209,262 @@ export function registerFieldLane(tools) {
371
209
  return { performed: { errcode: performed.errcode }, readback: { absentDraft: gone(draft.data), absentPublished: gone(published.data) } };
372
210
  });
373
211
  }
212
+ /** The proven field.update write body — shared by the write tool and the cascade path. */
213
+ async function runFieldUpdate(session, args) {
214
+ const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
215
+ let rows = wireRows(listed.data);
216
+ if (!rows.some((row) => (row.code ?? row.propertyCode) === args.code)) {
217
+ const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
218
+ rows = wireRows(published.data);
219
+ }
220
+ const record = rows.find((row) => (row.code ?? row.propertyCode) === args.code);
221
+ if (!record)
222
+ throw new WireError("business", `field ${args.code} not found on ${args.schemaCode}`, 301001);
223
+ const update = {
224
+ schemaCode: args.schemaCode,
225
+ code: args.code,
226
+ base: record,
227
+ ...(args.name !== undefined ? { name: args.name } : {}),
228
+ ...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
229
+ ...(args.options !== undefined ? { options: args.options } : {}),
230
+ ...(args.choices !== undefined ? { choices: args.choices } : {}),
231
+ ...(args.relativeCode !== undefined ? { relativeCode: args.relativeCode } : {}),
232
+ ...(args.subFields !== undefined ? { subFields: args.subFields } : {}),
233
+ ...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
234
+ };
235
+ const prepared = prepareFieldUpdate(update);
236
+ const performed = await wireCall(session, { path: ENDPOINTS.fieldUpdate, method: "POST", body: fieldUpdateWireBody(update, prepared) });
237
+ const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
238
+ const after = wireRows(readback.data).find((row) => (row.code ?? row.propertyCode) === args.code);
239
+ // Round 4 (B): a form-interaction condition must exist on BOTH faces to be
240
+ // complete — field options (portal runtime) AND a bizdatarule type3/4/5
241
+ // row (designer design-time Set/Unset). field.update historically wrote
242
+ // only options, so CLI-authored conditions showed "Unset" in the designer.
243
+ // Pair the changed condition key(s) into the rule projection; a sync
244
+ // failure never hides the field write (it is surfaced as formConditionSync.error).
245
+ let formConditionSync;
246
+ let formConditionSyncError;
247
+ if (args.options !== undefined && !prepared.isChildTable) {
248
+ const previous = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
249
+ try {
250
+ const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, previous, prepared.options);
251
+ if (sync.created.length + sync.updated.length + sync.deleted.length + sync.skipped.length > 0)
252
+ formConditionSync = sync;
253
+ }
254
+ catch (error) {
255
+ formConditionSyncError = error instanceof Error ? error.message : String(error);
256
+ }
257
+ }
258
+ // Round 4: echo the EFFECTIVE stored options (parsed), not just presence.
259
+ // An agent setting readonlyFormula/requiredFormula/scale had no way to
260
+ // confirm the write landed — the readback only named the field. Field
261
+ // options are the form-interaction contract (ADR 0005), so read them back.
262
+ const storedOptions = after !== undefined ? parseFieldOptionsForReadback(after.options) : undefined;
263
+ return {
264
+ performed: { errcode: performed.errcode },
265
+ readback: {
266
+ fieldPresent: after !== undefined,
267
+ ...(after?.name !== undefined ? { name: after.name } : {}),
268
+ ...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
269
+ ...(after?.type !== undefined || after?.propertyType !== undefined ? { type: after?.type ?? after?.propertyType } : {}),
270
+ ...(storedOptions !== undefined && storedOptions !== null ? { options: storedOptions } : {}),
271
+ ...(formConditionSync !== undefined ? { formConditionSync } : {}),
272
+ ...(formConditionSyncError !== undefined ? { formConditionSync: { error: formConditionSyncError } } : {}),
273
+ },
274
+ };
275
+ }
276
+ /**
277
+ * The proven field.publish write body. Shared by the field.publish write tool
278
+ * and the field.update cascade path — a cascade must never bypass the
279
+ * already-published short-circuit semantics, the transient-column retry, or
280
+ * the post-publish detail verification by inlining a bare wire call.
281
+ * `force:true` (cascade only) skips the already-published short-circuit: a
282
+ * cascaded republish exists precisely because the PUBLISHED face carries the
283
+ * stale shape while the field is present there.
284
+ */
285
+ export async function executeFieldPublish(session, args) {
286
+ // bizproperty/publish is MODEL-SCOPED: one call publishes every draft
287
+ // field of the schema. `code`/`codes` only select which fields to verify
288
+ // on the published face after the single publish; omit them to publish
289
+ // and read back all codes (the batch path).
290
+ //
291
+ // Ticket 81 idempotent retry: with explicit code/codes, read the
292
+ // published face FIRST (a retry after a timeout hits the wire read
293
+ // cache — zero extra wires) and skip the publish POST entirely when
294
+ // every requested field is already published. A genuinely missing
295
+ // field still drives the publish.
296
+ const requestedCodes = [...(args.code ? [args.code] : []), ...(args.codes ?? [])];
297
+ const codeOf = (row) => String(row.code ?? row.propertyCode ?? "");
298
+ // cpdocs base3.0 parity (cp-bizproperty verifyPlan): presence on the
299
+ // published LIST face is proven later, against a map of each field's
300
+ // pre-publish DRAFT shape (name + type). The draft face must be
301
+ // snapshotted BEFORE the publish because a model-scoped publish can leave
302
+ // it empty; the draft row is the caller-visible expectation.
303
+ const draftSnapshot = new Map(wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data)
304
+ .map((row) => [codeOf(row), { name: String(row.name ?? ""), propertyType: Number(row.type ?? row.propertyType) }]));
305
+ const verifyFieldDetails = async (targets) => {
306
+ const bounded = targets.slice(0, 25);
307
+ const mismatches = [];
308
+ for (const code of bounded) {
309
+ const expected = draftSnapshot.get(code);
310
+ if (!expected)
311
+ continue;
312
+ let detail;
313
+ try {
314
+ detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode: args.schemaCode, propertyCode: code } })).data;
315
+ }
316
+ catch {
317
+ // The detail endpoint is optional across builds: report the
318
+ // verification as unavailable rather than failing the publish.
319
+ return { mode: "unavailable", verified: false, note: "bizproperty/get 不可用,字段详情未核对(仅列表面存在性)" };
320
+ }
321
+ const actual = { name: String(detail.name ?? ""), propertyType: Number(detail.propertyType ?? detail.type) };
322
+ if (actual.name !== expected.name || actual.propertyType !== expected.propertyType)
323
+ mismatches.push({ code, expected, actual });
324
+ }
325
+ return { mode: "detail", verified: true, checked: bounded.length, ok: mismatches.length === 0, ...(mismatches.length > 0 ? { mismatches } : {}) };
326
+ };
327
+ const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
328
+ const prePublished = wireRows(preRead.data).map(codeOf).filter((code) => code.length > 0);
329
+ if (args.force !== true && requestedCodes.length > 0 && requestedCodes.every((code) => prePublished.includes(code))) {
330
+ return {
331
+ performed: { skipped: "already-published", scope: "model" },
332
+ readback: {
333
+ publishedCodes: prePublished,
334
+ ...(args.code ? { publishedFieldPresent: true } : {}),
335
+ ...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, true])) } : {}),
336
+ allRequestedPresent: true,
337
+ verification: await verifyFieldDetails(requestedCodes),
338
+ },
339
+ };
340
+ }
341
+ // Ticket 79 live-fidelity (docker log evidence, 2026-09-13 21:43-21:45,
342
+ // cloud8_launcher log_error.log + nginx access timings): under DB
343
+ // connection-pool contention a model publish can fail with a transient
344
+ // `表单:<t>_<schema>不存在列:<col>` 50000 — the addIndexes/updateColumns
345
+ // assert reads the serializer view while the failed attempt's DDL
346
+ // auto-commits, so the state SELF-HEALS across retries (live: three
347
+ // consecutive attempts each healed one column, the fourth succeeded).
348
+ // Retry that exact signature up to 3 attempts; anything else rethrows.
349
+ let performed;
350
+ // Backoff matches the observed healing cadence on the small env (live:
351
+ // attempts ~25s/73s apart each reconciled one column).
352
+ const backoffMs = [2000, 5000];
353
+ for (let attempt = 1; attempt <= 3; attempt++) {
354
+ try {
355
+ performed = await wireCall(session, {
356
+ path: ENDPOINTS.fieldPublish,
357
+ method: "POST",
358
+ body: { bizSchemaCode: args.schemaCode, sendMessage: false },
359
+ });
360
+ break;
361
+ }
362
+ catch (error) {
363
+ const transientColumnSync = error instanceof WireError && error.kind === "business" && error.errcode === 50000 && error.message.includes("不存在列");
364
+ if (!(transientColumnSync && attempt < 3))
365
+ throw error;
366
+ await new Promise((resolve) => setTimeout(resolve, backoffMs[attempt - 1] ?? 5000));
367
+ }
368
+ }
369
+ const publishOutcome = performed;
370
+ const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
371
+ const publishedCodes = wireRows(readback.data).map(codeOf).filter((code) => code.length > 0);
372
+ const requested = [...(args.code ? [args.code] : []), ...(args.codes ?? [])];
373
+ const present = requested.filter((code) => publishedCodes.includes(code));
374
+ const targets = (requested.length > 0 ? requested : [...draftSnapshot.keys()]).filter((code) => publishedCodes.includes(code));
375
+ const verification = await verifyFieldDetails(targets);
376
+ const mismatches = verification.mismatches;
377
+ if (mismatches !== undefined)
378
+ throw new ToolRefusalError({
379
+ status: "refused",
380
+ blocker: "field-publish-readback-mismatch",
381
+ performed: { errcode: publishOutcome.errcode, scope: "model" },
382
+ mismatches,
383
+ message: `字段已发布,但发布后详情回读与草稿面不一致(${mismatches.map((item) => String(item.code)).join("、")}):这是元数据与预期形状相异的信号,先核对再决定后续;不要当作成功`,
384
+ });
385
+ return {
386
+ performed: { errcode: publishOutcome.errcode, scope: "model" },
387
+ readback: {
388
+ publishedCodes,
389
+ ...(args.code ? { publishedFieldPresent: publishedCodes.includes(args.code) } : {}),
390
+ ...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, publishedCodes.includes(code)])) } : {}),
391
+ ...(requested.length > 0 ? { allRequestedPresent: present.length === requested.length } : {}),
392
+ verification,
393
+ },
394
+ };
395
+ }
396
+ /**
397
+ * Ticket T06 — field.update cascadeRepublish. After a successful field.update,
398
+ * republish the model's fields, rebuild the default form from the PUBLISHED
399
+ * face (non-structural control options are preserved by the shared scaffold),
400
+ * and force-promote the rebuilt draft. Steps follow the configure_model
401
+ * protocol: a mid-chain failure returns {status:"partial", steps,
402
+ * failedStep, envelope} — the completed prefix is reported honestly; there is
403
+ * no cross-endpoint transaction to roll back to.
404
+ */
405
+ async function runFieldUpdateCascade(session, args, update) {
406
+ const steps = [{ step: "field.update", errcode: update.performed.errcode, readback: update.readback }];
407
+ // The cascade targets the model's default form slot (sheetCode = schemaCode,
408
+ // the form lane's proven default rule).
409
+ const sheetCode = args.schemaCode;
410
+ const stop = (index, tool, error) => {
411
+ const envelope = error instanceof ToolRefusalError
412
+ ? Object.fromEntries(Object.entries(error.payload).filter(([key]) => key !== "status"))
413
+ : error instanceof WireError
414
+ ? { blocker: `wire-${error.kind}`, errcode: error.errcode, message: error.message }
415
+ : { blocker: "field-update-cascade-step-failed", message: error instanceof Error ? error.message : String(error) };
416
+ return {
417
+ status: "partial",
418
+ steps,
419
+ failedStep: { index, tool },
420
+ envelope: { ...envelope, message: `${String(envelope.message ?? "步骤失败")};后续步骤未执行` },
421
+ };
422
+ };
423
+ const stepOf = (step, outcome) => {
424
+ const skipped = outcome.performed.skipped;
425
+ return { step, ...(skipped !== undefined ? { skipped } : { errcode: outcome.performed.errcode }), readback: outcome.readback };
426
+ };
427
+ let published;
428
+ let formPublished;
429
+ try {
430
+ // force: the field IS present on the published face — with the stale
431
+ // shape. The cascade exists to replace that shape, so the
432
+ // already-published short-circuit must not skip the publish.
433
+ published = await executeFieldPublish(session, { schemaCode: args.schemaCode, code: args.code, force: true });
434
+ steps.push(stepOf("field.publish", published));
435
+ }
436
+ catch (error) {
437
+ return stop(1, "field.publish", error);
438
+ }
439
+ try {
440
+ // No fieldCodes / no caller layout: the scaffold binds EVERY published
441
+ // field (a draftAttributesJson would bypass the published-face source).
442
+ const draft = await executeFormDraftWrite(session, { schemaCode: args.schemaCode, sheetCode, rebuild: true });
443
+ steps.push(stepOf("form.draft", draft));
444
+ }
445
+ catch (error) {
446
+ return stop(2, "form.draft", error);
447
+ }
448
+ try {
449
+ // force: the form was published before the cascade; without force the
450
+ // promotion would short-circuit on `published === true`.
451
+ formPublished = await executeFormPublishWrite(session, { schemaCode: args.schemaCode, sheetCode, force: true });
452
+ steps.push(stepOf("form.publish", formPublished));
453
+ }
454
+ catch (error) {
455
+ return stop(3, "form.publish", error);
456
+ }
457
+ const fieldReadback = published.readback;
458
+ return {
459
+ performed: { errcode: steps[0].errcode, scope: "cascade" },
460
+ steps,
461
+ readback: {
462
+ ...update.readback,
463
+ cascade: {
464
+ fieldPublished: fieldReadback.publishedFieldPresent === true,
465
+ formPublished: formPublished.readback.published === true,
466
+ },
467
+ },
468
+ };
469
+ }
374
470
  //# sourceMappingURL=field.js.map
@@ -19,7 +19,7 @@ export const FORM_WIRE = Object.freeze({
19
19
  });
20
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
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.";
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. rebuild:true is the one-call refresh: discard the existing draft, re-scaffold from the published fields (non-structural control options preserved), then force-promote — use it after a field shape change.";
23
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
24
  export const FORM_LANE_CONTRACT = Object.freeze({
25
25
  lane: LANES.form,
@@ -77,7 +77,7 @@ export function assertFormContract() {
77
77
  assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["bizsheet/update", "draftViewJson", "draftAttributesJson", "scaffold", "draftHtmlJson", "form-empty-layout"]);
78
78
  if (!FORM_WIRE.redundantFieldVisibility.includes("propertyPermissions"))
79
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"]);
80
+ assertDescriptionCarries(FORM_LANE_CONTRACT, "form.publish", ["bizsheet/publish", "draft*Json", "published*Json", "verbatim", "form-publish-degrade", "history/list", "rebuild:true", "force-promote"]);
81
81
  assertDescriptionCarries(FORM_LANE_CONTRACT, "configure_form", ["form.draft", "form.publish", "scaffold", "draftHtmlJson"]);
82
82
  if (!FORM_WIRE.draftBody.includes("bizsheet/update"))
83
83
  throw new Error("form contract must pin the draft update endpoint");
File without changes
@@ -134,6 +134,10 @@ export function registerFormLane(tools) {
134
134
  publishRemarks: z.string().optional(),
135
135
  force: z.boolean().optional().describe("强制重新提拔(跳过 already-published 短路);重写草稿后的再发布需要它"),
136
136
  allowDegrade: z.boolean().optional().describe("显式放行降级提拔(草稿控件数 < 已发布面);默认拒绝(form-publish-degrade),防把空/残草稿提拔成发布版"),
137
+ rebuild: z
138
+ .boolean()
139
+ .optional()
140
+ .describe("true 时一步刷新:先丢弃现有草稿、按已发布字段重建脚手架(保留非结构性控件 options),再强制提拔——改字段形状后刷新表单用一个调用;默认只发布现有草稿(无草稿时自动补默认)"),
137
141
  }, async (session, args) => executeFormPublishWrite(session, { ...args, sheetCode: resolveSheetCode(args.schemaCode, args.sheetCode) }));
138
142
  registerPackageTool(tools, {
139
143
  name: "configure_form",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -197,7 +197,7 @@ export function registerMetaLane(tools) {
197
197
  layer: "meta",
198
198
  kind: "meta",
199
199
  description: META_TOOLS["doctor"].description,
200
- inputSchema: { environment: environmentArg },
200
+ inputSchema: { environment: environmentArg, serviceCode: z.string().min(1).optional().describe("可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出;非致命 WARN)") },
201
201
  handler: async (ctx, args, self) => {
202
202
  const store = ctx.sessions.config();
203
203
  const steps = [];
@@ -243,6 +243,16 @@ export function registerMetaLane(tools) {
243
243
  note: familyOk === null ? "缺少版本证据;写入将保守拒绝" : familyOk ? "兼容" : "不兼容;写入将保守拒绝",
244
244
  });
245
245
  steps.push({ step: "环境指纹", ok: true, fingerprint: session.environmentFingerprint() });
246
+ // T09 — optional per-service credential shape probe (non-fatal).
247
+ if (args.serviceCode !== undefined) {
248
+ const { probeServiceCredentials } = await import("../service-doctor.js");
249
+ const probe = await probeServiceCredentials(session, args.serviceCode);
250
+ steps.push(probe.status === "not-found"
251
+ ? { step: "服务凭据", ok: null, serviceCode: probe.serviceCode, note: "未找到(get_by_code 无行)" }
252
+ : probe.status === "error"
253
+ ? { step: "服务凭据", ok: null, serviceCode: probe.serviceCode, note: `探测失败:${probe.message}` }
254
+ : { step: "服务凭据", ok: probe.findings.length === 0, serviceCode: probe.serviceCode, adapterCode: probe.adapterCode, parameterCount: probe.parameterCount, findings: probe.findings, note: probe.findings.length === 0 ? "掩码/占位/全 0 均未检出" : "掩码/占位值不可用于真实调用;真值从任务环境事实取,不要抄 service.get 读回" });
255
+ }
246
256
  return { status: "ok", steps };
247
257
  }
248
258
  catch (error) {
File without changes
File without changes
@@ -916,7 +916,7 @@ export function registerModelLane(tools) {
916
916
  throw new ToolRefusalError({ status: "refused", blocker: "configure-model-listview-unavailable", message: "listview.configure 未注册写执行" });
917
917
  failed = await run(results.length, "listview.configure", async () => {
918
918
  const outcome = await target.writeExecute(session, { schemaCode: args.code, ...args.listview });
919
- return { step: "listview.configure", readback: outcome.readback };
919
+ return { step: "listview.configure", readback: "readback" in outcome ? outcome.readback : outcome.envelope };
920
920
  });
921
921
  if (failed)
922
922
  return failed;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1614,6 +1614,8 @@ export function registerWorkflowLane(tools) {
1614
1614
  throw new ToolRefusalError({ status: "refused", blocker: "configure-workflow-step-unavailable", message: `configure_workflow 需要 ${toolName},但它未注册写执行` });
1615
1615
  try {
1616
1616
  const outcome = await target.writeExecute(session, toolArgs);
1617
+ if ("status" in outcome)
1618
+ throw new ToolRefusalError({ status: "refused", blocker: "configure-workflow-inner-stop", message: `步骤 ${toolName} 返回中途停止:${String(outcome.envelope?.message ?? outcome.status)}` });
1617
1619
  steps.push({ step: toolName, ...outcome });
1618
1620
  return outcome.readback;
1619
1621
  }
@@ -0,0 +1,6 @@
1
+ let pending;
2
+ export function omcCore() {
3
+ pending ??= import("omc-core");
4
+ return pending;
5
+ }
6
+ //# sourceMappingURL=omc-core-lazy.js.map
File without changes