@stalfh233/omc-cli 0.4.49 → 0.4.51

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/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { resolve } from "node:path";
20
20
  import { buildToolRegistry } from "./tools.js";
21
21
  import { acquireWriteLock } from "./write-lock.js";
22
22
  import { cmdCompute } from "./compute.js";
23
+ import { cmdRuleWhen } from "./rule-when.js";
23
24
  // --utf8:单次调用明文输出(等效 OMC_ASCII_STDOUT=0)。在 asciiStdout 缓存读取前生效。
24
25
  if (process.argv.includes("--utf8"))
25
26
  process.env.OMC_ASCII_STDOUT = "0";
@@ -947,6 +948,10 @@ async function main() {
947
948
  const rest = ["compute", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(options.target ? ["--target", options.target] : []), ...(options.property ? ["--property", options.property] : []), ...(options.formula !== undefined ? ["--formula", options.formula] : []), ...(options.name ? ["--name", options.name] : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : [])];
948
949
  commandExitCode = await cmdCompute(rest);
949
950
  });
951
+ command.command("rule-when").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--on <trigger>").option("--when <condition>", undefined, { variadic: true }).option("--then <action>", undefined, { variadic: true }).option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n条件-动作规则拼装器:--when 条件(可多个 AND)→ --then set:字段=值/引用/公式。编译为设计器验证形状(预置触发+UPDATE filter/赋值)。默认 dry-run,--apply 保存并发布;前台生效按输出清单人工验证。\n").action(async (options) => {
952
+ const rest = ["rule-when", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(options.on ? ["--on", options.on] : []), ...(Array.isArray(options.when) ? options.when.flatMap((w) => ["--when", w]) : []), ...(Array.isArray(options.then) ? options.then.flatMap((a) => ["--then", a]) : []), ...(options.name ? ["--name", options.name] : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : [])];
953
+ commandExitCode = await cmdRuleWhen(rest);
954
+ });
950
955
  command.command("script [file]").option("--env <environment>", "行内无 env 时的默认环境").option("--continue", "某行失败后继续执行剩余行(默认停在首个失败)").description("NDJSON 多调脚本:每行 {\"tool\":\"<名称>\",\"args\":{...},\"env\":\"<环境>\",\"dryRun\":true}(# 注释/空行忽略;file 为 - 读 stdin);一个进程一次会话跑完全部,逐行输出 {index,tool,exitCode,result}").action(async (file, options) => {
951
956
  const { runScript, parseScript, readScriptInput } = await import("./script.js");
952
957
  let lines;
@@ -1023,11 +1023,13 @@ export function countConditionLeaves(container) {
1023
1023
  return count;
1024
1024
  }
1025
1025
  /** Validate one condition leaf; returns blockers (empty = clean). */
1026
- export function validateConditionLeaf(leaf, nodeCode, where, warnings) {
1026
+ export function validateConditionLeaf(leaf, nodeCode, where, warnings, legacyTolerant = false) {
1027
1027
  const blockers = [];
1028
1028
  const valueType = String(leaf.ruleValueType ?? "");
1029
1029
  if (!RULE_CONDITION_VALUE_TYPES.includes(valueType)) {
1030
- if (valueType === "specifyOption")
1030
+ if (legacyTolerant && valueType)
1031
+ warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: "ruleValueType", severity: "warning", message: `${where}: ruleValueType "${valueType}" 是存量携带的设计器取值类型(round-trip 原样保留);新增取值请用 ${RULE_CONDITION_VALUE_TYPES.join("/")}` });
1032
+ else if (valueType === "specifyOption")
1031
1033
  warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: "ruleValueType", severity: "warning", message: `${where}: ruleValueType "specifyOption" 是设计器 UI 独有下拉取值类型——按原样保留回传(round-trip 保真),OMC 不支持新建;新增取值请用 ${RULE_CONDITION_VALUE_TYPES.join("/")}` });
1032
1034
  else
1033
1035
  blockers.push({ code: "rule-graph-condition-value-type", nodeCode, key: "ruleValueType", message: `${where}: ruleValueType "${valueType}" 不在 ${RULE_CONDITION_VALUE_TYPES.join("/")}(当前叶:${JSON.stringify(leaf)})` });
@@ -1065,13 +1067,17 @@ export function validateConditionLeaf(leaf, nodeCode, where, warnings) {
1065
1067
  blockers.push({ code: "rule-graph-condition-today-offset-missing", nodeCode, key: DYNAMIC_CONDITION_SOURCE_KEY, message: `${where}: ruleValueType "today" 的日期偏移必须放在 ${DYNAMIC_CONDITION_SOURCE_KEY}(运行时 Integer.parseInt(source))——如 "0"=今天、"-7"=7天前、"30"=30天后、"-1-week"/"2-month" 带单位;缺失或非数字会在查询时抛 NumberFormatException,扫描 0 行、规则「永不触发」(真机 2026-10-08)。` });
1066
1068
  }
1067
1069
  if (leaf.ruleConditionType !== undefined && !RULE_CONDITION_TYPES.includes(String(leaf.ruleConditionType))) {
1068
- if (String(leaf.ruleConditionType) === "CHANGED")
1070
+ if (legacyTolerant)
1071
+ warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: "ruleConditionType", severity: "warning", message: `${where}: ruleConditionType "${String(leaf.ruleConditionType)}" 是存量携带的设计器算子(round-trip 原样保留);新增条件请用 ${RULE_CONDITION_TYPES.join("/")}` });
1072
+ else if (String(leaf.ruleConditionType) === "CHANGED")
1069
1073
  warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: "ruleConditionType", severity: "warning", message: `${where}: ruleConditionType "CHANGED" 是设计器 UI 独有变更检测算子——按原样保留回传(round-trip 保真),OMC 不支持新建此类算子;新增条件请用 ${RULE_CONDITION_TYPES.join("/")}` });
