@stalfh233/omc-cli 0.4.74 → 0.4.75
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.
- package/coverage/m1-coverage-manifest-v1.json +9 -3
- package/dist/capability-overview.js +12 -3
- package/dist/cli.js +18 -3
- package/dist/endpoints.js +24 -0
- package/dist/form-default.js +97 -10
- package/dist/form-support.js +309 -3
- package/dist/human-summary.js +4 -0
- package/dist/lanes/businessrule.js +7 -0
- package/dist/lanes/form-contract.js +13 -3
- package/dist/lanes/form-coverage.js +2 -1
- package/dist/lanes/form.js +43 -1
- package/dist/lanes/organization.js +37 -3
- package/dist/lanes/rule-lifecycle-contract.js +17 -0
- package/dist/lanes/rule-lifecycle-coverage.js +2 -0
- package/dist/lanes/rule-lifecycle.js +103 -19
- package/dist/lanes/workflow-contract.js +57 -10
- package/dist/lanes/workflow-coverage.js +4 -0
- package/dist/lanes/workflow.js +458 -40
- package/dist/rollback-spec.js +32 -1
- package/dist/usage-log.js +11 -2
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +20 -2
- package/skills/omc-capabilities.md +8 -7
- package/skills/omc-write-hazards.md +4 -4
|
@@ -28,6 +28,20 @@ export function ruleEnableDescription() {
|
|
|
28
28
|
export function ruleDisableDescription() {
|
|
29
29
|
return `停用业务规则(businessrule/update)。读回规则行后以整行 + \`enabled:false\` 作为请求体提交。${RULE_ENABLE_CHAIN.readbackRule}。${RULE_SCHEMA_GATE.reason}。`;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Manual execution of a CUSTOM-trigger rule — the SAME face the portal's
|
|
33
|
+
* `listview.configure` BUSINESS_RULE button drives. Verified live
|
|
34
|
+
* (2026-10-23, AI-Platform 1.1.25) by capturing the button click with
|
|
35
|
+
* Playwright and reading the launcher log:
|
|
36
|
+
* click → POST /api/api/launcher/domain_service_runtime/<schema>/<rule>
|
|
37
|
+
* body {schemaCode, ids:[bizObjectId], ruleCode, consolidation:false}
|
|
38
|
+
* → {errcode:0, errmsg:"Execution Completed", data:[…], traceId}
|
|
39
|
+
* launcher: "节点执行开始 … nodeCode=Start → btnFire(CUSTOM_NODE) → UPDATE_1 → End"
|
|
40
|
+
* and the rule's UPDATE node really rewrote the row's field.
|
|
41
|
+
*/
|
|
42
|
+
export function ruleRunDescription() {
|
|
43
|
+
return `手动执行一条【自定义触发】业务规则(平台按钮同款面:\`POST /api/api/launcher/domain_service_runtime/<schemaCode>/<ruleCode>\`,body \`{schemaCode, ids:[bizObjectId…], ruleCode, consolidation}\`)。用于端到端验证按钮/自定义规则——**列表或表单按钮(\`listview.configure\` 的 \`buttons\` + \`bindAction:BUSINESS_RULE\`)点击时发给平台的就是这笔请求**(Playwright 抓包 + 容器日志双证,1.1.25)。前提:规则必须是**自定义 code**(\`defaultRule:false\`,图里有 CUSTOM_NODE 触发节点)且 **enabled:true**——禁用规则按钮不可用,工具回 \`rule-run-disabled\` 并指向 \`rule.enable\`;预置触发码(Create/Update/…)由数据事件分发,按钮选择器不提供,工具回 \`rule-run-preset-rule\`。**返回的 data 是执行前快照**(前端发 reacquireDataFromDb:false),规则的副作用要用 \`data.load\`/\`data.list\` 另行读回。\`302045\` = 数据被占用(有人正在编辑该行),稍后重试。`;
|
|
44
|
+
}
|
|
31
45
|
export const RULE_LIFECYCLE_LANE_CONTRACT = Object.freeze({
|
|
32
46
|
lane: LANES.ruleLifecycle,
|
|
33
47
|
family: CONTRACT_FAMILY,
|
|
@@ -38,6 +52,7 @@ export const RULE_LIFECYCLE_LANE_CONTRACT = Object.freeze({
|
|
|
38
52
|
enable: "businessRuleEnable",
|
|
39
53
|
update: "businessRuleUpdate",
|
|
40
54
|
delete: "businessRuleDelete",
|
|
55
|
+
run: "ruleRuntimeExecuteBase",
|
|
41
56
|
}),
|
|
42
57
|
wire: Object.freeze({
|
|
43
58
|
chain: Object.freeze(["publish", "enable"]),
|
|
@@ -63,6 +78,7 @@ export const RULE_LIFECYCLE_LANE_CONTRACT = Object.freeze({
|
|
|
63
78
|
scope: "enable invoke disable delete",
|
|
64
79
|
description: `删除业务规则(businessrule/delete,id 先经 list 解析)并精确回读缺失。${RULE_SCHEMA_GATE.presetDelete}。${RULE_SCHEMA_GATE.reason}。`,
|
|
65
80
|
}),
|
|
81
|
+
"rule.run": Object.freeze({ scope: "manual rule execution (button trigger)", description: ruleRunDescription() }),
|
|
66
82
|
"rule.disableMany": Object.freeze({
|
|
67
83
|
scope: "enable invoke disable delete",
|
|
68
84
|
description: "批量停用某模型的业务规则(一次调用,逐条 businessrule/update enabled:false)。**默认停用全部匹配规则(含平台预置触发码 Create/Update/…)**——用户逻辑常写进预置行,跳过会静默漏停;`customOnly:true` 只停自定义、跳过预置,`ruleCodes` 限定子集。返回 disabled/skipped 与仍在启用的兜底核对。",
|
|
@@ -79,6 +95,7 @@ export function assertRuleLifecycleContract() {
|
|
|
79
95
|
throw new Error("rule-lifecycle contract pins the idempotent re-enable errcode to 550014");
|
|
80
96
|
assertDescriptionCarries(RULE_LIFECYCLE_LANE_CONTRACT, "rule.enable", ["businessrule/publish", "businessrule/enable", "DRAFT", "PUBLISH", "`enabled`", "`status`"]);
|
|
81
97
|
assertDescriptionCarries(RULE_LIFECYCLE_LANE_CONTRACT, "rule.disable", ["businessrule/update", "`enabled:false`", "`enabled`", "`status`"]);
|
|
98
|
+
assertDescriptionCarries(RULE_LIFECYCLE_LANE_CONTRACT, "rule.run", ["domain_service_runtime", "ids", "CUSTOM_NODE", "enabled:true", "rule-run-disabled", "rule-run-preset-rule", "302045", "reacquireDataFromDb"]);
|
|
82
99
|
assertDescriptionCarries(RULE_LIFECYCLE_LANE_CONTRACT, "rule.disableMany", ["businessrule/update", "enabled:false", "customOnly", "ruleCodes", "disabled"]);
|
|
83
100
|
assertDescriptionCarries(RULE_LIFECYCLE_LANE_CONTRACT, "rule.delete", ["businessrule/delete", "rule-preset-delete-unreachable", "预置", "rule-schema-unresolved"]);
|
|
84
101
|
assertDescriptionCarries(RULE_LIFECYCLE_LANE_CONTRACT, "configure_rule", ["rule.save", "rule.enable"]);
|
|
@@ -6,6 +6,7 @@ export const RULE_LIFECYCLE_COVERAGE = declareLaneCoverage(RULE_LIFECYCLE_LANE_C
|
|
|
6
6
|
capability: "S25 business-rule enable/invoke/disable/delete lifecycle",
|
|
7
7
|
evidence: [
|
|
8
8
|
".scratch/omc-business-rules/acceptance/s25-enable-20260910001115307.md",
|
|
9
|
+
"r11 2026-10-23 (AI-Platform 1.1.25): list button click captured with Playwright → POST /api/api/launcher/domain_service_runtime/<schema>/<rule> {schemaCode, ids, ruleCode, consolidation:false}; launcher log shows Start → CUSTOM_NODE → UPDATE → End and the UPDATE node rewrote the row",
|
|
9
10
|
".scratch/omc-business-rules/acceptance/s25-cleanup-20260910001140227.md",
|
|
10
11
|
],
|
|
11
12
|
tools: [
|
|
@@ -13,6 +14,7 @@ export const RULE_LIFECYCLE_COVERAGE = declareLaneCoverage(RULE_LIFECYCLE_LANE_C
|
|
|
13
14
|
claim("rule.disable", T["rule.disable"].scope),
|
|
14
15
|
claim("rule.delete", T["rule.delete"].scope),
|
|
15
16
|
claim("rule.disableMany", T["rule.disableMany"].scope),
|
|
17
|
+
claim("rule.run", T["rule.run"].scope),
|
|
16
18
|
claim("configure_rule", T["configure_rule"].scope),
|
|
17
19
|
],
|
|
18
20
|
},
|
|
@@ -9,6 +9,17 @@ import { prepareRuleGraph, validateRuleNodeGraph, PLATFORM_TRIGGER_RULE_CODES }
|
|
|
9
9
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
10
10
|
import { currentUsageArgsDigest, priorIdenticalSuccess } from "../usage-log.js";
|
|
11
11
|
import { RULE_ENABLE_CHAIN, RULE_LIFECYCLE_LANE_CONTRACT } from "./rule-lifecycle-contract.js";
|
|
12
|
+
/**
|
|
13
|
+
* The portal's custom-button execution face (live 2026-10-23, 1.1.25):
|
|
14
|
+
* `POST /api/api/launcher/domain_service_runtime/<schema>/<rule>` with
|
|
15
|
+
* `{schemaCode, ids:[bizObjectId], ruleCode, consolidation}`. The response's
|
|
16
|
+
* `data` is the PRE-execution snapshot (the frontend asks for
|
|
17
|
+
* reacquireDataFromDb:false), so a rule's side effects must be read back
|
|
18
|
+
* through data.load/data.list — never from this payload.
|
|
19
|
+
*/
|
|
20
|
+
function ruleRuntimeExecutePath(schemaCode, ruleCode) {
|
|
21
|
+
return `${ENDPOINTS.ruleRuntimeExecuteBase}/${encodeURIComponent(schemaCode)}/${encodeURIComponent(ruleCode)}`;
|
|
22
|
+
}
|
|
12
23
|
const RULE_LIFECYCLE_LANE = RULE_LIFECYCLE_LANE_CONTRACT.lane;
|
|
13
24
|
const RULE_LIFECYCLE_TOOLS = RULE_LIFECYCLE_LANE_CONTRACT.tools;
|
|
14
25
|
function lifecycleRollbackSpec(toolName) {
|
|
@@ -42,36 +53,55 @@ export async function executeRuleEnableWrite(session, args) {
|
|
|
42
53
|
const row = wireRows(list.data).find((item) => item.code === args.ruleCode);
|
|
43
54
|
if (!row?.id)
|
|
44
55
|
throw new WireError("business", `rule ${args.ruleCode} not found for enable`, 302024);
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Idempotency (r9 2026-10-22): identical `rule.enable` calls answered
|
|
57
|
-
// `alreadyEnabled:true` 14 times and each one still minted a new rule version
|
|
58
|
-
// row. The session log is the only durable memory a CLI has, so the memo is
|
|
59
|
-
// there: same (schemaCode, ruleCode) already enabled successfully, with no
|
|
60
|
-
// `rule.save` in between ⇒ nothing to do. Any save invalidates it.
|
|
56
|
+
// Idempotency (r9 2026-10-22, corrected in 0.4.75): identical `rule.enable`
|
|
57
|
+
// calls answered `alreadyEnabled:true` 14 times and each still minted a new
|
|
58
|
+
// rule version. The memo comes from the session log — but it MUST be
|
|
59
|
+
// invalidated by any write that could have changed the graph, including the
|
|
60
|
+
// ones the log cannot attribute (`rule.save --args-file`, `omc script`).
|
|
61
|
+
// r10 (2026-10-22) caught the 0.4.74 version answering `noop:true` right
|
|
62
|
+
// after a save, leaving the rule DISABLED while reporting ok.
|
|
63
|
+
//
|
|
64
|
+
// Placement matters: the check runs BEFORE the businessrule/get + publish +
|
|
65
|
+
// enable + readback round trip, and only when the platform's own list row
|
|
66
|
+
// already says `enabled` — a memo hit must never contradict live state.
|
|
61
67
|
const digest = currentUsageArgsDigest();
|
|
62
|
-
if (digest) {
|
|
63
|
-
const prior = priorIdenticalSuccess("call rule.enable", digest, {
|
|
68
|
+
if (digest && row.enabled === true) {
|
|
69
|
+
const prior = priorIdenticalSuccess("call rule.enable", digest, {
|
|
70
|
+
invalidators: [
|
|
71
|
+
"call rule.save",
|
|
72
|
+
"call rule.node.update",
|
|
73
|
+
"call rule.disable",
|
|
74
|
+
"call rule.delete",
|
|
75
|
+
"call configure_rule",
|
|
76
|
+
"capability rule-lifecycle",
|
|
77
|
+
"script -",
|
|
78
|
+
],
|
|
79
|
+
invalidateOnAny: true,
|
|
80
|
+
keyFields: ["schemaCode", "ruleCode"],
|
|
81
|
+
});
|
|
64
82
|
if (prior && prior.count > 0) {
|
|
65
83
|
return {
|
|
66
84
|
performed: { errcode: 0, alreadyEnabled: true, noop: true },
|
|
67
85
|
readback: {
|
|
68
86
|
enabled: true,
|
|
87
|
+
ruleStatus: row.status ?? null,
|
|
69
88
|
...(prior.requestIds.length > 0 ? { priorRequestIds: prior.requestIds } : {}),
|
|
70
|
-
nextAction: "本会话已用相同参数成功 enable 过该规则,且此后没有 rule.save;本次未发任何 wire
|
|
89
|
+
nextAction: "本会话已用相同参数成功 enable 过该规则,且此后没有 rule.save/rule.node.update/script;本次未发任何 wire 请求。改了图请先 rule.save,再 rule.enable(memo 会被 save 清空)",
|
|
71
90
|
},
|
|
72
91
|
};
|
|
73
92
|
}
|
|
74
93
|
}
|
|
94
|
+
// Publish must carry the FULL model, not the list-projection row. The list
|
|
95
|
+
// face (`list_by_schemacode`) omits `node`/`route`/`schedulerSetting`; for a
|
|
96
|
+
// TIMING_TASK rule that makes `BusinessRuleServiceImpl.updateTimerJob` see a
|
|
97
|
+
// null scheduler and early-return — the quartz job is NEVER created, so the
|
|
98
|
+
// rule reads back "enabled" but never fires (2026-10-06 live: rule.enable from
|
|
99
|
+
// the list row → no `create Job`, `qrtz_job_details` empty). Fetch the
|
|
100
|
+
// authoritative model via `businessrule/get?id` (same face `rule.get` uses),
|
|
101
|
+
// stamp enabled+status, then publish — which creates/reschedules the job.
|
|
102
|
+
const full = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(row.id) } })).data;
|
|
103
|
+
if (!full)
|
|
104
|
+
throw new WireError("business", `rule ${args.ruleCode} 完整模型读回为空(businessrule/get id ${row.id}),拒绝用列表投影发布`, 302024);
|
|
75
105
|
let published;
|
|
76
106
|
try {
|
|
77
107
|
published = await wireCall(session, { path: ENDPOINTS.businessRulePublish, method: "POST", body: { ...full, enabled: true, status: "ENABLED" } });
|
|
@@ -146,6 +176,60 @@ export function registerRuleLifecycleLane(tools) {
|
|
|
146
176
|
// N calls; this flips `enabled:false` on every matching rule in one tool. Only
|
|
147
177
|
// CUSTOM rules by default (platform preset trigger rows must not be disabled —
|
|
148
178
|
// they are platform-managed); `includePresets:true` opts in explicitly.
|
|
179
|
+
writeTool(tools, "rule.run", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.run"].scope, RULE_LIFECYCLE_TOOLS["rule.run"].description, {
|
|
180
|
+
schemaCode: z.string().min(1),
|
|
181
|
+
ruleCode: z.string().min(1),
|
|
182
|
+
ids: z.array(z.string().min(1)).min(1).describe("要执行的业务数据 id(data.load/data.list 的 bizObjectId / row.bizObjectId)——就是平台按钮点击时提交的 ids"),
|
|
183
|
+
consolidation: z.boolean().optional().describe("平台 body 字段,默认 false(按钮点击抓包值);合并执行场景才传 true"),
|
|
184
|
+
}, async (session, args) => {
|
|
185
|
+
await assertRuleSchemaExists(session, args.schemaCode);
|
|
186
|
+
const ids = [...new Set(args.ids.map(String))];
|
|
187
|
+
if (ids.length !== args.ids.length)
|
|
188
|
+
throw new ToolRefusalError({ status: "refused", blocker: "rule-run-ids-duplicated", message: "ids 有重复项——按去重后的列表执行,别让同一行在一条规则里被执行两次" });
|
|
189
|
+
const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
|
|
190
|
+
const row = wireRows(list.data).find((item) => String(item.code) === args.ruleCode);
|
|
191
|
+
if (!row)
|
|
192
|
+
throw new ToolRefusalError({ status: "refused", blocker: "rule-run-rule-absent", schemaCode: args.schemaCode, ruleCode: args.ruleCode, message: `模型 ${args.schemaCode} 没有规则 ${args.ruleCode}——先 rule.list 核对 code` });
|
|
193
|
+
if (row.defaultRule === true || PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode))
|
|
194
|
+
throw new ToolRefusalError({
|
|
195
|
+
status: "refused",
|
|
196
|
+
blocker: "rule-run-preset-rule",
|
|
197
|
+
ruleCode: args.ruleCode,
|
|
198
|
+
message: `${args.ruleCode} 是平台预置触发规则(defaultRule:true)——它由数据事件(新增/更新/删除/流程生效…)自动分发,设计器的按钮规则选择器不提供预置行。要手动跑请新建自定义规则(CUSTOM_NODE 触发)再用 button/rule.run`,
|
|
199
|
+
});
|
|
200
|
+
if (row.enabled !== true)
|
|
201
|
+
throw new ToolRefusalError({
|
|
202
|
+
status: "refused",
|
|
203
|
+
blocker: "rule-run-disabled",
|
|
204
|
+
ruleCode: args.ruleCode,
|
|
205
|
+
message: `规则 ${args.ruleCode} 当前未启用(enabled=${String(row.enabled)})——先 rule.enable(publish+enable 两步),否则设计器显示「失效·未启用」且按钮不会执行它`,
|
|
206
|
+
});
|
|
207
|
+
const performed = await wireCall(session, {
|
|
208
|
+
path: ruleRuntimeExecutePath(args.schemaCode, args.ruleCode),
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: { schemaCode: args.schemaCode, ids, ruleCode: args.ruleCode, consolidation: args.consolidation === true },
|
|
211
|
+
});
|
|
212
|
+
const payload = (performed.data ?? null);
|
|
213
|
+
const objects = Array.isArray(payload) ? payload : [];
|
|
214
|
+
return {
|
|
215
|
+
performed: { errcode: performed.errcode, ruleCode: args.ruleCode, ids },
|
|
216
|
+
readback: {
|
|
217
|
+
schemaCode: args.schemaCode,
|
|
218
|
+
ruleCode: args.ruleCode,
|
|
219
|
+
executedIds: ids,
|
|
220
|
+
returnedObjects: objects.length,
|
|
221
|
+
// 302045 = 数据被占用(前端把同样的码映射成"有人正在编辑")。
|
|
222
|
+
...(performed.errcode === 302045
|
|
223
|
+
? { dataInUse: true, hint: "302045:数据被占用(有人正在编辑该行 / 行锁未释放),稍后重试" }
|
|
224
|
+
: {}),
|
|
225
|
+
effectHint: "平台只回执行结果信封;返回的 data 是执行前快照(reacquireDataFromDb:false)。规则副作用必须另行读回:data.load {schemaCode, bizObjectId} 或 data.list(data.list 默认不含 DRAFT,草稿行请用 data.load)",
|
|
226
|
+
// NOT latestPublished: the platform's list projection reports
|
|
227
|
+
// latestPublished:false even for rows it executes fine (live
|
|
228
|
+
// 2026-10-23, documented in rule-support.ts) — a warning here would be
|
|
229
|
+
// noise. Liveness is `enabled`, which is gated above.
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}, undefined, undefined, undefined);
|
|
149
233
|
writeTool(tools, "rule.disableMany", RULE_LIFECYCLE_LANE, RULE_LIFECYCLE_TOOLS["rule.disableMany"].scope, RULE_LIFECYCLE_TOOLS["rule.disableMany"].description, {
|
|
150
234
|
schemaCode: z.string().min(1),
|
|
151
235
|
ruleCodes: z.array(z.string().min(1)).optional().describe("只停用这些 code;省略=该模型全部规则"),
|
|
@@ -18,6 +18,42 @@ export const WORKFLOW_EVENT_HANDLER_FIELDS = Object.freeze({
|
|
|
18
18
|
activate: "activateEventHandler",
|
|
19
19
|
end: "endEventHandler",
|
|
20
20
|
});
|
|
21
|
+
/**
|
|
22
|
+
* The eight NODE-level (per-activity) event handlers. Live 1.1.25 fidelity: every
|
|
23
|
+
* PARTICIPANT activity row carries this same ActivityEventHandler DTO
|
|
24
|
+
* `{messageDetails, receiver, …, bizActions, bizActionType}` under these exact
|
|
25
|
+
* keys (`workflow.get_published --full` readback, 2026-10-22 r10), alongside the
|
|
26
|
+
* flow-level `startEventHandler`/`cancelEventHandler`/… fields. START/END rows
|
|
27
|
+
* carry them as null. Binding a rule here means "when THIS node transitions",
|
|
28
|
+
* e.g. `endActivity` on the fill node = "申请人提交离开该节点时".
|
|
29
|
+
*
|
|
30
|
+
* The key and the field name are identical on purpose: the wire shape nests
|
|
31
|
+
* them on the activity object, so `activityCode` selects the row and `nodeEvent`
|
|
32
|
+
* selects the field inside it.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* `workflow.setButtons` writes the designer's 「表单操作」 checkboxes: each
|
|
36
|
+
* PARTICIPANT activity carries `buttonPermission.customBtns[]` naming the
|
|
37
|
+
* form-toolbar buttons enabled at that node. `ActionPermissionHandler`
|
|
38
|
+
* (setProcessButtonPermission) filters `getByTriggerTypeAndCode(FORM, formCode)`
|
|
39
|
+
* by this list — absent or enabled:false means the button does not render there.
|
|
40
|
+
*/
|
|
41
|
+
export const WORKFLOW_BUTTON_PERMISSION_WIRE = Object.freeze({
|
|
42
|
+
container: "activity.buttonPermission {containerCode, containerName, customBtns:[{code,name,name_i18n,enabled,type}], childTableBtns:[…]}",
|
|
43
|
+
filter: "ActionPermissionHandler.setProcessButtonPermission: customButtons.removeIf(c -> !customCodeSet.contains(c.getCode())) and enabled:false rows are dropped — a node that does not tick a button hides it",
|
|
44
|
+
source: "the button codes come from h_biz_button(triggerType=FORM, triggerCode=<formCode>), authored via form.publish {buttons:[…]} → bizsheet.options.customButtonJson",
|
|
45
|
+
merge: "per-node merge: listing a code enables it and adds a missing customBtns row (name taken from the form button); unlisted codes are set enabled:false; childTableBtns are preserved verbatim",
|
|
46
|
+
});
|
|
47
|
+
export const WORKFLOW_NODE_EVENT_HANDLER_FIELDS = Object.freeze({
|
|
48
|
+
beforeActivate: "beforeActivate",
|
|
49
|
+
afterActivate: "afterActivate",
|
|
50
|
+
endActivity: "endActivity",
|
|
51
|
+
cancelActivity: "cancelActivity",
|
|
52
|
+
activityApproved: "activityApproved",
|
|
53
|
+
activityRejected: "activityRejected",
|
|
54
|
+
jobSubmitted: "jobSubmitted",
|
|
55
|
+
jobRejected: "jobRejected",
|
|
56
|
+
});
|
|
21
57
|
export const WORKFLOW_WIRE = Object.freeze({
|
|
22
58
|
templateShape: '{"workflowCode":"<code>","activities":[{"id":"start","type":"START"},{"id":"approve","type":"PARTICIPANT","participant":"<user>"},{"id":"end","type":"END"}],"routes":[{"preNode":"start","postNode":"approve"}]}(ticket 112:routes 会被工具映射为平台 DTO 的 rules[preActivityCode/postActivityCode];templateType 勿传 "BusinessProcess"——非 WorkflowTemplateStatus 枚举成员,转换被吞成残缺模型后 50000;工具会把模板合并到 get_draft 全量行再上送,未给字段回显草稿)。条件分支连线见 routeBranch。',
|
|
23
59
|
/**
|
|
@@ -31,10 +67,10 @@ export const WORKFLOW_WIRE = Object.freeze({
|
|
|
31
67
|
*/
|
|
32
68
|
routeBranch: '条件分支连线(平台 Rule.java:28-59,routes[] 映射的目标 DTO):routes[] 条目可带 ruleConditionGroup = List<List<RuleConditionItem>>,外层 OR、内层 AND,满足才走该连线。RuleConditionItem = {"currentDataItem":"<fieldCode 或 childSchemaCode.fieldCode>","bizPropertyType":<BizPropertyType 的数字索引>,"conditionType":"<ConditionType 枚举名>","value":<任意>};**bizPropertyType 必须用数字索引**(平台 BizPropertyTypeDeserializer 读的是 int:SHORT_TEXT=0/NUMERICAL=2/DATE=3/LOGICAL=4/RADIO=12/DROPDOWN_BOX=14/STAFF_SELECTOR=50/DEPARTMENT_SELECTOR=60/WORK_SHEET=9/CHILD_TABLE=8…),写枚举名会被 castToInt 吞成 0 并让模板转换失败(真机报误导性的「流程名称为空!」);工具接受枚举名并自动转索引。**RuleConditionItem 只接受这 4 个键;多一个未知键(如照抄业务规则的 ruleValueType/targetDataItem)会让平台 workflow/update 返回误导性的 `401001 流程不存在!`**(真机 2026-09-25),工具写前拒绝未知键。conditionType 取值 EQ/NEQ/GT/LT/GTEQ/LTEQ/CT(包含)/NCT/EP(为空)/NEP(非空)/BETWEEN/NOTBETWEEN/OF/NOF/CT_ALL/NCT_ALL/CT_ANY/NCT_ANY/EQ_ANY/NEQ_ANY。子表字段(currentDataItem 含 ".")的行匹配由同级 childMatchType 决定:ALL=每行都满足,ANY=任一行满足(缺省 ANY)。连线另可带 text(显示名)、utilizeElse:true(else/兜底连线:其余条件不满足时走)、alternativeFlag:true(审批人可选分支)、formula(字符串表达式,与 ruleConditionGroup 二选一;两者都存在时结构化组先判,未命中再判 formula)。',
|
|
33
69
|
authoringChain: "workflow.create (header) → workflow.update (write draft template) → workflow.publish",
|
|
34
|
-
bindPoint: "startEventHandler|cancelEventHandler|activateEventHandler|endEventHandler{bizActions:[<ruleCode>], bizActionType:\"BIZ_RULE\"}",
|
|
35
|
-
bindRuleChain: "workflow.bindRule reads the draft, merges the handler selected by `handler` (start|cancel|activate|end, default end), workflow/update, then workflow/publish + published-face readback",
|
|
70
|
+
bindPoint: "startEventHandler|cancelEventHandler|activateEventHandler|endEventHandler{bizActions:[<ruleCode>], bizActionType:\"BIZ_RULE\"} (flow level; NODE level = activities[<activityCode>].{beforeActivate|afterActivate|endActivity|cancelActivity|activityApproved|activityRejected|jobSubmitted|jobRejected} with the same ActivityEventHandler shape)",
|
|
71
|
+
bindRuleChain: "workflow.bindRule reads the draft, merges the handler selected by `handler` (start|cancel|activate|end, default end — flow level) or by `activityCode` + `nodeEvent` (node level), workflow/update, then workflow/publish + published-face readback",
|
|
36
72
|
renderCompat: "workflow.update/publish/bindRule fail closed on 8.6.26 ActivityType values, unique non-empty activityCode values, rules references, and non-empty `x, y` points; missing points are derived from activity geometry and every write readback carries workflowRenderCompat with activities, rules, START/END, points, and danglingViolations",
|
|
37
|
-
bindRuleScope: "all four WorkflowTemplateModel event handlers (startEventHandler/cancelEventHandler/activateEventHandler/endEventHandler), each an ActivityEventHandler carrying bizActions + bizActionType=BIZ_RULE; `handler` defaults to end",
|
|
73
|
+
bindRuleScope: "flow level: all four WorkflowTemplateModel event handlers (startEventHandler/cancelEventHandler/activateEventHandler/endEventHandler), each an ActivityEventHandler carrying bizActions + bizActionType=BIZ_RULE; `handler` defaults to end. node level: the eight ActivityEventHandler fields on each activity row (beforeActivate/afterActivate/endActivity/cancelActivity/activityApproved/activityRejected/jobSubmitted/jobRejected), selected by `activityCode` + `nodeEvent`",
|
|
38
74
|
boundRuleMustBeEnabled: "流程事件绑定的规则**必须启用**:禁用/未发布的规则(rule.save 落 DRAFT/DISABLED 态)绑到事件处理器后设计器显示「失效·未启用」且事件不会执行它(真机 2026-10-08 SCM 沙盒)。workflow.bindRule 会先把绑定的自定义规则 publish+enable(rule.enable 链,回读 enabledRuleCodes/alreadyEnabledRuleCodes),并对不存在的 code 报 workflow-bind-rule-absent",
|
|
39
75
|
deleteAbsence: "workflow.delete posts /workflow/delete then confirms absence via workflow/get_header",
|
|
40
76
|
workItemPage: "GET /api/api/runtime/workflow/search_workitems returns PageVO<WorkItemVO> { totalPages, totalElements, content } directly (a page/data wrapper is tolerated)",
|
|
@@ -47,7 +83,7 @@ export const WORKFLOW_WIRE = Object.freeze({
|
|
|
47
83
|
/** Workflow template object (workflow/update|publish raw body). */
|
|
48
84
|
export const workflowTemplateArg = z
|
|
49
85
|
.record(z.string(), z.unknown())
|
|
50
|
-
.describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空(零成员/未挂部门/自由文本/角色码不存在)时引擎**把任务退回发起人**、流程继续,症状是「审批人成了我自己」,不是卡住;真正卡住的是参与人函数未注册(FunctionNotFoundException,无待办、PROCESSING、接口仍 errCode 0)。两者都用 workflow.instanceActivity 区分:无 status=1 节点=卡住;status=1 且参与人全是发起人=静默错派(r4-training 实证 2026-10-20)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template
|
|
86
|
+
.describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **连线写法**:routes(recipe)/ edges(同义)/ rules(平台 DTO 字段名,get_draft 原样读回)三选一,同传两种会被拒。**新节点必须带 type/activityType**(activityCode 命中草稿既有节点时可省)。**体积**:整模板含 propertyPermissions 常到 1.0–1.4MB,而 --args-file 上限 1MB(CLI_ARGS_MAX_BYTES=1048576)——只发改动节点/权限行,别回传整份正文。 **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空(零成员/未挂部门/自由文本/角色码不存在)时引擎**把任务退回发起人**、流程继续,症状是「审批人成了我自己」,不是卡住;真正卡住的是参与人函数未注册(FunctionNotFoundException,无待办、PROCESSING、接口仍 errCode 0)。两者都用 workflow.instanceActivity 区分:无 status=1 节点=卡住;status=1 且参与人全是发起人=静默错派(r4-training 实证 2026-10-20)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template(节点级事件用 bindRule 的 activityCode+nodeEvent)。`);
|
|
51
87
|
const WORKFLOW_SCOPE = "greenfield workflow authoring";
|
|
52
88
|
export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
53
89
|
lane: LANES.workflow,
|
|
@@ -73,7 +109,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
73
109
|
renderCompat: WORKFLOW_WIRE.renderCompat,
|
|
74
110
|
bindRuleScope: WORKFLOW_WIRE.bindRuleScope,
|
|
75
111
|
boundRuleMustBeEnabled: WORKFLOW_WIRE.boundRuleMustBeEnabled,
|
|
76
|
-
eventHandlerFields: "handler start→startEventHandler, cancel→cancelEventHandler, activate→activateEventHandler, end→endEventHandler (WorkflowTemplateModel.java:48-55)",
|
|
112
|
+
eventHandlerFields: "handler start→startEventHandler, cancel→cancelEventHandler, activate→activateEventHandler, end→endEventHandler (WorkflowTemplateModel.java:48-55); nodeEvent beforeActivate|afterActivate|endActivity|cancelActivity|activityApproved|activityRejected|jobSubmitted|jobRejected on activities[<activityCode>] (same field names, live 1.1.25)",
|
|
77
113
|
deleteAbsence: WORKFLOW_WIRE.deleteAbsence,
|
|
78
114
|
workItemPage: WORKFLOW_WIRE.workItemPage,
|
|
79
115
|
workItemIdentity: WORKFLOW_WIRE.workItemIdentity,
|
|
@@ -118,9 +154,13 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
118
154
|
scope: WORKFLOW_SCOPE,
|
|
119
155
|
description: "创建工作流模板头(`/api/api/workflow/create`)。随后用 `workflow.update` 写入模板 JSON,再 `workflow.publish` 发布。**平台会自动生成含发起节点的草稿**(START → PARTICIPANT「填写申请单」participant={Originator} → PARTICIPANT「审批」participant 空 → END)——审批参与人必须显式补平台关键字(如 `{Originator}`=发起人),留空行为未知;get_draft 可核对自动草稿形状。",
|
|
120
156
|
}),
|
|
157
|
+
"workflow.setButtons": Object.freeze({
|
|
158
|
+
scope: "workflow node form-button checkboxes",
|
|
159
|
+
description: `勾选/取消流程节点的**表单操作按钮**(设计器「表单操作」复选框)。每个 PARTICIPANT 活动用 \`buttonPermission.customBtns[]\` 列出该节点启用的表单按钮(${WORKFLOW_BUTTON_PERMISSION_WIRE.filter})。传 \`nodes:[{activityCode, buttonCodes:[…]}]\`:增量合并——列出的 code 置 enabled:true(缺行则从表单按钮补 name),未列出的置 enabled:false,\`childTableBtns\`(子表按钮)原样保留。\`buttonCodes\` 必须来自该模型已生成的表单按钮(\`form.get\` 的 \`formButtonCodes\`);不存在报 \`form-button-absent\` 并列出可用 code,可先用 \`form.publish {buttons:[…]}\` 建。默认 \`publish:true\`;\`publish:false\` 只写草稿。读回给每节点 enabled/disabled 清单,\`workflow.get_draft\` 紧凑面也带 \`formButtons\`。`,
|
|
160
|
+
}),
|
|
121
161
|
"workflow.update": Object.freeze({
|
|
122
162
|
scope: WORKFLOW_SCOPE,
|
|
123
|
-
description: "写入工作流模板草稿(`/api/api/workflow/update`,raw template JSON body)。cli 以当前草稿为基线做 merge,未列出的既有节点保留:**模板缺 START 或 END 时自动按增量 merge(部分模板),给全 START..END 才整模板替换**;也可显式 `merge:true/false`。**节点字段权限也走这里**:不必读 `get_draft --full`——直接发 `activities:[{activityCode, propertyPermissions:[{propertyCode, visible, editable, required, summary, subPropertyPermissions}]}]`(**type 可省**:activityCode 命中草稿节点时类型/名称继承草稿,不会铸 `_2` 新码;**改名=同 code 原地生效**——工具把新名写入 `name_i18n`(zh/zh_TW/en),activityName 平台侧保持首建值,读回以 `renamedInPlace.displayName` 为准;只有**类型变更**才铸新码并弃旧码),工具按 propertyCode 合并到草稿对应节点(未列的字段权限保留;`merge:true` 也可同用)——没有单独的权限工具,别把「找不到入口」当成不支持(dogfood 实测该误判会假阻塞)。`summary` **无需配置**:工具合并时自动补 `summary:true`(平台怪癖——缺省进不了运行时 `activitySummaries`、`form/load` 读不到你的 `editable:false`;显式传 `false` 才保留);子表字段用 `subPropertyPermissions`(同样自动补)。**子表属性不可设 `required:true`**(接口形状接受但设计器节点字段配置不支持,存入后无法渲染——工具直接拒 `workflow-child-table-not-required`);子表整行只配 visible/editable,字段级必填配到子表内部字段所在节点。**新增节点用语义 code**(如 `approval_finance`),不要模仿草稿的 `ActivityN` 再加 `_1/_2` 后缀。**设计器默认自动补齐**(调用方显式传值优先):填写节点({Originator})自动生成子表操作按钮 `buttonPermission.childTableBtns`(每个子表 ADD/DELETE/IMPORT/EXPORT 启用、BATCH_UPDATE 禁用——API 建的流程缺这份模板,设计器里子表按钮会全部不勾);`permittedAction` 归一为设计器默认(填写节点 showReject:false 不能驳回自己,审批节点 rejectToStart:true 驳回回发起节点)。用 `npm run test:live-permission`(非管理员参与者)验收,管理员会被后端强制 `e:true` 掩盖。",
|
|
163
|
+
description: "写入工作流模板草稿(`/api/api/workflow/update`,raw template JSON body)。cli 以当前草稿为基线做 merge,未列出的既有节点保留:**模板缺 START 或 END 时自动按增量 merge(部分模板),给全 START..END 才整模板替换**;也可显式 `merge:true/false`。**连线三种写法选一**:`routes`(recipe 写法)/ `edges`(同义)/ `rules`(平台 DTO 字段名,`get_draft` 读回原样)——同传两种会被拒(`workflow-routes-ambiguous`);merge 是并集(草稿边 + 你的边),**绝不删边**,若结果会比草稿少边则拒绝(`workflow-merge-would-drop-routes`,回 routesBefore/After + 缺失连线),dry-run 也会给出 `routesBefore/routesAfter/routesAdded/routesDropped`。**节点形状**:新增节点必须给 `type`/`activityType`(可选 START/PARTICIPANT/END…),`activityCode` 命中草稿既有节点时可省(类型/名称继承草稿);**字段权限只发改动行**——`propertyPermissions` 是载荷体积的主要来源(整模板单次常到 1.0–1.4MB,超过 `--args-file` 上限 `CLI_ARGS_MAX_BYTES`=1MB 会被拒),权限编辑只需 `activities:[{activityCode, propertyPermissions:[{propertyCode,…}]}]`。**节点字段权限也走这里**:不必读 `get_draft --full`——直接发 `activities:[{activityCode, propertyPermissions:[{propertyCode, visible, editable, required, summary, subPropertyPermissions}]}]`(**type 可省**:activityCode 命中草稿节点时类型/名称继承草稿,不会铸 `_2` 新码;**改名=同 code 原地生效**——工具把新名写入 `name_i18n`(zh/zh_TW/en),activityName 平台侧保持首建值,读回以 `renamedInPlace.displayName` 为准;只有**类型变更**才铸新码并弃旧码),工具按 propertyCode 合并到草稿对应节点(未列的字段权限保留;`merge:true` 也可同用)——没有单独的权限工具,别把「找不到入口」当成不支持(dogfood 实测该误判会假阻塞)。`summary` **无需配置**:工具合并时自动补 `summary:true`(平台怪癖——缺省进不了运行时 `activitySummaries`、`form/load` 读不到你的 `editable:false`;显式传 `false` 才保留);子表字段用 `subPropertyPermissions`(同样自动补)。**子表属性不可设 `required:true`**(接口形状接受但设计器节点字段配置不支持,存入后无法渲染——工具直接拒 `workflow-child-table-not-required`);子表整行只配 visible/editable,字段级必填配到子表内部字段所在节点。**新增节点用语义 code**(如 `approval_finance`),不要模仿草稿的 `ActivityN` 再加 `_1/_2` 后缀。**设计器默认自动补齐**(调用方显式传值优先):填写节点({Originator})自动生成子表操作按钮 `buttonPermission.childTableBtns`(每个子表 ADD/DELETE/IMPORT/EXPORT 启用、BATCH_UPDATE 禁用——API 建的流程缺这份模板,设计器里子表按钮会全部不勾);`permittedAction` 归一为设计器默认(填写节点 showReject:false 不能驳回自己,审批节点 rejectToStart:true 驳回回发起节点)。用 `npm run test:live-permission`(非管理员参与者)验收,管理员会被后端强制 `e:true` 掩盖。",
|
|
124
164
|
}),
|
|
125
165
|
"workflow.publish": Object.freeze({
|
|
126
166
|
scope: WORKFLOW_SCOPE,
|
|
@@ -128,7 +168,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
128
168
|
}),
|
|
129
169
|
"workflow.bindRule": Object.freeze({
|
|
130
170
|
scope: WORKFLOW_SCOPE,
|
|
131
|
-
description: "Bind a business rule to one of a workflow's event handlers (BIZ_RULE). `handler` selects the target: `start`→startEventHandler, `cancel`→cancelEventHandler, `activate`→activateEventHandler, `end`→endEventHandler (default `end`). Reads the current draft, merges `{ bizActions:[<ruleCode>], bizActionType:\"BIZ_RULE\" }` into the selected handler
|
|
171
|
+
description: "Bind a business rule to one of a workflow's event handlers (BIZ_RULE). **Flow level**: `handler` selects the target: `start`→startEventHandler, `cancel`→cancelEventHandler, `activate`→activateEventHandler, `end`→endEventHandler (default `end`). **Node level**(节点级事件,真机 1.1.25): pass `activityCode` + `nodeEvent` instead of `handler` — `nodeEvent` is one of `beforeActivate|afterActivate|endActivity|cancelActivity|activityApproved|activityRejected|jobSubmitted|jobRejected` (e.g. `{activityCode:\"Activity2\",nodeEvent:\"endActivity\"}` = 申请人提交离开该节点时). The two forms are mutually exclusive (`handler` 与 `activityCode/nodeEvent` 不能同传); `nodeEvent` without `activityCode` is refused, and a code absent from the draft lists the available codes. Reads the current draft, merges `{ bizActions:[<ruleCode>], bizActionType:\"BIZ_RULE\" }` into the selected handler(节点级写到 `activities[i][nodeEvent]`,其余模板字段原样保留), writes via `/api/api/workflow/update`, then publishes the template and reads back the published face to confirm the binding without overwriting the rest of the template. **A workflow-bound rule MUST be enabled**(真机 2026-10-08):禁用/未发布(rule.save 落 DRAFT/DISABLED)的规则绑上后设计器显示「失效·未启用」且事件不执行它;本工具先把绑定的自定义规则 publish+enable(回读 `enabledRuleCodes`/`alreadyEnabledRuleCodes`),不存在的 code 报 `workflow-bind-rule-absent`。The four flow handlers are isomorphic ActivityEventHandler fields on WorkflowTemplateModel, so when neither form is given the behaviour is identical to the original endEventHandler binding. Pass `publish:false` to leave the template draft-only.",
|
|
132
172
|
}),
|
|
133
173
|
"workflow.delete": Object.freeze({
|
|
134
174
|
scope: WORKFLOW_SCOPE,
|
|
@@ -152,7 +192,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
152
192
|
}),
|
|
153
193
|
"org.user.search": Object.freeze({
|
|
154
194
|
scope: "workflow participant resolution",
|
|
155
|
-
description: "按关键字搜索用户(`POST /api/api/organization/user/search`,body `{wd,page,size,filterType?}`;`wd` 必填,缺失报 10005
|
|
195
|
+
description: "按关键字搜索用户(`POST /api/api/organization/user/search`,body `{wd,page,size,filterType?}`;`wd` 必填,缺失报 10005 关键字不能为空;**空字符串也被工具参数校验拒绝(`String must contain at least 1 character`),无法用它列全员——列某部门的人请用 `org.department.users`,全量检索没有读面**),返回 {id,name,username,userId,departmentId,corpId,status}。用于把「张三」这类姓名解析成真实用户 id/username 再构造参与者对象(`[{\"code\":\"<username|userId>\",\"corpId\":\"<corpId|other>\",\"id\":\"<userId>\",\"name\":\"<姓名>\",\"type\":3}]`,type 3=用户 / 2=角色 / 多个用 `+` 连接);角色用 org.role.list、部门用 org.department.list。",
|
|
156
196
|
}),
|
|
157
197
|
"workitem.list": Object.freeze({
|
|
158
198
|
scope: "runtime workflow work-item query",
|
|
@@ -179,9 +219,10 @@ export function assertWorkflowContract() {
|
|
|
179
219
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.get_draft", ["get_draft", "endEventHandler"]);
|
|
180
220
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.get_published", ["get_published", "workflow.publish"]);
|
|
181
221
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.create", ["workflow/create", "workflow.update", "workflow.publish"]);
|
|
182
|
-
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.update", ["workflow/update", "template"]);
|
|
222
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.update", ["workflow/update", "template", "rules", "activityType", "propertyPermissions", "CLI_ARGS_MAX_BYTES", "workflow-merge-would-drop-routes"]);
|
|
183
223
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.publish", ["workflow/publish", "template"]);
|
|
184
|
-
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.
|
|
224
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.setButtons", ["buttonPermission", "customBtns", "childTableBtns", "formButtonCodes", "form-button-absent", "publish:false"]);
|
|
225
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.bindRule", ["startEventHandler", "cancelEventHandler", "activateEventHandler", "endEventHandler", "bizActions", "bizActionType", "BIZ_RULE", "publish:false", "enabledRuleCodes", "workflow-bind-rule-absent", "activityCode", "nodeEvent", "endActivity", "activityApproved"]);
|
|
185
226
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.role.list", ["organization/role/list", "type", "Originator"]);
|
|
186
227
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.department.list", ["organization/department/tree", "parentId"]);
|
|
187
228
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.user.search", ["organization/user/search", "wd", "type 3"]);
|
|
@@ -191,6 +232,12 @@ export function assertWorkflowContract() {
|
|
|
191
232
|
if (!(field.length > 0))
|
|
192
233
|
throw new Error("workflow event handler field names must be non-empty");
|
|
193
234
|
}
|
|
235
|
+
for (const [key, field] of Object.entries(WORKFLOW_NODE_EVENT_HANDLER_FIELDS)) {
|
|
236
|
+
if (key !== field)
|
|
237
|
+
throw new Error(`node-level event handler key ${key} must equal its activity field name (${field})`);
|
|
238
|
+
if (!WORKFLOW_WIRE.bindRuleScope.includes(key))
|
|
239
|
+
throw new Error(`workflow contract must document the node-level event handler ${key}`);
|
|
240
|
+
}
|
|
194
241
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.delete", ["workflow/delete", "缺失"]);
|
|
195
242
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.validate", ["workflow/validate", "ValidateMessage", "valid"]);
|
|
196
243
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "configure_workflow", ["workflow.create", "workflow.update", "workflow.publish", "workflow.bindRule", "bindRules"]);
|
|
@@ -7,6 +7,7 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
|
|
|
7
7
|
evidence: [
|
|
8
8
|
".scratch/omc-approval-workflow/acceptance/s29-probe-20260910012330184.md",
|
|
9
9
|
"packages/omc-cli/src/lanes/workflow.ts",
|
|
10
|
+
"r11 2026-10-23 (1.1.25): node form-button visibility = activity.buttonPermission.customBtns[].enabled; ActionPermissionHandler.setProcessButtonPermission filters FORM buttons by it (Playwright + backend source)",
|
|
10
11
|
],
|
|
11
12
|
tools: [
|
|
12
13
|
claim("workflow.get_draft", T["workflow.get_draft"].scope),
|
|
@@ -15,6 +16,7 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
|
|
|
15
16
|
claim("workflow.update", T["workflow.update"].scope),
|
|
16
17
|
claim("workflow.publish", T["workflow.publish"].scope),
|
|
17
18
|
claim("workflow.bindRule", T["workflow.bindRule"].scope),
|
|
19
|
+
claim("workflow.setButtons", T["workflow.setButtons"].scope),
|
|
18
20
|
claim("workflow.delete", T["workflow.delete"].scope),
|
|
19
21
|
claim("configure_workflow", T["configure_workflow"].scope),
|
|
20
22
|
claim("workflow.validate", T["workflow.validate"].scope),
|
|
@@ -45,6 +47,7 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
|
|
|
45
47
|
capability: "W7 S29 runtime workflow work-item query (search_workitems)",
|
|
46
48
|
evidence: [
|
|
47
49
|
"packages/omc-cli/src/lanes/workflow.ts",
|
|
50
|
+
"r11 2026-10-23 (1.1.25): node form-button visibility = activity.buttonPermission.customBtns[].enabled; ActionPermissionHandler.setProcessButtonPermission filters FORM buttons by it (Playwright + backend source)",
|
|
48
51
|
".scratch/omc-app-management/issues/06-workitem-list-capability.md",
|
|
49
52
|
],
|
|
50
53
|
tools: [claim("workitem.list", T["workitem.list"].scope), claim("workflow.instanceActivity", T["workflow.instanceActivity"].scope)],
|
|
@@ -54,6 +57,7 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
|
|
|
54
57
|
evidence: [
|
|
55
58
|
".scratch/omc-context-cost-round4/evidence/real-env-observation.md",
|
|
56
59
|
"packages/omc-cli/src/lanes/workflow.ts",
|
|
60
|
+
"r11 2026-10-23 (1.1.25): node form-button visibility = activity.buttonPermission.customBtns[].enabled; ActionPermissionHandler.setProcessButtonPermission filters FORM buttons by it (Playwright + backend source)",
|
|
57
61
|
],
|
|
58
62
|
tools: [claim("org.role.list", T["org.role.list"].scope), claim("org.department.list", T["org.department.list"].scope), claim("org.user.search", T["org.user.search"].scope)],
|
|
59
63
|
},
|