@stalfh233/omc-cli 0.4.99 → 0.4.101
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/README.md +13 -1
- package/coverage/m1-coverage-manifest-v1.json +9 -3
- package/dist/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli-contract.js +5 -1
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +230 -9
- package/dist/goal-routes.js +159 -57
- package/dist/governed-execution.js +1 -1
- package/dist/lanes/b2-registration.js +20 -3
- package/dist/lanes/businessrule-contract.js +4 -0
- package/dist/lanes/businessrule-coverage.js +1 -1
- package/dist/lanes/businessrule.js +93 -0
- package/dist/lanes/field.js +10 -1
- package/dist/lanes/form.js +2 -2
- package/dist/lanes/report.js +21 -5
- package/dist/lanes/workflow.js +38 -3
- package/dist/recipes/b2-registration.js +8 -7
- package/dist/recipes/businessrule.js +58 -1
- package/dist/refusal-guidance.js +207 -0
- package/dist/rule-support.js +96 -0
- package/dist/skills.js +89 -21
- package/dist/tools.js +35 -0
- package/dist/usage-log.js +21 -2
- package/dist/write.js +2 -2
- package/docs/images/omc-nixi/README.md +41 -0
- package/docs/omc-introduction.md +851 -0
- package/package.json +3 -3
- package/skills/omc-capabilities.md +3 -3
- package/skills/omc-refusals.md +1 -1
- package/skills/omc-start-here.md +2 -1
- package/skills/omc-tool-selection.md +1 -1
- package/skills/omc-write-hazards.md +5 -0
package/dist/lanes/workflow.js
CHANGED
|
@@ -1686,6 +1686,34 @@ function sanitizeWorkflowSnapshotForReplay(row) {
|
|
|
1686
1686
|
}),
|
|
1687
1687
|
};
|
|
1688
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* 2026-10-27 用户需求:创建流程时流程名默认与表单名一致。
|
|
1691
|
+
*
|
|
1692
|
+
* 解析链(全部真实读回,不捏造):
|
|
1693
|
+
* ① 显式 name → "args"
|
|
1694
|
+
* ② 该模型默认表单的名字(bizsheet/get 的 name 字段)→ "form-name"
|
|
1695
|
+
* ③ 模型名(bizmodels/list_schema 行的 name)→ "model-name"
|
|
1696
|
+
* ④ 都读不到(模型不存在/无名字段)→ 拒绝:让调用方显式给 name,
|
|
1697
|
+
* 绝不拿 schemaCode 冒充流程名(占位符冒充合法值是形状红线)。
|
|
1698
|
+
*/
|
|
1699
|
+
async function resolveWorkflowName(session, explicitName, schemaCode) {
|
|
1700
|
+
if (typeof explicitName === "string" && explicitName.trim().length > 0)
|
|
1701
|
+
return { name: explicitName, resolvedFrom: "args" };
|
|
1702
|
+
const sheet = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode: schemaCode } }).catch(() => null);
|
|
1703
|
+
const sheetName = sheet?.data?.name;
|
|
1704
|
+
if (typeof sheetName === "string" && sheetName.trim().length > 0)
|
|
1705
|
+
return { name: sheetName, resolvedFrom: "form-name" };
|
|
1706
|
+
const listed = await wireCall(session, { path: ENDPOINTS.modelList, method: "POST", body: { schemaCodes: [schemaCode] } }).catch(() => null);
|
|
1707
|
+
const rows = wireRows(listed?.data);
|
|
1708
|
+
const modelName = rows.find((row) => String(row.schemaCode ?? "") === schemaCode)?.name;
|
|
1709
|
+
if (typeof modelName === "string" && modelName.trim().length > 0)
|
|
1710
|
+
return { name: modelName, resolvedFrom: "model-name" };
|
|
1711
|
+
throw new ToolRefusalError({
|
|
1712
|
+
status: "refused",
|
|
1713
|
+
blocker: "invalid-input",
|
|
1714
|
+
message: `流程 name 缺省解析失败:模型 ${schemaCode} 读不到表单名或模型名(模型不存在?)。显式传 name,或先确认模型存在(omc call model.list --args '{"filter":"<名或码>"}')`,
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1689
1717
|
function workflowRollbackSpec(toolName) {
|
|
1690
1718
|
return {
|
|
1691
1719
|
target: (args) => `workflow:${String(args.schemaCode ?? "")}/${String(args.code ?? args.workflowCode ?? "")}`,
|
|
@@ -2121,7 +2149,13 @@ export function registerWorkflowLane(tools) {
|
|
|
2121
2149
|
return { workflowCode, valid: errors.length === 0, errorCount: errors.length, warningCount: warnings.length, errors, warnings };
|
|
2122
2150
|
}, { target: `workflow.validate:${args.workflowCode ?? args.schemaCode}` }),
|
|
2123
2151
|
}));
|
|
2124
|
-
writeTool(tools, "workflow.create", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.create"].scope, WORKFLOW_TOOLS["workflow.create"].description, {
|
|
2152
|
+
writeTool(tools, "workflow.create", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.create"].scope, WORKFLOW_TOOLS["workflow.create"].description, {
|
|
2153
|
+
schemaCode: z.string().min(1),
|
|
2154
|
+
// 2026-10-27 用户需求:流程名默认与表单名一致。省略时的解析链见
|
|
2155
|
+
// resolveWorkflowName——表单名 → 模型名 → 拒绝(不拿 schemaCode 冒充)。
|
|
2156
|
+
name: z.string().min(1).optional().describe("流程名称;省略时默认 = 该模型默认表单的名字(无表单草稿时用模型名),与设计器行为一致"),
|
|
2157
|
+
code: z.string().min(1),
|
|
2158
|
+
}, async (session, args) => {
|
|
2125
2159
|
// Ticket 76 idempotent retry: get_header FIRST — a post-timeout
|
|
2126
2160
|
// re-create of the same code short-circuits with the existing header
|
|
2127
2161
|
// and ZERO create wires. On the happy path this read REPLACES the
|
|
@@ -2131,12 +2165,13 @@ export function registerWorkflowLane(tools) {
|
|
|
2131
2165
|
if (existingHeader != null) {
|
|
2132
2166
|
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existingHeader } };
|
|
2133
2167
|
}
|
|
2168
|
+
const { name: workflowName, resolvedFrom } = await resolveWorkflowName(session, args.name, args.schemaCode);
|
|
2134
2169
|
let performed;
|
|
2135
2170
|
try {
|
|
2136
2171
|
performed = await wireCall(session, {
|
|
2137
2172
|
path: ENDPOINTS.workflowCreate,
|
|
2138
2173
|
method: "POST",
|
|
2139
|
-
body: { workflowCode: args.code, workflowName
|
|
2174
|
+
body: { workflowCode: args.code, workflowName, schemaCode: args.schemaCode, icon: "h-icon-all-logic" },
|
|
2140
2175
|
});
|
|
2141
2176
|
}
|
|
2142
2177
|
catch (error) {
|
|
@@ -2163,7 +2198,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2163
2198
|
const draft = await getWorkflowDraftOrNull(session, args.code);
|
|
2164
2199
|
return {
|
|
2165
2200
|
performed: { errcode: performed.errcode },
|
|
2166
|
-
readback: { workflowCode: args.code, headerPresent: true, draftPresent: draft != null, templateId: draft?.id ?? null },
|
|
2201
|
+
readback: { workflowCode: args.code, name: workflowName, nameResolvedFrom: resolvedFrom, headerPresent: true, draftPresent: draft != null, templateId: draft?.id ?? null },
|
|
2167
2202
|
};
|
|
2168
2203
|
}, undefined, undefined, workflowRollbackSpec("workflow.create"));
|
|
2169
2204
|
writeTool(tools, "workflow.update", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.update"].scope, WORKFLOW_TOOLS["workflow.update"].description, { schemaCode: z.string().min(1), code: z.string().min(1), template: workflowTemplateArg, merge: z.boolean().optional().describe("增量:只发改动/新增的活动与路由,工具按 activityCode 合并到草稿(草稿为基;新增节点自动接链),不必先读回整模板再重发。默认自动:模板缺 START 或 END 时按增量 merge(部分模板),给了完整 START..END 才整模板替换;显式 true 强制增量、false 强制替换。merge 是并集、绝不删边:结果比草稿少边会被拒(workflow-merge-would-drop-routes);--dry-run 会返回 routesBefore/routesAfter/routesAdded/routesDropped") }, async (session, args) => {
|
|
@@ -16,14 +16,15 @@ const restfulQueryService = Object.freeze({
|
|
|
16
16
|
},
|
|
17
17
|
methodName: "querySupplier",
|
|
18
18
|
methodConfig: { httpType: "Post", requestBodyType: "JSON", url: "/supplier/query", frontEndExtensionEnable: true },
|
|
19
|
-
methodInputParameters: [{ code: "keyword", name: "关键字", required: true, configJson: { position: "REQUEST_BODY" } }],
|
|
20
|
-
methodOutputParameters: [{ code: "name", name: "供应商名称", configJson: { position: "REQUEST_BODY" } }],
|
|
19
|
+
methodInputParameters: [{ code: "keyword", name: "关键字", required: true, bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
20
|
+
methodOutputParameters: [{ code: "name", name: "供应商名称", bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
21
21
|
},
|
|
22
22
|
preconditions: ["serviceCategoryId 由 service.category.list 解析(「全部」是空 id 虚拟项不可用);RESTFUL 服务 configJson 需要 auth 与 commonlist[url];带 methodName 时必须有 methodConfig"],
|
|
23
23
|
conditionalRequirements: [
|
|
24
24
|
"RESTFUL auth.code ∈ customer(parameterlist=[]) / basic(client… username/password) / oauth_password / oauth_client;parameterlist 每项 {code,value} 且必须与 auth.code 期望参数完全一致",
|
|
25
25
|
"RESTFUL 方法 configJson 必需键:httpType(Post/Get/Put/Patch/Delete)、requestBodyType(Form-urlencoded/JSON/Text)、url、frontEndExtensionEnable(布尔)",
|
|
26
26
|
"RESTFUL 每个输入/输出参数必须带 configJson.position ∈ QUERY_PARAMS/REQUEST_HEADER/REQUEST_BODY/PATH_PARAMS(URL_VARIABLE 被运行时拒 501007)",
|
|
27
|
+
"每个输入/输出参数必须带 bizPropertyType(如 SHORT_TEXT/NUMERICAL)——缺它提交即被 bizservice-parameter-face-invalid 拒(r19-p7 首拒根因,2026-10-26 修正本配方)",
|
|
27
28
|
"一次写只建一个方法;给既有服务加方法走 method.create(escape hatch)",
|
|
28
29
|
],
|
|
29
30
|
priorToolReferences: [
|
|
@@ -92,10 +93,10 @@ const methodCreateRestfulGet = Object.freeze({
|
|
|
92
93
|
name: "供应商查询",
|
|
93
94
|
methodConfig: { httpType: "Get", requestBodyType: "JSON", url: "/openapi/supplier/query", frontEndExtensionEnable: true },
|
|
94
95
|
methodInputParameters: [
|
|
95
|
-
{ code: "keyword", name: "关键字", required: true, configJson: { position: "QUERY_PARAMS" } },
|
|
96
|
-
{ code: "x-lowcode-corpId", name: "企业ID(平台注入常量)", required: false, codeType: 0, bizCode: "<corpId>", configJson: { position: "REQUEST_HEADER" } },
|
|
96
|
+
{ code: "keyword", name: "关键字", required: true, bizPropertyType: "SHORT_TEXT", configJson: { position: "QUERY_PARAMS" } },
|
|
97
|
+
{ code: "x-lowcode-corpId", name: "企业ID(平台注入常量)", required: false, bizPropertyType: "SHORT_TEXT", codeType: 0, bizCode: "<corpId>", configJson: { position: "REQUEST_HEADER" } },
|
|
97
98
|
],
|
|
98
|
-
methodOutputParameters: [{ code: "name", name: "供应商名称", configJson: { position: "REQUEST_BODY" } }],
|
|
99
|
+
methodOutputParameters: [{ code: "name", name: "供应商名称", bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
99
100
|
},
|
|
100
101
|
preconditions: ["服务已存在(method.create 只加方法);methodCode 未存在;RESTFUL 每个入参与出参都带 configJson.position"],
|
|
101
102
|
conditionalRequirements: [
|
|
@@ -127,8 +128,8 @@ const methodCreateRestfulPost = Object.freeze({
|
|
|
127
128
|
methodCode: "saveSupplier",
|
|
128
129
|
name: "供应商保存",
|
|
129
130
|
methodConfig: { httpType: "Post", requestBodyType: "JSON", url: "/openapi/supplier/save", frontEndExtensionEnable: true },
|
|
130
|
-
methodInputParameters: [{ code: "body", name: "供应商对象", required: true, configJson: { position: "REQUEST_BODY" } }],
|
|
131
|
-
methodOutputParameters: [{ code: "id", name: "主键", configJson: { position: "REQUEST_BODY" } }],
|
|
131
|
+
methodInputParameters: [{ code: "body", name: "供应商对象", required: true, bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
132
|
+
methodOutputParameters: [{ code: "id", name: "主键", bizPropertyType: "SHORT_TEXT", configJson: { position: "REQUEST_BODY" } }],
|
|
132
133
|
},
|
|
133
134
|
preconditions: ["服务已存在;methodCode 未存在;POST 入参 position=REQUEST_BODY,requestBodyType=JSON"],
|
|
134
135
|
conditionalRequirements: [
|
|
@@ -255,6 +255,7 @@ const ruleCrossModelCreate = Object.freeze({
|
|
|
255
255
|
"上游是 QUERY 时 `inputParam=\"_<QUERY节点code>_output\"`(QUERY 查 N 行→CREATE 逐条生成即 N→N 扇出,真机验证支持,同/跨模型均可);否则 `\"_input\"`。",
|
|
256
256
|
"`QUERY` 节点只能查**主模型行**:查子表 schema(`parent.child` 或子 schema 名)空条件返回 0 行、带条件 550018(真机 2026-10-08)——逐子表明细扇出才需要二开。",
|
|
257
257
|
"跨模型 CREATE/UPDATE 真机验证支持(2026-09-28 / 2026-10-08);承载规则的触发时机要对(Create 在提交时触发、Update 只在 data.update 触发、TIMING 定时触发)。",
|
|
258
|
+
"**CREATE 目标是已发布流程的模型时**(2026-10-27 真机四格矩阵):不传 startWorkflow → 建行**不发起流程**;要发起必须 startWorkflow:true + targetWorkflowCode:<该流程> + originatorValue 用户对象数组 [{\"id\":\"<userId>\",\"name\":\"<名>\"}](传对象 {} → 触发提交 50000 expect '[';无效 id → 50000 发起人不存在);设计器手工编辑可打开该开关,API 与设计器配置会漂移,rule.get 回读核对。",
|
|
258
259
|
],
|
|
259
260
|
priorToolReferences: [
|
|
260
261
|
{ tool: "field.list", args: { environment, schemaCode: "<targetModel>", published: true }, purpose: "确认目标模型/字段已发布" },
|
|
@@ -269,7 +270,63 @@ const ruleCrossModelCreate = Object.freeze({
|
|
|
269
270
|
],
|
|
270
271
|
requiresEnvironmentDerivedIds: false,
|
|
271
272
|
});
|
|
273
|
+
/**
|
|
274
|
+
* r19 p7(2026-10-26):「Create 规则调用二开服务回写」在真机上全链路走通,但
|
|
275
|
+
* agent 为 methodMapping 形状 describe 了 rule.save 4 次(最大 16.8KB)——五条
|
|
276
|
+
* 既有配方里只有 data-duplicate-flag 带单向 inputMappings。本配方以 p7 的真实
|
|
277
|
+
* 成功参数为模板(输入 cname→来源字段、输出 level→目标字段,codeType:1),
|
|
278
|
+
* 双向映射一步可抄。
|
|
279
|
+
*/
|
|
280
|
+
const ruleBizactionInvokeService = Object.freeze({
|
|
281
|
+
id: "rule-bizaction-invoke-service",
|
|
282
|
+
title: "Create 规则调用服务方法并映射回写(BIZ_ACTION 双向)",
|
|
283
|
+
aliases: ["服务方法", "调用服务", "BIZ_ACTION", "回写服务", "查服务回写"],
|
|
284
|
+
status: "requires-prior-readback",
|
|
285
|
+
tool: "rule.save",
|
|
286
|
+
args: {
|
|
287
|
+
environment,
|
|
288
|
+
schemaCode: model,
|
|
289
|
+
ruleCode: "Create",
|
|
290
|
+
name: "数据新增",
|
|
291
|
+
nodeGraph: {
|
|
292
|
+
nodes: [
|
|
293
|
+
{ nodeCode: "Start", nodeName: "开始", nodeType: "START" },
|
|
294
|
+
{ nodeCode: "A1", nodeName: "调用服务并回写", nodeType: "BIZ_ACTION", inputParam: "_input", methodMapping: { schemaCode: model, serviceCode: "<svc>", serviceMethodCode: "<method>", inputMappings: [{ serviceMethodParameterCode: "<in_code>", codeType: 1, bizCode: "<src_field>", required: true }], outputMappings: [{ serviceMethodParameterCode: "<out_code>", codeType: 1, bizCode: "<dst_field>" }] } },
|
|
295
|
+
{ nodeCode: "Create", nodeName: "新增数据", nodeType: "SYSTEM_CREATE", isSystemNode: true },
|
|
296
|
+
{ nodeCode: "End", nodeName: "结束", nodeType: "END" },
|
|
297
|
+
],
|
|
298
|
+
routes: [
|
|
299
|
+
{ preNode: "Start", postNode: "A1", routeCondition: true },
|
|
300
|
+
{ preNode: "A1", postNode: "Create", routeCondition: true },
|
|
301
|
+
{ preNode: "Create", postNode: "End", routeCondition: true },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
publish: true,
|
|
305
|
+
},
|
|
306
|
+
preconditions: [
|
|
307
|
+
"模型与来源/目标字段已发布;服务与方法已注册(service.register 的 readback.method 直接给参数 code)",
|
|
308
|
+
"方法出参类型(bizPropertyType)与目标字段类型匹配",
|
|
309
|
+
],
|
|
310
|
+
conditionalRequirements: [
|
|
311
|
+
"codeType:1 = 映射模型字段(bizCode=字段 code);codeType:0 = 常量(bizCode=常量值);serviceMethodParameterCode 必须与方法声明的参数 code 逐字一致",
|
|
312
|
+
"**接口响应必须在 JSON 根级**返回与出参 code 同名的键——嵌套在信封里(如 {data:{level}})会静默映射不上(p7 实测)",
|
|
313
|
+
"**平台自调自己(127.0.0.1)会被运行时鉴权拦**:errcode 500012「绑定的API需要提供正确的身份验证或凭据」——需要带出站凭据的真实外部服务,或服务端配置鉴权放行;这不是 BIZ_ACTION 不支持(p7 定案)",
|
|
314
|
+
"BIZ_ACTION 放在 SYSTEM_CREATE **之前**(与自更新 UPDATE 相反:ASSIGN/BIZ_ACTION 在系统节点前,UPDATE 在系统节点后)",
|
|
315
|
+
],
|
|
316
|
+
priorToolReferences: [
|
|
317
|
+
{ tool: "service.register", args: { environment }, purpose: "注册服务+方法;readback.method 内联参数面(code/bizPropertyType),不必再 service.get" },
|
|
318
|
+
],
|
|
319
|
+
dryRun: { supported: true, command: `omc call rule.save --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 跑图 lint + 映射键校验(schemaCode/serviceCode/serviceMethodCode 非空、codeType 枚举);服务参数面一致性真机 save 才核" },
|
|
320
|
+
expectedReadback: { readback: { published: true, enabled: false, next: "rule.enable → data.submit 触发 → data.load 读回目标字段" } },
|
|
321
|
+
refusalRecovery: [
|
|
322
|
+
{ code: "rule-graph-bizmethod-unresolved", action: "serviceCode/serviceMethodCode 必须是已注册服务(service.list);方法级详情 service.get" },
|
|
323
|
+
{ code: "rule-graph-bizaction-mapping-field-missing", action: "inputMappings 的 bizCode 必须是本模型已发布字段 code(field.list 核对)" },
|
|
324
|
+
...commonRecovery,
|
|
325
|
+
],
|
|
326
|
+
notes: "p7 实测链(sandbox-r19/.scratch/report-p7.md §4):注册查询服务 → 本配方建 Create 规则 → rule.enable → 提交触发。运行期若 500012 见 conditionalRequirements 第 3 条(出站凭据边界)",
|
|
327
|
+
requiresEnvironmentDerivedIds: true,
|
|
328
|
+
});
|
|
272
329
|
export const BUSINESSRULE_METADATA = Object.freeze({
|
|
273
|
-
"rule.save": Object.freeze({ recipes: [ruleGraph, ruleBranchGraph, ruleTimingTask, dataDuplicateFlag, ruleCrossModelCreate], relatedTools: ["field.list", "rule.get", "rule.enable"] }),
|
|
330
|
+
"rule.save": Object.freeze({ recipes: [ruleGraph, ruleBranchGraph, ruleTimingTask, dataDuplicateFlag, ruleCrossModelCreate, ruleBizactionInvokeService], relatedTools: ["field.list", "rule.get", "rule.enable", "service.get"] }),
|
|
274
331
|
});
|
|
275
332
|
//# sourceMappingURL=businessrule.js.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape-family refusals: the payload was malformed in a way the CLI can see
|
|
3
|
+
* offline. Each cites the recipe that carries the correct shape.
|
|
4
|
+
*/
|
|
5
|
+
const SHAPE_GATES = Object.freeze({
|
|
6
|
+
"relation-single-array-shape": {
|
|
7
|
+
whatWentWrong: "单选关联字段(propertyType 9)传了数组",
|
|
8
|
+
correctShape: "单选关联只接受 id 字符串或 {id} 对象:{\"字段\":\"<关联行id>\"} 或 {\"字段\":{\"id\":\"<id>\"}};多选关联(11)/人员(50)/组织(51)才用数组",
|
|
9
|
+
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
10
|
+
doNot: "不要对单选关联继续传数组重试——平台稳定回 50000,不是偶发",
|
|
11
|
+
},
|
|
12
|
+
"submit-date-shape": {
|
|
13
|
+
whatWentWrong: "日期字段的值形状不对(毫秒时间戳会落库 NULL)",
|
|
14
|
+
correctShape: "字符串且匹配字段 format:日期字段 \"YYYY-MM-DD\";日期时间字段 ISO8601 带时区 \"2026-01-01T10:00:00+08:00\"",
|
|
15
|
+
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
16
|
+
doNot: "不要传毫秒数(如 1790000000000)——不报错但落库 NULL,读回才发现",
|
|
17
|
+
},
|
|
18
|
+
"submit-child-rows-missing-id": {
|
|
19
|
+
whatWentWrong: "更新已有记录时,子表行没带 id",
|
|
20
|
+
correctShape: "更新时子表行必须带平台 id(新增行才省略);无 id 的行会被当**新增**插入,不判重",
|
|
21
|
+
command: undefined,
|
|
22
|
+
recipeCommand: { tool: "data.update", recipe: "plain-data-submit" },
|
|
23
|
+
doNot: "不要用同一批无 id 子行重试 update——每次都会再插一遍(r17 实测 2 行变 8 行)",
|
|
24
|
+
},
|
|
25
|
+
"submit-approval-agree-misplaced": {
|
|
26
|
+
whatWentWrong: "agree 放在了 approval 对象里,平台只读顶层 agree",
|
|
27
|
+
correctShape: "通过/驳回用顶层参数:{\"agree\": true|false, \"approvalNote\": \"意见\"};approval 对象只是评论载体(content/relUsers)",
|
|
28
|
+
recipeCommand: { tool: "data.submit", recipe: "workflow-backed-data-submit" },
|
|
29
|
+
doNot: "approval.agree 会被静默忽略且仍按通过处理(r18b 真机:agree:false 照样 APPROVE)——原地重试必然得到同样的错误结果",
|
|
30
|
+
},
|
|
31
|
+
"submit-approval-opinion-misplaced": {
|
|
32
|
+
whatWentWrong: "opinion 放在了 approval 对象里,平台不读它",
|
|
33
|
+
correctShape: "审批意见用顶层 approvalNote;approval.content 是评论内容,不是审批意见",
|
|
34
|
+
recipeCommand: { tool: "data.submit", recipe: "workflow-backed-data-submit" },
|
|
35
|
+
doNot: "改 approval.opinion 重试无意义——字段被静默忽略",
|
|
36
|
+
},
|
|
37
|
+
"aggregation-self-schema-cycle": {
|
|
38
|
+
whatWentWrong: "聚合目标是自己的子表——平台 publishFormula 必然判环(checkCycleOfAggregation:1032),无条件 305003",
|
|
39
|
+
correctShape: "聚合 schemaCode 指向**另一个模型**的主表标量列;要子表合计先物化到来源主表再聚合",
|
|
40
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
41
|
+
doNot: "同模型子表聚合是平台死路(8.6.26 与 1.1.25 逐行相同),改 payload 无用;也不要删平台自动生成的 t100 CALCULATE_RULE 镜像行",
|
|
42
|
+
},
|
|
43
|
+
"aggregation-target-is-child-schema": {
|
|
44
|
+
whatWentWrong: "聚合指向了子表 schema",
|
|
45
|
+
correctShape: "aggregation.schemaCode 必须是来源模型主表;数据项只能是来源主表的标量列",
|
|
46
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
47
|
+
doNot: "聚合指向子表会让目标模型整个不可写(40000 无明细)且匹配恒 0——先删这个字段配置再继续",
|
|
48
|
+
},
|
|
49
|
+
"aggregation-data-item-not-scalar": {
|
|
50
|
+
whatWentWrong: "聚合数据项不是来源主表的标量列",
|
|
51
|
+
correctShape: "跨模型汇总的数据项只能是来源主表标量列;子表列先在来源模型物化成主表字段",
|
|
52
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
53
|
+
doNot: "直接把子表列写进数据项不会报配置错误——回算静默恒 0,先回来源模型物化",
|
|
54
|
+
},
|
|
55
|
+
"aggregation-large-missing-k": {
|
|
56
|
+
whatWentWrong: "LARGE/SMALL 聚合缺 k(前 N 名的 N)",
|
|
57
|
+
correctShape: "algorithmOptions 里带 k(正整数);LARGE=前 k 大,SMALL=前 k 小",
|
|
58
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
59
|
+
doNot: "缺 k 时平台抛 NumberFormatException 掩码成 50000——重试同参数无用",
|
|
60
|
+
},
|
|
61
|
+
"aggregation-union-result-type": {
|
|
62
|
+
whatWentWrong: "UNION(文本拼接)的 resultType 越界",
|
|
63
|
+
correctShape: "UNION 的 resultType 只能 0 或 1(拼接分隔行为),不是数值型枚举",
|
|
64
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
65
|
+
doNot: "写 2 及以上平台直接 50000——改回 0/1 再发布",
|
|
66
|
+
},
|
|
67
|
+
"rule-graph-validation-failed": {
|
|
68
|
+
whatWentWrong: "规则图形状没过写前校验",
|
|
69
|
+
correctShape: "nodeGraph:nodes(Start/动作/End,系统节点标 isSystemNode)+ routes(preNode→postNode);条件分支用 BRANCH 节点",
|
|
70
|
+
recipeCommand: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
71
|
+
},
|
|
72
|
+
"rule-graph-routes-empty": {
|
|
73
|
+
whatWentWrong: "规则图缺 routes(节点连线)",
|
|
74
|
+
correctShape: "每条 route {preNode, postNode, routeCondition:true};Start→首节点→…→End 必须连通",
|
|
75
|
+
recipeCommand: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
76
|
+
},
|
|
77
|
+
"rule-graph-bizaction-mapping-field-missing": {
|
|
78
|
+
whatWentWrong: "BIZ_ACTION 节点的输入映射缺 serviceMethodParameterCode 或 bizCode",
|
|
79
|
+
correctShape: "inputMappings 每条:serviceMethodParameterCode(服务方法参数)↔ bizCode(模型字段 code),codeType 按类型",
|
|
80
|
+
recipeCommand: { tool: "rule.save", recipe: "rule-cross-model-create" },
|
|
81
|
+
missingInputs: [
|
|
82
|
+
{ name: "serviceCode/serviceMethodCode", source: "omc call bizservice.list --args '{}'(已注册的二开服务)" },
|
|
83
|
+
{ name: "bizCode(模型字段)", source: "omc call field.list --args '{\"schemaCode\":\"<模型>\",\"all\":true}'" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
"rule-graph-bizmethod-unresolved": {
|
|
87
|
+
whatWentWrong: "规则图引用的二开服务方法不存在或未注册",
|
|
88
|
+
correctShape: "methodMapping.schemaCode/serviceCode/serviceMethodCode 必须是已注册服务的真实 code",
|
|
89
|
+
recipeCommand: { tool: "rule.save", recipe: "rule-cross-model-create" },
|
|
90
|
+
missingInputs: [
|
|
91
|
+
{ name: "serviceCode/serviceMethodCode", source: "omc call bizservice.list --args '{}';方法级详情用 bizservice.get" },
|
|
92
|
+
],
|
|
93
|
+
doNot: "不要凭记忆拼写服务 code——静默不匹配时规则保存成功但运行为空",
|
|
94
|
+
},
|
|
95
|
+
"field-relation-target-missing": {
|
|
96
|
+
whatWentWrong: "关联字段的目标模型缺失或不可解析",
|
|
97
|
+
correctShape: "关联字段 options 里目标模型 schemaCode 必须已存在且已发布",
|
|
98
|
+
missingInputs: [
|
|
99
|
+
{ name: "目标模型 schemaCode", source: "omc call model.list --args '{}'" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Platform/permission/environment families: honest "fix this first" with NO
|
|
105
|
+
* retry command — a command that would fail again is worse than none.
|
|
106
|
+
*/
|
|
107
|
+
const FIX_FIRST_GATES = Object.freeze({
|
|
108
|
+
"workflow-start-permission-denied": "40009 有三个独立同码原因:① 发起人角色没挂该流程的发起权限(ouScope ∩ 发起人部门链,全空会静默回退发起人);② 流程未启用/未发布;③ 模型未绑定流程。先 omc call workflow.get_published 核对 ②,再查角色 ouScope——不要反复换参数重试 data.submit",
|
|
109
|
+
"environment-write-disabled": "当前环境 profile 不可写:换可写环境,或让维护者在配置里开 writable——CLI 侧没有绕过开关",
|
|
110
|
+
"production-confirmation-required": "生产环境的写需要显式 confirmProduction:true——确认这次写入真的要落生产再加该参数,不要靠重试",
|
|
111
|
+
"app-not-published": "目标应用未发布:先 workflow/form/field 各面发布(或 app 级发布),再重试运行面操作",
|
|
112
|
+
"submit-workitem-not-mine": "该 workItem 不在当前登录人的待办里:用 omc call workitem.list --args '{\"status\":\"PENDING\"}' 拿**自己的** workItemId,不要用别人的 id 提交",
|
|
113
|
+
"capability-out-of-coverage": "需求不在覆盖面内:这份拒绝就是答案——用 omc capabilities 核对运行时清单,不要换相近工具伪造实现",
|
|
114
|
+
"version-family-evidence-missing": "平台版本证据缺失,写被关闭:先 omc doctor / discover_environment 取版本证据,非 8.6/1.1.25 家族按边界处理",
|
|
115
|
+
});
|
|
116
|
+
/** `tool-input-invalid` and friends: point at the refused tool's own describe. */
|
|
117
|
+
function inputInvalidGuidance(toolName) {
|
|
118
|
+
if (!toolName)
|
|
119
|
+
return undefined;
|
|
120
|
+
return {
|
|
121
|
+
whatWentWrong: "入参没过工具的 schema 校验(缺必填或类型不对)",
|
|
122
|
+
correctShape: "必填参数与类型见 command 指向的 describe(firstCall 自带最小可跑形状)",
|
|
123
|
+
command: `omc describe ${toolName}`,
|
|
124
|
+
doNot: "不要盲目重试——同参数必然同拒绝",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* r19 p14 — the report BI boundary (`/dashboard/v1/*`). A deployment without
|
|
129
|
+
* the report service answers every BI call with a gateway 502 (live p9
|
|
130
|
+
* evidence); that is an ENVIRONMENT boundary, not a transient failure, so the
|
|
131
|
+
* teaching says which faces still work and to stop retrying. Matched from the
|
|
132
|
+
* message/detail text so both the refused and the frozen (uncertain) envelope
|
|
133
|
+
* paths can carry it.
|
|
134
|
+
*/
|
|
135
|
+
function dashboardBoundaryGuidance() {
|
|
136
|
+
return {
|
|
137
|
+
whatWentWrong: "报表 BI 通道(/dashboard/v1/*)不可达——报表服务未启动(本机有此服务;网关 502,p9 实证)",
|
|
138
|
+
fixFirst: "报表能力暂不优先支持(维护者裁决,先隐藏):不要重试同一子面。平台层仍可用:report.tree / report.update / report.get 存在性判定;确需 BI 详情/数据源/图表/配置时,先启动本机报表服务再深进",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function mentionsDashboardChannel(payload) {
|
|
142
|
+
return JSON.stringify({ m: payload.message, d: payload.detail, r: payload.reason }).includes("/dashboard/v1/");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the authored recipe command against the registry: the guidance must
|
|
146
|
+
* never cite a recipe the registry does not ship (that would be a second,
|
|
147
|
+
* contradictory shape source). Falls back to the tool's describe; if even the
|
|
148
|
+
* tool is unknown, the command is omitted.
|
|
149
|
+
*/
|
|
150
|
+
function resolveCommand(authored, registry) {
|
|
151
|
+
if (!authored.recipeCommand || !registry)
|
|
152
|
+
return undefined;
|
|
153
|
+
const tool = registry.find((entry) => entry.name === authored.recipeCommand.tool);
|
|
154
|
+
const recipe = tool?.executionMetadata?.recipes.find((entry) => entry.id === authored.recipeCommand.recipe);
|
|
155
|
+
if (tool && recipe)
|
|
156
|
+
return `omc describe ${tool.name} --example ${recipe.id}`;
|
|
157
|
+
if (tool)
|
|
158
|
+
return `omc describe ${tool.name}`;
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
export function refusalRecovery(blocker, payload, options = {}) {
|
|
162
|
+
// A refusal that already teaches (write-lock-held carries lock recovery) is
|
|
163
|
+
// not overwritten — single owner per envelope.
|
|
164
|
+
if (payload.recovery !== undefined)
|
|
165
|
+
return payload.recovery;
|
|
166
|
+
// r19 p14: the report BI boundary is matched from the wire path in the
|
|
167
|
+
// message/detail, so both refused and frozen (uncertain) envelopes teach it.
|
|
168
|
+
if (mentionsDashboardChannel(payload))
|
|
169
|
+
return dashboardBoundaryGuidance();
|
|
170
|
+
const shape = SHAPE_GATES[blocker];
|
|
171
|
+
if (shape) {
|
|
172
|
+
const command = resolveCommand(shape, options.registry);
|
|
173
|
+
const { recipeCommand: _drop, ...rest } = shape;
|
|
174
|
+
return { ...rest, ...(command !== undefined ? { command } : {}) };
|
|
175
|
+
}
|
|
176
|
+
const fixFirst = FIX_FIRST_GATES[blocker];
|
|
177
|
+
if (fixFirst)
|
|
178
|
+
return { whatWentWrong: String(payload.message ?? blocker).slice(0, 160), fixFirst };
|
|
179
|
+
if (blocker === "tool-input-invalid")
|
|
180
|
+
return inputInvalidGuidance(options.toolName);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Attach guidance to a refused result at the `runToolCall` choke point.
|
|
185
|
+
* Idempotent, refuses to overwrite, and leaves non-refused results untouched.
|
|
186
|
+
*/
|
|
187
|
+
export function attachRefusalRecovery(result, toolName, registry) {
|
|
188
|
+
// r19 p14: `/dashboard/v1/*` failures surface as UNCERTAIN (frozen) — the
|
|
189
|
+
// http-status wire kind is classified "uncertain" by design, and frozen
|
|
190
|
+
// envelopes carry `reason`/`detail` but NO `blocker` field. The dashboard
|
|
191
|
+
// boundary is definitive (gateway 502, report service not started), so those
|
|
192
|
+
// frozen envelopes get the teaching too; every other uncertain result stays
|
|
193
|
+
// untouched (never relabel network jitter as an environment boundary).
|
|
194
|
+
const frozenWithBoundary = result.status === "uncertain" && mentionsDashboardChannel(result);
|
|
195
|
+
if (result.status !== "refused" && !frozenWithBoundary)
|
|
196
|
+
return result;
|
|
197
|
+
if (result.recovery !== undefined)
|
|
198
|
+
return result;
|
|
199
|
+
const blocker = typeof result.blocker === "string" ? result.blocker : "";
|
|
200
|
+
if (!blocker && !frozenWithBoundary)
|
|
201
|
+
return result;
|
|
202
|
+
const recovery = refusalRecovery(blocker, result, { registry, toolName });
|
|
203
|
+
if (!recovery)
|
|
204
|
+
return result;
|
|
205
|
+
return { ...result, recovery };
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=refusal-guidance.js.map
|
package/dist/rule-support.js
CHANGED
|
@@ -446,6 +446,100 @@ export function isModernAssignVersion(version) {
|
|
|
446
446
|
* supported alternatives (BRANCH, or two rules).
|
|
447
447
|
*/
|
|
448
448
|
const CHOICE_CONDITION_TYPES = new Set([12, 13, 14, 15]);
|
|
449
|
+
/**
|
|
450
|
+
* 2026-10-27 用户反馈 + r24 沙盒四格矩阵(1.1.25 真机实测):
|
|
451
|
+
* CREATE(新增数据)节点打到**已发布流程的模型**时的流程发起开关语义——
|
|
452
|
+
*
|
|
453
|
+
* 不传 startWorkflow → 行落库、不发起流程(API 默认,实测)
|
|
454
|
+
* true + originatorValue "{}" → 触发表单提交 50000(fastjson expect '[', but {)
|
|
455
|
+
* true + 数组但 id 无效 → 50000「业务规则发起人不存在」
|
|
456
|
+
* true + [{id,name}] 真实用户 + targetWorkflowCode → 行落库并自动发起流程
|
|
457
|
+
*
|
|
458
|
+
* 契约:目标有已发布流程而未声明开关 → warning 教学显式做法(合法的两种选择
|
|
459
|
+
* 都要写明);开了开关但 originatorValue/targetWorkflowCode 不合法 → error
|
|
460
|
+
* 写前拒绝——那两种形状会把**触发表单的提交**整个打挂,正是 fail-closed 该拦的。
|
|
461
|
+
*/
|
|
462
|
+
async function createNodeFlowStartWarnings(session, nodes) {
|
|
463
|
+
const creates = nodes.filter((node) => String(node.nodeType ?? "") === "CREATE" && String(node.targetObjectCode ?? "") !== "");
|
|
464
|
+
if (creates.length === 0)
|
|
465
|
+
return [];
|
|
466
|
+
const flowByTarget = new Map();
|
|
467
|
+
for (const target of [...new Set(creates.map((node) => String(node.targetObjectCode)))]) {
|
|
468
|
+
try {
|
|
469
|
+
const isWorkflow = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode: target } })).data === true;
|
|
470
|
+
if (!isWorkflow)
|
|
471
|
+
continue;
|
|
472
|
+
const row = (await wireCall(session, { path: ENDPOINTS.workflowGetBySchema, query: { schemaCode: target } })).data;
|
|
473
|
+
const code = row && typeof row.workflowCode === "string" && row.workflowCode ? row.workflowCode : null;
|
|
474
|
+
if (!code)
|
|
475
|
+
continue;
|
|
476
|
+
// get_published 原样回模板(fake 保真:未发布也回 DRAFT 形状——Ticket 77,
|
|
477
|
+
// 发布证据是 templateType:PUBLISH 标记,不是有数据);流程码用已解析的 code。
|
|
478
|
+
const published = (await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: code } })).data;
|
|
479
|
+
const publishMarker = published !== null && published !== undefined ? String(published.templateType ?? "").toUpperCase() : "ABSENT";
|
|
480
|
+
if (published && (publishMarker === "" || publishMarker === "PUBLISH"))
|
|
481
|
+
flowByTarget.set(target, code);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// 读面失败不阻塞写面(与 radioConditionWarnings 同策略:读不到就不评)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const findings = [];
|
|
488
|
+
for (const node of creates) {
|
|
489
|
+
const target = String(node.targetObjectCode);
|
|
490
|
+
const flowCode = flowByTarget.get(target);
|
|
491
|
+
if (flowCode === undefined)
|
|
492
|
+
continue;
|
|
493
|
+
const nodeCode = String(node.nodeCode ?? "");
|
|
494
|
+
if (node.startWorkflow === undefined || node.startWorkflow === false) {
|
|
495
|
+
findings.push({
|
|
496
|
+
code: "rule-create-target-has-flow",
|
|
497
|
+
nodeCode,
|
|
498
|
+
key: "startWorkflow",
|
|
499
|
+
severity: "warning",
|
|
500
|
+
message: `CREATE 目标模型 ${target} 已发布流程 ${flowCode}:不传 startWorkflow 时规则建行**不发起流程**(实测 2026-10-27,行落库即止)。` +
|
|
501
|
+
`要让规则新建的行直接进流程:显式 startWorkflow:true + targetWorkflowCode:"${flowCode}",且 originatorValue 必须是用户对象数组 [{"id":"<userId>","name":"<名>"}](id 用 org.user.search 取;传对象 {} 或无效 id 会让**触发表单的提交**直接 50000,实测)。` +
|
|
502
|
+
`注意:设计器里手工编辑该节点可打开此开关——API 与设计器两侧配置可能漂移,改完用 rule.get 回读核对 startWorkflow/targetWorkflowCode。`,
|
|
503
|
+
});
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const raw = typeof node.originatorValue === "string" ? node.originatorValue.trim() : node.originatorValue;
|
|
507
|
+
let parsed = raw;
|
|
508
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(raw);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
parsed = null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const originatorValid = Array.isArray(parsed)
|
|
517
|
+
&& parsed.length > 0
|
|
518
|
+
&& parsed.every((entry) => entry && typeof entry === "object" && typeof entry.id === "string" && entry.id.length > 0);
|
|
519
|
+
if (!originatorValid) {
|
|
520
|
+
findings.push({
|
|
521
|
+
code: "rule-create-startworkflow-originator-shape",
|
|
522
|
+
nodeCode,
|
|
523
|
+
key: "originatorValue",
|
|
524
|
+
message: `CREATE "${nodeCode}" 开了 startWorkflow 但 originatorValue 不是用户对象数组:期望 [{"id":"<userId>","name":"<名>"}]。` +
|
|
525
|
+
`实测(2026-10-27,1.1.25):传对象 {} → 触发表单提交 50000(fastjson "expect '[', but {");数组里 id 无效 → 50000「业务规则发起人不存在」。` +
|
|
526
|
+
`id 用 org.user.search 取真实用户后再写。`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// 「startWorkflow:true 必须带 targetWorkflowCode」由既有的离线同步检查
|
|
530
|
+
// rule-graph-create-workflow-code 负责(rule-graph.ts:2055),这里不重复——
|
|
531
|
+
// 但流程码要能对上:给了但不是该模型的已发布流程时补一条 error。
|
|
532
|
+
if (typeof node.targetWorkflowCode === "string" && node.targetWorkflowCode.length > 0 && node.targetWorkflowCode !== flowCode) {
|
|
533
|
+
findings.push({
|
|
534
|
+
code: "rule-create-startworkflow-target-mismatch",
|
|
535
|
+
nodeCode,
|
|
536
|
+
key: "targetWorkflowCode",
|
|
537
|
+
message: `CREATE "${nodeCode}" 的 targetWorkflowCode="${node.targetWorkflowCode}" 不是目标模型 ${target} 的已发布流程(应为 "${flowCode}")——发到不匹配的流程会运行期失败。`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return findings;
|
|
542
|
+
}
|
|
449
543
|
async function radioConditionWarnings(session, schemaCode, nodes) {
|
|
450
544
|
const sources = new Set();
|
|
451
545
|
const collect = (condition) => {
|
|
@@ -659,6 +753,8 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
659
753
|
const blockers = [...prepared.warnings, ...validateRuleNodeGraph(preparedGraph, args.ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined, carriedNodeCodes ? { carriedNodeCodes } : {})];
|
|
660
754
|
// 1.2: needs the published field types (pure lint cannot see them).
|
|
661
755
|
blockers.push(...(await radioConditionWarnings(session, args.schemaCode, Array.isArray(preparedGraph.nodes) ? preparedGraph.nodes : [])));
|
|
756
|
+
// r24 反馈(2026-10-27 真机四格矩阵):CREATE 目标带已发布流程时的发起开关教学/拦截。
|
|
757
|
+
blockers.push(...(await createNodeFlowStartWarnings(session, Array.isArray(preparedGraph.nodes) ? preparedGraph.nodes : [])));
|
|
662
758
|
// Ticket 113: warning-severity findings (e.g. unknown mapping keys) do not
|
|
663
759
|
// gate the write; they surface in the readback as lintWarnings.
|
|
664
760
|
const warningFindings = blockers.filter((blocker) => blocker.severity === "warning");
|