@stalfh233/omc-cli 0.6.7 → 0.6.10

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 (67) hide show
  1. package/README.md +2 -1
  2. package/dist/approval-flow.js +5 -3
  3. package/dist/bizservice-config.js +26 -10
  4. package/dist/call.js +47 -0
  5. package/dist/capability-overview.js +47 -24
  6. package/dist/cli-contract.js +73 -26
  7. package/dist/cli-output.js +41 -2
  8. package/dist/cli-stdout.js +74 -0
  9. package/dist/cli.js +135 -52
  10. package/dist/command-transport.js +8 -1
  11. package/dist/config-store.js +17 -2
  12. package/dist/discovery.js +62 -3
  13. package/dist/field-families.js +35 -6
  14. package/dist/goal-routes.js +118 -11
  15. package/dist/human-summary.js +11 -0
  16. package/dist/lanes/app-contract.js +12 -5
  17. package/dist/lanes/app.js +221 -134
  18. package/dist/lanes/apply-changes.js +2 -1
  19. package/dist/lanes/b2-registration-contract.js +3 -3
  20. package/dist/lanes/b2-registration.js +890 -693
  21. package/dist/lanes/businessrule.js +75 -51
  22. package/dist/lanes/custom-page.js +166 -115
  23. package/dist/lanes/datarule-contract.js +2 -2
  24. package/dist/lanes/datarule.js +238 -154
  25. package/dist/lanes/dictionary.js +253 -110
  26. package/dist/lanes/environment.js +57 -9
  27. package/dist/lanes/field.js +160 -107
  28. package/dist/lanes/folder-contract.js +7 -3
  29. package/dist/lanes/folder.js +249 -118
  30. package/dist/lanes/form-contract.js +1 -1
  31. package/dist/lanes/form.js +188 -64
  32. package/dist/lanes/list-view.js +405 -369
  33. package/dist/lanes/meta.js +31 -6
  34. package/dist/lanes/model-contract.js +1 -1
  35. package/dist/lanes/model.js +610 -512
  36. package/dist/lanes/online-js-contract.js +1 -1
  37. package/dist/lanes/online-js.js +160 -146
  38. package/dist/lanes/organization.js +905 -640
  39. package/dist/lanes/programming.js +274 -136
  40. package/dist/lanes/report-contract.js +2 -2
  41. package/dist/lanes/report.js +354 -198
  42. package/dist/lanes/rule-lifecycle.js +294 -140
  43. package/dist/lanes/runtime-data-contract.js +4 -4
  44. package/dist/lanes/runtime-data.js +869 -745
  45. package/dist/lanes/workflow-contract.js +3 -3
  46. package/dist/lanes/workflow.js +732 -605
  47. package/dist/model-scaffold.js +26 -5
  48. package/dist/path-projection.js +196 -14
  49. package/dist/plan.js +25 -0
  50. package/dist/programming-workflow.js +55 -3
  51. package/dist/read-shapes.js +146 -0
  52. package/dist/read.js +40 -9
  53. package/dist/recipes/businessrule.js +74 -24
  54. package/dist/recipes/dictionary.js +7 -4
  55. package/dist/recipes/form.js +46 -0
  56. package/dist/recipes/runtime-data.js +3 -2
  57. package/dist/refusal-guidance.js +29 -5
  58. package/dist/rule-support.js +14 -1
  59. package/dist/rule-when.js +2 -2
  60. package/dist/tool-types.js +9 -3
  61. package/dist/version-family-gaps.js +101 -0
  62. package/dist/write.js +2 -1
  63. package/package.json +6 -4
  64. package/skills/omc-glossary.md +13 -0
  65. package/skills/omc-refusals.md +13 -5
  66. package/skills/omc-tool-selection.md +2 -1
  67. package/skills/omc-write-hazards.md +5 -2
@@ -103,6 +103,9 @@ export async function executeRuleEnableWrite(session, args) {
103
103
  if (!full)
104
104
  throw new WireError("business", `rule ${args.ruleCode} 完整模型读回为空(businessrule/get id ${row.id}),拒绝用列表投影发布`, 302024);
105
105
  let published;
106
+ // 8.1 族走 `businessrule/update` 下探时,平台**没有** DRAFT/PUBLISH 两面
107
+ // (没有 publish 端点就没有双面模型),下面的 DRAFT 交叉校验对它不适用。
108
+ let publishEndpointAbsent = false;
106
109
  try {
107
110
  published = await wireCall(session, { path: ENDPOINTS.businessRulePublish, method: "POST", body: { ...full, enabled: true, status: "ENABLED" } });
108
111
  }
@@ -123,7 +126,7 @@ export async function executeRuleEnableWrite(session, args) {
123
126
  // `businessrule/publish` 端点(HTTP 404)。已验证的等价路径是
124
127
  // `businessrule/update` 携完整实体 + enabled:true —— 实体完整时一次调用
125
128
  // 同时翻转 enabled 与 status(见 issue 复现脚本)。仅 404 下探,其余原样抛。
126
- const publishEndpointAbsent = error instanceof WireError && error.kind === "http-status" && error.diagnostic?.httpStatus === 404;
129
+ publishEndpointAbsent = error instanceof WireError && error.kind === "http-status" && error.diagnostic?.httpStatus === 404;
127
130
  if (!publishEndpointAbsent)
128
131
  throw error;
129
132
  published = await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: { ...full, enabled: true, status: "ENABLED" } });
