@stalfh233/omc-cli 0.6.25 → 0.6.26

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.
@@ -349,6 +349,7 @@ const CAPABILITY_TOPICS = Object.freeze({
349
349
  "rule-lifecycle": ["app-scope", "omc-business-configuration", "omc-refusals"],
350
350
  workflow: ["omc-business-configuration", "omc-refusals"],
351
351
  onlinejs: ["omc-business-configuration", "omc-write-hazards"],
352
+ programming: ["omc-backend-online-coding", "omc-business-configuration", "omc-write-hazards"],
352
353
  bizservice: ["omc-business-configuration", "omc-write-hazards"],
353
354
  report: ["omc-business-configuration", "omc-write-hazards"],
354
355
  data: ["omc-business-configuration", "omc-refusals"],
package/dist/cli.js CHANGED
@@ -2418,6 +2418,7 @@ async function main() {
2418
2418
  .option("--code <schemaCode>")
2419
2419
  .option("--fields <json>", "JSON 数组,每条是一个 field.create 参数(choices/字典 options 原样透传)")
2420
2420
  .option("--fields-json <json>")
2421
+ .option("--fields-file <path>", "从配置根内文件读字段 JSON 数组;与 --fields 互斥")
2421
2422
  .option("--env <environment>")
2422
2423
  .option("--environment <environment>")
2423
2424
  .option("--no-form", "跳过表单重建")
@@ -2436,6 +2437,7 @@ async function main() {
2436
2437
  ...(options.code ? ["--code", options.code] : []),
2437
2438
  ...(options.fields !== undefined ? ["--fields", options.fields] : []),
2438
2439
  ...(options.fieldsJson !== undefined ? ["--fields-json", options.fieldsJson] : []),
2440
+ ...(options.fieldsFile !== undefined ? ["--fields-file", options.fieldsFile] : []),
2439
2441
  ...(options.env ? ["--env", options.env] : []),
2440
2442
  ...(options.environment ? ["--environment", options.environment] : []),
2441
2443
  ...(options.form === false ? ["--no-form"] : []),
package/dist/field-add.js CHANGED
@@ -17,6 +17,9 @@ import { argvValue } from "./args.js";
17
17
  import { runToolCall } from "./call.js";
18
18
  import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from "./cli-contract.js";
19
19
  import { writeEnvelope } from "./cli-output.js";
20
+ import { assertSafeInputPath } from "./artifacts.js";
21
+ import { readFileSync } from "node:fs";
22
+ import { resolve } from "node:path";
20
23
  /**
21
24
  * 字典简写展开(r30/r36 实证:字典字段要先 names 查 id、records 取记录、
22
25
  * 再拼 optionsSet.dictionary 三段 4–5 调;`dictCode` 把它折进 field-add 一步):
@@ -108,7 +111,9 @@ export async function cmdFieldAdd(args, contextForTest) {
108
111
  const request = requestId();
109
112
  const flag = (name) => argvValue(args, name);
110
113
  const schemaCode = flag("--schema") ?? flag("--schema-code") ?? flag("--code");
111
- const fieldsJson = flag("--fields") ?? flag("--fields-json");
114
+ const fieldsFile = flag("--fields-file");
115
+ const fieldsFlag = flag("--fields");
116
+ const fieldsJson = fieldsFlag === "-" ? readFileSync(0, "utf8") : fieldsFlag ?? flag("--fields-json");
112
117
  const environment = flag("--env") ?? flag("--environment");
113
118
  const apply = args.includes("--apply");
114
119
  const publishForm = !args.includes("--no-form");
@@ -117,7 +122,7 @@ export async function cmdFieldAdd(args, contextForTest) {
117
122
  const conditionsJson = flag("--conditions") ?? flag("--conditions-json");
118
123
  const demoRowsJson = flag("--demo-rows") ?? flag("--demo-rows-json");
119
124
  const usage = [
120
- "用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--apply]",
125
+ "用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--fields-file <路径>|--fields -] [--apply]",
121
126
  " --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options 原样透传);",
122
127
  " 字典简写:{\"dictCode\":\"<字典code>\"} 代替手拼 options.optionsSet(内部解析字典 id+启用记录,recipe 同形状)。",
123
128
  " 默认 dry-run;--apply 依次执行 建字段 → 发布字段 → 重建表单 → 列表追加列。",
@@ -125,6 +130,7 @@ export async function cmdFieldAdd(args, contextForTest) {
125
130
  " --no-form 跳过表单重建;--no-list 跳过列表加列(默认两者都做)。",
126
131
  " --conditions '<JSON 数组>' 列表固定过滤条件(listview.configure 契约;**整体替换**既有固定条件——要保留旧的须一并带上;不传则不动);需 --list(默认开)。",
127
132
  " --demo-rows '<JSON 数组>' 演示数据行(每项一份 formData,逐行 data.submit);非幂等——重复执行会重复插行。",
133
+ " --fields-file 从配置根内文件读 JSON 数组;--fields - 从 stdin 读 JSON 数组。",
128
134
  ].join("\n");
129
135
  const invalid = (message) => {
130
136
  writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
@@ -132,11 +138,23 @@ export async function cmdFieldAdd(args, contextForTest) {
132
138
  };
133
139
  if (!schemaCode)
134
140
  return invalid("--schema 必填(存量模型 code)");
135
- if (fieldsJson === undefined)
136
- return invalid("--fields 必填(JSON 数组)");
141
+ if (fieldsFile !== undefined && (fieldsFlag !== undefined || flag("--fields-json") !== undefined))
142
+ return invalid("--fields-file 不能与 --fields/--fields-json 同时使用");
143
+ let inputJson = fieldsJson;
144
+ if (fieldsFile !== undefined) {
145
+ try {
146
+ const root = resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
147
+ inputJson = readFileSync(assertSafeInputPath(root, fieldsFile, "--fields-file"), "utf8");
148
+ }
149
+ catch (error) {
150
+ return invalid(`--fields-file 读取失败:${error instanceof Error ? error.message : String(error)}`);
151
+ }
152
+ }
153
+ if (inputJson === undefined)
154
+ return invalid("--fields 必填(JSON 数组;或使用 --fields-file/--fields -)");
137
155
  let fields;
138
156
  try {
139
- const parsed = JSON.parse(fieldsJson);
157
+ const parsed = JSON.parse(inputJson);
140
158
  if (!Array.isArray(parsed) || parsed.length === 0)
141
159
  throw new Error("--fields 必须是非空 JSON 数组");
142
160
  fields = parsed;
@@ -938,14 +938,14 @@ export function assertSelectionChoicesAllowed(args) {
938
938
  }
939
939
  function refuseFieldOptions(reason, message) {
940
940
  const citation = {
941
- code: "capability-out-of-coverage",
941
+ code: "field-options-shape",
942
942
  lane: "field-families",
943
943
  capability: "field family full chain",
944
944
  status: "blocked",
945
945
  reason,
946
946
  message,
947
947
  };
948
- throw new ToolRefusalError({ status: "refused", blocker: "capability-out-of-coverage", citation });
948
+ throw new ToolRefusalError({ status: "refused", blocker: "field-options-shape", citation });
949
949
  }
950
950
  /**
951
951
  * Aggregation query conditions are deserialized by Jackson into Java enums that
@@ -30,6 +30,9 @@ export const FORMULA_FUNCTION_ALIASES = Object.freeze({
30
30
  STRCAT: "STRING",
31
31
  JOIN: "STRING",
32
32
  });
33
+ export const FORMULA_LINT_CODES = new Set([
34
+ "formula-function-unknown", "formula-function-alias", "formula-text-plus-concat",
35
+ ]);
33
36
  const TEXT_PLUS_CONCAT = /(\{[^{}]+\}|[A-Za-z_][A-Za-z0-9_]*\s*\([^()]*\))\s*\+\s*["']|["']\s*\+\s*(\{[^{}]+\}|[A-Za-z_][A-Za-z0-9_]*\s*\()/;
34
37
  /**
35
38
  * Lint one formula string. `where` names the carrier in the caller's own terms
@@ -75,6 +75,7 @@ const RULE_SAVE_DESCRIPTION = describeTool("保存业务规则(upsert):`ru
75
75
  "**EXPRESSION 值形状**:`ruleValueType:EXPRESSION` 时 `value` 必须是 {\"formula\":\"{字段A}+{字段B}\"}(大括号裸 code);门户设计器的 [code] 方括号式在这里不认。",
76
76
  "**幻影 schema 门(ticket 96 live 事故)**:rule.save 写前先以 bizmodels/get_all 探测模型存在(缺失 → refused `rule-schema-unresolved`,零规则面 wire 调用)——平台的 list/save 面会对任意 schemaCode 自动补建 7 条预置规则且 API 删除不可达(list 即重灌),幻影 schema 必须在写前拒绝。",
77
77
  "**输出回写排序(ticket 36 live 铁证)**:BIZ_ACTION 的 outputMappings 由 SYSTEM_CREATE/SYSTEM_UPDATE 持久化节点消费,动作节点必须排在系统节点之前(`Start→BizAction→SYSTEM_CREATE→End`);工具默认把系统节点补在 START 之后,有回写动作时须显式提供系统节点并置于动作之后,否则合并结果不落库(echoResult 恒 null)。",
78
+ "**跨模型 UPDATE 三段语义(1.1.25 反编译定案)**:`inputParam`=输入源(触发行或 QUERY 上游输出,`currentSchemaDataItem` 属此侧)→ `filterCondition` 按 `targetSchemaCode` 查目标行 → `dataActions` 把输入源值写入目标行。QUERY 目标行不自动成为输入源;child fan-out 保持 fail-closed(用显式中间字段/映射/二开)。",
78
79
  ]);
79
80
  export const BUSINESSRULE_LANE_CONTRACT = Object.freeze({
80
81
  lane: LANES.businessrule,
@@ -479,6 +479,7 @@ export function registerBusinessRuleLane(tools) {
479
479
  name: z.string().min(1),
480
480
  nodeGraph: ruleNodeGraphArg,
481
481
  publish: z.boolean().optional().describe("also publish the new version in this call (businessrule/publish) so the designer sees it; enabling stays with rule.enable — default false keeps the legacy draft-only save"),
482
+ allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
482
483
  customTrigger: z.boolean().optional().describe("默认开:自定义 ruleCode + bizRuleType 0(非平台触发码 Create/Update/Delete/GetList/Load/Available/Cancel)即按钮/流程规则,工具在 START 后合成平台标准 CUSTOM_NODE「自定义按钮/流程触发」系统触发节点(nodeCode=ruleCode,live 布局 x558/y233);调用方已写 CUSTOM_NODE 则原样保留;显式 false 退回"),
483
484
  },
484
485
  execute: async (session, args) => executeRuleSaveWrite(session, args),
@@ -481,6 +481,11 @@ export function dataRuleFormulaWarnings(dataRuleType, options) {
481
481
  return [];
482
482
  return lintFormulaText(formula, "options.shortTextStitch.formula");
483
483
  }
484
+ function assertDataRuleFormulaLint(dataRuleType, options, allowAdvisory) {
485
+ const findings = dataRuleFormulaWarnings(dataRuleType, options);
486
+ if (findings.length > 0 && allowAdvisory !== true)
487
+ throw new ToolRefusalError({ status: "refused", blocker: "formula-lint-failed", validation: findings, nextCommand: "修正公式函数/字符串拼接;调查用途显式传 allowAdvisory:true", message: "公式写前校验拒绝:未知函数、平台别名或字符串 + 拼接。文本合并请使用 STRING()。" });
488
+ }
484
489
  /** Extract the expression of a type-2 CALCULATE rule (mirrors calcExpressionShapeError's carrier choice). */
485
490
  function type2FormulaText(options) {
486
491
  const raw = typeof options.shortTextStitch === "string" ? options.shortTextStitch : typeof options.value === "string" ? options.value : undefined;
@@ -872,9 +877,11 @@ export function registerDataRuleLane(tools) {
872
877
  childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
873
878
  name: z.string().optional(),
874
879
  sortKey: z.number().int().optional(),
880
+ allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
875
881
  },
876
882
  execute: async (session, args) => {
877
883
  assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
884
+ assertDataRuleFormulaLint(args.dataRuleType, args.options, args.allowAdvisory);
878
885
  assertDataRuleCheckType(args.dataRuleType, args.checkType);
879
886
  assertDataRuleTarget(args);
880
887
  const codes = dataRuleTargetCodes(args);
@@ -957,6 +964,7 @@ export function registerDataRuleLane(tools) {
957
964
  propertyCodes: propertyCodesArg,
958
965
  dataRuleType: dataRuleTypeArg,
959
966
  options: dataRuleOptionsArg,
967
+ allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
960
968
  checkType: checkTypeArg,
961
969
  childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
962
970
  name: z.string().optional(),
@@ -964,6 +972,7 @@ export function registerDataRuleLane(tools) {
964
972
  },
965
973
  execute: async (session, args) => {
966
974
  assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
975
+ assertDataRuleFormulaLint(args.dataRuleType, args.options, args.allowAdvisory);
967
976
  assertDataRuleCheckType(args.dataRuleType, args.checkType);
968
977
  const id = await locateDataRuleId(session, args, "update POST");
969
978
  const before = await listDataRules(session, args.schemaCode);
@@ -41,6 +41,61 @@ export async function modelSchemaCodes(session) {
41
41
  .map((row) => String(row.schemaCode ?? row.bizSchemaCode ?? row.code ?? ""))
42
42
  .filter((code) => code.length > 0);
43
43
  }
44
+ function relationTypeOf(row) {
45
+ const value = Number(row.type ?? row.propertyType);
46
+ return Number.isInteger(value) ? value : undefined;
47
+ }
48
+ /** Semantic relation gate: the designer only offers published business fields. */
49
+ async function assertRelationOptionsFields(session, schemaCode, propertyType, relativeCode, options) {
50
+ const type = typeof propertyType === "number" ? propertyType : FIELD_PROPERTY_TYPES[String(propertyType ?? "")];
51
+ if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
52
+ return;
53
+ const target = String(relativeCode ?? options.schemaCode ?? "").trim();
54
+ if (!target)
55
+ return;
56
+ const targetRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: target, isPublish: "true" } })).data);
57
+ const ownRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
58
+ const business = (rows) => rows
59
+ .map((row) => ({ code: String(row.code ?? row.propertyCode ?? "").trim(), type: relationTypeOf(row) }))
60
+ .filter((row) => row.code && !SYSTEM_FIELD_CODES.has(row.code));
61
+ const targetFields = business(targetRows);
62
+ const ownFields = business(ownRows);
63
+ // A compatibility/fake transport may expose an empty field face even though
64
+ // the model exists. Empty metadata is not proof that a requested field is
65
+ // absent; preserve the existing relation write path rather than rejecting a
66
+ // valid payload on an unavailable read face.
67
+ if (targetRows.length === 0 || ownRows.length === 0)
68
+ return;
69
+ const candidates = targetFields.map((row) => row.code).sort();
70
+ const display = typeof options.displayField === "string" ? options.displayField.trim() : "";
71
+ if (display) {
72
+ const field = targetFields.find((row) => row.code === display);
73
+ const declaredType = options.displayFieldType === undefined ? undefined : Number(options.displayFieldType);
74
+ if (!field || (declaredType !== undefined && Number.isInteger(declaredType) && field.type !== undefined && field.type !== declaredType)) {
75
+ throw new ToolRefusalError({ status: "refused", blocker: "relation-display-field-not-found", schemaCode, relationTarget: target, displayField: display, candidates, message: `关联显示字段 ${target}.${display} 不存在于发布面或类型不兼容;请从目标模型业务字段中选择。候选:${candidates.join("、") || "(无)"}` });
76
+ }
77
+ }
78
+ let mappings = options.mappings;
79
+ if (typeof mappings === "string" && mappings) {
80
+ try {
81
+ mappings = JSON.parse(mappings);
82
+ }
83
+ catch {
84
+ mappings = [];
85
+ }
86
+ }
87
+ if (!Array.isArray(mappings))
88
+ return;
89
+ const sourceCandidates = targetFields.map((row) => row.code).sort();
90
+ const targetCandidates = ownFields.map((row) => row.code).sort();
91
+ for (const [index, row] of mappings.entries()) {
92
+ const source = row && typeof row === "object" ? String(row.source ?? "").trim() : "";
93
+ const mappedTarget = row && typeof row === "object" ? String(row.target ?? "").trim() : "";
94
+ if (!sourceCandidates.includes(source) || !targetCandidates.includes(mappedTarget)) {
95
+ throw new ToolRefusalError({ status: "refused", blocker: "relation-mapping-field-not-found", schemaCode, relationTarget: target, mappingIndex: index, source, targetField: mappedTarget, sourceCandidates, targetCandidates, message: `关联映射第 ${index + 1} 行字段不存在:source=${source || "(空)"}(目标模型候选:${sourceCandidates.join("、") || "(无)"}),target=${mappedTarget || "(空)"}(本模型候选:${targetCandidates.join("、") || "(无)"})。请使用发布面业务字段 code。` });
96
+ }
97
+ }
98
+ }
44
99
  async function readFieldRollback(session, args) {
45
100
  for (const published of [false, true]) {
46
101
  const row = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: String(args.schemaCode), isPublish: String(published) } })).data).find((entry) => String(entry.code ?? entry.propertyCode) === String(args.code));
