@stalfh233/omc-cli 0.6.14 → 0.6.16

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.
@@ -20,6 +20,54 @@
20
20
  */
21
21
  import { refusalText } from "./cli-contract.js";
22
22
  import { runScript } from "./script.js";
23
+ function inputBusinessKey(args) {
24
+ const keys = ["code", "name", "bizObjectId", "id"];
25
+ const out = {};
26
+ for (const key of keys)
27
+ if (args[key] !== undefined)
28
+ out[key] = args[key];
29
+ return out;
30
+ }
31
+ function failureDetails(entry, args) {
32
+ const message = refusalText(entry.result);
33
+ const formData = args.formData && typeof args.formData === "object" && !Array.isArray(args.formData)
34
+ ? args.formData
35
+ : args;
36
+ const numericName = Object.keys(formData).find((key) => /amount|qty|quantity|price|number|count/i.test(key));
37
+ const numericValue = numericName === undefined ? undefined : formData[numericName];
38
+ if (numericName !== undefined && typeof numericValue === "string" && /50000|服务异常|wire-business/i.test(message)) {
39
+ return {
40
+ fieldCode: numericName,
41
+ value: numericValue,
42
+ reason: `NUMERICAL 字段收到字符串 "${numericValue}"`,
43
+ };
44
+ }
45
+ return { hint: "平台回了无明细 50000,常见成因见 help omc-refusals;请用该行输入与已发布字段逐项核对" };
46
+ }
47
+ export function batchWarnings(argsList) {
48
+ const warnings = [];
49
+ const seenCodes = new Map();
50
+ argsList.forEach((args, index) => {
51
+ const row = index + 1;
52
+ const data = args.formData && typeof args.formData === "object" && !Array.isArray(args.formData) ? args.formData : args;
53
+ if (data.code === undefined || data.code === null || data.code === "")
54
+ warnings.push({ row, fieldCode: "code", code: "required", message: "必填字段 code 缺失" });
55
+ else if (typeof data.code === "string") {
56
+ const previous = seenCodes.get(data.code);
57
+ if (previous !== undefined)
58
+ warnings.push({ row, fieldCode: "code", code: "duplicate", message: `code 与第 ${previous} 行重复` });
59
+ else
60
+ seenCodes.set(data.code, row);
61
+ }
62
+ for (const [fieldCode, value] of Object.entries(data)) {
63
+ if (typeof value === "string" && value.length > 255)
64
+ warnings.push({ row, fieldCode, code: "maxLength", message: `${fieldCode} 长度 ${value.length} 超过常见上限 255` });
65
+ if (typeof value === "number" && !Number.isInteger(value) && /qty|quantity|count|level/i.test(fieldCode))
66
+ warnings.push({ row, fieldCode, code: "scale-exceeded", message: `${fieldCode} 含小数,但字段可能是 0 位数值字段` });
67
+ }
68
+ });
69
+ return warnings;
70
+ }
23
71
  /** Cap mirrors `omc script`: a runaway list should be a refusal, not a 20-min hang. */
24
72
  export const BATCH_MAX_ENTRIES = 200;
25
73
  /**
@@ -69,9 +117,12 @@ export async function runBatchCall(options) {
69
117
  .filter((entry) => entry.status !== "ok")
70
118
  .map((entry) => ({
71
119
  index: entry.index,
120
+ row: entry.index + 1,
121
+ indexBase: 0,
72
122
  status: String(entry.result.status ?? "failed"),
73
123
  blocker: entry.result.blocker ?? null,
74
124
  message: refusalText(entry.result),
125
+ ...failureDetails(entry, options.argsList[entry.index] ?? {}),
75
126
  ...(Array.isArray(entry.result.issues) ? { issues: entry.result.issues } : {}),
76
127
  }));
77
128
  const executed = entries.length;
@@ -92,7 +143,10 @@ export async function runBatchCall(options) {
92
143
  const skippedMarker = performed?.skipped;
93
144
  return {
94
145
  index: entry.index,
146
+ row: entry.index + 1,
147
+ indexBase: 0,
95
148
  ok: entry.status === "ok",
149
+ ...inputBusinessKey(options.argsList[entry.index] ?? {}),
96
150
  ...(bizObjectId !== undefined ? { bizObjectId } : {}),
97
151
  ...(entry.status === "ok" ? {} : { status: String(entry.result.status ?? "failed"), blocker: entry.result.blocker ?? null }),
98
152
  ...(skippedMarker !== undefined ? { skipped: String(skippedMarker) } : {}),
@@ -112,6 +166,7 @@ export async function runBatchCall(options) {
112
166
  // 每条一行摘要(index/ok/bizObjectId)——不是完整信封,但足以回答
113
167
  // 「第 3 条建成了吗、它的 id 是什么」。失败项另有 failures 带完整原因。
114
168
  entries: entriesInline,
169
+ ...(batchWarnings(options.argsList).length > 0 ? { warnings: batchWarnings(options.argsList) } : {}),
115
170
  ...(entries.length > entriesInline.length
116
171
  ? { entriesOmitted: entries.length - entriesInline.length, entriesHint: `条目超过 ${ENTRIES_INLINE_LIMIT} 条,只内联前 ${ENTRIES_INLINE_LIMIT} 条摘要;完整结果走 detailArtifactId / omc artifact get` }
117
172
  : {}),
@@ -252,12 +252,12 @@ export const CAPABILITY_CUES = Object.freeze({
252
252
  programming: ["在线开发", "在线编码", "后端源码", "java源码", "编码模式", "handler", "programming", "源码编译"],
253
253
  bizservice: ["集成", "接口", "服务", "回调", "对接", "外部系统", "datasql", "restful"],
254
254
  report: ["报表", "图表", "看板", "bi", "dashboard"],
255
+ data: ["运行数据", "提交数据", "保存记录", "查记录", "造数据", "测试数据", "测试单据", "造单据", "造单", "录数据", "填数据", "模拟数据", "业务数据", "造几条", "造两张", "造几张", "验数据", "批量导入", "批量写入", "批量删除", "导入数据"],
255
256
  // r18 (2026-10-26): "造数据 / 造测试单据 / 录数据" is the RUN-DATA capability, but
256
257
  // the goal usually also says 子表/明细 (field cues) — without these compound
257
258
  // cues the router sent the probe to `field` in read mode (write:false), so it
258
259
  // never reached data.submit. Compound (not bare 数据) to avoid stealing
259
260
  // ordinary model/field asks that merely mention data.
260
- data: ["运行数据", "提交数据", "保存记录", "查记录", "造数据", "测试数据", "测试单据", "造单据", "造单", "录数据", "填数据", "模拟数据", "业务数据", "造几条", "造两张", "造几张", "验数据"],
261
261
  });
262
262
  /**
263
263
  * Boundary cues — authored alongside CAPABILITY_CUES (same runtime authority).
package/dist/cli.js CHANGED
@@ -463,9 +463,10 @@ async function cmdBatchCall(store, toolName, rawList, options) {
463
463
  failures.push({ index, blocker: payload.blocker ?? null, message: refusalText(payload), ...(Array.isArray(payload.validation) ? { validation: payload.validation } : {}), ...(payload.citation !== undefined ? { citation: payload.citation } : {}) });
464
464
  }
465
465
  });
466
- const result = {
466
+ let result = {
467
467
  status: failures.length > 0 ? "partial" : "ok",
468
468
  mode: "preflight",
469
+ shapeOnly: true,
469
470
  tool: toolName,
470
471
  requested: entries.length,
471
472
  shapeChecked: entries.length,
@@ -473,8 +474,16 @@ async function cmdBatchCall(store, toolName, rawList, options) {
473
474
  writes: 0,
474
475
  // On failure the caller has to change the JSON first, so there is no
475
476
  // runnable command yet; on success, hand over the exact execution command.
476
- ...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list ${JSON.stringify(JSON.stringify(entries))}` }),
477
+ ...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list '<file|stdin>'` }),
477
478
  };
479
+ const preflightPaths = [...argvValues(options.args, "--extract"), ...argvValues(options.args, "--jq")]
480
+ .flatMap((path) => path.split(",")).map((path) => path.trim()).filter(Boolean);
481
+ const preflightKeys = options.args.includes("--extract-keys") || options.args.includes("--keys") || options.args.some((token, index) => token === "--jq" && (options.args[index + 1] === undefined || options.args[index + 1].startsWith("--")));
482
+ if (preflightPaths.length > 0 || preflightKeys) {
483
+ const projected = projectResultExtract(result, preflightPaths, preflightKeys);
484
+ if (projected !== undefined)
485
+ result = projected;
486
+ }
478
487
  const classified = classifyResult(result);
479
488
  writeEnvelope(envelope(classified.status, result, request, classified.error));
480
489
  return failures.length > 0 ? 1 : 0;
@@ -525,7 +534,7 @@ async function cmdCall(store, args) {
525
534
  return emitTransportResult({ status: "ok", ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, { request, maxOutputBytes });
526
535
  }
527
536
  }
528
- console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
537
+ console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径(必须位于配置根内;跨目录临时文件用 --args -(stdin))>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
529
538
  console.error("输出预算:--max-output-bytes 默认 65536(只约束最终 stdout 信封;读面 diet ~2048 独立外置 detailArtifactId);artifact get 内联上限 8192");
530
539
  console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
531
540
  return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
@@ -536,6 +545,11 @@ async function cmdCall(store, args) {
536
545
  const argsList = flag("--args-list");
537
546
  if (argsList !== undefined)
538
547
  return cmdBatchCall(store, toolName, argsList, { environment, requestValue, args, preflight: args.includes("--preflight") });
548
+ if (args.includes("--preflight")) {
549
+ const message = "--preflight 仅用于 --args-list 批量形状;单次调用没有批量预检语义。请改用 --args-list,或移除 --preflight 后执行单次调用。";
550
+ writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message, retryable: false }));
551
+ return CLI_EXIT_CODES.invalidInput;
552
+ }
539
553
  // 1.2 of issue 2026-10-23: read faces answer {detailArtifactId, sha256} and the
540
554
  // caller then needs a SECOND call to see one value (16 two-step chains in one
541
555
  // session; 15 more were pure --keys probes). `--extract <path>` (repeatable)
@@ -685,7 +699,7 @@ async function cmdCall(store, args) {
685
699
  const otherUnknown = unknownKeys.filter((key) => !APP_SCOPE_ARG_KEYS.has(key));
686
700
  if (tool.kind !== "write" && otherUnknown.length > 0) {
687
701
  if (!lenientArgs) {
688
- const suggestions = otherUnknown.map((key) => ({ key, ...suggestArgumentKey(key, [...schemaKeys]) }));
702
+ const suggestions = otherUnknown.map((key) => ({ key, ...suggestArgumentKey(key, [...schemaKeys], toolName) }));
689
703
  const pairs = suggestions.map((entry) => {
690
704
  if (entry.closest)
691
705
  return `${entry.key}→${entry.closest}`;
@@ -705,7 +719,7 @@ async function cmdCall(store, args) {
705
719
  return emit("error", result, CLI_EXIT_CODES.invalidInput);
706
720
  }
707
721
  for (const key of otherUnknown) {
708
- ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys]) });
722
+ ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys], toolName) });
709
723
  }
710
724
  }
711
725
  }
@@ -781,6 +795,10 @@ async function cmdCall(store, args) {
781
795
  const projected = projectResultExtract(result, extractPaths, extractKeys);
782
796
  if (projected !== undefined)
783
797
  result = projected;
798
+ // Projected reads are represented by extract[] (and rows[] for multi-path).
799
+ // Do not also copy the single-path value to top-level content.
800
+ if (extractPaths.length > 0 && !extractKeys && typeof result === "object" && result !== null)
801
+ delete result.content;
784
802
  }
785
803
  // 读面未知键的非阻断回显:放在投影**之后**,否则会被 --jq 的窄投影吞掉。
786
804
  if (ignoredReadKeys.length > 0 && typeof result === "object" && result !== null) {
@@ -828,8 +846,15 @@ const APP_SCOPE_ARG_KEYS = new Set(["appCode", "applicationCode", "application"]
828
846
  * 短键(≤5 字符)靠距离算不准,这里先查显式缩写表(与 tool-types 的 KEY_ALIAS_HINTS
829
847
  * 同源),命中且参数真实存在才用;否则退回 closestMatches。
830
848
  */