@@ -144,160 +147,311 @@ export async function executeRuleEnableWrite(session, args) {
144
147
  const after = wireRows(readback.data).find((item) => item.code === args.ruleCode);
145
148
  if (after?.enabled !== true)
146
149
  throw new ToolRefusalError({ status: "refused", blocker: "rule-enable-readback-failed", message: `rule ${args.ruleCode} 发布后未启用;enabled 必须为 true` });
147
- return { performed: { errcode: published.errcode, enableErrcode, ...(alreadyEnabled ? { alreadyEnabled: true } : {}) }, readback: { enabled: true } };
150
+ // r28g F23/F27:list.enabled 是 PUBLISH 投影,运行时读 DRAFT。交叉读 get,
151
+ // 把 DRAFT 的 enabled 钉进 readback。硬门槛只认 draft.enabled===true(list
152
+ // 已闸过);status 列可能滞后(平台 quirk / fake 复现),标 statusLag 而不
153
+ // 因此 fail closed——否则会把「enabled 已翻、status 还 DISABLED」的合法态拒掉。
154
+ const draft = publishEndpointAbsent
155
+ ? null
156
+ : (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(row.id) } })).data;
157
+ const draftEnabled = draft === null ? null : draft.enabled === true;
158
+ const draftStatus = draft?.status == null ? null : String(draft.status);
159
+ // draftEnabled === null 表示"本版本族没有 DRAFT 面"(8.1 下探路径)——
160
+ // 此时以 list(唯一的面)为准,不做 DRAFT 闸。
161
+ if (draftEnabled === false) {
162
+ throw new ToolRefusalError({
163
+ status: "refused",
164
+ blocker: "rule-enable-not-effective",
165
+ schemaCode: args.schemaCode,
166
+ ruleCode: args.ruleCode,
167
+ message: `rule.enable 后 list 报 enabled=true,但 DRAFT 行仍 enabled=false(status=${draftStatus ?? "null"})。运行时 getExecutableBySchemaCodeAndCode 读的是 DRAFT;请重试 rule.enable,或 rule.get 核对后再 rule.run。`,
168
+ readback: { listEnabled: true, draftEnabled: false, draftStatus, executable: false },
169
+ });
170
+ }
171
+ const statusLag = draftStatus !== null && draftStatus !== "ENABLED";
172
+ return {
173
+ performed: { errcode: published.errcode, enableErrcode, ...(alreadyEnabled ? { alreadyEnabled: true } : {}) },
174
+ readback: {
175
+ enabled: true,
176
+ listEnabled: after?.enabled === true,
177
+ listStatus: after?.status ?? null,
178
+ draftEnabled,
179
+ draftStatus,
180
+ executable: true,
181
+ verified: true,
182
+ ...(statusLag ? { statusLag: true, statusNote: "DRAFT status 列仍非 ENABLED(平台滞后常见);enabled=true 才是分发真相" } : {}),
183
+ source: "draft-get+list",
184
+ effectHint: "executable/verified 以 DRAFT get.enabled 为准;listEnabled 来自 PUBLISH 投影。rule.run 前若仍拒,再 rule.get 看 draftStatus",
185
+ },
186
+ };
148
187
  }