@@ -314,7 +369,11 @@ export function registerFieldLane(tools) {
314
369
  ...(conditions.length > 0 ? { conditions } : {}),
315
370
  ...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
316
371
  ...(field.origin !== undefined ? { origin: field.origin } : {}),
317
- ...(args.full === true && "options" in field && field.options !== undefined ? { options: field.options, optionsPresent: true } : {}),
372
+ ...(args.full === true
373
+ ? ("options" in field && field.options !== undefined
374
+ ? { options: field.options, optionsPresent: true }
375
+ : { optionsPresent: false })
376
+ : {}),
318
377
  };
319
378
  });
320
379
  return {
@@ -373,6 +432,11 @@ export function registerFieldLane(tools) {
373
432
  ...(conditions.length > 0 ? { conditions } : {}),
374
433
  ...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
375
434
  ...(field.origin !== undefined ? { origin: field.origin } : {}),
435
+ // r46a 接线(2026-11-08,r45 普查 R5a):压缩占位行保留**标量判别键**
436
+ // optionsPresent——不然 >10 行的 full 读回内联面完全看不到 presence
437
+ // 信号,agent 误判「条件没写进去」(S1-G3 预言命中:通用层把「有值
438
+ // 但正文外置」和「确实没有」混在一起)。正文仍在 artifact。
439
+ ...(field.optionsPresent !== undefined ? { optionsPresent: field.optionsPresent } : {}),
376
440
  };
377
441
  }),