1070
1074
  else
1071
1075
  blockers.push({ code: "rule-graph-condition-type", nodeCode, key: "ruleConditionType", message: `${where}: ruleConditionType "${String(leaf.ruleConditionType)}}" 不在 ${RULE_CONDITION_TYPES.join("/")}` });
1072
1076
  }
1073
1077
  if (leaf[BRANCH_EXIT_OPERATOR_KEY] !== undefined && !RULE_CONDITION_TYPES.includes(String(leaf[BRANCH_EXIT_OPERATOR_KEY]))) {
1074
- if (String(leaf[BRANCH_EXIT_OPERATOR_KEY]) === "CHANGED")
1078
+ if (legacyTolerant)
1079
+ warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: BRANCH_EXIT_OPERATOR_KEY, severity: "warning", message: `${where}: ${BRANCH_EXIT_OPERATOR_KEY} "${String(leaf[BRANCH_EXIT_OPERATOR_KEY])}" 是存量携带的设计器算子(round-trip 原样保留);新增条件请用 ${RULE_CONDITION_TYPES.join("/")}` });
1080
+ else if (String(leaf[BRANCH_EXIT_OPERATOR_KEY]) === "CHANGED")
1075
1081
  warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: BRANCH_EXIT_OPERATOR_KEY, severity: "warning", message: `${where}: ${BRANCH_EXIT_OPERATOR_KEY} "CHANGED" 是设计器 UI 独有变更检测算子——按原样保留回传(round-trip 保真),OMC 不支持新建此类算子;新增条件请用 ${RULE_CONDITION_TYPES.join("/")}` });
1076
1082
  else
1077
1083
  blockers.push({ code: "rule-graph-condition-type", nodeCode, key: BRANCH_EXIT_OPERATOR_KEY, message: `${where}: ${BRANCH_EXIT_OPERATOR_KEY} "${String(leaf[BRANCH_EXIT_OPERATOR_KEY])}" 不在 ${RULE_CONDITION_TYPES.join("/")}` });
@@ -1328,7 +1334,7 @@ export function validatePresetSystemNodeIdentity(nodesInput, ruleCode) {
1328
1334
  }