149
188
  export function registerRuleLifecycleLane(tools) {
150
- writeTool(tools, "rule.enable", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.enable"].scope, RULE_LIFECYCLE_TOOLS["rule.enable"].description, { schemaCode: z.string().min(1), ruleCode: z.string().min(1) }, async (session, args) => executeRuleEnableWrite(session, args), undefined, undefined, lifecycleRollbackSpec("rule.enable"));
151
- writeTool(tools, "rule.disable", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.disable"].scope, RULE_LIFECYCLE_TOOLS["rule.disable"].description, { schemaCode: z.string().min(1), ruleCode: z.string().min(1) }, async (session, args) => {
152
- await assertRuleSchemaExists(session, args.schemaCode);
153
- const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
154
- const row = wireRows(list.data).find((item) => item.code === args.ruleCode);
155
- if (!row?.id)
156
- throw new WireError("business", `rule ${args.ruleCode} not found for disable`, 302024);
157
- // 2026-11-04(源码实证,GitLab #13):8.6 与 8.1 都有 businessrule/disable
158
- // 端点(body={schemaCode,ruleCode}),UI 开关同款——直打,不再用 update
159
- // 拼载荷(残缺实体在 8.1 会回无明细 50000,正是 issue 的 disable 故障形态)。
160
- const performed = await wireCall(session, { path: ENDPOINTS.businessRuleDisable, method: "POST", body: { schemaCode: args.schemaCode, ruleCode: args.ruleCode } });
161
- const readback = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
162
- const after = wireRows(readback.data).find((item) => item.code === args.ruleCode);
163
- return { performed: { errcode: performed.errcode }, readback: { enabled: after?.enabled === true } };
164
- }, undefined, undefined, lifecycleRollbackSpec("rule.disable"));
165
- writeTool(tools, "rule.delete", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.delete"].scope, RULE_LIFECYCLE_TOOLS["rule.delete"].description, { schemaCode: z.string().min(1), ruleCode: z.string().min(1) }, async (session, args) => {
166
- // Ticket 96 finding: the preset rule codes are platform-managed —
167
- // listViewsBySchemaCode re-fills any missing preset for the schema, so
168
- // deleting one via the API is unreachable (it resurrects on the next
169
- // list). Refuse up front instead of "succeeding" and multiplying rows.
170
- if (PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode))
171
- throw new ToolRefusalError({
172
- status: "refused",
173
- blocker: "rule-preset-delete-unreachable",
174
- message: `ruleCode "${args.ruleCode}" 是平台预置触发规则——list_by_schemacode 会自动重灌缺失预置(BusinessRuleFacadeImpl.listViewsBySchemaCode:179-185),API 删除不可达且会增殖版本行;预置规则请用 rule.save 覆盖图或 rule.disable 停用`,
189
+ writeTool({
190
+ tools,
191
+ name: "rule.enable",
192
+ lane: RULE_LIFECYCLE_LANE,
193
+ scope: RULE_LIFECYCLE_TOOLS["rule.enable"].scope,
194
+ description: RULE_LIFECYCLE_TOOLS["rule.enable"].description,
195
+ inputSchema: { schemaCode: z.string().min(1), ruleCode: z.string().min(1) },
196
+ execute: async (session, args) => executeRuleEnableWrite(session, args),
197
+ rollback: lifecycleRollbackSpec("rule.enable"),
198
+ });
199
+ writeTool({
200
+ tools,
201
+ name: "rule.disable",
202
+ lane: RULE_LIFECYCLE_LANE,
203
+ scope: RULE_LIFECYCLE_TOOLS["rule.disable"].scope,
204
+ description: RULE_LIFECYCLE_TOOLS["rule.disable"].description,
205
+ inputSchema: { schemaCode: z.string().min(1), ruleCode: z.string().min(1) },
206
+ execute: async (session, args) => {
207
+ await assertRuleSchemaExists(session, args.schemaCode);
208
+ const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
209
+ const row = wireRows(list.data).find((item) => item.code === args.ruleCode);
210
+ if (!row?.id)
211
+ throw new WireError("business", `rule ${args.ruleCode} not found for disable`, 302024);
212
+ // r28g F22/F24 真机(1.1.25,2026-11-05):`businessrule/disable` 只把 DRAFT
213
+ // 行的 status 置 DISABLED,**不翻 enabled**——随后 list 仍报 enabled:true/
214
+ // ENABLED,get 报 enabled:true+ruleStatus:DISABLED+statusLag,enable 误报
215
+ // alreadyEnabled,run 仍放行。`rule.disableMany` 走 update 写
216
+ // `{enabled:false,status:DISABLED}` 才是真停用(stillEnabled:[] + get.enabled=false)。
217
+ // 契约本来就钉 `businessrule/update`;这里与 disableMany 对齐。残缺 list
218
+ // 投影在 8.1 会 50000 时,改读完整 get 模型再 update(同 enable 路径)。
219
+ const alreadyDisabled = row.enabled !== true;
220
+ if (alreadyDisabled) {
221
+ return {
222
+ performed: { errcode: 0, alreadyDisabled: true, noop: true },
223
+ readback: { enabled: false, status: row.status ?? null, source: "list", note: "list 行已 enabled=false,未发 wire" },
224
+ };
225
+ }
226
+ // 与 disableMany 同路径:list 投影 + update{enabled:false,status:DISABLED}。
227
+ // 真机上这条路径会同时翻 PUBLISH/DRAFT;假平台也认 list 投影。
228
+ // 读回以 list.enabled 为门(disableMany.stillEnabled 同口径);get 作补充,
229
+ // 若 get 仍滞后只标 statusLag,不因 get 单独 fail closed。
230
+ const performed = await wireCall(session, {
231
+ path: ENDPOINTS.businessRuleUpdate,
232
+ method: "POST",
233
+ body: { ...row, enabled: false, status: "DISABLED" },
175
234
  });
176
- await assertRuleSchemaExists(session, args.schemaCode);
177
- const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
178
- const row = wireRows(list.data).find((item) => item.code === args.ruleCode);
179
- if (!row?.id)
180
- throw new WireError("business", `rule ${args.ruleCode} not found for delete`, 302024);
181
- const performed = await wireCall(session, { path: ENDPOINTS.businessRuleDelete, method: "POST", query: { id: String(row.id) } });
182
- const readback = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
183
- return { performed: { errcode: performed.errcode }, readback: { absent: !wireRows(readback.data).some((item) => item.code === args.ruleCode) } };
184
- }, undefined, undefined, lifecycleRollbackSpec("rule.delete"));
235
+ const afterList = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data).find((item) => item.code === args.ruleCode);
236
+ const listEnabled = afterList?.enabled === true;
237
+ if (listEnabled) {
238
+ throw new ToolRefusalError({
239
+ status: "refused",
240
+ blocker: "rule-disable-not-effective",
241
+ schemaCode: args.schemaCode,
242
+ ruleCode: args.ruleCode,
243
+ message: `rule.disable 已发 businessrule/update(enabled:false),但 list 仍 enabled=true;不要信任 summary。可改试 rule.disableMany {schemaCode, ruleCodes:[${JSON.stringify(args.ruleCode)}]}`,
244
+ readback: { enabled: true, listEnabled: true, listStatus: afterList?.status ?? null },
245
+ });
246
+ }
247
+ let draftEnabled = null;
248
+ let draftStatus = null;
249
+ try {
250
+ const afterGet = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(row.id) } })).data;
251
+ if (afterGet) {
252
+ draftEnabled = afterGet.enabled === true;
253
+ draftStatus = afterGet.status == null ? null : String(afterGet.status);
254
+ }
255
+ }
256
+ catch {
257
+ // list 已证明停用;get 失败不挡
258
+ }
259
+ return {
260
+ performed: { errcode: performed.errcode },
261
+ readback: {
262
+ enabled: false,
263
+ listEnabled: false,
264
+ listStatus: afterList?.status ?? null,
265
+ ...(draftEnabled !== null ? { draftEnabled, draftStatus } : {}),
266
+ ...(draftEnabled === true ? { statusLag: true, statusNote: "list.enabled=false 已生效;DRAFT get.enabled 仍 true(面滞后)——以 list / 本 readback.enabled 为准" } : {}),
267
+ source: "list",
268
+ effectHint: "停用读回以本 readback.enabled(list 面,与 disableMany 同口径)为准;rule.get 可作交叉核对",
269
+ },
270
+ };
271
+ },
272
+ rollback: lifecycleRollbackSpec("rule.disable"),
273
+ });
274
+ writeTool({
275
+ tools,
276
+ name: "rule.delete",
277
+ lane: RULE_LIFECYCLE_LANE,
278
+ scope: RULE_LIFECYCLE_TOOLS["rule.delete"].scope,
279
+ description: RULE_LIFECYCLE_TOOLS["rule.delete"].description,
280
+ inputSchema: { schemaCode: z.string().min(1), ruleCode: z.string().min(1) },
281
+ execute: async (session, args) => {
282
+ // Ticket 96 finding: the preset rule codes are platform-managed —
283
+ // listViewsBySchemaCode re-fills any missing preset for the schema, so
284
+ // deleting one via the API is unreachable (it resurrects on the next
285
+ // list). Refuse up front instead of "succeeding" and multiplying rows.
286
+ if (PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode))
287
+ throw new ToolRefusalError({
288
+ status: "refused",
289
+ blocker: "rule-preset-delete-unreachable",
290
+ message: `ruleCode "${args.ruleCode}" 是平台预置触发规则——list_by_schemacode 会自动重灌缺失预置(BusinessRuleFacadeImpl.listViewsBySchemaCode:179-185),API 删除不可达且会增殖版本行;预置规则请用 rule.save 覆盖图或 rule.disable 停用`,
291
+ });
292
+ await assertRuleSchemaExists(session, args.schemaCode);
293
+ const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
294
+ const row = wireRows(list.data).find((item) => item.code === args.ruleCode);
295
+ if (!row?.id)
296
+ throw new WireError("business", `rule ${args.ruleCode} not found for delete`, 302024);
297
+ const performed = await wireCall(session, { path: ENDPOINTS.businessRuleDelete, method: "POST", query: { id: String(row.id) } });
298
+ const readback = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
299
+ return { performed: { errcode: performed.errcode }, readback: { absent: !wireRows(readback.data).some((item) => item.code === args.ruleCode) } };
300
+ },
301
+ rollback: lifecycleRollbackSpec("rule.delete"),
302
+ });
185
303
  // Round 10 issue 05 — lifecycle batch. Disabling a model's rules one by one was