378
442
  hint: "字段详情(options/subSchema)用 omc call field.list --args '{\"schemaCode\":\"...\",\"published\":true,\"code\":\"<field>\"}'",
@@ -418,6 +482,7 @@ export function registerFieldLane(tools) {
418
482
  // (cpdocs issue #8:缺键让设计器选项弹窗硬崩)。
419
483
  const targetVersion = session.detectedVersion();
420
484
  const prepared = prepareFieldChange({ ...args, ...(targetVersion ? { targetVersion } : {}) });
485
+ await assertRelationOptionsFields(session, args.schemaCode, prepared.propertyType, args.relativeCode, prepared.options);
421
486
  let performed;
422
487
  try {
423
488
  performed = await wireCall(session, { path: ENDPOINTS.fieldCreate, method: "POST", body: fieldChangeWireBody(args, prepared) });
@@ -730,6 +795,7 @@ async function fieldUpdateDryRun(session, args) {
730
795
  ...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
731
796
  };
732
797
  const prepared = prepareFieldUpdate(update);
798
+ await assertRelationOptionsFields(session, args.schemaCode, prepared.propertyType, args.relativeCode ?? record.relativeCode, prepared.options);
733
799
  const currentOptions = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
734
800
  const changes = [];
735
801
  const pushChange = (field, from, to) => {
@@ -554,8 +554,11 @@ export function registerOrganizationLane(tools) {
554
554
  // A 32-hex id absent from the tree may be a group created moments ago in
555
555
  // this same session; let it through and rely on the visibility backstop.
556
556
  if (!match && args.roleGroupCode) {
557
- throw new ToolInputError({
557
+ throw new ToolRefusalError({
558
+ status: "refused",
559
+ blocker: "role-group-not-found",
558
560
  message: `角色组 ${requested} 不存在。可用组:${groups.map((group) => `${group.code}(id=${group.id},${group.name})`).join(";")}`,
561
+ candidates: groups.map((group) => ({ code: group.code, id: group.id, name: group.name })),
559
562
  });
560
563
  }
561
564
  const roleGroupId = match?.id ?? String(requested);
@@ -878,7 +878,15 @@ async function assertRoleParticipantScopes(session, body) {
878
878
  if (String(activity.activityType ?? "") !== "PARTICIPANT")
879
879
  continue;
880
880
  const participant = typeof activity.participant === "string" ? activity.participant.trim() : "";
881
- const match = /^FindUserByRole\s*\(\s*[^,]+?\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)\s*$/.exec(participant);
881
+ if (/^FindUserByRole\s*\(/i.test(participant)) {
882
+ const strict = /^FindUserByRole\s*\(\s*\{Originator\.OU\}\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)\s*$/i.exec(participant);
883
+ if (!strict)
884
+ throw new ToolRefusalError({ status: "refused", blocker: "workflow-find-user-by-role-parameters-invalid", activityCode: String(activity.activityCode ?? ""), nextCommand: "使用 FindUserByRole({Originator.OU},\"<真实角色code>\",\"random\");组织范围不得手填 id", message: `节点 ${String(activity.activityCode ?? "")} 的 FindUserByRole 参数不合法:x 必须是单一 {Originator.OU},不得手填 id;y 必须是带引号的单一角色 code。` });
885
+ const order = strict[2].slice(1, -1);
886
+ if (!new Set(["dept", "sortKey", "random"]).has(order))
887
+ throw new ToolRefusalError({ status: "refused", blocker: "workflow-find-user-by-role-order-invalid", activityCode: String(activity.activityCode ?? ""), order, candidates: ["dept", "sortKey", "random"], nextCommand: "将第三参数改为 dept、sortKey 或 random", message: `FindUserByRole 第三参数 ${order} 不在允许枚举 dept/sortKey/random。` });
888
+ }
889
+ const match = /^FindUserByRole\s*\(\s*\{Originator\.OU\}\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)\s*$/.exec(participant);
882
890
  if (!match)
883
891
  continue;
884
892
  const quoted = match[1];
@@ -902,9 +910,7 @@ async function assertRoleParticipantScopes(session, body) {
902
910
  roleListAvailable = true;
903
911
  }
904
912
  catch {
905
- // Role existence is advisory when the organization read face is unavailable.
906
- // Do not turn a transient read failure into a blanket workflow write outage.
907
- roleListAvailable = false;
913
+ throw new ToolRefusalError({ status: "refused", blocker: "workflow-participant-role-read-failed", nextCommand: "omc call org.role.list --args '{}'", message: "无法读取真实角色树,不能确认 FindUserByRole 的 role code;为避免流程静默回退发起人,已 fail-closed 拒绝写入。请恢复组织读面后重试。" });
908
914
  }
909
915
  if (roleListAvailable) {
910
916
  const byCode = new Map(listedRoles
@@ -385,6 +385,7 @@ const ruleCrossModelCreate = Object.freeze({
385
385
  "**`originatorValueType:\"DYNAMIC\"` 的 `originatorValue` 必须指向人员字段**(STAFF_SELECTOR 等);指向文本/编号字段会致发起人错误或为空。",
386
386
  "「当前时间」不是规则的 value 类型:`ruleValueType:\"SYSTEM\"` 不会填 now(真机 2026-10-08 落 null)。要让下游日期=生成时刻,把该**日期字段的默认值**设为「当前时间」(平台 `{value:\"current\"}`),或走二开;纯规则映射填不出 now。",
387
387
  "上游是 QUERY 时 `inputParam=\"_<QUERY节点code>_output\"`(QUERY 查 N 行→CREATE 逐条生成即 N→N 扇出,真机验证支持,同/跨模型均可);否则 `\"_input\"`。",
388
+ "跨模型 UPDATE 三段:`inputParam`/`currentSchemaDataItem`=输入源(触发行或 QUERY 输出)→ `filterCondition` 查 `targetSchemaCode` 目标行 → `dataActions` 写入。QUERY 目标行非隐式输入源;child fan-out 保持 fail-closed。",
388
389
  "`QUERY` 节点只能查**主模型行**:查子表 schema(`parent.child` 或子 schema 名)空条件返回 0 行、带条件 550018(真机 2026-10-08)——逐子表明细扇出才需要二开。",
389
390
  "跨模型 CREATE/UPDATE 真机验证支持(2026-09-28 / 2026-10-08);承载规则的触发时机要对(Create 在提交时触发、Update 只在 data.update 触发、TIMING 定时触发)。",
390
391
  "**CREATE 目标是已发布流程的模型时**(2026-10-27 真机四格矩阵):不传 startWorkflow → 建行**不发起流程**;要发起必须 startWorkflow:true + targetWorkflowCode:<该流程> + originatorValue 用户对象数组 [{\"id\":\"<userId>\",\"name\":\"<名>\"}](传对象 {} → 触发提交 50000 expect '[';无效 id → 50000 发起人不存在);设计器手工编辑可打开该开关,API 与设计器配置会漂移,rule.get 回读核对。",
@@ -107,7 +107,7 @@ const numericTwoDecimals = Object.freeze({
107
107
  priorToolReferences: [{ tool: "discover_environment", args: { environment }, purpose: "确认目标模型已存在" }],
108
108
  dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 校验 propertyType/scale/治理,不创建" },
109
109
  expectedReadback: { readback: { draftFieldPresent: true }, next: ["field.publish"] },
110
- refusalRecovery: [{ code: "capability-out-of-coverage", action: "NUMERICAL 必须带 options.scale;金额两位小数用 scale:2,整数用 scale:0" }, ...commonRecovery],
110
+ refusalRecovery: [{ code: "field-options-shape", action: "NUMERICAL 必须带 options.scale;金额两位小数用 scale:2,整数用 scale:0" }, ...commonRecovery],
111
111
  requiresEnvironmentDerivedIds: false,
112
112
  });
113
113
  const dictionarySelection = Object.freeze({
@@ -6,6 +6,7 @@ import { ToolRefusalError } from "./tool-types.js";
6
6
  import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
7
7
  import { RULE_BIZ_RULE_TYPE_DESCRIPTION, RULE_CREATE_BODY, RULE_ROUTES_DESCRIPTION, ruleNodeGraphDescription, schedulerSettingDescription, } from "./lanes/businessrule-contract.js";
8
8
  import { validateRuleNodeGraph, prepareRuleGraph, PLATFORM_TRIGGER_RULE_CODES, DEFAULT_SYSTEM_NODE_BY_RULE_CODE } from "./lanes/rule-graph.js";
9
+ import { FORMULA_LINT_CODES } from "./formula-lint.js";
9
10
  import { compileMethodConfigPlain } from "./bizservice-config.js";
10
11
  import { RESERVED_CODES } from "./code-rules.js";
11
12
  import { FIELD_PROPERTY_TYPES } from "./field-families.js";
@@ -757,6 +758,16 @@ export async function executeRuleSaveWrite(session, args) {
757
758
  // plain advisory.
758
759
  const lintWarnings = warningFindings.filter((blocker) => blocker.hazard !== true);
759
760
  const lintHazards = warningFindings.filter((blocker) => blocker.hazard === true);
761
+ const formulaWarnings = lintWarnings.filter((finding) => FORMULA_LINT_CODES.has(finding.code));
762
+ if (formulaWarnings.length > 0 && args.allowAdvisory !== true) {
763
+ throw new ToolRefusalError({
764
+ status: "refused",
765
+ blocker: "formula-lint-failed",
766
+ validation: formulaWarnings,
767
+ nextCommand: "将未知函数改为平台注册函数(文本拼接使用 STRING),仅调查时显式传 allowAdvisory:true",
768
+ message: "公式写前校验拒绝:发现未知函数、平台别名或字符串 + 拼接。请修正公式;调查用途可显式 allowAdvisory:true(默认 fail-closed)。",
769
+ });
770
+ }
760
771
  if (blockers.some((blocker) => blocker.severity !== "warning")) {
761
772
  throw new ToolRefusalError({
762
773
  status: "refused",
package/dist/skills.js CHANGED
@@ -276,6 +276,8 @@ export function readSkillSection(name, requested) {
276
276
  const matches = (heading) => {
277
277
  if (heading === normalized)
278
278
  return true;
279
+ if (heading.replace(/\s+`[^`]+`\s*$/, "").trim() === normalized)
280
+ return true;
279
281
  if (heading.replace(/[^\p{L}\p{N}]+/gu, "-").toLowerCase() === normalized)
280
282
  return true;
281
283
  if (requestedOrdinal !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "commander": "14.0.3",
58
- "omc-core": "0.6.25",
58
+ "omc-core": "0.6.26",
59
59
  "proper-lockfile": "4.1.2",
60
60
  "write-file-atomic": "6.0.0",
61
61
  "zod": "^3.23.8",
@@ -73,6 +73,6 @@
73
73
  "directory": "packages/omc-cli"
74
74
  },
75
75
  "optionalDependencies": {
76
- "@stalfh233/omc-fake-platform": "0.6.25"
76
+ "@stalfh233/omc-fake-platform": "0.6.26"
77
77
  }
78
78
  }
@@ -13,6 +13,7 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
13
13
 
14
14
  1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"` → `omc intent schema`
15
15
  2. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '<JSON数组>' --apply`(字段含 `amount` NUMERICAL scale=2、`<主表code>_details` CHILD_TABLE 及 item/qty/price/lineAmount 子列;子表 code 必须全局唯一)
16
+ 3. (`model-scaffold` 已在第 2 步完成字段发布与表单发布;无需另起命令)
16
17
  4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
17
18
  5. 条件审批(金额>50000 两级):**不要** `approval-flow --condition`(无此参数)。线性先:`omc approval-flow --schema <code> --steps '经理@role:<mgr>' --steps '总监@role:<dir>' --apply`;条件分流改 `omc describe workflow.update --example workflow-conditional-branch` 后 `workflow.update`。
18
19
  6. 发起权限:`omc call workflow.addPermission --args '{"workflowCode":"<code>_wf","unitId":"<deptOrRoleId>","unitType":1}'`(这是**谁能发起**,不是行级可见范围)