831
- export function suggestArgumentKey(key, schemaKeys) {
849
+ export function suggestArgumentKey(key, schemaKeys, toolName) {
832
850
  const known = new Set(schemaKeys);
851
+ const normalizedTool = toolName?.toLowerCase() ?? "";
852
+ if ((normalizedTool.startsWith("workflow.") || normalizedTool.startsWith("workitem.")) && key === "code" && known.has("workflowCode"))
853
+ return { closest: "workflowCode" };
854
+ if (normalizedTool.startsWith("data.") && key === "id" && known.has("bizObjectId"))
855
+ return { closest: "bizObjectId" };
856
+ if (normalizedTool.startsWith("data.") && key === "ids" && known.has("bizObjectIds"))
857
+ return { closest: "bizObjectIds" };
833
858
  const aliases = KEY_ALIAS_HINTS[key.toLowerCase()] ?? [];
834
859
  for (const candidate of aliases) {
835
860
  if (known.has(candidate))
@@ -1616,17 +1641,22 @@ async function cmdGraphPull(environment, concurrency, transport) {
1616
1641
  return emitThrown(error, transport);
1617
1642
  }
1618
1643
  }
1619
- function cmdGraphReferences(kind, code, transport) {
1644
+ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
1620
1645
  if (!["service", "model", "field"].includes(kind))
1621
1646
  return emitInvalid(transport, { message: "kind 必须是 service / model / field(同 rule.references 第一版边界)" });
1622
1647
  try {
1623
1648
  const result = queryGraphReferences(kind, code);
1649
+ if (requestedEnvironment !== undefined && requestedEnvironment !== result.index.environmentName)
1650
+ return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${result.index.environmentName},不是 ${requestedEnvironment};请先 omc graph pull --env ${requestedEnvironment}`, index: result.index }, transport);
1624
1651
  return emitResult({
1625
1652
  status: "ok",
1626
1653
  kind: result.kind,
1627
1654
  code: result.code,
1628
1655
  matches: result.matches,
1629
1656
  index: result.index,
1657
+ // r35a-F4:覆盖声明必须透传——CLI 这里是白名单式重建,漏加就等于没修
1658
+ // (matches:[] 曾被当成可安全删除的依据)。
1659
+ ...(result.coverage !== undefined ? { coverage: result.coverage } : {}),
1630
1660
  ...(result.note !== undefined ? { note: result.note } : {}),
1631
1661
  ...(result.gapWarning !== undefined ? { gapWarning: result.gapWarning } : {}),
1632
1662
  }, transport);
@@ -2129,7 +2159,7 @@ async function main() {
2129
2159
  commandExitCode = CLI_EXIT_CODES.ok;
2130
2160
  });
2131
2161
  command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
2132
- command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
2162
+ command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
2133
2163
  const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
2134
2164
  ...(options.keys ? ["--keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
2135
2165
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
@@ -2412,18 +2442,19 @@ async function main() {
2412
2442
  console.error(`已清理 ${result.removed} 个读回产物,释放 ${formatBytes(result.freedBytes)};保留 ${result.kept} 个(${formatBytes(result.keptBytes)})`);
2413
2443
  commandExitCode = CLI_EXIT_CODES.ok;
2414
2444
  });
2415
- // r28h F3(真机 2026-11-05):`omc call` 用 `--extract`、`artifact get` 用 `--jq`,
2416
- // 语义完全相同。探针先试 `--extract` 拿到 `unknown option`(白花一次往返),
2417
- // 才想起两者不同名。别名照收,help 里也标出来。
2418
- artifactCommand.command("get <id>").option("--jq [path]").option("--extract <path>", "同 --jq(与 omc call 的 --extract 同名,省一次命名切换)").option("--path-only").option("--summary").option("--keys [a,b]").option("--full", "整包返回正文(默认:正文超过 8KB 时只回形状 outline + fetch,按需 --jq/--keys 定向取值)").option("--max-output-bytes <bytes>").action(async (id, options) => {
2445
+ artifactCommand.command("get <id>").addOption(new Option("--jq [path]").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract <path>", "同 --jq(与 omc call 的 --extract 同名,省一次命名切换)").option("--path-only").option("--summary").option("--keys [a,b]").option("--full", "整包返回正文(默认:正文超过 8KB 时只回形状 outline + fetch,按需 --jq/--keys 定向取值)").option("--max-output-bytes <bytes>").action(async (id, options) => {
2419
2446
  // A bare `--jq`/`--keys` is a key-inventory request, not a missing argument:
2420
2447
  // commander's "argument missing" (invalid-input + retry) is a dead end the
2421
2448
  // caller cannot fix without already knowing the flag takes a value (r11
2422
2449
  // burned six rounds on `artifact get <id> --jq` before trying `--summary`).
2423
2450
  // --extract 是 --jq 的同义别名(r28h F3)。
2424
- const jqPath = typeof options.jq === "string" ? options.jq : typeof options.extract === "string" ? options.extract : undefined;
2451
+ // 重复 --jq 的多路径合并为逗号路径透传(r34c-F8:此前静默只留最后一个)。
2452
+ const jqPaths = [
2453
+ ...(Array.isArray(options.jq) ? options.jq.filter((v) => typeof v === "string") : []),
2454
+ ...(typeof options.extract === "string" ? [options.extract] : []),
2455
+ ];
2425
2456
  const bareJq = options.jq === true;
2426
- commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(jqPath !== undefined ? { jq: jqPath } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true || bareJq,
2457
+ commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(jqPaths.length > 0 ? { jq: jqPaths.join(",") } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true || bareJq,
2427
2458
  pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
2428
2459
  });
2429
2460
  command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
@@ -2496,8 +2527,8 @@ async function main() {
2496
2527
  const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
2497
2528
  commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport));
2498
2529
  });
2499
- graphCommand.command("references <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references 第一版边界;结果带索引新鲜度与扫描缺口(有缺口时零命中不构成「无引用」证明)。\n").action(async (kind, code, options) => {
2500
- commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport));
2530
+ graphCommand.command("references <kind> <code>").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references 第一版边界;结果带索引新鲜度与扫描缺口(有缺口时零命中不构成「无引用」证明)。覆盖:规则图/数据规则/视图;不含表单、workflow.permissions、流程绑定、在线 JS。在线版 rule.references 不含数据规则;两者都不含上述未扫描面。\n").action(async (kind, code, options) => {
2531
+ commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport, options.environment ?? options.env));
2501
2532
  });
2502
2533
  graphCommand.command("impact <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n影响面(离线):跳1=引用该编码的规则(同 references),跳2=这些规则还读写哪些 service/model/field——改前看爆炸半径。截断上限 50 规则×每类 20 码。\n").action(async (kind, code, options) => {
2503
2534
  commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
package/dist/discovery.js CHANGED
@@ -588,7 +588,13 @@ export function describeParam(registry, name, param) {
588
588
  // r28e 真机(2026-11-05):调用方看到 `status:"ok"` + 一堆键,误以为"参数存在、
589
589
  // 只是描述为空"。`found:false` 是机器信号,但人读时需要一句话点明——否则
590
590
  // 会照着 `available` 之外的名字继续试。
591
- const nearest = closestMatch(param, Object.keys(schema.properties ?? {}));
591
+ const availableKeys = Object.keys(schema.properties ?? {});
592
+ // Prefer a same-tool semantic alias over a cross-tool same-name suggestion.
593
+ // `datarule.create --param type` means the rule discriminator, whose wire
594
+ // name is dataRuleType; recommending custompage.list's `type` sends the
595
+ // caller to the wrong tool family.
596
+ const semantic = name.startsWith("datarule.") && param === "type" && availableKeys.includes("dataRuleType") ? "dataRuleType" : undefined;
597
+ const nearest = semantic ?? closestMatch(param, availableKeys);
592
598
  return {
593
599
  name: tool.name,
594
600
  param,
@@ -672,6 +672,29 @@ export function goalRoute(registry, goal) {
672
672
  firstAnswer: { state: "blocked", why: boundary.note },
673
673
  };
674
674
  }
675
+ if (/批量导入|批量写入|批量删除|导入数据/.test(goal)) {
676
+ const action = /批量删除/.test(goal) ? "data.delete" : "data.save";
677
+ return {
678
+ status: "ok", goal, confident: true, mode: "modify", write: true,
679
+ match: { kind: "capability", ref: "data", title: "运行数据批量写入(--args-list)" },
680
+ route: { capability: "data", lane: "runtime-data", note: "运行数据批量操作逐条执行,失败继续;先用 --preflight 做零写入形状校验", readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: [`omc call ${action} --args-list '<file|stdin>' --preflight`, `omc call ${action} --args-list '<file|stdin>'`], write: true },
681
+ firstAnswer: { state: "needs-input", question: "目标模型 schemaCode 和输入文件/JSON 数组是什么?示例:omc call data.save --args-list '<file|stdin>' --preflight", action: LIST_MODELS_ACTION, pendingCommand: `omc call ${action} --args-list '<file|stdin>'`, why: "批量导入/写入/删除使用 --args-list;先预检形状,再逐条执行" },
682
+ };
683
+ }
684
+ // Cross-application model moves/copies are not an intent migration. There
685
+ // is no model.move/model.copy path across app boundaries: model.move only
686
+ // reparents a model within its current application. Keep the useful
687
+ // declarative route for rebuilding the target model, but put this fact in
688
+ // the route itself so callers cannot mistake intent for an in-place move.
689
+ if (/模型/.test(goal) && /(跨应用|一个应用.*另一个应用|从一个应用.*到另一个应用)/.test(goal) && /(移动|复制|迁移|搬)/.test(goal)) {
690
+ const routed = wholePackageRoute(registry, goal, index, ["app", "model"]);
691
+ const clarification = "事实澄清:没有跨应用迁移/复制模型的专用工具;model.move 仅支持同一应用内换目录。跨应用移植需要在新应用重建模型,并手工搬运数据。";
692
+ return {
693
+ ...routed,
694
+ route: { ...routed.route, note: clarification },
695
+ firstAnswer: { ...routed.firstAnswer, why: `${clarification} ${routed.firstAnswer.why}` },
696
+ };
697
+ }
675
698
  // Composer before the capability solver: a recurring write SHAPE has a single
676
699
  // command that does the job with preflight + idempotency, which beats a ladder
677
700
  // of atomic tools even when the ladder also matches (r13: 26 field.create).
package/dist/graph.js CHANGED
@@ -607,16 +607,26 @@ export function queryGraphReferences(kind, code) {
607
607
  via: row.via,
608
608
  }));
609
609
  const gapped = summary.gaps.count > 0;
610
+ // r35a-F4(真机 2026-11-06):探针拿 matches:[] 差点把一个**被表单引用**的
611
+ // 字段判成「可安全删除」——note 只说了「不含规则条件叶子/流程绑定」,从没
612
+ // 明说表单与流程权限表不在扫描范围。散文字段不承担安全契约:这里给结构化
613
+ // coverage,让「没人引用」与「这一面没扫」在机器可读层面可分。
614
+ const coverage = {
615
+ scanned: ["businessrule", "datarule", "listview"],
616
+ notScanned: ["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"],
617
+ note: "matches:[] 只代表 scanned 面零命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除",
618
+ };
610
619
  return {
611
620
  kind,
612
621
  code,
613
622
  matches,
623
+ coverage,
614
624
  index: summary,
615
625
  ...(matches.length === 0
616
626
  ? {
617
627
  note: (gapped
618
628
  ? "零命中,但索引存在扫描缺口——不可作为「无引用」证明;先 omc graph pull 重建再查"
619
- : "零命中=索引覆盖范围内没有引用它(规则面:service→methodMapping.serviceCode;model→targetObjectCode/methodMapping.schemaCode;field→dataActions.targetDataItem/映射 bizCode;数据规则面:propertyCodes 目标与 options {字段} 引用;视图面:列/排序/条件的 propertyCode。不含规则条件叶子/流程绑定)") +
629
+ : "零命中=仅 scanned 面(规则/数据规则/视图)没有引用它;form(表单控件)与 workflow.permissions 未扫描,见 coverage.notScanned——不可据此判定可安全删除") +
620
630
  (zeroHitKindHint(db, kind, code, "references") ?? ""),
621
631
  }
622
632
  : {}),
@@ -167,7 +167,10 @@ function phraseOf(tool, args, outcome) {
167
167
  case "datarule.create":
168
168
  case "datarule.update": {
169
169
  const type = DATA_RULE_TYPE_NAMES_ZH[Number(args.dataRuleType)] ?? `类型${String(args.dataRuleType ?? "?")}`;
170
- return `${tool === "datarule.create" ? "创建" : "更新"}字段规则(${stringArg(args, "propertyCode") ?? "?"},${type}),挂在 ${schemaCode ?? "?"}`;
170
+ const readbackRule = outcome?.readback?.after ?? outcome?.readback?.before;
171
+ const ruleName = readbackRule && typeof readbackRule === "object" ? stringArg(readbackRule, "name") : undefined;
172
+ const identity = ruleName ?? stringArg(args, "propertyCode") ?? (readbackRule && typeof readbackRule === "object" ? stringArg(readbackRule, "id")?.slice(0, 8) : undefined) ?? "?";
173
+ return `${tool === "datarule.create" ? "创建" : "更新"}字段规则(${identity},${type}),挂在 ${schemaCode ?? "?"}`;
171
174
  }
172
175
  case "datarule.delete":
173
176
  return `删除字段规则 ${stringArg(args, "id") ?? "?"}(${schemaCode ?? "?"})`;
@@ -141,12 +141,25 @@ export function registerBusinessRuleLane(tools) {
141
141
  // key (schemaCode+ruleCode), so the row id is rarely needed.
142
142
  const fullRows = args.full === true;
143
143
  const visible = args.onlyCustom === true ? rows.filter((row) => row.defaultRule !== true) : rows;
144
+ const statusLag = visible.some((row) => row.enabled === true && String(row.status ?? "") !== "ENABLED");
144
145
  return {
145
146
  schemaCode: args.schemaCode,
146
147
  modelPresent: true,
147
148
  // r28g F18:list 投影的是 PUBLISH 行;运行时可执行性以 rule.get / rule.enable.verified 为准。
148
149
  source: "published-list",
149
150
  executableHint: "rules[].enabled/status 来自 PUBLISH 投影;运行时判定读 DRAFT(rule.get 的 enabled+ruleStatus,或 rule.enable 回执的 executable/verified)。不要仅凭本面判定可 rule.run。",
151
+ ...(statusLag
152
+ ? {
153
+ statusLag: true,
154
+ statusNote: "本面 status 是 PUBLISH 投影列,与 rule.get 的 ruleStatus 可不同;检测到 enabled=true 而 status 非 ENABLED 的滞后行——enabled=true 才是分发真相,运行时以 rule.get/enable 复核",
155
+ }
156
+ : {
157
+ // r35a-F7(真机 2026-11-06):`status` 与 rule.get 的 `ruleStatus` 是
158
+ // **不同 wire 列**,可一个 ENABLED 一个 DISABLED(探针实测
159
+ // list status=ENABLED/ruleVersion=1 vs get ruleStatus=DISABLED/0)。
160
+ // 常态化给一句短注(~90B/次),避免把本面 status 当运行时真相。
161
+ statusNote: "本面 status 是 PUBLISH 投影列,与 rule.get 的 ruleStatus 可不同;运行时是否生效以 rule.get/enable 为准",
162
+ }),
150
163
  ...(args.onlyCustom === true ? { filtered: { onlyCustom: true, total: rows.length, returned: visible.length } } : {}),
151
164
  ...(endpointFamily ? { endpointFamily } : {}),
152
165
  rules: visible.map((row) => ({
@@ -154,6 +167,7 @@ export function registerBusinessRuleLane(tools) {
154
167
  name: row.name,
155
168
  enabled: row.enabled,
156
169
  status: row.status,
170
+ ...(row.enabled === true && String(row.status ?? "") !== "ENABLED" ? { statusLag: true } : {}),
157
171
  ...(row.ruleVersion !== undefined ? { ruleVersion: row.ruleVersion } : {}),
158
172
  ...(row.defaultRule !== undefined ? { defaultRule: row.defaultRule, preset: row.defaultRule === true } : {}),
159
173
  // ticket-80's update-mode readback teaches following the same-code
@@ -270,6 +284,14 @@ export function registerBusinessRuleLane(tools) {
270
284
  code: wanted,
271
285
  scanned: { models: scoped.length, rules: scannedRules, ...(parseFailures > 0 ? { parseFailures } : {}) },
272
286
  matches,
287
+ // r35a-F4(真机 2026-11-06):matches:[] 曾被当成「可安全删除」的依据,
288
+ // 而探针实测该字段**被表单引用**(form.get draftControlCodes)且出现在
289
+ // 流程节点权限里——本面两者都没扫。散文字段不承担安全契约:结构化声明。
290
+ coverage: {
291
+ scanned: ["businessrule"],
292
+ notScanned: ["datarule", "form", "workflow.permissions", "workflow.bindings", "onlinejs", "listview"],
293
+ note: "matches:[] 只代表 businessrule 面零命中;datarule/form/流程权限/视图等未扫描(离线 graph references 覆盖更广,见其 coverage),不可据此判定可安全删除",
294
+ },
273
295
  ...(scope !== undefined ? { applicationResolution: applicationResolutionOf(scope) } : {}),
274
296
  ...(matches.length === 0
275
297
  ? {
@@ -17,7 +17,7 @@ export const DATARULE_WIRE = Object.freeze({
17
17
  delete: "bizdatarule/delete?id=<ruleId> (@RequestParam String id) with list exact-absence readback",
18
18
  dataRuleTypes: "DataRuleType: 1 CHECK_RULE (校验), 2 CALCULATE_RULE (计算/拼接), 3 DISPLAY_RULE (显示), 4 OTHER_RULE (必填), 5 READ_ONLY_RULE (只读); 8 EXTENSION_RULE is out of scope",
19
19
  dataRuleCheckTypes: "dataRuleType 1 (CHECK_RULE 提交校验规则) 的 checkType 判别(cloudpivot-shared DataRuleCheckType.java)——**校验类需求走这里,不要写在线 JS**:1 REGEX_CHECK 正则校验 / 2 SUBMIT_CHECK 提交校验(含跨字段比较)/ 3 EMPTY_ROW_CHECK 空行校验 / 4 MAX_LENGTH_CHECK 最大长度校验 / 5 REPEAT_CHECK 重复校验;checkType 只配 dataRuleType 1,缺省或非法值由工具拒绝。**数值上下限没有专用类型——用 checkType 2 提交校验表达(如 数量≥0:options 提交校验条件 {quantity}>=0,r21 实证)**",
20
- optionsByCheckType: "各 checkType 的 options(engine/bo/datarule/impl/checkType/*):1 REGEX_CHECK{regexp:'/…/',regexpText:{zh,en}}(regexp 必须斜杠包裹,RegexCheckValidator 会去掉首尾字符);2 SUBMIT_CHECK{conditions:'<字段code> 操作符 <值> [&&| || 连接]', verifyType:'submitDenied'?(满足条件即拒绝提交,缺省为满足才允许), prompt:'<提示,**纯文本**,勿 URL 编码——这种顶层 options.prompt 门户原样显示,编码过会显示成 %E5..乱码>' }(操作符 等于/不等于/大于/小于/大于等于/小于等于/为空/不为空…;**字段 vs 字面量**直接写值如 {qty} 大于 100;**跨字段**右值必须写实为值对象 {\"valueType\":\"currentModel\",\"value\":\"<字段code>\"},如「结束日期不早于开始日期」= {end_date} 小于 {\"valueType\":\"currentModel\",\"value\":\"start_date\"}——裸 {字段code} 右值会原样落库但前端/服务端都不解析成字段值,规则静默不生效,工具拒绝);3 EMPTY_ROW_CHECK{isEmptyRow,isEmptyRowTips};4 MAX_LENGTH_CHECK{maxLength};5 REPEAT_CHECK{properties:[<字段code>],isSubTableValidationAllData?,prompt:[{isDataItem,code}]}(重复提示逐项/数组解析,RepeatCheckValidator:258-272:**isDataItem=0 → code 是字面文案**,如 [{\"isDataItem\":0,\"code\":\"单号重复\"}];**isDataItem=1 → code 是字段 code**,提示取该字段(值/名)。写成 isDataItem:1 + 字段code 会把提示显示成字段值而不是你要的文案;`promptText` 等未识别键平台会忽略,勿用)",
20
+ optionsByCheckType: "各 checkType 的 options 白名单(工具会拒绝 unknownOptionKeys,避免平台静默忽略;运行版 1.1.25 SubmitCheckValidator/RepeatCheckValidator 反编译实证 2026-11-06):1 REGEX_CHECK{regexp,regexpText};2 SUBMIT_CHECK{conditions,verifyType,prompt}(verifyType 的拒用取值=submitDenied——它是 verifyType 的值,不是独立键);3 EMPTY_ROW_CHECK{isEmptyRow,isEmptyRowTips};4 MAX_LENGTH_CHECK{maxLength};5 REPEAT_CHECK{noRepeat,properties,isSubTableValidationAllData,prompt}。checkType-2 的 prompt 是纯文本(门户 raw 渲染 options.prompt,不做富文本解析)。重复提示逐项/数组解析,RepeatCheckValidator:258-272:isDataItem=0 → code 是字面文案;isDataItem=1 → code 是字段 code。checkType=5 的唯一去重键是 options.noRepeat(配合 properties),平台 REST 也会拦重复;conditions/regexp/maxLength 等只在门户前端提交面执行。",
21
21
  optionsByType: "options is a JSON string read per type: 1 check keys (verifyFormula/regexp/maxLength/isEmptyRow/noRepeat/conditions), 2 FormulaConfig — `shortTextStitch` (falling back to `value`) MUST be a JSON OBJECT STRING whose `formula` key holds the expression, e.g. {\"shortTextStitch\":\"{\\\"formula\\\":\\\"{field}\\\",\\\"editorText\\\":\\\"{字段名}\\\"}\"}; a bare \"{field}\" string fails JSONUtil.isJson → getCalRuleExpression returns null → false 305003 dead-cycle; `formulaType:\"AGGREGATION\"` instead reads `aggregation`; a top-level `expression` is NEVER read (it is only a key INSIDE that nested JSON), 3 displayFormula, 4 requiredFormula, 5 readonlyCondition",
22
22
  formulaReference: "类型2 公式引用按位置解析:裸 {字段}=**主记录**字段;带点 {子表code.字段}=**子表**字段(FormulaProcessor.constructSubExprParamsValueMap 要求点前等于子表 schema)。所以**主表公式**用裸 {字段}(带 {主表code.字段} 会按子表解析 → 值空/静默 null),**子表公式**(childRule:true)引用同级子字段必须带点 {子表code.字段}(裸引用被当主记录字段 → 值缺失 → 提交 50000)。工具在写前按子表已发布字段判定并拒绝裸引用(blocker datarule-formula-child-bare-reference),读回对存量子表公式加 formulaHint 提示复核。",
23
23
  optionsEncoding: "the engine JSON-parses the `options` column and reads requiredFormula / readonlyCondition / displayFormula / verifyFormula / shortTextStitch (whose VALUE is itself a JSON string carrying `formula`); send options as a stringified object",
@@ -85,6 +85,7 @@ export const DATARULE_LANE_CONTRACT = Object.freeze({
85
85
  scope: DATARULE_SCOPE,
86
86
  description: describeTool("Create a field-level data rule (bizdatarule/create) and read it back from the authoritative list. **表单运行时条件优先 `field.update`**(本工具=字段级引擎/设计器读面)。", [
87
87
  "`dataRuleType` → options 键:1→conditions/prompt/regexp/maxLength(+checkType);2→shortTextStitch|expression;3→displayFormula;4→requiredFormula(无条件必填=`\"true\"`);5→readonlyCondition。8 EXTENSION out of scope.",
88
+ "`childRule:true` 仅用于已发布的子表列(propertyCode/propertyCodes 必须是子表列 code;主表字段省略)。子表列在发布面不可见时先用 field.create + field.publish 生成列,不要直接 POST datarule.create(r34a-F12 真机:childRule 三种形态全被发布面校验拒)。",
88
89
  "**⚠️ type3/4/5 行的两个读面**:门户**运行时**读**字段 options**(`requiredFormula`/`readonlyCondition`/`readonlyFormula`/`displayFormula`,`field.create/update` 写);表单设计器**设计态** Set/Unset 读 **bizdatarule/list**(本行,network capture 2026-09-23)。**实测:type3/4/5 规则单独不会让门户表单控件变必填/只读**(中文或符号操作符都一样),符号式经 `historyOpt` 镜像还会让表单渲染崩溃——运行时条件请用中文操作符。表单条件优先走 `field.update`:它会**自动配对**本行(readback.formConditionSync)。本工具用于字段级引擎规则或独立增删改。",
89
90
  "**类型3/4/5 条件串协议(平台 create 不校验,坏形状落库即雷,工具按此协议拒绝)**:`{字段code} 操作符 [值]`。操作符取 **bizdatarule 词表**(与字段 options 的表单交互引擎不同——那是符号式):**中文** 等于/不等于/大于/小于/大于等于/小于等于/包含/不包含/包含全部/不包含全部/包含任意一个/不包含任意一个/等于任意一个/属于/不属于/介于/为空/不为空(实测 146 条真实规则以此为主),也接受符号 `==` `!=` `>` `<` `>=` `<=` `Contains` `IsEmpty` 等。为空/不为空**不带值**(`{field} 不为空`);其余带值(JSON 对象或标量)。多条件 ` && `、多组 ` || `。示例 type4:`options:{requiredFormula:\"{amount} 等于 {\\\"valueType\\\":\\\"specifyOption\\\",\\\"value\\\":\\\"A\\\"}\"}`;无条件必填用字面量 \"true\"。**字段绑定用 `propertyCodes`(数组)**,工具自动把 `options.propertyCodes` 也补上;存量的坏规则由 datarule.list 以 shapeWarning 标出。",
90
91
  "`options` is a JSON object sent to the wire as a JSON string; the engine reads `requiredFormula` (4), `readonlyCondition` (5), `displayFormula` (3), `verifyFormula` (1) and `shortTextStitch` (2).",
@@ -50,6 +50,15 @@ export const DATARULE_CHECK_TYPES = Object.freeze({
50
50
  4: "MAX_LENGTH_CHECK 最大长度校验(options.maxLength)",
51
51
  5: "REPEAT_CHECK 重复校验(options.properties 数组 + isSubTableValidationAllData + prompt)",
52
52
  });
53
+ /** Options the 8.6.26 engine actually reads for each data-rule discriminator. */
54
+ export const DATARULE_CHECK_OPTION_KEYS = Object.freeze({
55
+ 1: Object.freeze(["regexp", "regexpText"]),
56
+ 2: Object.freeze(["conditions", "verifyType", "prompt"]),
57
+ 3: Object.freeze(["isEmptyRow", "isEmptyRowTips"]),
58
+ 4: Object.freeze(["maxLength"]),
59
+ 5: Object.freeze(["noRepeat", "properties", "isSubTableValidationAllData", "prompt"]),
60
+ });
61
+ const DATARULE_CALCULATE_MIRROR_KEYS = ["formulaType", "resultType"];
53
62
  const checkTypeArg = z
54
63
  .number()
55
64
  .int()
@@ -120,6 +129,16 @@ function decodeDataRule(row) {
120
129
  sortKey: row.sortKey ?? null,
121
130
  options,
122
131
  };
132
+ const optionKeys = effectiveOptionKeys(Number(row.dataRuleType), Number(row.checkType));
133
+ if (options !== null && typeof options === "object" && !Array.isArray(options)) {
134
+ const storedKeys = Object.keys(options).filter((key) => key !== "propertyCodes");
135
+ decoded.effectiveKeys = storedKeys.filter((key) => optionKeys.includes(key));
136
+ decoded.ignoredKeys = storedKeys.filter((key) => !optionKeys.includes(key));
137
+ }
138
+ else {
139
+ decoded.effectiveKeys = [];
140
+ decoded.ignoredKeys = [];
141
+ }
123
142
  // Round 4 (ADR 0005): read-side annotation. The platform accepted and stored
124
143
  // malformed option shapes for years; the same protocol validator that gates
125
144
  // writes marks stored landmines here. Report-only — stored data is never
@@ -159,6 +178,21 @@ function decodeDataRule(row) {
159
178
  }
160
179
  return decoded;
161
180
  }
181
+ function effectiveOptionKeys(dataRuleType, checkType) {
182
+ if (dataRuleType === 1) {
183
+ if (checkType !== undefined)
184
+ return DATARULE_CHECK_OPTION_KEYS[checkType] ?? [];
185
+ return [...new Set(Object.values(DATARULE_CHECK_OPTION_KEYS).flat())];
186
+ }
187
+ return dataRuleType === 2 ? [...(DATARULE_TYPE_OPTION_KEYS[2] ?? []), ...DATARULE_CALCULATE_MIRROR_KEYS] : DATARULE_TYPE_OPTION_KEYS[dataRuleType] ?? [];
188
+ }
189
+ function dataRuleExecutionWarning(checkType) {
190
+ if (checkType === 5)
191
+ return "重复校验 noRepeat 由服务端拦截,REST `data.submit` 亦会拒绝重复数据(errcode 800150006);properties 指定参与去重的字段。";
192
+ if (checkType === 1 || checkType === 2 || checkType === 3 || checkType === 4)
193
+ return "该校验规则仅由门户前端提交校验执行;REST `data.submit` 不拦截违规数据。需要服务端拦截请改用业务规则(BIZ_ACTION/条件)。";
194
+ return "校验规则的执行面取决于 checkType;请在门户提交面验收,未提供 checkType 时不要把 REST 行为视为已验证。";
195
+ }
162
196
  /**
163
197
  * Fail closed when the options object cannot carry the key the engine reads
164
198
  * for this type (RequiredDataRuleValidator / ReadOnlyValidator /
@@ -354,6 +388,20 @@ export function assertDataRuleOptions(dataRuleType, options, checkType) {
354
388
  message: `dataRuleType ${dataRuleType} requires at least one of ${required.join(", ")} in options (those are the keys the 8.6.26 engine reads)`,
355
389
  });
356
390
  }
391
+ const ignored = Object.keys(options).filter((key) => key !== "propertyCodes" && !effectiveOptionKeys(dataRuleType, checkType).includes(key));
392
+ if (ignored.length > 0) {
393
+ const allowed = effectiveOptionKeys(dataRuleType, checkType);
394
+ const checkLabel = dataRuleType === 1 ? `checkType=${checkType ?? "?"}` : `dataRuleType=${dataRuleType}`;
395
+ throw new ToolRefusalError({
396
+ status: "refused",
397
+ blocker: "datarule-unknown-option-keys",
398
+ dataRuleType,
399
+ ...(checkType === undefined ? {} : { checkType }),
400
+ unknownOptionKeys: ignored,
401
+ effectiveKeys: allowed,
402
+ message: `${checkLabel} 不读取 options 键:${ignored.join(", ")};本类型有效键:${allowed.join(", ") || "(无)"}。平台会静默忽略这些键,拒绝写入以避免误以为规则生效`,
403
+ });
404
+ }
357
405
  // Round 4 (ADR 0005): types 3/4/5 gate on the reproduced condition protocol.
358
406
  // The platform itself does not validate these shapes at create time, so an
359
407
  // unguarded write stores a portal-renderer landmine that only fires when a
@@ -818,7 +866,7 @@ export function registerDataRuleLane(tools) {
818
866
  dataRuleType: dataRuleTypeArg,
819
867
  options: dataRuleOptionsArg,
820
868
  checkType: checkTypeArg,
821
- childRule: z.boolean().optional(),
869
+ childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
822
870
  name: z.string().optional(),
823
871
  sortKey: z.number().int().optional(),
824
872
  },
@@ -832,20 +880,21 @@ export function registerDataRuleLane(tools) {
832
880
  if (published !== null) {
833
881
  const missing = codes.filter((code) => !published.has(code));
834
882
  if (missing.length > 0)
835
- throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST` });
883
+ throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST。子表列规则请随 field.create 生成/发布;datarule.create 的发布面校验不含子表列(childRule 不改变此行为,r34a-F12 真机三形态全拒)` });
836
884
  }
837
885
  const existingRules = await listDataRules(session, args.schemaCode);
838
886
  // dataRuleType 1 (CHECK_RULE) fans out into 5 checkTypes that COEXIST on one
839
887
  // field (the platform auto-creates a checkType 4 maxLength row per text
840
888
  // field), so the clash key there is field + `checkType`, not field + type.
841
- if (existingRules.some((row) => {
889
+ const duplicate = existingRules.find((row) => {
842
890
  if (Number(row.dataRuleType) !== args.dataRuleType)
843
891
  return false;
844
892
  if (!storedTargetCodes(row).some((code) => codes.includes(code)))
845
893
  return false;
846
894
  return args.dataRuleType === 1 ? (row.checkType ?? null) === (args.checkType ?? null) : true;
847
- }))
848
- throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", schemaCode: args.schemaCode, propertyCodes: codes, dataRuleType: args.dataRuleType, ...(args.dataRuleType === 1 ? { checkType: args.checkType } : {}), message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝重复 POST` });
895
+ });
896
+ if (duplicate)
897
+ throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", schemaCode: args.schemaCode, propertyCodes: codes, dataRuleType: args.dataRuleType, ...(args.dataRuleType === 1 ? { checkType: args.checkType } : {}), nextCommand: `omc call datarule.update --args '${JSON.stringify({ id: String(duplicate.id ?? "<existingId>"), schemaCode: args.schemaCode, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes.length > 1 ? codes : undefined, dataRuleType: args.dataRuleType, checkType: args.checkType, options: args.options })}'`, message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝重复 POST;如需修改请使用该规则 id 更新` });
849
898
  const createRule = () => wireCall(session, {
850
899
  path: ENDPOINTS.dataRuleCreate,
851
900
  method: "POST",
@@ -872,7 +921,7 @@ export function registerDataRuleLane(tools) {
872
921
  // 提交违规数据(-50 / 超阈值)errcode 0 放行、原值落库——校验在门户前端
873
922
  // 提交管线执行,REST 不拦。先告知执行面,省掉「先实测两轮才发现不生效」。
874
923
  ...(args.dataRuleType === 1
875
- ? { executionWarning: "校验规则在门户前端提交时执行;REST `data.submit` 不拦截违规数据(r35 真机:-50 与超阈值均 errcode 0 放行)。验收拦截效果需走门户,或改用业务规则(BIZ_ACTION/条件)在服务端拦。" }
924
+ ? { executionWarning: dataRuleExecutionWarning(args.checkType) }
876
925
  : {}),
877
926
  ...(() => { const w = dataRuleFormulaWarnings(args.dataRuleType, args.options); return w.length ? { lintWarnings: w } : {}; })(),
878
927
  },
@@ -906,7 +955,7 @@ export function registerDataRuleLane(tools) {
906
955
  dataRuleType: dataRuleTypeArg,
907
956
  options: dataRuleOptionsArg,
908
957
  checkType: checkTypeArg,
909
- childRule: z.boolean().optional(),
958
+ childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
910
959
  name: z.string().optional(),
911
960
  sortKey: z.number().int().optional(),
912
961
  },
@@ -926,7 +975,7 @@ export function registerDataRuleLane(tools) {
926
975
  const requested = dataRuleTargetCodes(patchArgs);
927
976
  const codes = requested.length > 0 ? requested : storedTargetCodes(current);
928
977
  const boundArgs = { ...patchArgs, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes };
929
- if (before.some((row) => {
978
+ const duplicate = before.find((row) => {
930
979
  if (row.id === id)
931
980
  return false;
932
981
  if (Number(row.dataRuleType) !== args.dataRuleType)
@@ -934,8 +983,9 @@ export function registerDataRuleLane(tools) {
934
983
  if (!storedTargetCodes(row).some((code) => codes.includes(code)))
935
984
  return false;
936
985
  return args.dataRuleType === 1 ? (row.checkType ?? null) === (args.checkType ?? null) : true;
937
- }))
938
- throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝 update POST` });
986
+ });
987
+ if (duplicate)
988
+ throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", nextCommand: `omc call datarule.update --args '${JSON.stringify({ id: String(duplicate.id ?? "<existingId>"), schemaCode: args.schemaCode, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes.length > 1 ? codes : undefined, dataRuleType: args.dataRuleType, checkType: args.checkType, options: args.options })}'`, message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝 update POST;如需修改请使用该规则 id 更新` });
939
989
  const beforeDecoded = decodeDataRule(current);
940
990
  const performed = await wireCall(session, {
941
991
  path: ENDPOINTS.dataRuleUpdate,
@@ -172,7 +172,7 @@ export const FIELD_LANE_CONTRACT = Object.freeze({
172
172
  "For single/multi-select pass `choices` (e.g. [\"A\",\"B\",\"C\"]) or `options.optionsSet` with optionsType custom/dictionary/businessModel; dictionary uses checkedDictionary/checkedDictionaryCode, businessModel uses schemaCode/sheetDataItem.",
173
173
  "For a relation pass `relativeCode` and `options` (schemaCode/displayField/...). `searchFormula` and `conditions` are JSON strings for the relation data restriction/query conditions.",
174
174
  "For a relation display field the tool writes the field-row top-level `relativePropertyCode` = options.displayField (the runtime reads THAT key, not options.displayField — QueryRuntimeController.java:1214-1226) and defaults options.displayFieldType:0 + displayFieldFormat:\"\".",
175
- "For FORMULA pass `options.formulaConfig` (nested JSON string) + `options.persistent:true`; for RELEVANCE_DATA pass `options.relevanceField` + `options.referenceField` + `options.referenceFieldPropertyType`.",
175
+ "For FORMULA pass `options.formulaConfig` (nested JSON string) + `options.persistent:true`; for RELEVANCE_DATA pass `options.relevanceField` + `options.referenceField` + `options.referenceFieldPropertyType`. 数值字段的小数位写 `options.scale`;顶层 `precision` 不被读取,若误传请改为 `precision→options.scale(数值小数位)`。",
176
176
  "org 选人族(STAFF_SELECTOR 等 5/50/51/60/61)字段行 options 自动写成实例观测的发布安全形状(yst/fwb 发布面 2656 行:null 或 {defaultValue, mappings, virtualField[, noRepeat]};单选 defaultValue \"\",多选 [])。deptVisible/multi 是**表单控件面**判别键(disposeUpdateType/updateSelectionType 读 control.options,票 69 已由表单脚手架写入控件面),不出现在字段行。",
177
177
  ]),
178
178
  }),
@@ -217,7 +217,7 @@ export function assertFieldContract() {
217
217
  throw new Error("field contract must carry the blocked families");
218
218
  if (FIELD_LANE_CONTRACT.wire.publishScope !== "model")
219
219
  throw new Error("field contract pins the publish scope to model");
220
- assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.create", ["bizproperty/create", "OCR/ELECTRONIC_SIGN", "`choices`", "options.optionsSet", "`relativeCode`", "searchFormula", "`options.formulaConfig`", "`options.relevanceField`", "code_reserved", "WORK_SHEET"]);
220
+ assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.create", ["bizproperty/create", "OCR/ELECTRONIC_SIGN", "`choices`", "options.optionsSet", "`relativeCode`", "searchFormula", "`options.formulaConfig`", "`options.relevanceField`", "precision→options.scale", "code_reserved", "WORK_SHEET"]);
221
221
  assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.update", ["bizproperty/update", "`options`", "field.publish", "cascadeRepublish", "form.draft(rebuild)", "form.publish(force)", "published fields", "non-structural", "partial"]);
222
222
  assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.publish", ["bizproperty/publish", "published", "Model-scoped", "publishedCodes", "unpublishedRequested", "nonexistentCodes", "stillDraftCodes"]);
223
223
  assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.delete", ["bizproperty/force_delete", "absence"]);
@@ -732,10 +732,12 @@ async function fieldUpdateDryRun(session, args) {
732
732
  }),
733
733
  };
734
734
  }
735
+ const impact = await collectFieldDeleteImpact(session, args, { full: true });
735
736
  return {
736
737
  wouldChange: changes.length > 0,
737
738
  changes,
738
739
  ...(dataImpact !== undefined ? { dataImpact } : {}),
740
+ impact,
739
741
  schemaCode: args.schemaCode,
740
742
  code: args.code,
741
743
  };
@@ -951,6 +953,11 @@ async function collectFieldDeleteImpact(session, args, options) {
951
953
  ...(counted.note ? { dataRowsNote: counted.note } : counted.dataRows === "unknown" ? { dataRowsNote: "拿不到行数:data.list total 不可用" } : {}),
952
954
  references,
953
955
  dependentUpdates: surfaces.dependentUpdates,
956
+ coverage: {
957
+ scanned: options.full ? ["businessrule", "form", "listview", "onlinejs", "data"] : ["form", "listview", "onlinejs", "data"],
958
+ notScanned: options.full ? ["workflow.permissions", "workflow.bindings", "rule.conditions"] : ["businessrule", "workflow.permissions", "workflow.bindings", "rule.conditions"],
959
+ coveredSurfaces: options.full ? ["businessrule", "form", "listview", "onlinejs", "data"] : ["form", "listview", "onlinejs", "data"],
960
+ },
954
961
  ...(unknowns.length > 0 ? { unknown: unknowns } : {}),
955
962
  note: options.full
956
963
  ? "完整影响面(规则+表单+视图+在线JS+行数)。删除不会自动清理依赖面残留。"
@@ -1062,6 +1069,7 @@ async function runFieldUpdate(session, args) {
1062
1069
  ...(args.propertyEmpty !== undefined ? ["propertyEmpty"] : []),
1063
1070
  ];
1064
1071
  const unverifiable = submittedPropertyEmpty !== undefined && !hasReadBackPropertyEmpty ? ["propertyEmpty"] : [];
1072
+ const impact = await collectFieldDeleteImpact(session, args, { full: false });
1065
1073
  return {
1066
1074
  performed: { errcode: performed.errcode },
1067
1075
  ...(prepared.conditionCompiled !== undefined && prepared.conditionCompiled.length > 0
@@ -1069,6 +1077,7 @@ async function runFieldUpdate(session, args) {
1069
1077
  : {}),
1070
1078
  readback: {
1071
1079
  face: "draft",
1080
+ impact,
1072
1081
  fieldPresent: after !== undefined,
1073
1082
  ...(after?.name !== undefined ? { name: after.name } : {}),
1074
1083
  ...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
@@ -476,23 +476,6 @@ export function registerListViewLane(tools) {
476
476
  schemaCode: z.string().min(1),
477
477
  name: z.string().min(1),
478
478
  code: z.string().optional().describe("视图 code;省略时**默认取 schemaCode**(与 listview.get / listview.list / rollback 的默认视图口径一致)。平台侧 code 必填,本工具会替你填,不要再依赖不回执的猜测(r26 真机:不传直接被 errcode 10005「编码不能为空」拒绝,白跑一轮线上往返)"),
479
- // r29b F5a: create 没有列参数。接受 fieldCodes 只是为了给可执行 nextCommand,
480
- // 避免未知键算法给出「fieldCodes→code」这种南辕北辙的建议。
481
- fieldCodes: z.array(z.string()).optional().describe("本工具不配列——若传了会拒绝并指引 listview.configure"),
482
- },
483
- // dry-run 不跑 execute——fieldCodes 必须在 preflight 拦,否则 dry-run 仍会放行。
484
- preflight: (args) => {
485
- if (Array.isArray(args.fieldCodes) && args.fieldCodes.length > 0) {
486
- const code = args.code ?? args.schemaCode;
487
- throw new ToolInputError({
488
- status: "error",
489
- blocker: "tool-input-invalid",
490
- tool: "listview.create",
491
- message: "listview.create 不接受列参数:先 create(schemaCode/name/code),再 listview.configure 配 fieldCodes。" +
492
- "不要把 fieldCodes 当成 create 的拼写错误去改成 code。",
493
- nextCommand: `omc call listview.configure --args '${JSON.stringify({ schemaCode: args.schemaCode, code, fieldCodes: args.fieldCodes })}'`,
494
- });
495
- }
496
479
  },
497
480
  execute: async (session, args) => {
498
481
  // r26 真机:`code` 在 schema 里可选,但三处读面/rollback 都用
@@ -500,16 +483,6 @@ export function registerListViewLane(tools) {
500
483
  // `errcode 10005 编码不能为空`,探针白跑一轮。默认视图 code 就是
501
484
  // schemaCode(listview.list 的 hint 也这么写),这里对齐。
502
485
  const code = args.code ?? args.schemaCode;
503
- if (Array.isArray(args.fieldCodes) && args.fieldCodes.length > 0) {
504
- throw new ToolInputError({
505
- status: "error",
506
- blocker: "tool-input-invalid",
507
- tool: "listview.create",
508
- message: "listview.create 不接受列参数:先 create(schemaCode/name/code),再 listview.configure 配 fieldCodes。" +
509
- "不要把 fieldCodes 当成 create 的拼写错误去改成 code。",
510
- nextCommand: `omc call listview.configure --args '${JSON.stringify({ schemaCode: args.schemaCode, code, fieldCodes: args.fieldCodes })}'`,
511
- });
512
- }
513
486
  // Ticket 81: read the header FIRST — a same-code retry short-circuits
514
487
  // with the existing view and sends ZERO create wires (the live
515
488
  // post-timeout retry otherwise answers 303002 编码重复).
@@ -393,7 +393,11 @@ export function registerModelLane(tools) {
393
393
  // delete). Same SYSTEM_FIELD_CODES set field.list uses.
394
394
  const visibleColumns = all ? listviewColumns : listviewColumns.filter((column) => !SYSTEM_FIELD_CODES.has(String(column.propertyCode)));
395
395
  const listview = listviewFace && listviewFace.columns
396
- ? { code: listviewFace.code ?? schemaCode, columnCount: visibleColumns.length, columns: visibleColumns.map((column) => ({ propertyCode: column.propertyCode, propertyType: column.propertyType, visible: column.visible })) }
396
+ ? {
397
+ code: listviewFace.code ?? schemaCode,
398
+ columnCount: visibleColumns.length,
399
+ columns: visibleColumns.map((column) => ({ propertyCode: column.propertyCode, propertyType: column.propertyType, visible: column.visible })),
400
+ }
397
401
  : undefined;
398
402
  const ruleRows = Array.isArray(ruleFace?.rules) ? ruleFace.rules : [];
399
403
  // All rules are kept: the platform pre-creates trigger rows (Create/Update/
@@ -433,6 +437,7 @@ export function registerModelLane(tools) {
433
437
  const workflow = draftFace?.exists === false || (draftFace === undefined && publishedFace === undefined)
434
438
  ? (draftFace && draftFace.schemaCode ? { exists: false } : undefined)
435
439
  : {
440
+ exists: true,
436
441
  workflowCode: summary?.workflowCode ?? workflowCode,
437
442
  published: publishedFace?.published === true || pubSummary?.templateType === "PUBLISH",
438
443
  activities: Array.isArray(summary?.activities) ? summary.activities.map((activity) => ({ code: activity.code, type: activity.type, name: activity.name, participant: activity.participant })) : [],
@@ -447,6 +452,7 @@ export function registerModelLane(tools) {
447
452
  const absent = fieldRefusalErrcode === 300004 || faceErrcodes.get("field.list") === 300004;
448
453
  const modelPresent = absent ? false : fieldFace !== undefined && !fieldRefused ? true : undefined;
449
454
  return {
455
+ code: schemaCode,
450
456
  schemaCode,
451
457
  ...(modelName !== undefined ? { name: modelName } : {}),
452
458
  ...(modelPresent !== undefined ? { modelPresent } : {}),
@@ -482,12 +488,24 @@ export function registerModelLane(tools) {
482
488
  return result;
483
489
  return {
484
490
  schemaCode: result.schemaCode,
491
+ // r35a-F3:compact 投影必须保留 code(model.list 有 code,inspect 此前
492
+ // 只剩 schemaCode,`--jq '[].code'` 必 pathMiss 白跑一轮)。
493
+ ...(result.code !== undefined ? { code: result.code } : {}),
485
494
  fieldCodes: (result.fields ?? []).map((field) => field.code),
486
495
  form: result.form,
487
496
  listview: result.listview ? { code: result.listview.code, columnCount: result.listview.columnCount } : undefined,
488
497
  ruleCodes: (result.rules ?? []).map((rule) => rule.code),
489
498
  datarules: result.datarules,
490
- workflow: result.workflow ? { workflowCode: result.workflow.workflowCode, published: result.workflow.published } : undefined,
499
+ // r35a-F2:workflow 摘要必须恒有 exists 布尔——compact 投影此前把它丢了,
500
+ // 真机(走 compact 的分支)于是永远读不到 exists(探针只能靠
501
+ // missedEntries 反推「缺失=有流程」,语义反直觉且有安全风险)。
502
+ workflow: result.workflow
503
+ ? {
504
+ exists: result.workflow.exists !== false,
505
+ workflowCode: result.workflow.workflowCode,
506
+ published: result.workflow.published,
507
+ }
508
+ : { exists: false },
491
509
  hint: result.hint,
492
510
  };
493
511
  },
@@ -392,19 +392,36 @@ export function registerOrganizationLane(tools) {
392
392
  return scope.id;
393
393
  return null;
394
394
  };
395
- const members = rows.map((row) => ({
396
- id: roleUserId(row),
397
- // r32 探针:把角色 id 当成员 id 传给 userScope/addUsers → 查询为空。
398
- // 明示这个 id 的身份与正确来源,防角色/用户 id 混用。
399
- idType: "user",
400
- name: row.name ?? null,
401
- username: row.username ?? null,
402
- departmentId: memberDepartment(row),
403
- departmentName: row.departmentName ?? null,
404
- // `ouScope` decides whether a role member can be resolved as a workflow
405
- // participant. Surface the count so an empty scope is visible here.
406
- scopeCount: Array.isArray(row.ouScope) ? row.ouScope.length : null,
407
- unitType: row.unitType ?? null,
395
+ const members = await Promise.all(rows.map(async (row) => {
396
+ const id = roleUserId(row);
397
+ let username = row.username ?? null;
398
+ if (username == null && id !== "") {
399
+ try {
400
+ const user = (await wireCall(session, {
401
+ path: ENDPOINTS.organizationUserInfo,
402
+ query: { ...orgQuery(session), id },
403
+ })).data;
404
+ username = user?.username ?? null;
405
+ }
406
+ catch (error) {
407
+ if (!(error instanceof WireError) || error.errcode !== 10011)
408
+ throw error;
409
+ }
410
+ }
411
+ return {
412
+ id,
413
+ // r32 探针:把角色 id 当成员 id 传给 userScope/addUsers → 查询为空。
414
+ // 明示这个 id 的身份与正确来源,防角色/用户 id 混用。
415
+ idType: "user",
416
+ name: row.name ?? null,
417
+ username,
418
+ departmentId: memberDepartment(row),
419
+ departmentName: row.departmentName ?? null,
420
+ // `ouScope` decides whether a role member can be resolved as a workflow
421
+ // participant. Surface the count so an empty scope is visible here.
422
+ scopeCount: Array.isArray(row.ouScope) ? row.ouScope.length : null,
423
+ unitType: row.unitType ?? null,
424
+ };
408
425
  }));
409
426
  return {
410
427
  roleId,
@@ -426,7 +443,7 @@ export function registerOrganizationLane(tools) {
426
443
  environment: environmentArg,
427
444
  roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
428
445
  roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称;与 roleId 二选一"),
429
- userId: z.string().min(1).describe("成员的用户记录 id(recId)"),
446
+ userId: z.string().min(1).optional().describe("成员的用户记录 id(recId);roleId/roleCode 二者其一必填"),
430
447
  capability: capabilityArg,
431
448
  },
432
449
  handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
@@ -976,8 +993,20 @@ export function registerOrganizationLane(tools) {
976
993
  if (!(error instanceof WireError) || error.errcode !== 10011)
977
994
  throw error;
978
995
  }
979
- if (!data)
980
- return { userId: String(args.userId), exists: false };
996
+ // r34b-F6(真机 2026-11-06,1.1.25):死 id 平台**不回 null 也不回 10011**,
997
+ // 而是 200 + 空壳对象——`!data` 分支真机永远走不到,exists:true + 全 null
998
+ // 把「不存在」伪装成「存在但没数据」。真机形状:name/username/mobile/email/
999
+ // departmentId/status 全空且 roleIds 无。全身份字段空 = 按 exists:false 处理。
1000
+ const emptyShell = data !== null && [
1001
+ data.name, data.username, data.mobile, data.email, data.departmentId, data.status,
1002
+ ].every((v) => v == null || v === "") && (!Array.isArray(data.roleIds) || data.roleIds.length === 0);
1003
+ if (!data || emptyShell)
1004
+ return {
1005
+ userId: String(args.userId), exists: false,
1006
+ existsNote: emptyShell ? "平台回 200 空壳(身份字段全空)——按不存在处理(r34b-F6 真机)" : undefined,
1007
+ name: null, username: null, departmentId: null, mobile: null, email: null,
1008
+ active: null, userStatus: null, roleIds: [],
1009
+ };
981
1010
  return {
982
1011
  userId: String(args.userId), exists: true,
983
1012
  name: data.name ?? null, username: data.username ?? null, departmentId: data.departmentId ?? null,
@@ -2238,6 +2238,8 @@ export function registerRuntimeDataLane(tools) {
2238
2238
  ...readback,
2239
2239
  workflowInstanceId: effectiveInstanceId,
2240
2240
  objectId,
2241
+ // The submit response often has no workItemId; when the engine created
2242
+ // the next todo, its first item is the authoritative continuation id.
2241
2243
  workItemId: typeof performedData.workItemId === "string" ? performedData.workItemId : null,
2242
2244
  sequenceStatus,
2243
2245
  submittedData: approvalDelta ? projectApprovalReadback(loadedRecord, Object.keys(formData ?? {})) : projectReadbackRecord(loadedRecord, args),
@@ -2290,6 +2292,8 @@ export function registerRuntimeDataLane(tools) {
2290
2292
  }
2291
2293
  if (nextWorkItems.length > 0) {
2292
2294
  readback.nextWorkItems = nextWorkItems;
2295
+ if (readback.workItemId === null)
2296
+ readback.workItemId = nextWorkItems[0].workItemId;
2293
2297
  const nextName = nextWorkItems[0].activityName ?? nextWorkItems[0].activityCode ?? "下一节点";
2294
2298
  if (rejected) {
2295
2299
  readback.returnedTo = {
@@ -208,11 +208,11 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
208
208
  }),
209
209
  "workitem.list": Object.freeze({
210
210
  scope: "runtime workflow work-item query",
211
- description: "查询当前登录人的待办(`GET /api/api/runtime/workflow/search_workitems`)。WorkItemSearchVO 经 @ModelAttribute 传参,服务端以登录态注入 userId,故无需传处理人——结果天然是「我的待办」。返回每项 workItemId(id) + workflowInstanceId(instanceId) + activityName + state;并尽力经 `GET /api/api/runtime/workflow/getBizObjectByWfInstanceId` 解析 bizObjectId/schemaCode 供 data.submit 审批。`workflowInstanceId` 为客户端精确过滤(服务端 WorkItemSearchVO.filterWorkflowInstanceId 实为 notEqualsTo 排除,不能用于命中实例);带该过滤时 total 为过滤后的条数,原服务端总数在 unfilteredTotal。**想知道「流程停在哪」,用 `workflow.instanceActivity`,不要靠反复 workitem.list**——待办查询只返回在办项,实例停住时永远是空数组。",
211
+ description: "查询当前登录人的待办(`GET /api/api/runtime/workflow/search_workitems`)。WorkItemSearchVO 经 @ModelAttribute 传参,服务端以登录态注入 userId,故无需传处理人——结果天然是「我的待办」。返回每项完整键集:`workItemId, workflowInstanceId, workflowCode, activityCode, activityName, state, sequenceNo, originator, originatorName, participant, participantName, bizObjectId, schemaCode`;外置大结果另带 `itemsCount, ids, itemsPreview, truncated, truncatedKeys, omittedKeys, detailFile/detailSha256`,其中 itemsPreview 为前 3 个 `{id}` 引用。并尽力经 `GET /api/api/runtime/workflow/getBizObjectByWfInstanceId` 解析 bizObjectId/schemaCode 供 data.submit 审批。`workflowInstanceId` 为客户端精确过滤(服务端 WorkItemSearchVO.filterWorkflowInstanceId 实为 notEqualsTo 排除,不能用于命中实例);带该过滤时 total 为过滤后的条数,原服务端总数在 unfilteredTotal。**想知道「流程停在哪」,用 `workflow.instanceActivity`,不要靠反复 workitem.list**——待办查询只返回在办项,实例停住时永远是空数组。",
212
212
  }),
213
213
  "workflow.instanceActivity": Object.freeze({
214
214
  scope: "runtime workflow instance node-state diagnosis",
215
- description: "按实例查全部节点状态(`GET /api/api/runtime/workflow/list_workflow_instance_activity`,WorkflowMaintenanceAssembler.listActivateActivity)。返回模板里每个活动节点的 activityCode/activityName/activityType + status(1=当前激活,0=已完成或未到达)+ 解析出的 participants(含姓名/id)。用于「data.submit 回 PROCESSING 但没有下一待办」的自诊断:例子 `omc call workflow.instanceActivity --args '{\"workflowInstanceId\":\"<id>\"}'`。当没有任何节点 status=1 时,readback 的 diagnosis.code=no-active-activity 并列出排查顺序(参与人为空的审批节点 → 参与人写法 → 容器日志 GetParticipantInfo error)。审批推进正常时该工具不需要——data.submit 的 readback.nextSubmit 已给出下一级参数。",
215
+ description: "按实例查全部节点状态(`activities[].status` 数字枚举:`1=当前激活/持有待办`、`0=已完成或尚未到达`,别与实例级 `instanceStatus:\"PROCESSING\"` 混用——后者只表示流程仍在运行)。返回每个节点 `{activityCode,activityName,activityType,status,participantCount,participants:[{name,id}],sheetTrigger?}`(`GET /api/api/runtime/workflow/list_workflow_instance_activity`,WorkflowMaintenanceAssembler.listActivateActivity)。用于「data.submit 回 PROCESSING 但没有下一待办」的自诊断:例子 `omc call workflow.instanceActivity --args '{\"workflowInstanceId\":\"<id>\"}'`。当没有任何节点 status=1 时,readback 的 diagnosis.code=no-active-activity 并列出排查顺序(参与人为空的审批节点 → 参与人写法 → 容器日志 GetParticipantInfo error)。审批推进正常时该工具不需要——data.submit 的 readback.nextSubmit 已给出下一级参数。",
216
216
  }),
217
217
  }),
218
218
  });
@@ -19,6 +19,24 @@ const WORKFLOW_LANE = WORKFLOW_LANE_CONTRACT.lane;
19
19
  const WORKFLOW_TOOLS = WORKFLOW_LANE_CONTRACT.tools;
20
20
  const WORKFLOW_ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
21
21
  const WORKFLOW_POINT = /^-?\d+(?:\.\d+)?,\s-?\d+(?:\.\d+)?$/;
22
+ /** Keep workitem.list's external envelope aligned with data.list. */
23
+ export function workItemListCompactProjection(result) {
24
+ const compact = {};
25
+ for (const [key, value] of Object.entries(result)) {
26
+ if (key !== "items" || !Array.isArray(value)) {
27
+ if (key !== "ids" && key !== "itemsPreview" && key !== "listArtifactHint")
28
+ compact[key] = value;
29
+ continue;
30
+ }
31
+ const rows = value;
32
+ const ids = rows.map((row) => row.workItemId).filter((id) => typeof id === "string" && id.length > 0);
33
+ compact.itemsCount = rows.length;
34
+ compact.ids = ids;
35
+ compact.itemsPreview = ids.slice(0, 3).map((id) => ({ id }));
36
+ compact.listArtifactHint = "items 已外置(truncatedKeys/omittedKeys 含 items):完整待办见 artifact;ids=本页全部 workItemId,itemsPreview=前 3 个 id 引用。";
37
+ }
38
+ return compact;
39
+ }
22
40
  /**
23
41
  * Conditional branch lines (round7). `routes[]` maps to the platform `Rule`
24
42
  * DTO (Rule.java:28-59), which carries a structured branch condition
@@ -3199,13 +3217,13 @@ export function registerWorkflowLane(tools) {
3199
3217
  total: page.total,
3200
3218
  // The JPA-spec path reports no page count; only emit it when known.
3201
3219
  ...("totalPages" in page && page.totalPages !== undefined ? { totalPages: page.totalPages } : {}),
3202
- count: items.length,
3220
+ itemsCount: items.length,
3203
3221
  ...(purposeHint ? { purposeHint } : {}),
3204
3222
  ...(args.participant !== undefined ? { participant: args.participant, state: args.state ?? "PENDING", source: "list_workitems" } : {}),
3205
3223
  ...("unfilteredTotal" in page && page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
3206
3224
  items,
3207
3225
  };
3208
- }),
3226
+ }, { compact: workItemListCompactProjection }),
3209
3227
  }));
3210
3228
  tools.push(defineTool({
3211
3229
  name: "workflow.instanceActivity",
@@ -3678,6 +3696,12 @@ export function registerWorkflowLane(tools) {
3678
3696
  };
3679
3697
  }
3680
3698
  return flat;
3699
+ }, {
3700
+ compact: (result) => ({
3701
+ ...result,
3702
+ roles: Array.isArray(result.roles) ? result.roles : [],
3703
+ groups: Array.isArray(result.groups) ? result.groups : [],
3704
+ }),
3681
3705
  }),
3682
3706
  }));
3683
3707
  tools.push(defineTool({
@@ -3729,7 +3753,7 @@ export function registerWorkflowLane(tools) {
3729
3753
  kind: "read",
3730
3754
  lane: WORKFLOW_LANE,
3731
3755
  scope: WORKFLOW_TOOLS["org.user.search"].scope,
3732
- description: WORKFLOW_TOOLS["org.user.search"].description,
3756
+ description: `${WORKFLOW_TOOLS["org.user.search"].description} 返回 count=本页 users 条数、total=总命中数;没有本页结果时两者均为 0。`,
3733
3757
  inputSchema: {
3734
3758
  environment: environmentArg,
3735
3759
  // `wd` is the platform's own key; `keyword`/`q` are accepted aliases so a
@@ -3826,7 +3850,8 @@ export function registerWorkflowLane(tools) {
3826
3850
  corpId: user.corpId ?? null,
3827
3851
  status: user.status ?? null,
3828
3852
  }));
3829
- return { keyword, count: users.length, total: data?.totalElements ?? users.length, users };
3853
+ const totalHits = users.length === 0 ? 0 : (typeof data?.totalElements === "number" ? data.totalElements : users.length);
3854
+ return { keyword, count: users.length, total: totalHits, users };
3830
3855
  }, { compact: userSearchCompact });
3831
3856
  },
3832
3857
  }));
@@ -94,7 +94,10 @@ const READ_SHAPES = {
94
94
  },
95
95
  "workitem.list": {
96
96
  // Flat rows — no fields bag. Wire id is projected as workItemId.
97
- keys: ["items:array<{workItemId,activityCode,...}>", "total", "count"],
97
+ // r34c-F4(真机 2026-11-06):此前 `{workItemId,activityCode,...}` 的省略号让
98
+ // 探针必须实调一次才敢确定标题键叫 activityName(--full 也一样省略)。
99
+ // 展开真实键集(真机 item 实测);外置信封的 ids/itemsPreview 由 F7 补。
100
+ keys: ["items:array<{workItemId,workflowInstanceId,workflowCode,activityCode,activityName,state,sequenceNo,originator,originatorName,participant,participantName,bizObjectId,schemaCode}>", "total", "count"],
98
101
  notes: ["待办 id 键是 workItemId,不是 id(平铺,不套 fields)"],
99
102
  },
100
103
  "rule.list": {
package/dist/read.js CHANGED
@@ -121,7 +121,10 @@ function externalizedShapePlaceholders(result, dropped) {
121
121
  // 自相矛盾的标记会让调用方以为还得去 artifact 取。
122
122
  if (dropped.length === 0)
123
123
  return {};
124
- const placeholders = { truncatedKeys: [...dropped], truncated: true };
124
+ // Keep the A1 empty/null placeholder shape, but explicitly distinguish it
125
+ // from a genuine empty payload. `omittedKeys` is the machine-readable face;
126
+ // `truncatedKeys` remains the legacy list of payload keys moved to artifact.
127
+ const placeholders = { truncatedKeys: [...dropped], omittedKeys: [...dropped], truncated: true };
125
128
  for (const key of dropped)
126
129
  placeholders[key] = Array.isArray(result[key]) ? [] : null;
127
130
  return placeholders;
@@ -291,6 +291,44 @@ const formulaAggregationCrossModel = Object.freeze({
291
291
  requiresEnvironmentDerivedIds: true,
292
292
  prerequisiteData: { source: "field.list(published:true) ×2(来源模型 + 目标模型)", fields: ["source.model", "source.dataItemCode", "source.matchField", "target.matchField"] },
293
293
  });
294
+ const formulaExpressionSameModel = Object.freeze({
295
+ id: "formula-expression-same-model",
296
+ title: "同模型基础公式字段:FORMULA + EXPRESSION(amount×2)",
297
+ aliases: ["同模型公式", "基础公式字段", "表达式字段", "公式字段", "amount*2"],
298
+ status: "offline-valid",
299
+ tool: "field.create",
300
+ args: {
301
+ environment,
302
+ schemaCode: model,
303
+ code: "total",
304
+ name: "合计",
305
+ propertyType: "FORMULA",
306
+ options: {
307
+ formulaConfig: JSON.stringify({
308
+ resultType: 2,
309
+ formulaType: "EXPRESSION",
310
+ expression: JSON.stringify({ formula: "{amount}*2" }),
311
+ }),
312
+ persistent: true,
313
+ },
314
+ },
315
+ preconditions: ["目标模型已存在;amount 是同一模型上已存在并已发布的数值字段;total 尚不存在"],
316
+ conditionalRequirements: [
317
+ "formulaConfig 是 JSON 字符串,内层 expression 仍是 JSON 字符串:CLI recipe 已代做两层序列化,直接把 args 传给 field.create,不要再手工转义。",
318
+ "这是同模型的基础 EXPRESSION;跨模型汇总请改用 formula-aggregation-cross-model。",
319
+ ],
320
+ priorToolReferences: [
321
+ { tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认 amount 已发布并确认目标 code 未占用" },
322
+ ],
323
+ dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 校验 FORMULA/EXPRESSION 序列化形状,不创建字段" },
324
+ expectedReadback: { readback: { fieldPresent: true, options: { formulaConfig: "<EXPRESSION: {amount}*2>" } }, next: ["field.publish", "form.draft", "form.publish"] },
325
+ refusalRecovery: [
326
+ { code: "duplicate-field", action: "字段已存在改用 field.update 或换 code" },
327
+ { code: "formula-expression-source-missing", action: "先创建并发布同模型的 amount 数值字段,再创建 total" },
328
+ ...commonRecovery,
329
+ ],
330
+ requiresEnvironmentDerivedIds: false,
331
+ });
294
332
  /**
295
333
  * r29(2026-10-29 中冶终轮)+ 用户领域知识:外部数据(企查查/NC 等)在表单上
296
334
  * 「填表时即带出」的标准模式——两段式组合。此前三轮探针(r24/r28b/r29)都发明
@@ -346,6 +384,6 @@ const externalLookupModel = Object.freeze({
346
384
  });
347
385
  export const FIELD_METADATA = Object.freeze({
348
386
  "field.update": Object.freeze({ recipes: [fieldSerialNumber, formConditionalRequired, formAlwaysReadonly, formConditionalReadonly], relatedTools: ["field.list", "field.publish", "form.draft", "form.publish"] }),
349
- "field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, dictionaryMultiSelection, childTable, businessModelDropdown, formulaAggregationCrossModel, externalLookupModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
387
+ "field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, dictionaryMultiSelection, childTable, businessModelDropdown, formulaAggregationCrossModel, formulaExpressionSameModel, externalLookupModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
350
388
  });
351
389
  //# sourceMappingURL=field.js.map
package/dist/skills.js CHANGED
@@ -102,6 +102,19 @@ const SYNTHETIC_SKILLS = Object.freeze({
102
102
  "",
103
103
  ].join("\n"),
104
104
  },
105
+ "batch-write": {
106
+ name: "batch-write",
107
+ description: "批量运行数据写入、形状预检与逐条失败定位",
108
+ content: [
109
+ "# 批量写",
110
+ "",
111
+ "通用批量入口是 `omc call <tool> --args-list '<json数组>'`,逐条执行且默认不中断。",
112
+ "先做零写入形状校验:`omc call data.save --args-list '<file|stdin>' --preflight`;回执 `shapeOnly:true`,只校验 JSON 形状与 schema 必填键,不做业务语义校验;业务校验要真写一次才知道。",
113
+ "支持 `data.save`、`data.delete`、`field.create` 等同一工具逐条批量调用;没有专用 `data.import`,导入数据就是逐条 `data.save`。",
114
+ "失败项回 `row`(1 基)、`index`(0 基)和尽力推断的字段/原因;成功项回输入中的 `code` 等业务标识。",
115
+ "",
116
+ ].join("\n"),
117
+ },
105
118
  });
106
119
  /** Every methodology topic exposed by `omc help`. */
107
120
  export function listSkills() {
@@ -152,6 +152,41 @@ export const KEY_ALIAS_HINTS = Object.freeze({
152
152
  order: ["orders"],
153
153
  orderby: ["orders"],
154
154
  });
155
+ /** Semantic directions for keys whose wire name is not an edit-distance match. */
156
+ export const KEY_SEMANTIC_HINTS = Object.freeze({
157
+ precision: "options.scale(数值小数位)",
158
+ });
159
+ /**
160
+ * r34a-F18(batch2 挂账收尾,2026-11-06 真机):`data.delete` 传 `id` 时写面
161
+ * preflight 建议到无关的 `requestId`(编辑距离子串命中)。读面路径(cli.ts 的
162
+ * suggestArgumentKey)已按工具族给 `id→bizObjectId`,写面必须同一语义,否则
163
+ * 两条通道互相矛盾。**语境而非全局**:`id` 在多数工具是合法键,不能进
164
+ * KEY_SEMANTIC_HINTS(那是无条件的)。
165
+ */
166
+ function contextualClosestKey(toolName, key, schemaKeys) {
167
+ const t = toolName.toLowerCase();
168
+ if (t.startsWith("data.") && key === "id" && schemaKeys.has("bizObjectId"))
169
+ return "bizObjectId";
170
+ if (t.startsWith("data.") && key === "ids" && schemaKeys.has("bizObjectIds"))
171
+ return "bizObjectIds";
172
+ return undefined;
173
+ }
174
+ /**
175
+ * r35a-F8(真机 2026-11-06):`field.update` 传 `propertyType` 时,编辑距离
176
+ * 建议到 `propertyEmpty`(风马牛不相及)——探针要「改字段类型」却拿不到任何
177
+ * 可行动信息。字段 type/code 在平台设计态不可原地修改,只能删重建(且会波及
178
+ * 引用),所以这里给的是**替代路径**而不是一个错误参数名。
179
+ */
180
+ const CONTEXTUAL_KEY_GUIDANCE = Object.freeze({
181
+ "field.update": Object.freeze({
182
+ propertyType: "平台不支持原地改字段类型:需 field.delete(先看其 impact)→ field.create;同名 code 的改 type 会丢该列存量值",
183
+ type: "同上:改类型需 field.delete → field.create;改名(显示名)用 name,改 code 无工具(等同删+建)",
184
+ code: "字段 code 不可改:只能 field.delete → field.create(引用面会断,先看 field.delete 的 impact)",
185
+ }),
186
+ "field.create": Object.freeze({
187
+ precision: "precision→options.scale(数值小数位,不是顶层 precision)",
188
+ }),
189
+ });
155
190
  function closestSchemaKey(key, schemaKeys) {
156
191
  if (schemaKeys.has(key))
157
192
  return key;
@@ -198,7 +233,9 @@ export function parseToolInput(tool, input) {
198
233
  const schemaKeys = new Set(Object.keys(tool.inputSchema));
199
234
  const unknown = Object.keys(normalized).filter((key) => !schemaKeys.has(key));
200
235
  if (unknown.length > 0) {
201
- const suggestions = unknown.map((key) => `${key}→${closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
236
+ const suggestions = unknown.map((key) => `${key}→${KEY_SEMANTIC_HINTS[key] ?? contextualClosestKey(tool.name, key, schemaKeys) ?? closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
237
+ // r35a-F8:有替代路径指引时单独列(不是「最接近的合法键」,而是「该走哪条路」)。
238
+ const guidanceLines = unknown.map((key) => CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key]).filter((hint) => typeof hint === "string" && hint.length > 0);
202
239
  // r25 实证:`publish→(无相近键)` 等于没说——配置与发布是两步,用户要的是
203
240
  // 另一个工具。有语义指引时优先给可执行的下一步。
204
241
  const twoStep = unknown
@@ -209,24 +246,27 @@ export function parseToolInput(tool, input) {
209
246
  // 漏掉发布指引(报告原文:「若用程序消费回执,会选 issues[],就会漏掉」)。
210
247
  // 两条通道必须同源,否则等于修了一半。
211
248
  const issueHintFor = (key) => {
212
- const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key];
249
+ const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key] ?? CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key];
213
250
  if (guidance !== undefined)
214
251
  return guidance;
252
+ const semantic = KEY_SEMANTIC_HINTS[key];
253
+ const contextual = contextualClosestKey(tool.name, key, schemaKeys);
215
254
  const closest = closestSchemaKey(key, schemaKeys);
216
255
  // 语言规则(HANDOFF §三.3-8):人读句子中文、机器字段/键名英文。issues[]
217
256
  // 与人读 message 同源(B4),所以这里也是中文句子 + 英文键名。
218
- return closest === undefined ? "未知键;本工具的合法键里没有相近的" : `未知键;最接近的合法键:${closest}`;
257
+ return semantic !== undefined ? `未知键;语义上应改用:${semantic}` : contextual !== undefined ? `未知键;本工具语境最接近的合法键:${contextual}` : closest === undefined ? "未知键;本工具的合法键里没有相近的" : `未知键;最接近的合法键:${closest}`;
219
258
  };
220
259
  // 未知键的下一步应指向**正确的那个参数**,不是整份 describe(4–17 KB)。
221
260
  // 有近邻时 `--param <nearKey>` 是 150–900 B,且直接回答「本来想写哪个键」。
222
- const nearestKey = closestSchemaKey(unknown[0], schemaKeys);
261
+ // 语境建议优先于编辑距离(r34a-F18:data.delete 的 id 指 requestId 是噪声)。
262
+ const nearestKey = contextualClosestKey(tool.name, unknown[0], schemaKeys) ?? closestSchemaKey(unknown[0], schemaKeys);
223
263
  const paramCommand = nearestKey !== undefined ? `omc describe ${tool.name} --param ${nearestKey}` : `omc describe ${tool.name}`;
224
264
  throw new ToolInputError({
225
265
  status: "error",
226
266
  blocker: "tool-input-unknown-keys",
227
267
  tool: tool.name,
228
- message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `最接近的合法键:${suggestions.join(";")}。参数表:${paramCommand}`}`,
229
- ...(twoStep.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
268
+ message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `${guidanceLines.length > 0 ? guidanceLines.join(";") : `最接近的合法键:${suggestions.join(";")}`}。参数表:${paramCommand}`}`,
269
+ ...(twoStep.length > 0 || guidanceLines.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
230
270
  issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: issueHintFor(key) })),
231
271
  });
232
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
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.14",
58
+ "omc-core": "0.6.16",
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.14"
76
+ "@stalfh233/omc-fake-platform": "0.6.16"
77
77
  }
78
78
  }