186
304
  // N calls; this flips `enabled:false` on every matching rule in one tool. Only
187
305
  // CUSTOM rules by default (platform preset trigger rows must not be disabled —
188
306
  // they are platform-managed); `includePresets:true` opts in explicitly.
189
- writeTool(tools, "rule.run", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.run"].scope, RULE_LIFECYCLE_TOOLS["rule.run"].description, {
190
- schemaCode: z.string().min(1),
191
- ruleCode: z.string().min(1),
192
- ids: z.array(z.string().min(1)).min(1).describe("要执行的业务数据 id(data.load/data.list 的 bizObjectId / row.bizObjectId)——就是平台按钮点击时提交的 ids"),
193
- consolidation: z.boolean().optional().describe("平台 body 字段,默认 false(按钮点击抓包值);合并执行场景才传 true"),
194
- }, async (session, args) => {
195
- await assertRuleSchemaExists(session, args.schemaCode);
196
- const ids = [...new Set(args.ids.map(String))];
197
- if (ids.length !== args.ids.length)
198
- throw new ToolRefusalError({ status: "refused", blocker: "rule-run-ids-duplicated", message: "ids 有重复项——按去重后的列表执行,别让同一行在一条规则里被执行两次" });
199
- const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
200
- const row = wireRows(list.data).find((item) => String(item.code) === args.ruleCode);
201
- if (!row)
202
- throw new ToolRefusalError({ status: "refused", blocker: "rule-run-rule-absent", schemaCode: args.schemaCode, ruleCode: args.ruleCode, message: `模型 ${args.schemaCode} 没有规则 ${args.ruleCode}——先 rule.list 核对 code` });
203
- if (row.defaultRule === true || PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode))
204
- throw new ToolRefusalError({
205
- status: "refused",
206
- blocker: "rule-run-preset-rule",
207
- ruleCode: args.ruleCode,
208
- message: `${args.ruleCode} 是平台预置触发规则(defaultRule:true)——它由数据事件(新增/更新/删除/流程生效…)自动分发,设计器的按钮规则选择器不提供预置行。要手动跑请新建自定义规则(CUSTOM_NODE 触发)再用 button/rule.run`,
209
- });
210
- if (row.enabled !== true)
211
- throw new ToolRefusalError({
212
- status: "refused",
213
- blocker: "rule-run-disabled",
214
- ruleCode: args.ruleCode,
215
- message: `规则 ${args.ruleCode} 当前未启用(enabled=${String(row.enabled)})——先 rule.enable(publish+enable 两步),否则设计器显示「失效·未启用」且按钮不会执行它`,
216
- });
217
- let performed;
218
- try {
219
- performed = await wireCall(session, {
220
- path: ruleRuntimeExecutePath(args.schemaCode, args.ruleCode),
221
- method: "POST",
222
- body: { schemaCode: args.schemaCode, ids, ruleCode: args.ruleCode, consolidation: args.consolidation === true },
223
- });
224
- }
225
- catch (error) {
226
- // 550001 业务规则不存在 on the launcher runtime path means the rule was
227
- // not REGISTERED as a custom trigger (r15: a data-event graph saved with
228
- // customTrigger:true still 550001s). Name the semantic instead of
229
- // forwarding a bare platform code.
230
- if (error instanceof WireError && error.kind === "business" && error.errcode === 550001)
307
+ writeTool({
308
+ tools,
309
+ name: "rule.run",
310
+ lane: RULE_LIFECYCLE_LANE,
311
+ scope: RULE_LIFECYCLE_TOOLS["rule.run"].scope,
312
+ description: RULE_LIFECYCLE_TOOLS["rule.run"].description,
313
+ inputSchema: {
314
+ schemaCode: z.string().min(1),
315
+ ruleCode: z.string().min(1),
316
+ ids: z.array(z.string().min(1)).min(1).describe("要执行的业务数据 id(data.load/data.list 的 bizObjectId / row.bizObjectId)——就是平台按钮点击时提交的 ids"),
317
+ consolidation: z.boolean().optional().describe("平台 body 字段,默认 false(按钮点击抓包值);合并执行场景才传 true"),
318
+ },
319
+ execute: async (session, args) => {
320
+ await assertRuleSchemaExists(session, args.schemaCode);
321
+ const ids = [...new Set(args.ids.map(String))];
322
+ if (ids.length !== args.ids.length)
323
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-run-ids-duplicated", message: "ids 有重复项——按去重后的列表执行,别让同一行在一条规则里被执行两次" });
324
+ const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
325
+ const row = wireRows(list.data).find((item) => String(item.code) === args.ruleCode);
326
+ if (!row)
327
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-run-rule-absent", schemaCode: args.schemaCode, ruleCode: args.ruleCode, message: `模型 ${args.schemaCode} 没有规则 ${args.ruleCode}——先 rule.list 核对 code` });
328
+ if (row.defaultRule === true || PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode))
231
329
  throw new ToolRefusalError({
232
330
  status: "refused",
233
- blocker: "rule-not-executable",
234
- schemaCode: args.schemaCode,
331
+ blocker: "rule-run-preset-rule",
235
332
  ruleCode: args.ruleCode,
236
- nextCommand: `omc call rule.enable --args '${JSON.stringify({ schemaCode: args.schemaCode, ruleCode: args.ruleCode })}'`,
237
- message: `550001「业务规则不存在」在这里的真实含义是「该规则当前不可执行」,不是规则不存在,也不是「只认 CUSTOM_NODE」。` +
238
- `反编译 8.6.26 的 BusinessRuleTableImpl#getExecutableBySchemaCodeAndCode 是唯一判定:` +
239
- `① **DRAFT 行**必须存在;② DRAFT 行 enabled=true **且** status != DISABLED;③ 至少一条 PUBLISH 版本。` +
240
- `rule.save --publish 只写 PUBLISH 行,**DRAFT 行的 status 仍是 DISABLED**——所以发布完直接 rule.run 必然 550001。` +
241
- `修法:rule.enable(它先 publish 再调平台 /enable,把 DRAFT 行 status 置 ENABLED)。` +
242
- `注意 rule.list 显示的是 PUBLISH 行的 enabled/status,看起来"已启用"但运行期读的是 DRAFT 行——不要用 list 判定可执行性。`,
333
+ message: `${args.ruleCode} 是平台预置触发规则(defaultRule:true)——它由数据事件(新增/更新/删除/流程生效…)自动分发,设计器的按钮规则选择器不提供预置行。要手动跑请新建自定义规则(CUSTOM_NODE 触发)再用 button/rule.run`,
243
334
  });
244
- throw error;
245
- }
246
- const payload = (performed.data ?? null);
247
- const objects = Array.isArray(payload) ? payload : [];
248
- return {
249
- performed: { errcode: performed.errcode, ruleCode: args.ruleCode, ids },
250
- readback: {
251
- schemaCode: args.schemaCode,
252
- ruleCode: args.ruleCode,
253
- executedIds: ids,
254
- returnedObjects: objects.length,
255
- // 302045 = 数据被占用(前端把同样的码映射成"有人正在编辑")。
256
- ...(performed.errcode === 302045
257
- ? { dataInUse: true, hint: "302045:数据被占用(有人正在编辑该行 / 行锁未释放),稍后重试" }
258
- : {}),
259
- effectHint: "平台只回执行结果信封;返回的 data 是执行前快照(reacquireDataFromDb:false)。规则副作用必须另行读回:data.load {schemaCode, bizObjectId} 或 data.list(data.list 默认不含 DRAFT,草稿行请用 data.load)",
260
- // NOT latestPublished: the platform's list projection reports
261
- // latestPublished:false even for rows it executes fine (live
262
- // 2026-10-23, documented in rule-support.ts) — a warning here would be
263
- // noise. Liveness is `enabled`, which is gated above.
264
- },
265
- };
266
- }, undefined, undefined, undefined);
267
- writeTool(tools, "rule.disableMany", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.disableMany"].scope, RULE_LIFECYCLE_TOOLS["rule.disableMany"].description, {
268
- schemaCode: z.string().min(1),
269
- ruleCodes: z.array(z.string().min(1)).optional().describe("只停用这些 code;省略=该模型全部规则"),
270
- customOnly: z.boolean().optional().describe("默认 false:停用全部匹配规则(含平台预置触发码 Create/Update/…,因为用户逻辑常写在其中);true 只停用自定义规则、跳过预置。"),
271
- }, async (session, args) => {
272
- await assertRuleSchemaExists(session, args.schemaCode);
273
- const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
274
- const wanted = args.ruleCodes ? new Set(args.ruleCodes) : undefined;
275
- const targets = wireRows(list.data).filter((row) => {
276
- const code = String(row.code ?? "");
277
- if (wanted && !wanted.has(code))
278
- return false;
279
- // NOTE: presets are INCLUDED by default — user logic is frequently
280
- // authored INTO preset trigger rows (Create/Update/…), so skipping them
281
- // would silently leave the user's rules enabled. customOnly:true opts out.
282
- if (args.customOnly === true && PLATFORM_TRIGGER_RULE_CODES.has(code))
283
- return false;
284
- return true;
285
- });
286
- const disabled = [];
287
- const skipped = [];
288
- for (const row of targets) {
289
- const code = String(row.code ?? "");
335
+ // r28g F27:list.enabled 是 PUBLISH 投影;运行时门禁读 DRAFT.enabled。
336
+ // status 列可滞后(enable 后仍 DISABLED),不单独据此拒绝——与 rule.get
337
+ // 的 statusLag 注释一致。只看 list 会在 disable 半生效时假通过。
338
+ let draftEnabled = row.enabled === true;
339
+ let draftStatus = row.status == null ? null : String(row.status);
340
+ if (row.id) {
341
+ try {
342
+ const draft = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(row.id) } })).data;
343
+ if (draft) {
344
+ draftEnabled = draft.enabled === true;
345
+ draftStatus = draft.status == null ? null : String(draft.status);
346
+ }
347
+ }
348
+ catch {
349
+ // fall back to list projection
350
+ }
351
+ }
352
+ if (!draftEnabled)
353
+ throw new ToolRefusalError({
354
+ status: "refused",
355
+ blocker: "rule-run-disabled",
356
+ ruleCode: args.ruleCode,
357
+ listEnabled: row.enabled === true,
358
+ draftEnabled,
359
+ draftStatus,
360
+ message: `规则 ${args.ruleCode} 当前未启用(DRAFT enabled=false, status=${draftStatus ?? "null"};list.enabled=${String(row.enabled)})——先 rule.enable(publish+enable 两步),否则设计器显示「失效·未启用」且按钮不会执行它`,
361
+ });
362
+ let performed;
290
363
  try {
291
- await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: { ...row, enabled: false, status: "DISABLED" } });
292
- disabled.push(code);
364
+ performed = await wireCall(session, {
365
+ path: ruleRuntimeExecutePath(args.schemaCode, args.ruleCode),
366
+ method: "POST",
367
+ body: { schemaCode: args.schemaCode, ids, ruleCode: args.ruleCode, consolidation: args.consolidation === true },
368
+ });
293
369
  }
294
370
  catch (error) {
295
- skipped.push({ ruleCode: code, reason: error instanceof Error ? error.message : String(error) });
371
+ // 550001 业务规则不存在 on the launcher runtime path means the rule was
372
+ // not REGISTERED as a custom trigger (r15: a data-event graph saved with
373
+ // customTrigger:true still 550001s). Name the semantic instead of
374
+ // forwarding a bare platform code.
375
+ if (error instanceof WireError && error.kind === "business" && error.errcode === 550001)
376
+ throw new ToolRefusalError({
377
+ status: "refused",
378
+ blocker: "rule-not-executable",
379
+ schemaCode: args.schemaCode,
380
+ ruleCode: args.ruleCode,
381
+ nextCommand: `omc call rule.enable --args '${JSON.stringify({ schemaCode: args.schemaCode, ruleCode: args.ruleCode })}'`,
382
+ message: `550001「业务规则不存在」在这里的真实含义是「该规则当前不可执行」,不是规则不存在,也不是「只认 CUSTOM_NODE」。` +
383
+ `反编译 8.6.26 的 BusinessRuleTableImpl#getExecutableBySchemaCodeAndCode 是唯一判定:` +
384
+ `① **DRAFT 行**必须存在;② DRAFT 行 enabled=true **且** status != DISABLED;③ 至少一条 PUBLISH 版本。` +
385
+ `rule.save --publish 只写 PUBLISH 行,**DRAFT 行的 status 仍是 DISABLED**——所以发布完直接 rule.run 必然 550001。` +
386
+ `修法:rule.enable(它先 publish 再调平台 /enable,把 DRAFT 行 status 置 ENABLED)。` +
387
+ `注意 rule.list 显示的是 PUBLISH 行的 enabled/status,看起来"已启用"但运行期读的是 DRAFT 行——不要用 list 判定可执行性。`,
388
+ });
389
+ throw error;
296
390
  }