1329
1335
  return blockers;
1330
1336
  }
1331
- export function validateRuleNodeGraph(graph, ruleCode, schemaCode) {
1337
+ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {}) {
1332
1338
  const blockers = [];
1333
1339
  const warnings = [];
1334
1340
  const merged = () => [...blockers, ...warnings];
@@ -1338,6 +1344,8 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode) {
1338
1344
  return [{ code: "rule-graph-empty", message: "nodeGraph.nodes 不能为空" }];
1339
1345
  }
1340
1346
  const nodeRecords = nodes.filter((node) => Boolean(node && typeof node === "object"));
1347
+ const carried = options.carriedNodeCodes;
1348
+ const relaxEnums = options.relaxEnums === true;
1341
1349
  if (nodeRecords.length !== nodes.length)
1342
1350
  blockers.push({ code: "rule-graph-node-not-object", message: "nodes 中存在非对象元素" });
1343
1351
  // Cross-model write-back: SUPPORTED (verified live 2026-09-28). An UPDATE whose
@@ -1503,7 +1511,7 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode) {
1503
1511
  if (!filter || typeof filter !== "object")
1504
1512
  blockers.push({ code: "rule-graph-filter-missing", nodeCode, key: "filterCondition", message: `${nodeType} 节点缺 filterCondition(mainConditions/sheetConditions)` });
1505
1513
  else {
1506
- walkConditionLeaves(filter, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings)));
1514
+ walkConditionLeaves(filter, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings, (relaxEnums || carried?.has(String(node.nodeCode ?? "")) === true))));
1507
1515
  // Platform label (HANDOFF C.2 / evidence §4): UPDATE.filterCondition =
1508
1516
  // 「查找满足条件的主表数据」(which row to update), DELETE.filterCondition =
1509
1517
  // 「删除满足条件的数据」. An EMPTY mainConditions therefore means the node
@@ -1544,7 +1552,7 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode) {
1544
1552
  if (node.targetConditionGroups !== undefined) {
1545
1553
  if (Array.isArray(node.targetConditionGroups))
1546
1554
  blockers.push({ code: "rule-graph-query-condition-group-shape", nodeCode, key: "targetConditionGroups", message: "QUERY 的 targetConditionGroups 必须是对象 {mainConditions:[[叶…]],sheetConditions:[[叶…]]}(TableConditionGroup.java),不能是裸数组 [[…]](那是 filterCondition/exitConditionGroups 的形状)——裸数组 save 通过但 live businessrule/update 50000(真机 2026-10-07)。" });
1547
- walkConditionLeaves(node.targetConditionGroups, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings)));
1555
+ walkConditionLeaves(node.targetConditionGroups, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings, (relaxEnums || carried?.has(String(node.nodeCode ?? "")) === true))));
1548
1556
  scanConditionKeyCollisions(node.targetConditionGroups, nodeCode, warnings);
1549
1557
  }
1550
1558
  }
@@ -1584,7 +1592,7 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode) {
1584
1592
  blockers.push({ code: "rule-graph-branch-conditions", nodeCode, key: "exitConditionGroups", message: "BRANCH 节点缺 exitConditionGroups([[叶]] 与或组)" });
1585
1593
  else {
1586
1594
  walkConditionLeaves(node.exitConditionGroups, (leaf, where) => {
1587
- blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings));
1595
+ blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings, (relaxEnums || carried?.has(String(node.nodeCode ?? "")) === true)));
1588
1596
  // 8.6.26: exitConditionGroups deserialize into DataFilterCondition, whose
1589
1597
  // left/source key is currentSchemaDataItem and operator key is
1590
1598
  // ruleDataConditionType. The UI/line-shaped targetSchemaDataItem +
