@stalfh233/omc-cli 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +304 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { wireCall, wireRows, WireError } from "./wire.js";
|
|
3
|
+
import { ENDPOINTS } from "./endpoints.js";
|
|
4
|
+
import { ToolRefusalError } from "./tool-types.js";
|
|
5
|
+
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
|
|
6
|
+
import { RULE_BIZ_RULE_TYPE_DESCRIPTION, RULE_CREATE_BODY, RULE_ROUTES_DESCRIPTION, ruleNodeGraphDescription, schedulerSettingDescription, } from "./lanes/businessrule-contract.js";
|
|
7
|
+
import { validateRuleNodeGraph, prepareRuleGraph, PLATFORM_TRIGGER_RULE_CODES } from "./lanes/rule-graph.js";
|
|
8
|
+
import { compileMethodConfigPlain } from "./bizservice-config.js";
|
|
9
|
+
import { RESERVED_CODES } from "./code-rules.js";
|
|
10
|
+
/** Business-rule node graph. Platform trigger rules are addressed by ruleCode: Create / Update / Delete / Available / Cancel / GetList / Load. */
|
|
11
|
+
export const ruleNodeGraphArg = z
|
|
12
|
+
.object({
|
|
13
|
+
nodes: z
|
|
14
|
+
.union([z.array(z.record(z.string(), z.unknown())), z.string()])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe(ruleNodeGraphDescription()),
|
|
17
|
+
routes: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional().describe(RULE_ROUTES_DESCRIPTION),
|
|
18
|
+
bizRuleType: z.number().optional().describe(RULE_BIZ_RULE_TYPE_DESCRIPTION),
|
|
19
|
+
schedulerSetting: z
|
|
20
|
+
.union([z.record(z.string(), z.unknown()), z.string()])
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(schedulerSettingDescription()),
|
|
23
|
+
})
|
|
24
|
+
.passthrough();
|
|
25
|
+
function declaredParameterCodes(decoded) {
|
|
26
|
+
if (!Array.isArray(decoded))
|
|
27
|
+
return [];
|
|
28
|
+
return decoded
|
|
29
|
+
.filter((entry) => Boolean(entry && typeof entry === "object"))
|
|
30
|
+
.map((entry) => (typeof entry.code === "string" ? entry.code.trim() : ""))
|
|
31
|
+
.filter((code) => code.length > 0);
|
|
32
|
+
}
|
|
33
|
+
/** inputParametersJson/outputParametersJson decode; policy: non-strings null-coalesce, unparseable passes through. */
|
|
34
|
+
function parseJsonColumn(value) {
|
|
35
|
+
return decodeJsonColumn(value, JSON_COLUMN_NULL_COALESCE);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ticket 35: the platform gates only SERVICE existence at rule update/publish
|
|
39
|
+
* (BusinessRuleFacadeImpl.addBizMethodMapping throws BUSINESS_SERVICE_NOT_EXIST),
|
|
40
|
+
* and the publish dry gate (550018, checkBizRuleIsEnable → filterProertyVo)
|
|
41
|
+
* rejects when a mapped serviceMethodParameterCode is missing from the method's
|
|
42
|
+
* declared parameter face (live-proven 2026-09-12: a parameterless registration
|
|
43
|
+
* shell + inputMappings → 550018 保存规则失败). This pre-write wire gate mirrors
|
|
44
|
+
* both checks: resolve every BIZ_ACTION / GET_LIST(BIZ_SERVICE) methodMapping
|
|
45
|
+
* against get_by_code + list_by_service_code (existence AND parameter face)
|
|
46
|
+
* before any write, so the caller learns it before burning the rule write.
|
|
47
|
+
*/
|
|
48
|
+
async function resolveBizMethodMappings(session, refs) {
|
|
49
|
+
const methodsByService = new Map();
|
|
50
|
+
const adapterByService = new Map();
|
|
51
|
+
const serviceNameByCode = new Map();
|
|
52
|
+
const serviceAbsent = new Set();
|
|
53
|
+
for (const serviceCode of new Set(refs.map((ref) => ref.serviceCode))) {
|
|
54
|
+
try {
|
|
55
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode } });
|
|
56
|
+
if (service.data == null)
|
|
57
|
+
serviceAbsent.add(serviceCode);
|
|
58
|
+
else {
|
|
59
|
+
adapterByService.set(serviceCode, String(service.data.adapterCode ?? ""));
|
|
60
|
+
serviceNameByCode.set(serviceCode, String(service.data.name ?? ""));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// get_by_code answers 500001 业务服务不存在 when the service is gone.
|
|
65
|
+
serviceAbsent.add(serviceCode);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode, filterFlag: "false" } })).data);
|
|
70
|
+
const byCode = new Map();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const code = String(row.code ?? "").trim();
|
|
73
|
+
if (!code)
|
|
74
|
+
continue;
|
|
75
|
+
byCode.set(code, { code, name: String(row.name ?? ""), inputParameters: parseJsonColumn(row.inputParametersJson), outputParameters: parseJsonColumn(row.outputParametersJson), configJson: parseJsonColumn(row.configJson) });
|
|
76
|
+
}
|
|
77
|
+
methodsByService.set(serviceCode, byCode);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Service present but the method list failed: keep the empty map so
|
|
81
|
+
// each ref is reported as method-absent rather than silently skipped.
|
|
82
|
+
methodsByService.set(serviceCode, new Map());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const unresolved = [];
|
|
86
|
+
const resolved = [];
|
|
87
|
+
for (const ref of refs) {
|
|
88
|
+
if (serviceAbsent.has(ref.serviceCode)) {
|
|
89
|
+
unresolved.push({ nodeCode: ref.nodeCode, serviceCode: ref.serviceCode, serviceMethodCode: ref.serviceMethodCode, reason: `service "${ref.serviceCode}" 不存在(get_by_code 500001;平台 update/publish 门只拦服务不拦方法——本预检是唯一 fail-closed 存在性门)` });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const method = methodsByService.get(ref.serviceCode)?.get(ref.serviceMethodCode);
|
|
93
|
+
if (!method) {
|
|
94
|
+
unresolved.push({ nodeCode: ref.nodeCode, serviceCode: ref.serviceCode, serviceMethodCode: ref.serviceMethodCode, reason: `method "${ref.serviceMethodCode}" 不在服务 ${ref.serviceCode} 的方法列表(list_by_service_code filterFlag=false;平台设计时不拦、app 包导入时静默 enabled=false——BusinessRuleFacadeImpl 只拦服务缺失)` });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const inputCodes = declaredParameterCodes(method.inputParameters);
|
|
98
|
+
const outputCodes = declaredParameterCodes(method.outputParameters);
|
|
99
|
+
const missingInput = ref.inputParameterCodes.filter((code) => !inputCodes.includes(code));
|
|
100
|
+
const missingOutput = ref.outputParameterCodes.filter((code) => !outputCodes.includes(code));
|
|
101
|
+
if (missingInput.length > 0 || missingOutput.length > 0) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
if (missingInput.length > 0)
|
|
104
|
+
parts.push(`inputMappings 参数 ${missingInput.join("/")} 不在方法声明的输入参数面(声明:${inputCodes.length ? inputCodes.join("/") : "空"})`);
|
|
105
|
+
if (missingOutput.length > 0)
|
|
106
|
+
parts.push(`outputMappings 参数 ${missingOutput.join("/")} 不在方法声明的输出参数面(声明:${outputCodes.length ? outputCodes.join("/") : "空"})`);
|
|
107
|
+
unresolved.push({ nodeCode: ref.nodeCode, serviceCode: ref.serviceCode, serviceMethodCode: ref.serviceMethodCode, reason: `${parts.join(";")}——平台 publish 干门同语义拒绝(550018,live 2026-09-12:无参数面注册壳 + inputMappings → 保存规则失败);service.register 用 methodInputParameters 声明参数面` });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
resolved.push({
|
|
111
|
+
nodeCode: ref.nodeCode,
|
|
112
|
+
serviceCode: ref.serviceCode,
|
|
113
|
+
serviceName: serviceNameByCode.get(ref.serviceCode) ?? "",
|
|
114
|
+
serviceMethodCode: ref.serviceMethodCode,
|
|
115
|
+
serviceMethodName: method.name,
|
|
116
|
+
adapterCode: adapterByService.get(ref.serviceCode) ?? "",
|
|
117
|
+
inputFace: parameterFaceRecords(method.inputParameters),
|
|
118
|
+
outputFace: parameterFaceRecords(method.outputParameters),
|
|
119
|
+
methodConfig: method.configJson,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return { unresolved, resolved, checked: refs.length };
|
|
123
|
+
}
|
|
124
|
+
/** Parameter face decoded into records (empty when the column is absent/garbage). */
|
|
125
|
+
function parameterFaceRecords(decoded) {
|
|
126
|
+
return Array.isArray(decoded) ? decoded.filter((entry) => Boolean(entry && typeof entry === "object")) : [];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Ticket 96 follow-up — designer-parity enrichment for BIZ_ACTION bindings.
|
|
130
|
+
* The platform rule designer's own save payload carries node-level
|
|
131
|
+
* `nodeCategory:"actionNode"` / `serviceType:"BIZ_METHOD"` and a
|
|
132
|
+
* `methodMapping` with `serviceName` / `serviceMethodName`, plus per-element
|
|
133
|
+
* `index`. The 绑定业务方法 dialog re-hydrates its mapping rows from this
|
|
134
|
+
* richer shape; a bare name-less binding renders the node as a generic
|
|
135
|
+
* 业务动作 and (transiently) as empty mapping rows. OMC resolves the
|
|
136
|
+
* service/method faces anyway for validation, so it fills the designer's
|
|
137
|
+
* denormalized display fields with what it already fetched. Values the caller
|
|
138
|
+
* supplied are never overwritten.
|
|
139
|
+
*/
|
|
140
|
+
function enrichBizActionBindings(nodes, resolved) {
|
|
141
|
+
const byNode = new Map(resolved.map((ref) => [ref.nodeCode, ref]));
|
|
142
|
+
const fillIndex = (list) => {
|
|
143
|
+
if (!Array.isArray(list))
|
|
144
|
+
return;
|
|
145
|
+
list.forEach((element, position) => {
|
|
146
|
+
if (!element || typeof element !== "object" || Array.isArray(element))
|
|
147
|
+
return;
|
|
148
|
+
const row = element;
|
|
149
|
+
if (row.index === undefined)
|
|
150
|
+
row.index = position + 1;
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
for (const entry of nodes) {
|
|
154
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
155
|
+
continue;
|
|
156
|
+
const node = entry;
|
|
157
|
+
if (String(node.nodeType ?? "") !== "BIZ_ACTION")
|
|
158
|
+
continue;
|
|
159
|
+
const ref = byNode.get(String(node.nodeCode ?? ""));
|
|
160
|
+
if (node.nodeCategory === undefined)
|
|
161
|
+
node.nodeCategory = "actionNode";
|
|
162
|
+
if (node.serviceType === undefined)
|
|
163
|
+
node.serviceType = "BIZ_METHOD";
|
|
164
|
+
if (!ref)
|
|
165
|
+
continue;
|
|
166
|
+
const mapping = node.methodMapping;
|
|
167
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping))
|
|
168
|
+
continue;
|
|
169
|
+
const m = mapping;
|
|
170
|
+
if ((m.serviceName === undefined || m.serviceName === "") && ref.serviceName)
|
|
171
|
+
m.serviceName = ref.serviceName;
|
|
172
|
+
if ((m.serviceMethodName === undefined || m.serviceMethodName === "") && ref.serviceMethodName)
|
|
173
|
+
m.serviceMethodName = ref.serviceMethodName;
|
|
174
|
+
fillIndex(m.inputMappings);
|
|
175
|
+
fillIndex(m.outputMappings);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Ticket 96 — registration-shell gate: a config-bearing adapter method
|
|
180
|
+
* (RESTFUL / DATA_SQL) must carry its adapter-required configJson keys to be
|
|
181
|
+
* bindable. A name-plus-parameter-face-only record is a registration shell
|
|
182
|
+
* (CONTEXT.md 业务集成注册壳): never bound by a rule. INTERNAL /
|
|
183
|
+
* extension-package methods carry no adapter config and stay bindable.
|
|
184
|
+
*/
|
|
185
|
+
function assertBindableBizMethodConfig(resolved) {
|
|
186
|
+
const shells = resolved
|
|
187
|
+
.filter((ref) => ref.adapterCode === "RESTFUL" || ref.adapterCode === "DATA_SQL")
|
|
188
|
+
.map((ref) => {
|
|
189
|
+
if (!ref.methodConfig || typeof ref.methodConfig !== "object" || Array.isArray(ref.methodConfig))
|
|
190
|
+
return { nodeCode: ref.nodeCode, serviceCode: ref.serviceCode, serviceMethodCode: ref.serviceMethodCode, adapterCode: ref.adapterCode, reason: `方法 configJson 缺失或非对象——只有名字/参数面的注册壳(CONTEXT.md 业务集成注册壳),不是可消费的 BizMethod` };
|
|
191
|
+
try {
|
|
192
|
+
compileMethodConfigPlain(ref.adapterCode, ref.methodConfig);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return { nodeCode: ref.nodeCode, serviceCode: ref.serviceCode, serviceMethodCode: ref.serviceMethodCode, adapterCode: ref.adapterCode, reason: `方法 configJson 不含 ${ref.adapterCode} 必需键(${error.message})` };
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
.filter((row) => row !== null);
|
|
200
|
+
if (shells.length > 0) {
|
|
201
|
+
throw new ToolRefusalError({
|
|
202
|
+
status: "refused",
|
|
203
|
+
blocker: "rule-graph-bizmethod-registration-shell",
|
|
204
|
+
message: "methodMapping 指向注册壳(无适配器配置的业务集成方法);注册壳不是已发布 BizMethod,绝不可被规则绑定",
|
|
205
|
+
shells,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Ticket 96 — `required` mirror: the method parameter face is the
|
|
211
|
+
* authoritative source for a mapping element's `required` (CONTEXT.md 方法
|
|
212
|
+
* 参数面). Caller-omitted → filled from the face; face key absent → the
|
|
213
|
+
* mapping stays absent (never coerced to false); caller-explicit value that
|
|
214
|
+
* contradicts the face → local refusal BEFORE any rule write.
|
|
215
|
+
*/
|
|
216
|
+
function mirrorRequiredFromParameterFace(nodes, resolved) {
|
|
217
|
+
const byNode = new Map(resolved.map((ref) => [ref.nodeCode, ref]));
|
|
218
|
+
const mismatches = [];
|
|
219
|
+
const mirrorList = (list, face, nodeCode, listKey) => {
|
|
220
|
+
if (!Array.isArray(list))
|
|
221
|
+
return;
|
|
222
|
+
list.forEach((element, index) => {
|
|
223
|
+
if (!element || typeof element !== "object" || Array.isArray(element))
|
|
224
|
+
return;
|
|
225
|
+
const row = element;
|
|
226
|
+
const paramCode = typeof row.serviceMethodParameterCode === "string" ? row.serviceMethodParameterCode.trim() : "";
|
|
227
|
+
const faceParam = face.find((candidate) => String(candidate.code ?? "").trim() === paramCode);
|
|
228
|
+
const faceRequired = faceParam?.required;
|
|
229
|
+
if (row.required === undefined) {
|
|
230
|
+
// Omitted → mirror the face; a face that does not declare stays absent.
|
|
231
|
+
if (typeof faceRequired === "boolean")
|
|
232
|
+
row.required = faceRequired;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (typeof row.required !== "boolean" || row.required !== faceRequired) {
|
|
236
|
+
mismatches.push({
|
|
237
|
+
nodeCode,
|
|
238
|
+
key: `methodMapping.${listKey}[${index}].required`,
|
|
239
|
+
message: `required=${JSON.stringify(row.required)} 与方法参数面${typeof faceRequired === "boolean" ? `声明的 required=${faceRequired}` : "(未声明 required)"}矛盾——参数面是权威(调用方省略则自动镜像;面缺席则映射保持缺席)`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
for (const entry of nodes) {
|
|
245
|
+
if (!entry || typeof entry !== "object")
|
|
246
|
+
continue;
|
|
247
|
+
const node = entry;
|
|
248
|
+
const ref = byNode.get(String(node.nodeCode ?? ""));
|
|
249
|
+
if (!ref)
|
|
250
|
+
continue;
|
|
251
|
+
const mapping = node.methodMapping;
|
|
252
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping))
|
|
253
|
+
continue;
|
|
254
|
+
const m = mapping;
|
|
255
|
+
mirrorList(m.inputMappings, ref.inputFace, ref.nodeCode, "inputMappings");
|
|
256
|
+
mirrorList(m.outputMappings, ref.outputFace, ref.nodeCode, "outputMappings");
|
|
257
|
+
}
|
|
258
|
+
if (mismatches.length > 0) {
|
|
259
|
+
throw new ToolRefusalError({
|
|
260
|
+
status: "refused",
|
|
261
|
+
blocker: "rule-graph-bizmethod-required-mismatch",
|
|
262
|
+
message: "methodMapping 的 required 与方法参数面矛盾(参数面是权威);修正映射或更新参数面后重试",
|
|
263
|
+
validation: mismatches,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function assertCreateSelectionCompatibility(session, schemaCode, nodes) {
|
|
268
|
+
const createNodes = nodes.filter((entry) => Boolean(entry && typeof entry === "object" && String(entry.nodeType ?? "") === "CREATE"));
|
|
269
|
+
if (createNodes.length === 0)
|
|
270
|
+
return;
|
|
271
|
+
const fields = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
272
|
+
const dictionaryFields = fields.filter((field) => {
|
|
273
|
+
const type = Number(field.type ?? field.propertyType);
|
|
274
|
+
if (![12, 13, 14, 15].includes(type))
|
|
275
|
+
return false;
|
|
276
|
+
const options = decodeJsonColumn(field.options, JSON_COLUMN_NULL_COALESCE);
|
|
277
|
+
return Boolean(options && typeof options === "object" && !Array.isArray(options) && options.optionsSet && typeof options.optionsSet === "object" && options.optionsSet.optionsType === "dictionary");
|
|
278
|
+
});
|
|
279
|
+
if (dictionaryFields.length === 0)
|
|
280
|
+
return;
|
|
281
|
+
const targets = new Set(dictionaryFields.map((field) => String(field.code ?? field.propertyCode ?? "")));
|
|
282
|
+
const touched = createNodes.flatMap((node) => Array.isArray(node.dataActions) ? node.dataActions.filter((action) => Boolean(action && typeof action === "object")) : []).filter((action) => targets.has(String(action.targetDataItem ?? "")));
|
|
283
|
+
if (touched.length > 0)
|
|
284
|
+
throw new ToolRefusalError({
|
|
285
|
+
status: "refused",
|
|
286
|
+
blocker: "rule-create-dictionary-selection-unsupported",
|
|
287
|
+
schemaCode,
|
|
288
|
+
fields: [...targets],
|
|
289
|
+
message: "已知不兼容组合:CREATE 节点直接写 dictionary-backed selection 字段不会可靠持久化;改用已验证的 UPDATE 节点写入,或先使用普通 selection/custom choices",
|
|
290
|
+
recovery: [{ code: "use-update-for-selection", action: "保留 CREATE 后接 UPDATE,令 UPDATE 节点写入字典选择字段;先 field.list(published:true) 核对字段" }],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function collectBizMethodRefs(nodes) {
|
|
294
|
+
const refs = [];
|
|
295
|
+
const mappingParameterCodes = (list) => Array.isArray(list)
|
|
296
|
+
? list
|
|
297
|
+
.filter((entry) => Boolean(entry && typeof entry === "object"))
|
|
298
|
+
.map((entry) => (typeof entry.serviceMethodParameterCode === "string" ? entry.serviceMethodParameterCode.trim() : ""))
|
|
299
|
+
.filter((code) => code.length > 0)
|
|
300
|
+
: [];
|
|
301
|
+
for (const entry of nodes) {
|
|
302
|
+
if (!entry || typeof entry !== "object")
|
|
303
|
+
continue;
|
|
304
|
+
const node = entry;
|
|
305
|
+
const isBizAction = String(node.nodeType ?? "") === "BIZ_ACTION";
|
|
306
|
+
const isGetListBizService = String(node.nodeType ?? "") === "GET_LIST" && String(node.dataSourceType ?? "") === "BIZ_SERVICE";
|
|
307
|
+
if (!isBizAction && !isGetListBizService)
|
|
308
|
+
continue;
|
|
309
|
+
const mapping = node.methodMapping;
|
|
310
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping))
|
|
311
|
+
continue;
|
|
312
|
+
const m = mapping;
|
|
313
|
+
const serviceCode = typeof m.serviceCode === "string" ? m.serviceCode.trim() : "";
|
|
314
|
+
const serviceMethodCode = typeof m.serviceMethodCode === "string" ? m.serviceMethodCode.trim() : "";
|
|
315
|
+
if (!serviceCode || !serviceMethodCode)
|
|
316
|
+
continue; // shape lint already blocked these
|
|
317
|
+
refs.push({
|
|
318
|
+
nodeCode: String(node.nodeCode ?? ""),
|
|
319
|
+
serviceCode,
|
|
320
|
+
serviceMethodCode,
|
|
321
|
+
inputParameterCodes: mappingParameterCodes(m.inputMappings),
|
|
322
|
+
outputParameterCodes: mappingParameterCodes(m.outputMappings),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return refs;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Round 21 live incident — fail-closed field-existence gate for BIZ_ACTION /
|
|
329
|
+
* GET_LIST(BIZ_SERVICE) mappings. A PROPERTY mapping (`codeType:1`) whose
|
|
330
|
+
* `bizCode` names a field that does not exist (or is not published) makes the
|
|
331
|
+
* platform's rule publish answer a DETAIL-LESS `550018 保存规则失败,规则存在
|
|
332
|
+
* 异常,请点击检测并修正!` — the real cause only surfaces via the designer's
|
|
333
|
+
* 检测 (`POST /api/app/businessrule/node_run_map → notExistPropertyObjectList`),
|
|
334
|
+
* which OMC's local dry-run did not mirror. Reproduced live 2026-10-01: a
|
|
335
|
+
* BIZ_ACTION mapping `bizCode:"omcprobe_title"` on a model where that field was
|
|
336
|
+
* never created → 550018; after field.create+field.publish the same rule
|
|
337
|
+
* published. This gate resolves the schema's PUBLISHED fields first and refuses
|
|
338
|
+
* on a missing `bizCode` — turning the opaque 550018 into an actionable local
|
|
339
|
+
* error (the platform requires the field to be PUBLISHED, not merely drafted).
|
|
340
|
+
*/
|
|
341
|
+
const RULE_RESERVED_MAPPING_CODES = RESERVED_CODES;
|
|
342
|
+
async function assertBizActionMappingFieldsExist(session, schemaCode, nodes) {
|
|
343
|
+
const rows = [];
|
|
344
|
+
for (const entry of nodes) {
|
|
345
|
+
if (!entry || typeof entry !== "object")
|
|
346
|
+
continue;
|
|
347
|
+
const node = entry;
|
|
348
|
+
const isBizAction = String(node.nodeType ?? "") === "BIZ_ACTION";
|
|
349
|
+
const isGetListBizService = String(node.nodeType ?? "") === "GET_LIST" && String(node.dataSourceType ?? "") === "BIZ_SERVICE";
|
|
350
|
+
if (!isBizAction && !isGetListBizService)
|
|
351
|
+
continue;
|
|
352
|
+
const mapping = node.methodMapping;
|
|
353
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping))
|
|
354
|
+
continue;
|
|
355
|
+
const m = mapping;
|
|
356
|
+
for (const listKey of ["inputMappings", "outputMappings"]) {
|
|
357
|
+
const list = m[listKey];
|
|
358
|
+
if (!Array.isArray(list))
|
|
359
|
+
continue;
|
|
360
|
+
for (const element of list) {
|
|
361
|
+
if (!element || typeof element !== "object" || Array.isArray(element))
|
|
362
|
+
continue;
|
|
363
|
+
const row = element;
|
|
364
|
+
if (Number(row.codeType) !== 1)
|
|
365
|
+
continue; // only PROPERTY mappings resolve to a schema field
|
|
366
|
+
const bizCode = typeof row.bizCode === "string" ? row.bizCode.trim() : "";
|
|
367
|
+
if (!bizCode)
|
|
368
|
+
continue; // missing bizCode is a shape-lint concern
|
|
369
|
+
rows.push({ nodeCode: String(node.nodeCode ?? ""), listKey, serviceMethodParameterCode: typeof row.serviceMethodParameterCode === "string" ? row.serviceMethodParameterCode : "", bizCode });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (rows.length === 0)
|
|
374
|
+
return;
|
|
375
|
+
const fields = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
376
|
+
const known = new Set(fields.map((field) => String(field.code ?? field.propertyCode ?? "").trim()).filter((code) => code.length > 0));
|
|
377
|
+
for (const code of RULE_RESERVED_MAPPING_CODES)
|
|
378
|
+
known.add(code);
|
|
379
|
+
const missing = rows.filter((row) => !known.has(row.bizCode));
|
|
380
|
+
if (missing.length > 0) {
|
|
381
|
+
throw new ToolRefusalError({
|
|
382
|
+
status: "refused",
|
|
383
|
+
blocker: "rule-graph-bizaction-mapping-field-missing",
|
|
384
|
+
schemaCode,
|
|
385
|
+
missing,
|
|
386
|
+
message: "methodMapping 的 PROPERTY(codeType:1) 映射 bizCode 指向该模型上不存在(或未发布)的字段——平台 publish 会只回一个无明细的 550018「规则存在异常」。先 field.create 建字段并 field.publish 发布,或把映射改到已发布的真实字段 code",
|
|
387
|
+
recovery: [{ code: "create-and-publish-mapped-field", action: "field.create 建该字段 → field.publish 发布 → 重试 rule.save;或先 field.list(published:true) 核对字段 code" }],
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Ticket 96 live incident fix — fail-closed schema-existence gate for every
|
|
393
|
+
* RULE WRITE. The platform's businessrule list/save face auto-creates the 7
|
|
394
|
+
* preset rules for ANY schemaCode (BusinessRuleFacadeImpl.listViewsBySchemaCode
|
|
395
|
+
* :179-185 → checkAndFillDefaultBusinessRule, BusinessRuleServiceImpl.java
|
|
396
|
+
* :315-344 — no model-existence check; publish gates the model only via 601003),
|
|
397
|
+
* so writing a rule against a phantom schema silently litters preset rows that
|
|
398
|
+
* the API cannot cleanly delete (each list re-fills them). This gate resolves
|
|
399
|
+
* the schema against the authoritative model list BEFORE the first rule wire
|
|
400
|
+
* call and refuses on miss — zero pollution.
|
|
401
|
+
*/
|
|
402
|
+
export async function assertRuleSchemaExists(session, schemaCode) {
|
|
403
|
+
// Authoritative per-schema probe: GET /bizmodels/get_all (BizSchemaController
|
|
404
|
+
// .java:615-633) resolves getBizSchemaBySchemaCode and answers
|
|
405
|
+
// SCHEMA_NOT_FOUNT_ERR (10011 业务模型不存在) on miss — the same facade
|
|
406
|
+
// primitive the rule faces rely on. The fake answers ok+null for an absent
|
|
407
|
+
// schema, so BOTH signals decode as a miss.
|
|
408
|
+
let missing = false;
|
|
409
|
+
try {
|
|
410
|
+
const probe = await wireCall(session, { path: ENDPOINTS.modelGet, query: { schemaCode } });
|
|
411
|
+
missing = probe.data == null;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 10011)
|
|
415
|
+
missing = true;
|
|
416
|
+
else
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
if (missing) {
|
|
420
|
+
throw new ToolRefusalError({
|
|
421
|
+
status: "refused",
|
|
422
|
+
blocker: "rule-schema-unresolved",
|
|
423
|
+
message: `schemaCode "${schemaCode}" 不是已存在的业务模型(bizmodels/get_all 10011/空)——平台的规则面会对任意 schemaCode 自动补建 7 条预置规则(list 即触发重灌,API 删除不可达),幻影 schema 必须在写前拒绝;请先 model.create 建模或改用真实模型编码`,
|
|
424
|
+
schemaCode,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export async function executeRuleSaveWrite(session, args) {
|
|
429
|
+
// Pre-flight dry lint (ticket 14): a mis-shaped graph must be refused BEFORE
|
|
430
|
+
// any wire call, with structured blockers locating the node/key — never
|
|
431
|
+
// silently forwarded to the platform (ticket 09 lesson).
|
|
432
|
+
//
|
|
433
|
+
// Preset trigger rules (Create/Update/Delete) route the trigger-row write
|
|
434
|
+
// through the platform system node; the adapter injects it when absent
|
|
435
|
+
// before linting, so the caller only authors their action nodes (ticket 17
|
|
436
|
+
// live finding: dropping SYSTEM_CREATE pinned the row to DRAFT).
|
|
437
|
+
//
|
|
438
|
+
// Ticket 80: a button/flow-triggered rule (explicit customTrigger signal)
|
|
439
|
+
// gets the platform's CUSTOM_NODE 「自定义按钮/流程触发」 system trigger node
|
|
440
|
+
// synthesized after START when the caller did not author one, and EVERY
|
|
441
|
+
// route leaves with explicit string coordinates (empty points break the
|
|
442
|
+
// designer canvas).
|
|
443
|
+
const graph = args.nodeGraph;
|
|
444
|
+
// Ticket 80 + CFG-02/CFG-26: ONE normalization shared with the write
|
|
445
|
+
// preflight (prepareRuleGraph), so preview and execute validate the same
|
|
446
|
+
// graph: preset system node → button/flow CUSTOM_NODE trigger → route shape.
|
|
447
|
+
const prepared = prepareRuleGraph(args.ruleCode, graph, args.customTrigger, args.schemaCode);
|
|
448
|
+
const preparedGraph = prepared.graph;
|
|
449
|
+
const blockers = [...prepared.warnings, ...validateRuleNodeGraph(preparedGraph, args.ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined)];
|
|
450
|
+
// Ticket 113: warning-severity findings (e.g. unknown mapping keys) do not
|
|
451
|
+
// gate the write; they surface in the readback as lintWarnings.
|
|
452
|
+
const lintWarnings = blockers.filter((blocker) => blocker.severity === "warning");
|
|
453
|
+
if (blockers.some((blocker) => blocker.severity !== "warning")) {
|
|
454
|
+
throw new ToolRefusalError({
|
|
455
|
+
status: "refused",
|
|
456
|
+
blocker: "rule-graph-validation-failed",
|
|
457
|
+
message: "nodeGraph 未过出门前校验(dry lint);按 validation 逐项修正,校验器不猜不改",
|
|
458
|
+
validation: blockers,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const nodes = preparedGraph.nodes ?? [];
|
|
462
|
+
const routes = preparedGraph.routes ?? [];
|
|
463
|
+
await assertCreateSelectionCompatibility(session, args.schemaCode, Array.isArray(nodes) ? nodes : []);
|
|
464
|
+
await assertBizActionMappingFieldsExist(session, args.schemaCode, Array.isArray(nodes) ? nodes : []);
|
|
465
|
+
// Ticket 96: phantom-schema gate — BEFORE the first businessrule wire call
|
|
466
|
+
// (the rule list itself auto-creates preset rows for unknown schemas).
|
|
467
|
+
await assertRuleSchemaExists(session, args.schemaCode);
|
|
468
|
+
// Ticket 35: wire existence gate for BizMethod bindings — runs after the
|
|
469
|
+
// local lint, before the create/update wire call (fail-closed; the platform
|
|
470
|
+
// itself only gates the SERVICE, never the METHOD, at design time).
|
|
471
|
+
const bizMethodRefs = collectBizMethodRefs(Array.isArray(nodes) ? nodes : []);
|
|
472
|
+
let bizMethodChecks = 0;
|
|
473
|
+
if (bizMethodRefs.length > 0) {
|
|
474
|
+
const { unresolved, resolved, checked } = await resolveBizMethodMappings(session, bizMethodRefs);
|
|
475
|
+
bizMethodChecks = checked;
|
|
476
|
+
if (unresolved.length > 0) {
|
|
477
|
+
throw new ToolRefusalError({
|
|
478
|
+
status: "refused",
|
|
479
|
+
blocker: "rule-graph-bizmethod-unresolved",
|
|
480
|
+
message: "nodeGraph 的 methodMapping 指向不存在的服务/方法(写前 wire 校验失败);绝不用注册壳冒充已发布 BizMethod",
|
|
481
|
+
unresolved,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
// Ticket 96: registration-shell gate (config-bearing adapters must carry
|
|
485
|
+
// their adapter-required configJson keys) runs BEFORE the required mirror
|
|
486
|
+
// so a shell is reported as a shell, not as a face contradiction.
|
|
487
|
+
assertBindableBizMethodConfig(resolved);
|
|
488
|
+
// Ticket 96: mirror `required` from the authoritative method parameter
|
|
489
|
+
// face onto the mapping elements (mutating the prepared graph in place),
|
|
490
|
+
// refusing an explicit value that contradicts the face.
|
|
491
|
+
mirrorRequiredFromParameterFace(Array.isArray(nodes) ? nodes : [], resolved);
|
|
492
|
+
// Ticket 96 follow-up: designer-parity enrichment (serviceName /
|
|
493
|
+
// serviceMethodName / nodeCategory / element index) so the platform rule
|
|
494
|
+
// designer's 绑定业务方法 dialog re-hydrates the stored binding.
|
|
495
|
+
enrichBizActionBindings(Array.isArray(nodes) ? nodes : [], resolved);
|
|
496
|
+
}
|
|
497
|
+
// Read bizRuleType from the PREPARED graph (not the raw args): prepareRuleGraph
|
|
498
|
+
// infers bizRuleType=2 for SCHEDULER/TIMING_TASK graphs, and that inferred face
|
|
499
|
+
// must reach the wire row (see lanes/rule-graph.ts prepareRuleGraph).
|
|
500
|
+
const bizRuleType = typeof preparedGraph.bizRuleType === "number" ? preparedGraph.bizRuleType : 0;
|
|
501
|
+
// SCHEDULER rules keep the schedule on the rule row (schedulerSetting), not
|
|
502
|
+
// on the node: normalize object → JSON string exactly like node/route.
|
|
503
|
+
const schedulerInput = preparedGraph.schedulerSetting ?? preparedGraph.scheduler;
|
|
504
|
+
const schedulerSetting = schedulerInput === undefined || schedulerInput === null
|
|
505
|
+
? undefined
|
|
506
|
+
: typeof schedulerInput === "string"
|
|
507
|
+
? schedulerInput
|
|
508
|
+
: JSON.stringify(schedulerInput);
|
|
509
|
+
const nodeJson = typeof nodes === "string" ? nodes : JSON.stringify(nodes);
|
|
510
|
+
const routeJson = typeof routes === "string" ? routes : JSON.stringify(routes);
|
|
511
|
+
const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
|
|
512
|
+
const existing = wireRows(list.data).find((row) => row.code === args.ruleCode);
|
|
513
|
+
const schedulerField = schedulerSetting === undefined ? {} : { schedulerSetting };
|
|
514
|
+
// Preset trigger rules (Create/Update/Delete/Load/GetList/Available/Cancel
|
|
515
|
+
// and 流程 code) are platform-managed: their `name` is a platform default
|
|
516
|
+
// (数据新增/数据更新/…). A caller's `name` must NEVER rename them — user logic
|
|
517
|
+
// is authored INTO the preset row, but its identity stays the platform's.
|
|
518
|
+
// Only CUSTOM rule codes take the caller's name. `existing` already carries
|
|
519
|
+
// the platform name, so the spread preserves it.
|
|
520
|
+
const isPresetRule = PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode);
|
|
521
|
+
const performed = existing?.id
|
|
522
|
+
? await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: { ...existing, ...(isPresetRule ? {} : { name: args.name }), node: nodeJson, route: routeJson, bizRuleType, ...schedulerField, enabled: existing.enabled === true, status: existing.status ?? "DISABLED" } })
|
|
523
|
+
: await wireCall(session, { path: ENDPOINTS.businessRuleCreate, method: "POST", body: { code: args.ruleCode, name: args.name, schemaCode: args.schemaCode, bizRuleType, node: nodeJson, route: routeJson, schedulerSetting: schedulerSetting ?? null, ...RULE_CREATE_BODY } });
|
|
524
|
+
// Ticket 62: a bare save only writes the DRAFT working row — the designer
|
|
525
|
+
// and the runtime read the PUBLISHED version, so "saved" ≠ "visible". An
|
|
526
|
+
// explicit publish:true pushes the new version out in the same call
|
|
527
|
+
// (businessrule/publish, the same face rule.enable drives); enabling the
|
|
528
|
+
// DRAFT switch stays a separate rule.enable concern.
|
|
529
|
+
const enabled = existing?.enabled === true;
|
|
530
|
+
// Ticket 80/109: updating the same ruleCode mints a NEW record id — the old
|
|
531
|
+
// id stays readable (rule.get by it shows the HISTORICAL graph). The
|
|
532
|
+
// readback therefore pins the CURRENT effective row id/version so callers
|
|
533
|
+
// never have to guess which row the designer now renders.
|
|
534
|
+
const versionNote = "同一 ruleCode 的更新会产生新规则记录 ID(旧 ID 仍可读、前端持旧 ID 显示旧图);readback.id 是当前生效行(rule.get 请用它,或以 rule.list 同编码且 latestPublished:true 的新 ID 为准)";
|
|
535
|
+
const currentRow = async () => wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data).find((item) => item.code === args.ruleCode);
|
|
536
|
+
if (args.publish === true) {
|
|
537
|
+
const row = await currentRow();
|
|
538
|
+
if (row?.id) {
|
|
539
|
+
// Publish the FULL model, not the list-projection row: `list_by_schemacode`
|
|
540
|
+
// omits `node`/`route`/`schedulerSetting`, so publishing it drops the
|
|
541
|
+
// schedule and a TIMING_TASK rule never gets a quartz job (2026-10-06). Use
|
|
542
|
+
// the authoritative `businessrule/get?id` model (same as rule.get/enable).
|
|
543
|
+
const full = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(row.id) } })).data;
|
|
544
|
+
await wireCall(session, { path: ENDPOINTS.businessRulePublish, method: "POST", body: full ?? row });
|
|
545
|
+
}
|
|
546
|
+
const effective = await currentRow();
|
|
547
|
+
// Live 8.6.26 (2026-09-17): businessrule/publish mints a version but does
|
|
548
|
+
// NOT flip a publish marker that this readback can see — the working row
|
|
549
|
+
// keeps templateType DRAFT and the list projection reports
|
|
550
|
+
// latestPublished:false even for the platform's own ENABLED presets. So
|
|
551
|
+
// `published` must be DERIVED from the effective row, never assumed; a
|
|
552
|
+
// hardcoded true made a draft rule look live and a workflow end-event
|
|
553
|
+
// binding on it was silently dead. Liveness itself is `rule.enable`.
|
|
554
|
+
const published = effective?.status === "ENABLED" || effective?.latestPublished === true || effective?.templateType === "PUBLISH";
|
|
555
|
+
return {
|
|
556
|
+
performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", publishRequested: true, ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
|
|
557
|
+
readback: {
|
|
558
|
+
rulePresent: true,
|
|
559
|
+
published,
|
|
560
|
+
enabled,
|
|
561
|
+
status: effective?.status,
|
|
562
|
+
...(lintWarnings.length > 0 ? { lintWarnings } : {}),
|
|
563
|
+
id: effective?.id ?? undefined,
|
|
564
|
+
ruleVersion: effective?.ruleVersion,
|
|
565
|
+
latestPublished: effective?.latestPublished,
|
|
566
|
+
...(existing?.id ? { versionNote } : {}),
|
|
567
|
+
...(enabled ? {} : { nextAction: "rule.enable(businessrule/publish + businessrule/enable 两步)后规则才会分发并对流程/按钮可见;publish:true 只写入版本" }),
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const effective = await currentRow();
|
|
572
|
+
return {
|
|
573
|
+
performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
|
|
574
|
+
readback: {
|
|
575
|
+
rulePresent: true,
|
|
576
|
+
published: false,
|
|
577
|
+
enabled,
|
|
578
|
+
...(lintWarnings.length > 0 ? { lintWarnings } : {}),
|
|
579
|
+
id: effective?.id ?? undefined,
|
|
580
|
+
ruleVersion: effective?.ruleVersion,
|
|
581
|
+
latestPublished: effective?.latestPublished,
|
|
582
|
+
...(existing?.id ? { versionNote } : {}),
|
|
583
|
+
nextAction: "rule.enable 发布后设计器才可见(businessrule/publish 出新版本 + 翻 DRAFT 启用开关);或 rule.save 传 publish:true 只发布",
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
//# sourceMappingURL=rule-support.js.map
|