297
- }
298
- const after = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data);
299
- const stillEnabled = after.filter((row) => row.enabled === true && disabled.includes(String(row.code ?? ""))).map((row) => String(row.code));
300
- return { performed: { disabled }, readback: { disabledCount: disabled.length, disabled, skipped, stillEnabled } };
391
+ const payload = (performed.data ?? null);
392
+ const objects = Array.isArray(payload) ? payload : [];
393
+ return {
394
+ performed: { errcode: performed.errcode, ruleCode: args.ruleCode, ids },
395
+ readback: {
396
+ schemaCode: args.schemaCode,
397
+ ruleCode: args.ruleCode,
398
+ executedIds: ids,
399
+ returnedObjects: objects.length,
400
+ // 302045 = 数据被占用(前端把同样的码映射成"有人正在编辑")。
401
+ ...(performed.errcode === 302045
402
+ ? { dataInUse: true, hint: "302045:数据被占用(有人正在编辑该行 / 行锁未释放),稍后重试" }
403
+ : {}),
404
+ effectHint: "平台只回执行结果信封;返回的 data 是执行前快照(reacquireDataFromDb:false)。规则副作用必须另行读回:data.load {schemaCode, bizObjectId} 或 data.list(data.list 默认不含 DRAFT,草稿行请用 data.load)",
405
+ // NOT latestPublished: the platform's list projection reports
406
+ // latestPublished:false even for rows it executes fine (live
407
+ // 2026-10-23, documented in rule-support.ts) — a warning here would be
408
+ // noise. Liveness is `enabled`, which is gated above.
409
+ },
410
+ };
411
+ },
412
+ });
413
+ writeTool({
414
+ tools,
415
+ name: "rule.disableMany",
416
+ lane: RULE_LIFECYCLE_LANE,
417
+ scope: RULE_LIFECYCLE_TOOLS["rule.disableMany"].scope,
418
+ description: RULE_LIFECYCLE_TOOLS["rule.disableMany"].description,
419
+ inputSchema: {
420
+ schemaCode: z.string().min(1),
421
+ ruleCodes: z.array(z.string().min(1)).optional().describe("只停用这些 code;省略=该模型全部规则"),
422
+ customOnly: z.boolean().optional().describe("默认 false:停用全部匹配规则(含平台预置触发码 Create/Update/…,因为用户逻辑常写在其中);true 只停用自定义规则、跳过预置。"),
423
+ },
424
+ execute: async (session, args) => {
425
+ await assertRuleSchemaExists(session, args.schemaCode);
426
+ const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
427
+ const wanted = args.ruleCodes ? new Set(args.ruleCodes) : undefined;
428
+ const targets = wireRows(list.data).filter((row) => {
429
+ const code = String(row.code ?? "");
430
+ if (wanted && !wanted.has(code))
431
+ return false;
432
+ // NOTE: presets are INCLUDED by default — user logic is frequently
433
+ // authored INTO preset trigger rows (Create/Update/…), so skipping them
434
+ // would silently leave the user's rules enabled. customOnly:true opts out.
435
+ if (args.customOnly === true && PLATFORM_TRIGGER_RULE_CODES.has(code))
436
+ return false;
437
+ return true;
438
+ });
439
+ const disabled = [];
440
+ const skipped = [];
441
+ for (const row of targets) {
442
+ const code = String(row.code ?? "");
443
+ try {
444
+ await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: { ...row, enabled: false, status: "DISABLED" } });
445
+ disabled.push(code);
446
+ }
447
+ catch (error) {
448
+ skipped.push({ ruleCode: code, reason: error instanceof Error ? error.message : String(error) });
449
+ }
450
+ }
451
+ const after = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data);
452
+ const stillEnabled = after.filter((row) => row.enabled === true && disabled.includes(String(row.code ?? ""))).map((row) => String(row.code));
453
+ return { performed: { disabled }, readback: { disabledCount: disabled.length, disabled, skipped, stillEnabled } };
454
+ },
301
455
  });
302
456
  registerPackageTool(tools, {
303
457
  name: "configure_rule",
@@ -5,7 +5,7 @@ export { RUNTIME_DATA_WIRE, RUNTIME_QUERY_OPERATORS, ENGINE_OWNED_KEYS, SUBMIT_S
5
5
  import { RUNTIME_DATA_WIRE } from "omc-core/wire-facts";
6
6
  export function dataListDescription() {
7
7
  const wire = RUNTIME_DATA_WIRE;
8
- return `按简化过滤 DSL 查询业务对象(runtime/query/list);DSL 在 CLI 内翻译为平台 queryCondition(${wire.dslShape}),并按发布字段元数据为每个条件注入 propertyType(未知过滤字段将被拒绝,可用 rawCondition 逃生);响应仅返回选定 fields 与分页元数据;空模型返回空列表;未配列的自定义字段投影为 null——这是显示面缺列配置、不是数据缺失(data.load 有值),显式选列在结果中全为 null 而发布字段元数据显示其存在时,readback 追加 projectionHint(先用 listview.configure 配置该模型的默认视图列,或用 data.load 读单条权威值)。`;
8
+ return `按简化过滤 DSL 查询业务对象(runtime/query/list);DSL 在 CLI 内翻译为平台 queryCondition(${wire.dslShape}),并按发布字段元数据为每个条件注入 propertyType(未知过滤字段将被拒绝,可用 rawCondition 逃生);响应行在 items[],业务字段在 items[].fields.<code>(不是 rows[],也不是 item 顶层——--jq 常用 items[].fields.sku);空模型返回空列表;未配列的自定义字段投影为 null——这是显示面缺列配置、不是数据缺失(data.load 有值),显式选列在结果中全为 null 而发布字段元数据显示其存在时,readback 追加 projectionHint(先用 listview.configure 配置该模型的默认视图列,或用 data.load 读单条权威值)。`;
9
9
  }
10
10
  export function dataUpdateDescription() {
11
11
  const wire = RUNTIME_DATA_WIRE;
@@ -21,7 +21,7 @@ export function dataUpdateDescription() {
21
21
  }
22
22
  export function dataSaveDescription() {
23
23
  const wire = RUNTIME_DATA_WIRE;
24
- return describeTool("M2 runtime data write (runtime/form/save) — returns bizObjectId for authoritative readback via data.load.", [
24
+ return describeTool("默认存草稿(DRAFT,不触发规则);要提交态用顶层 submit:true 或随后 data.submit;formData.name 会被平台改写为「用户名+模型名+时间」,要控标题请传业务字段。M2 runtime/form/save — returns bizObjectId for authoritative readback via data.load.", [
25
25
  `带可选 workflowCode 时按门户暂存形状创建流程草稿:${wire.workflowDraftSave}。`,
26
26
  "formData 禁止携带 id(带 id 的就地发起在 8.6.26 无 REST 面,会被拒绝;既有无实例草稿请改用两段式或 data.submit 的 workItemId 审批链)。",
27
27
  "关联值形状(live 2026-09-24):**单选关联(propertyType 9)传 id 字符串或 { id };传数组会 50000**;多选关联(11)才用数组。**人员/组织(50/51/60/61)传选择对象数组 `[{\"id\":\"<id>\",\"type\":3,\"name\":\"<名>\"}]`——`type` 必填**(BizField.parseFromClient 把 `type` 强转 Integer 并 intValue(),缺 `type` 稳定 50000);`type` 取 UnitType 索引:用户=3、部门=1。字段 `mappings` 由表单运行时在用户选择时套用,**服务端 data.save 不会自动回填映射目标字段**——API 侧调用需自行写入被映射字段,否则读回为空(不要误判为映射配置失败)。",
@@ -138,8 +138,8 @@ function desc(lead) {
138
138
  }
139
139
  export function assertRuntimeDataContract() {
140
140
  assertLaneContract(RUNTIME_DATA_LANE_CONTRACT);
141
- assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.list", ["queryCondition", "propertyType", "rawCondition", "projectionHint", "listview.configure", "data.load"]);
142
- assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.save", ["bizObjectId", "sheetCode", "bizsheet/get", "workflowCode", "sequenceStatus", "workItem", "propertyType 9", "50000", "mappings", "rowStatus"]);
141
+ assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.list", ["queryCondition", "propertyType", "rawCondition", "projectionHint", "listview.configure", "data.load", "items[].fields"]);
142
+ assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.save", ["bizObjectId", "sheetCode", "bizsheet/get", "workflowCode", "sequenceStatus", "workItem", "propertyType 9", "50000", "mappings", "rowStatus", "DRAFT", "submit:true"]);
143
143
  assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.update", ["runtime/form/save", "bizObjectId", "COMPLETED", "304001", "form/load", "rowStatus"]);
144
144
  assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.delete", ["absence", "sheetCode"]);
145
145
  assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.submit", ["getReplayToken", "replayToken", "bizObjectId", "objectId", "DRAFT", "COMPLETED", "10009", "100005", "NPE", "两段式", "finishStart=FALSE"]);