@@ -1652,7 +1660,7 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode) {
1652
1660
  if (node.aggregation !== undefined && typeof node.aggregation !== "boolean")
1653
1661
  blockers.push({ code: "rule-graph-create-aggregation", nodeCode, key: "aggregation", message: "CREATE 节点 aggregation 存在时必须是布尔(true=分组新增 / false=单条依次新增)" });
1654
1662
  if (node.findMainData !== undefined)
1655
- walkConditionLeaves(node.findMainData, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings)));
1663
+ walkConditionLeaves(node.findMainData, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where, warnings, (relaxEnums || carried?.has(String(node.nodeCode ?? "")) === true))));
1656
1664
  if (node.filterCondition !== undefined)
1657
1665
  walkConditionLeaves(node.filterCondition, (leaf, where) => blockers.push(...validateConditionLeaf(leaf, nodeCode, where)));
1658
1666
  scanConditionKeyCollisions(node.filterCondition, nodeCode, warnings);
@@ -425,6 +425,44 @@ export async function assertRuleSchemaExists(session, schemaCode) {
425
425
  });
426
426
  }
427
427
  }
428
+ /** 与存量图逐节点稳定序列化比对,得出"原样携带"节点集合(round-trip 放宽)。 */
429
+ export async function computeCarriedNodeCodes(session, schemaCode, ruleCode, preparedGraph) {
430
+ try {
431
+ const { resolveRuleRowId } = await import("./identity.js");
432
+ const { wireCall } = await import("./wire.js");
433
+ const { ENDPOINTS } = await import("./endpoints.js");
434
+ const existingId = await resolveRuleRowId(session, schemaCode, ruleCode);
435
+ if (!existingId)
436
+ return undefined;
437
+ const row = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: existingId } })).data;
438
+ if (!row || typeof row.node !== "string")
439
+ return undefined;
440
+ const currentNodes = JSON.parse(row.node);
441
+ if (!Array.isArray(currentNodes) || !Array.isArray(preparedGraph.nodes))
442
+ return undefined;
443
+ const stable = (value) => {
444
+ if (Array.isArray(value))
445
+ return "[" + value.map(stable).join(",") + "]";
446
+ if (value && typeof value === "object") {
447
+ return "{" + Object.keys(value).sort().map((k) => JSON.stringify(k) + ":" + stable(value[k])).join(",") + "}";
448
+ }
449
+ return JSON.stringify(value) ?? "null";
450
+ };
451
+ const carried = new Set();
452
+ for (const node of preparedGraph.nodes) {
453
+ const code = String(node?.nodeCode ?? "");
454
+ if (!code)
455
+ continue;
456
+ const match = currentNodes.find((cur) => String(cur?.nodeCode ?? "") === code);
457
+ if (match && stable(match) === stable(node))
458
+ carried.add(code);
459
+ }
460
+ return carried;
461
+ }
462
+ catch {
463
+ return undefined; // 取不到存量图 → 维持严格闭集,行为与放宽前一致
464
+ }
465
+ }
428
466
  export async function executeRuleSaveWrite(session, args) {
429
467
  // Pre-flight dry lint (ticket 14): a mis-shaped graph must be refused BEFORE
430
468
  // any wire call, with structured blockers locating the node/key — never
@@ -446,7 +484,11 @@ export async function executeRuleSaveWrite(session, args) {
446
484
  // graph: preset system node → button/flow CUSTOM_NODE trigger → route shape.
447
485
  const prepared = prepareRuleGraph(args.ruleCode, graph, args.customTrigger, args.schemaCode);
448
486
  const preparedGraph = prepared.graph;
449
- const blockers = [...prepared.warnings, ...validateRuleNodeGraph(preparedGraph, args.ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined)];
487
+ // 回读差异放宽(中冶 2026-10-16):更新已存在规则时取存量图逐节点比对,
488
+ // 与存量完全一致的"原样携带"节点免闭集硬校验(设计器 UI 独有算子降级
489
+ // legacy warning 原样回传);新增/改动节点维持严格。存量永远可编辑。
490
+ const carriedNodeCodes = await computeCarriedNodeCodes(session, args.schemaCode, args.ruleCode, preparedGraph);
491
+ const blockers = [...prepared.warnings, ...validateRuleNodeGraph(preparedGraph, args.ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined, carriedNodeCodes ? { carriedNodeCodes } : {})];
450
492
  // Ticket 113: warning-severity findings (e.g. unknown mapping keys) do not
451
493
  // gate the write; they surface in the readback as lintWarnings.
452
494
  const lintWarnings = blockers.filter((blocker) => blocker.severity === "warning");
@@ -0,0 +1,138 @@
1
+ /**
2
+ * `omc rule-when` — 条件-动作规则拼装器(v1:预置触发 + 单 UPDATE 节点)。
3
+ *
4
+ * 中冶 2026-10-16 后续:规则面三大高频形态之三(计算=omc compute、定时=recipe、
5
+ * 条件动作=本命令)。把「当 X 满足条件 → 给字段设值」压成一条平铺命令:
6
+ *
7
+ * omc rule-when --schema s --on update \
8
+ * --when "status=履行中" --when "amount>=100000" \
9
+ * --then "set:level=A" --then "set:total={amount}*0.9" [--apply]
10
+ *
11
+ * 编译目标只用**设计器验证过**的形状(前台生效约束,2026-10-16 用户裁决:后端
12
+ * 接口大量不拦截,save/publish 通过≠门户生效):
13
+ * - 预置触发行 Create/Update(设计器原生行,非自定义)
14
+ * - 单 UPDATE 节点:filterCondition(查找满足条件的主表数据)+ dataActions(赋值)
15
+ * - FIXED 字面量 / DYNAMIC 字段引用 / EXPRESSION 公式(含 DATEDIF 语法,均真机验证)
16
+ * - 布局/路由交给 prepareRuleGraph 注入(points 画布兼容,Ticket 109)
17
+ *
18
+ * 产出必带前台验证清单:运行时行为(分支是否真跑、公式是否真算)由测试数据 +
19
+ * 人工前台确认——工具保证结构与设计器兼容性,不冒充运行时断言。
20
+ */
21
+ import { runToolCall } from "./call.js";
22
+ import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contract.js";
23
+ import { writeEnvelope } from "./cli-output.js";
24
+ const WHEN_OPS = { "=": "EQ", "==": "EQ", "!=": "NEQ", ">": "GT", "<": "LT", ">=": "GTEQ", "<=": "LTEQ", "~=": "CT", "!~": "NCT" };
25
+ /** 编译 --when/--then DSL 为设计器验证形状的规则图(导出供测试断言)。 */
26
+ export function composeRuleWhenGraph(options) {
27
+ if (options.when.length === 0)
28
+ throw new Error("至少给一个 --when 条件;无条件更新整表是 bench23 病灶(空 mainConditions=无条件更新全部行)");
29
+ if (options.then.length === 0)
30
+ throw new Error("至少给一个 --then 动作");
31
+ const leaves = [];
32
+ for (const raw of options.when) {
33
+ // 最长操作符优先:>= <= ~= !~ == != > <
34
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)(>=|<=|~=|!~|==|!=|=|>|<)([\s\S]+)$/.exec(raw.trim());
35
+ if (!match)
36
+ throw new Error(`--when "${raw}" 解析失败:形如 字段=值 / 字段!=值 / 字段>=数字 / 字段~=文本(包含)`);
37
+ const [, field, op, rawValue] = match;
38
+ if (rawValue.startsWith("="))
39
+ throw new Error(`--when "${raw}" 疑似操作符手误(值以 = 开头):检查是否多打了等号`);
40
+ const opCode = WHEN_OPS[op];
41
+ if (!opCode)
42
+ throw new Error(`--when 操作符 "${op}" 不受支持`);
43
+ const value = /^-?\d+(\.\d+)?$/.test(rawValue.trim()) ? Number(rawValue.trim()) : rawValue.trim();
44
+ leaves.push({ targetSchemaDataItem: field, ruleConditionType: opCode, ruleValueType: "FIXED", value });
45
+ }
46
+ const actions = [];
47
+ for (const raw of options.then) {
48
+ const match = /^set:([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/.exec(raw.trim());
49
+ if (!match)
50
+ throw new Error(`--then "${raw}" 解析失败:v1 只支持 set:<字段>=<字面量>、set:<字段>={来源字段} 或 set:<字段>={公式}(如 set:total={amount}*0.9)`);
51
+ const [, field, rawValue] = match;
52
+ if (/^\{[A-Za-z_][A-Za-z0-9_.]*\}$/.test(rawValue.trim())) {
53
+ // 纯字段引用 → DYNAMIC
54
+ actions.push({ targetDataItem: field, ruleValueType: "DYNAMIC", value: "", ruleActionType: "EQUALS", currentDataItemValue: rawValue.trim().slice(1, -1) });
55
+ }
56
+ else if (rawValue.includes("{") && rawValue.includes("}")) {
57
+ // 含引用的表达式 → EXPRESSION(公式语言真机验证:IF/DATEDIF/INTDOWN 等)
58
+ actions.push({ targetDataItem: field, ruleValueType: "EXPRESSION", ruleActionType: "EQUALS", value: { formula: rawValue.trim() } });
59
+ }
60
+ else {
61
+ const value = /^-?\d+(\.\d+)?$/.test(rawValue.trim()) ? Number(rawValue.trim()) : rawValue.trim();
62
+ actions.push({ targetDataItem: field, ruleValueType: "FIXED", ruleActionType: "EQUALS", value });
63
+ }
64
+ }
65
+ return {
66
+ ruleCode: options.on === "create" ? "Create" : "Update",
67
+ nodeGraph: {
68
+ nodes: [
69
+ { nodeCode: "Start", nodeType: "START" },
70
+ { nodeCode: "UpdateData1", nodeType: "UPDATE", targetObjectCode: options.schema, inputParam: "_input",
71
+ filterCondition: { mainConditions: [leaves], sheetConditions: [] },
72
+ dataActions: actions },
73
+ { nodeCode: "End", nodeType: "END" },
74
+ ],
75
+ routes: [
76
+ { preNode: "Start", postNode: "UpdateData1", routeCondition: true, matchType: "ANY" },
77
+ { preNode: "UpdateData1", postNode: "End", routeCondition: true, matchType: "ANY" },
78
+ ],
79
+ },
80
+ };
81
+ }
82
+ export function composeRuleWhenArgs(options) {
83
+ const compiled = composeRuleWhenGraph(options);
84
+ return {
85
+ schemaCode: options.schema,
86
+ ruleCode: compiled.ruleCode,
87
+ name: options.name ?? `${options.schema} ${compiled.ruleCode} 条件动作`,
88
+ nodeGraph: compiled.nodeGraph,
89
+ ...(options.apply ? { publish: true } : {}),
90
+ ...(options.environment ? { environment: options.environment } : {}),
91
+ dryRun: !options.apply,
92
+ };
93
+ }
94
+ export async function cmdRuleWhen(args) {
95
+ const request = requestId();
96
+ const flag = (name) => {
97
+ const index = args.indexOf(name);
98
+ return index >= 0 ? args[index + 1] : undefined;
99
+ };
100
+ const multi = (name) => {
101
+ const values = [];
102
+ for (let i = 0; i < args.length; i++) {
103
+ if (args[i] === name && args[i + 1] !== undefined)
104
+ values.push(args[i + 1]);
105
+ }
106
+ return values;
107
+ };
108
+ const schema = flag("--schema") ?? flag("--schema-code");
109
+ const on = (flag("--on") ?? "update").toLowerCase();
110
+ const when = multi("--when");
111
+ const then = multi("--then");
112
+ const name = flag("--name");
113
+ const environment = flag("--env") ?? flag("--environment");
114
+ const apply = args.includes("--apply");
115
+ const usage = '用法:omc rule-when --schema <模型> --on create|update --when "字段=值" [--when ...可多个,AND] --then "set:字段=值|{来源字段}|{公式}" [--then ...] [--apply]\n条件操作符:= != > < >= <= ~=(包含) !~(不包含)。默认 dry-run;--apply 保存并发布(rule.enable 仍需显式执行)。';
116
+ const invalid = (message) => {
117
+ writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
118
+ return CLI_EXIT_CODES.invalidInput;
119
+ };
120
+ if (!schema)
121
+ return invalid("--schema 必填");
122
+ if (on !== "create" && on !== "update")
123
+ return invalid("--on 只支持 create|update");
124
+ let createArgs;
125
+ try {
126
+ createArgs = composeRuleWhenArgs({ schema, on: on, when, then, name, environment, apply });
127
+ }
128
+ catch (error) {
129
+ return invalid(error instanceof Error ? error.message : String(error));
130
+ }
131
+ const { result, exitCode } = await runToolCall("rule.save", { args: createArgs, requestId: request, environment });
132
+ const classified = classifyResult(result);
133
+ writeEnvelope(envelope(classified.status, result, request, classified.error));
134
+ if (apply && classified.status === "ok")
135
+ process.stderr.write("[omc] 前台生效验证清单(后端通过≠门户生效):1) omc call rule.enable 启用;2) 造一条命中 --when 的测试数据提交;3) 门户打开该记录核对 --then 字段已变更;4) 再造一条不命中的数据确认不误改;5) 设计器打开本规则确认节点/条件显示正常。\n");
136
+ return exitCode;
137
+ }
138
+ //# sourceMappingURL=rule-when.js.map
package/dist/write.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
4
- import { join } from "node:path";
4
+ import { basename, join } from "node:path";
5
5
  /** Parse a JSON string for the dry-lint; unparseable input returns undefined so the shape validators report it. */
6
6
  function parseJsonOrUndefined(text) {
7
7
  try {
@@ -151,7 +151,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
151
151
  rollbackable: rollbackBefore !== undefined && !rollbackCaptureError,
152
152
  ...(rollbackCaptureError ? { note: `before-capture failed: ${rollbackCaptureError}` } : {}),
153
153
  artifactFile: artifactRef.detailFile,
154
- artifactId: artifactRef.detailFile.split("/").pop()?.replace(/\.json$/, "") ?? "",
154
+ artifactId: basename(artifactRef.detailFile).replace(/\.artifact$/, "") || basename(artifactRef.detailFile),
155
155
  artifactSha: artifactRef.detailSha256,
156
156
  });
157
157
  rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
@@ -211,7 +211,10 @@ export function writeTool(tools, name, lane, scope, description, inputSchema, ex
211
211
  // would wave through the shapes it does not touch.
212
212
  const ruleCode = String(args.ruleCode ?? "");
213
213
  const prepared = prepareRuleGraph(ruleCode, (args.nodeGraph ?? {}), args.customTrigger, typeof args.schemaCode === "string" ? args.schemaCode : undefined);
214
- const blockers = [...prepared.warnings, ...validateRuleNodeGraph(prepared.graph, ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined)];
214
+ const blockers = [...prepared.warnings, ...validateRuleNodeGraph(prepared.graph, ruleCode, typeof args.schemaCode === "string" ? args.schemaCode : undefined, { relaxEnums: true })];
215
+ // relaxEnums(中冶 2026-10-16):本闸无 session 取不到存量图,枚举类(UI 独有
216
+ // 算子/取值)在预览与实调前闸一律降级 warning——权威判定在
217
+ // executeRuleSaveWrite 的回读差异放宽(carried 精确 diff,错拼仍被拦)。
215
218
  // Ticket 113: warning-severity findings ride the payload but never gate.
216
219
  if (blockers.some((blocker) => blocker.severity !== "warning"))
217
220
  throw new ToolRefusalError({ status: "refused", blocker: "rule-graph-validation-failed", message: "nodeGraph 未过出门前校验(dry lint);按 validation 逐项修正,校验器不猜不改", validation: blockers });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.49",
3
+ "version": "0.4.51",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {