@stalfh233/omc-cli 0.6.23 → 0.6.25

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.
@@ -1449,6 +1449,30 @@
1449
1449
  "custom page body publish (custom/page/content/publish)",
1450
1450
  "custom page delete (custom/delete)"
1451
1451
  ]
1452
+ },
1453
+ {
1454
+ "capability": "rule execution log search (trigger rows, optional node chains)",
1455
+ "status": "executable",
1456
+ "scope": "POST /api/api/launcher/domain_service_runtime/{schemaCode}/search_rule_log paged rule-execution-log search (triggerTime desc) with client-side bizObjectId row matching over inputSummaries (bounded 5-page scan) and optional query_rule_activity_log node chains for the first 3 hits; honest notes for ~5s async flush, retention cleanup, and CODING-mode absence.",
1457
+ "version_family": "8.6",
1458
+ "anchor_version": "8.6.26",
1459
+ "evidence": [
1460
+ {
1461
+ "path": ".scratch/r43-forensic/FORENSIC-rulelog.md",
1462
+ "anchor": "r43 源码+真机双证(DomainServiceController/RuleLogSearchAssembler/RuleLogManagerImpl + seal_apply 历史日志 + r43x 自建触发)"
1463
+ },
1464
+ {
1465
+ "path": "packages/omc-cli/test/rulelog.test.ts",
1466
+ "anchor": "contract tests"
1467
+ }
1468
+ ],
1469
+ "lane": "B1-businessrule",
1470
+ "tools": [
1471
+ "rulelog.search"
1472
+ ],
1473
+ "scopes": [
1474
+ "rule execution log search (trigger-time desc, row match client-side)"
1475
+ ]
1452
1476
  }
1453
1477
  ]
1454
1478
  }
@@ -238,8 +238,10 @@ export const CAPABILITY_CUES = Object.freeze({
238
238
  "不早于", "不能早于", "不晚于", "不能晚于", "早于今天", "晚于今天", "日期校验", "提交校验", "校验不通过", "必填校验", "正则校验", "重复校验", "长度校验"],
239
239
  // NOTE: bare "自动" was a cue (round 10 bug: "自动识别"/"自动发邮件" matched
240
240
  // businessrule and routed an UNSUPPORTED ask onto the write ladder). A business
241
- // rule is named by 业务规则/触发器/规则图, not by "automatic".
242
- businessrule: ["业务规则", "触发器", "规则图", "校验", "事件", "按钮", "定时任务", "定时触发", "定时器", "每天", "每日", "周期执行", "调度", "SCHEDULER", "TIMING_TASK"],
241
+ // rule is named by 业务规则/触发器/规则图, not by "automatic". r44fc(2026-11-08)
242
+ // 重犯过一次(round10 测试当场抓住)——复合词(累加/联动/审计/规则触发/自动累加)才是
243
+ // 规则面线索,裸「自动」永远不是。
244
+ businessrule: ["业务规则", "触发器", "规则图", "校验", "事件", "按钮", "定时任务", "定时触发", "定时器", "每天", "每日", "周期执行", "调度", "SCHEDULER", "TIMING_TASK", "累加", "联动", "审计", "规则触发", "自动累加", "自动回写"],
243
245
  "rule-lifecycle": ["启用规则", "停用规则", "删除规则", "规则启停", "触发规则", "执行规则", "跑规则", "按钮规则", "规则验收"],
244
246
  // r28h F1/F7: narrow org cues so "管理组织:部门/角色/角色组/用户/权限范围"
245
247
  // hits workflow (where org.* lives) instead of field→workflow. Keep them
package/dist/cli.js CHANGED
@@ -2225,7 +2225,7 @@ async function main() {
2225
2225
  commandExitCode = CLI_EXIT_CODES.ok;
2226
2226
  });
2227
2227
  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); });
2228
- command.command("call <tool>").option("--env <environment>", "环境名;项目白名单按当前 cwd 解析,须在含 .omc-env 的目录内运行").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(数组批量请改用 --args-list;跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象,最多 200 条);超过上限请分批,或用 omc script 读文件。逐条执行、默认不中断,最后回 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) => {
2228
+ command.command("call <tool>").option("--env <environment>", "环境名;项目白名单按当前 cwd 解析,须在含 .omc-env 的目录内运行").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(数组批量请改用 --args-list;跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象,最多 200 条);超过上限请分批,或用 omc script 读文件。逐条执行、默认不中断,最后回 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>", "仅 `omc call` / `omc intent apply` 支持;写锁冲突时有界排队(0=立刻拒绝)").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
2229
2229
  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"] : []),
2230
2230
  ...(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"] : [])];
2231
2231
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
@@ -2254,7 +2254,7 @@ async function main() {
2254
2254
  .option("--env <environment>")
2255
2255
  .option("--environment <environment>")
2256
2256
  .option("--apply")
2257
- .addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 → 模型(字段/表单随模型级联)。默认 dry-run。\n业务数据:--purge-instances 删流程实例;普通业务行(无流程)不会自动带走——先 data.delete 清(sweep-all-readback 配方),再 purge,否则留孤儿数据。\n")
2257
+ .addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 → 模型(字段/表单随模型级联,并级联删除流程模板 <code>_wf 及其发起权限绑定)。默认 dry-run。\n业务数据:--purge-instances 删流程实例;普通业务行(无流程)不会自动带走——先 data.delete 清(sweep-all-readback 配方),再 purge,否则留孤儿数据。\n")
2258
2258
  .action(async (options) => {
2259
2259
  const rest = ["model-purge",
2260
2260
  ...(options.code ? ["--code", options.code] : []),
@@ -2371,6 +2371,7 @@ async function main() {
2371
2371
  .option("--name <name>", "模型名")
2372
2372
  .option("--fields <json>", "JSON 数组,每条是一个 field.create 参数;子表加 subFields")
2373
2373
  .option("--fields-json <json>")
2374
+ .option("--fields-file <path>", "从配置根内文件读字段 JSON 数组;与 --fields 互斥")
2374
2375
  .option("--folder <folderId>", "建在哪个应用目录(省略=默认目录)")
2375
2376
  .option("--app <applicationCode>")
2376
2377
  .option("--env <environment>")
@@ -2396,6 +2397,7 @@ async function main() {
2396
2397
  ...(options.name ? ["--name", options.name] : []),
2397
2398
  ...(options.fields !== undefined ? ["--fields", options.fields] : []),
2398
2399
  ...(options.fieldsJson !== undefined ? ["--fields-json", options.fieldsJson] : []),
2400
+ ...(options.fieldsFile !== undefined ? ["--fields-file", options.fieldsFile] : []),
2399
2401
  ...(options.folder ? ["--folder", options.folder] : []),
2400
2402
  ...(options.app ? ["--app", options.app] : []),
2401
2403
  ...(options.env ? ["--env", options.env] : []),
@@ -2530,7 +2532,7 @@ async function main() {
2530
2532
  pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
2531
2533
  });
2532
2534
  command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
2533
- command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true 即零残留。清理入口:omc model-purge --code <schemaCode>(按依赖倒序,默认 dry-run)。只读零写入;report 面暂停维护未扫描").option("--env <environment>", "环境名(默认按 cwd 的 .omc-env 解析)").option("--apps <codes>", "all(默认,全租户)或逗号分隔应用码").option("--no-deep", "只扫 model/folder/custompage,不深查命中模型").option("--max-output-bytes <bytes>").action(async (prefix, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdResidue(prefix, { env: options.env, apps: options.apps, ...(options.deep === false ? { noDeep: true } : {}) }, transport)); });
2535
+ command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true 即零残留。清理入口:omc model-purge --code <schemaCode>(按依赖倒序,默认 dry-run)。只读零写入;report 面暂停维护未扫描").option("--env <environment>", "环境名(默认按 cwd 的 .omc-env 解析)").option("--apps <codes>", "all(默认,全租户)或逗号分隔应用码").option("--no-deep", "只扫 model/folder/custompage,不深查命中模型").option("--all-faces", "列出全部应用×面(含 count:0;默认只回非零面+byFace/userResidues 紧凑形)").option("--max-output-bytes <bytes>").action(async (prefix, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdResidue(prefix, { env: options.env, apps: options.apps, ...(options.deep === false ? { noDeep: true } : {}), ...(options.allFaces ? { allFaces: true } : {}) }, transport)); });
2534
2536
  const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
2535
2537
  command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) })); });
2536
2538
  command.command("describe <tool>").option("--full").option("--cache-ok", "允许 --full 在同会话重复读时短路为 artifact 指针(默认每次回全文)").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (tool, options) => {
package/dist/endpoints.js CHANGED
@@ -108,6 +108,8 @@ export const ENDPOINTS = {
108
108
  * reacquireDataFromDb:false), so effects must be read back separately.
109
109
  */
110
110
  ruleRuntimeExecuteBase: "/api/api/launcher/domain_service_runtime",
111
+ ruleLogSearch: "/api/api/launcher/domain_service_runtime/{schemaCode}/search_rule_log",
112
+ ruleActivityLog: "/api/api/launcher/domain_service_runtime/{schemaCode}/query_rule_activity_log",
111
113
  /**
112
114
  * Form-toolbar button execution — a DIFFERENT face from the list-button one.
113
115
  * Live 2026-10-23 (1.1.25, Playwright capture of a `triggerType=FORM` button
@@ -625,6 +625,8 @@ export function normalizeMappingsOption(options) {
625
625
  const target = record.target;
626
626
  if (typeof source !== "string" || source.trim() === "" || typeof target !== "string" || target.trim() === "")
627
627
  mappingsError("每项的 source/target 必须是非空字符串");
628
+ if (source.includes(".") || target.includes("."))
629
+ mappingsError("source/target 应为目标模型字段 code,不带模型前缀(不要使用 模型.字段)");
628
630
  return { source: source.trim(), target: target.trim() };
629
631
  });
630
632
  normalized = JSON.stringify(rows);
@@ -644,6 +646,8 @@ export function normalizeMappingsOption(options) {
644
646
  }
645
647
  if (!Array.isArray(parsed) || parsed.some((row) => row === null || typeof row !== "object" || Array.isArray(row) || typeof row.source !== "string" || typeof row.target !== "string" || row.source === "" || row.target === ""))
646
648
  mappingsError("解析结果必须是 [{source,target}] 形状(source/target 非空字符串)");
649
+ if (parsed.some((row) => String(row.source).includes(".") || String(row.target).includes(".")))
650
+ mappingsError("source/target 应为目标模型字段 code,不带模型前缀(不要使用 模型.字段)");
647
651
  normalized = JSON.stringify(parsed.map(({ source, target }) => ({ source: source.trim(), target: target.trim() })));
648
652
  }
649
653
  }
@@ -1338,6 +1338,7 @@ export async function executeFormDraftWrite(session, args) {
1338
1338
  attributes = undefined;
1339
1339
  }
1340
1340
  let rebuildPreservedControls = [];
1341
+ let controlsUpdated = 0;
1341
1342
  if (typeof view !== "string" || typeof attributes !== "string") {
1342
1343
  // Ticket 48: an explicit fieldCodes selection picks (and orders) the
1343
1344
  // scaffolded business fields; omitted scaffolds every published field.
@@ -1364,6 +1365,7 @@ export async function executeFormDraftWrite(session, args) {
1364
1365
  attributes = JSON.stringify(scaffold.attributes);
1365
1366
  }
1366
1367
  if (args.draftAttributesJson === undefined && typeof attributes === "string") {
1368
+ const beforeDerived = attributes;
1367
1369
  attributes = propagateRelationMappings(attributes, publishedFields);
1368
1370
  // Ticket 69: repair/densify org-selection controls on the existing draft.
1369
1371
  attributes = normalizeOrgSelectorControls(attributes, publishedFields);
@@ -1374,6 +1376,16 @@ export async function executeFormDraftWrite(session, args) {
1374
1376
  // control even when the form already existed — a non-rebuild draft keeps
1375
1377
  // controls verbatim, so a later field.update never propagated (bench20).
1376
1378
  attributes = syncFieldDerivedControlOptions(attributes, publishedFields);
1379
+ if (args.refreshFromFields === true && attributes !== beforeDerived) {
1380
+ try {
1381
+ const before = JSON.parse(beforeDerived);
1382
+ const after = JSON.parse(attributes);
1383
+ controlsUpdated = Object.keys(after).filter((code) => JSON.stringify(before[code]) !== JSON.stringify(after[code])).length;
1384
+ }
1385
+ catch {
1386
+ controlsUpdated = 0;
1387
+ }
1388
+ }
1377
1389
  }
1378
1390
  // Fail-closed: a rebuild that would drop a designer formula must never reach
1379
1391
  // the wire. The merge above normally carries them, but it cannot when the
@@ -1428,6 +1440,7 @@ export async function executeFormDraftWrite(session, args) {
1428
1440
  }
1429
1441
  : {}),
1430
1442
  ...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}),
1443
+ ...(args.refreshFromFields === true ? { refreshFromFields: true, controlsUpdated } : {}),
1431
1444
  // r36(2026-11-01):rebuild 后草稿与重建前逐字节一致时明示——rebuild 的
1432
1445
  // 语义就是"按已发布字段重搭",一致通常意味着预期的新控件没进来(字段未
1433
1446
  // 发布/写错槽位),静默 no-op 会让"新增字段后刷新表单"看起来成功实则没变。
@@ -973,7 +973,7 @@ export function goalRoute(registry, goal) {
973
973
  goal,
974
974
  confident: true,
975
975
  mode: whole ? "whole-package" : mode,
976
- match: { kind: readish ? "read-ladder" : "ladder", ref: ordered.join("→"), capabilities: ordered },
976
+ match: { kind: readish ? "read-ladder" : "ladder", ref: `${ordered.join("→")}${ordered.includes("businessrule") && /累加|联动|审计|规则触发|自动累加|自动回写/.test(goal) ? "(规则面不在本阶梯)" : ""}`, capabilities: ordered },
977
977
  route: {
978
978
  ...(whole ? { primary: "intent", atomicSupplement } : { ladder }),
979
979
  // r28e F2(真机复现 2026-11-05):`next "建表单布局、在线JS、表单按钮"`
@@ -1059,7 +1059,7 @@ export function goalRoute(registry, goal) {
1059
1059
  mode: whole ? "whole-package" : mode,
1060
1060
  match: {
1061
1061
  kind: readish ? "read-capability" : recipe ? "recipe" : "capability",
1062
- ref: recipe ? recipe.recipe : capabilityId,
1062
+ ref: recipe ? recipe.recipe : `${capabilityId}${capabilityId === "businessrule" && /累加|联动|审计|规则触发|自动累加|自动回写/.test(goal) ? "(规则面不在本阶梯)" : ""}`,
1063
1063
  title: firstSentence(recipe ? recipe.title : route.title),
1064
1064
  capability: capabilityId,
1065
1065
  ...(recipe ? { tool: recipe.tool, recipe: recipe.recipe } : {}),
@@ -72,6 +72,7 @@ const RULE_SAVE_DESCRIPTION = describeTool("保存业务规则(upsert):`ru
72
72
  "**条件执行**:① 单分支→动作节点自身 `filterCondition`(UPDATE/DELETE/ASSIGN 的 `mainConditions`:身份匹配 DYNAMIC + 固定条件 FIXED,同叶组 AND)。② 多路分支→`BRANCH` 节点:出口叶(DataFilterCondition)键为 `{currentSchemaDataItem(左值),ruleDataConditionType(操作符),ruleValueType,value}`,出路由 `routeCondition:true` 走条件、`false` 兜底 else。**别照抄其它条件组的 targetSchemaDataItem+ruleConditionType——未知键 save+publish 过但运行时 50000(lint fail closed `rule-graph-branch-condition-keys`)**。按数据走流程流转用 `ruleConditionGroup`。",
73
73
  "**生命周期铁律(2026-09-11 live 取证)**:update 只写 DRAFT 工作行,get/list 读 PUBLISH 面因此看不到变化——必须再 `rule.enable`(publish 出新版本 + 翻 DRAFT 启用开关)才会生效;数据事件分发:DRAFT 保存不触发,submit(DRAFT→COMPLETED)触发 Create。保存 ≠ 设计器可见:readback 的 `published:false` + nextAction 标明尚未发布;传 `publish:true` 可在同一次调用内发布新版本(仅发布,启用仍走 `rule.enable`)。",
74
74
  "**BizMethod 绑定(ticket 35/96)**:BIZ_ACTION / GET_LIST(BIZ_SERVICE) 的 methodMapping 须逐键对 8.6.26 DTO(codeType int 0=CONST/1=PROPERTY…;非语义键原样透传);写前 wire 校验服务+方法真实存在(get_by_code + list_by_service_code),悬空即 refused `rule-graph-bizmethod-unresolved`——平台只拦服务不拦方法,绝不用注册壳冒充已发布 BizMethod;RESTFUL/DATA_SQL 方法必须带适配器必需 configJson 键,只有名字/参数面的注册壳 refused `rule-graph-bizmethod-registration-shell`;映射 `required` 以方法参数面为权威镜像(省略→按面填入,面缺席→保持缺席,显式矛盾→本地拒绝零写入)。",
75
+ "**EXPRESSION 值形状**:`ruleValueType:EXPRESSION` 时 `value` 必须是 {\"formula\":\"{字段A}+{字段B}\"}(大括号裸 code);门户设计器的 [code] 方括号式在这里不认。",
75
76
  "**幻影 schema 门(ticket 96 live 事故)**:rule.save 写前先以 bizmodels/get_all 探测模型存在(缺失 → refused `rule-schema-unresolved`,零规则面 wire 调用)——平台的 list/save 面会对任意 schemaCode 自动补建 7 条预置规则且 API 删除不可达(list 即重灌),幻影 schema 必须在写前拒绝。",
76
77
  "**输出回写排序(ticket 36 live 铁证)**:BIZ_ACTION 的 outputMappings 由 SYSTEM_CREATE/SYSTEM_UPDATE 持久化节点消费,动作节点必须排在系统节点之前(`Start→BizAction→SYSTEM_CREATE→End`);工具默认把系统节点补在 START 之后,有回写动作时须显式提供系统节点并置于动作之后,否则合并结果不落库(echoResult 恒 null)。",
77
78
  ]);
@@ -1,5 +1,6 @@
1
1
  import { claim, declareLaneCoverage } from "./coverage-declaration.js";
2
2
  import { BUSINESSRULE_LANE_CONTRACT } from "./businessrule-contract.js";
3
+ import { RULELOG_LANE_CONTRACT } from "./rulelog-contract.js";
3
4
  const T = BUSINESSRULE_LANE_CONTRACT.tools;
4
5
  export const BUSINESSRULE_COVERAGE = declareLaneCoverage(BUSINESSRULE_LANE_CONTRACT.lane, [
5
6
  {
@@ -12,5 +13,12 @@ export const BUSINESSRULE_COVERAGE = declareLaneCoverage(BUSINESSRULE_LANE_CONTR
12
13
  evidence: ["packages/omc-cli/src/tools.ts", "packages/omc-cli/test/rule-list.test.ts"],
13
14
  tools: [claim("rule.list", T["rule.list"].scope), claim("rule.get", T["rule.get"].scope), claim("rule.references", T["rule.references"].scope)],
14
15
  },
16
+ {
17
+ // r43a(2026-11-08):规则执行日志只读面。证据=运行版 v1.1.25 反编译(DomainServiceController/
18
+ // RuleLogSearchAssembler/RuleLogManagerImpl)+ 真机双证(seal_apply 历史日志 + r43x 自建触发)。
19
+ capability: "rule execution log search (trigger rows, optional node chains)",
20
+ evidence: [".scratch/r43-forensic/FORENSIC-rulelog.md", "packages/omc-cli/test/rulelog.test.ts"],
21
+ tools: [claim("rulelog.search", RULELOG_LANE_CONTRACT.tools["rulelog.search"].scope)],
22
+ },
15
23
  ]);
16
24
  //# sourceMappingURL=businessrule-coverage.js.map
@@ -745,14 +745,17 @@ const FORM_CONDITION_RULE_TYPES = Object.freeze({
745
745
  export async function syncFormConditionDataRules(session, schemaCode, fieldCode, prevOptions, nextOptions) {
746
746
  const result = { created: [], updated: [], deleted: [], skipped: [] };
747
747
  const text = (options, key) => (typeof options[key] === "string" ? options[key].trim() : "");
748
- const changedKeys = Object.keys(FORM_CONDITION_RULE_TYPES).filter((key) => text(prevOptions, key) !== text(nextOptions, key));
749
- if (changedKeys.length === 0)
750
- return result;
751
748
  const existing = await listDataRules(session, schemaCode);
752
749
  // The designer's rule rows live on the PUBLISHED face (like datarule.create's
753
750
  // own guard); a draft-only field cannot carry a designer-visible row yet.
754
751
  const publishedCodes = await readPublishedFieldCodesOrNull(session, schemaCode);
755
752
  const published = publishedCodes === null ? true : publishedCodes.has(fieldCode);
753
+ const changedKeys = Object.keys(FORM_CONDITION_RULE_TYPES).filter((key) => {
754
+ const changed = text(prevOptions, key) !== text(nextOptions, key);
755
+ const desired = text(nextOptions, key);
756
+ const present = existing.some((row) => Number(row.dataRuleType) === FORM_CONDITION_RULE_TYPES[key] && storedTargetCodes(row).includes(fieldCode));
757
+ return changed || (desired.length > 0 && !present);
758
+ });
756
759
  for (const key of changedKeys) {
757
760
  const dataRuleType = FORM_CONDITION_RULE_TYPES[key];
758
761
  const next = text(nextOptions, key);
@@ -266,7 +266,7 @@ export function registerFieldLane(tools) {
266
266
  // 不带这个键,于是不会出现;write-only 的确认走 field.update 的
267
267
  // readback.committed。
268
268
  ...(Object.prototype.hasOwnProperty.call(row, "propertyEmpty") ? { propertyEmpty: row.propertyEmpty } : {}),
269
- ...(options !== undefined ? { options } : {}),
269
+ ...(options !== undefined ? { options, optionsPresent: true } : { optionsPresent: false }),
270
270
  ...(scaleWarning !== undefined ? { shapeWarning: scaleWarning, ...(system ? { origin: "system" } : {}) } : {}),
271
271
  ...(system && scaleWarning === undefined ? { origin: "system" } : {}),
272
272
  };
@@ -292,7 +292,7 @@ export function registerFieldLane(tools) {
292
292
  const projected = detailed
293
293
  ? visible
294
294
  : visible.map((field) => {
295
- const options = (field.options ?? {});
295
+ const options = ("options" in field ? field.options : undefined) ?? {};
296
296
  const conditions = Object.keys(FORM_CONDITION_OPTION_KEYS).filter((key) => {
297
297
  const value = options[key];
298
298
  return key === "readonlyFormula" ? value === true : typeof value === "string" && value.trim() !== "";
@@ -314,6 +314,7 @@ export function registerFieldLane(tools) {
314
314
  ...(conditions.length > 0 ? { conditions } : {}),
315
315
  ...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
316
316
  ...(field.origin !== undefined ? { origin: field.origin } : {}),
317
+ ...(args.full === true && "options" in field && field.options !== undefined ? { options: field.options, optionsPresent: true } : {}),
317
318
  };
318
319
  });
319
320
  return {
@@ -1117,6 +1118,19 @@ async function runFieldUpdate(session, args) {
1117
1118
  if (args.choices !== undefined && !same(choiceCodesOf(currentOptions) ?? [], [...args.choices]))
1118
1119
  requestedChanges.push("choices");
1119
1120
  if (requestedChanges.length === 0) {
1121
+ let unchangedSync;
1122
+ let unchangedSyncError;
1123
+ const conditionKeys = new Set(Object.keys(FORM_CONDITION_OPTION_KEYS));
1124
+ if (effectiveOptions !== undefined && [...conditionKeys].some((key) => typeof prepared.options[key] === "string" && String(prepared.options[key]).trim() !== "")) {
1125
+ try {
1126
+ const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, currentOptions, prepared.options);
1127
+ if (sync.created.length + sync.updated.length + sync.deleted.length + sync.skipped.length > 0)
1128
+ unchangedSync = sync;
1129
+ }
1130
+ catch (error) {
1131
+ unchangedSyncError = error instanceof Error ? error.message : String(error);
1132
+ }
1133
+ }
1120
1134
  return {
1121
1135
  performed: { errcode: 0, unchanged: true },
1122
1136
  readback: {
@@ -1126,6 +1140,7 @@ async function runFieldUpdate(session, args) {
1126
1140
  ...(record.name !== undefined ? { name: record.name } : {}),
1127
1141
  ...(record.name_i18n !== undefined ? { name_i18n: record.name_i18n } : {}),
1128
1142
  availableFields: ["name", "name_i18n", "options", "choices", "relativeCode", "propertyEmpty"],
1143
+ ...(unchangedSync !== undefined ? { formConditionSync: unchangedSync } : unchangedSyncError !== undefined ? { formConditionSync: { error: unchangedSyncError } } : {}),
1129
1144
  },
1130
1145
  };
1131
1146
  }
@@ -296,6 +296,7 @@ export function registerFormLane(tools) {
296
296
  .optional()
297
297
  .describe("把子表列绑进**已有** Sheet201 控件的 columns(issue #24):[{parent:\"items\",fieldCodes:[\"category\"]}]。只追加列,不动其它控件/布局行;无已存布局时退回 scaffold"),
298
298
  rebuild: z.boolean().optional().describe("true 时丢弃现有草稿布局,按已发布字段重新生成(新增字段/子表要用它;默认保留现有草稿)"),
299
+ refreshFromFields: z.boolean().optional().describe("true 时不重建布局,仅按当前已发布字段 options 刷新现有控件派生配置"),
299
300
  fromReadback: z.boolean().optional().describe("true = 本次 draftViewJson/draftAttributesJson 来自 form.get 的**逐字未改动**原文(平台自身布局可能含子表列行/系统行超宽,逐字回写放行)。语义是「回写」不是「我读过」:读过之后改过一个字符就不再是回读,会被拒(去掉本参数按普通写面校验)。只加子表列请用 configure_form 的 childColumns"),
300
301
  allowEmptyLayout: z.boolean().optional().describe("显式放行空布局(\"[]\"/\"{}\")写入;默认拒绝(form-empty-layout),防设计器 Save 清空后照写丢控件"),
301
302
  buttons: z
@@ -457,7 +457,22 @@ export function registerListViewLane(tools) {
457
457
  description: LIST_VIEW_TOOLS["listview.list"].description,
458
458
  inputSchema: { environment: environmentArg, schemaCode: z.string().min(1), capability: capabilityArg },
459
459
  handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
460
- const rows = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode: args.schemaCode, clientType: "PC" } })).data);
460
+ let rows;
461
+ try {
462
+ rows = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode: args.schemaCode, clientType: "PC" } })).data);
463
+ }
464
+ catch (error) {
465
+ if (error instanceof WireError && error.errcode === 50000) {
466
+ try {
467
+ await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
468
+ }
469
+ catch (probe) {
470
+ if (probe instanceof WireError && probe.errcode === 300004)
471
+ return { absent: true, schemaCode: args.schemaCode };
472
+ }
473
+ }
474
+ throw error;
475
+ }
461
476
  return {
462
477
  schemaCode: args.schemaCode,
463
478
  count: rows.length,
@@ -419,7 +419,14 @@ export function registerModelLane(tools) {
419
419
  // 产物缺失/损坏时保持空摘要——不因解引用失败拖垮整个 inspect
420
420
  }
421
421
  }
422
- const datarules = dataruleRows.map((rule) => ({ propertyCode: rule.propertyCode, propertyCodes: rule.propertyCodes, dataRuleType: rule.dataRuleType, name: rule.name, checkType: rule.checkType }));
422
+ const datarules = dataruleRows.map((rule) => {
423
+ const hasCondition = ["condition", "conditions", "conditionJson", "ruleCondition", "formula"].some((key) => {
424
+ const value = rule[key];
425
+ return value !== undefined && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0);
426
+ });
427
+ const auto = rule.name == null && Number(rule.checkType) === 4 && !hasCondition;
428
+ return { propertyCode: rule.propertyCode, propertyCodes: rule.propertyCodes, dataRuleType: rule.dataRuleType, name: rule.name, checkType: rule.checkType, origin: auto ? "auto" : "user" };
429
+ });
423
430
  // Workflow summary: use the PUBLISHED face for the `published` flag. The
424
431
  // draft face's templateType is ALWAYS "DRAFT" (even for a published
425
432
  // workflow — draft and published are separate faces), so deriving
@@ -496,7 +503,10 @@ export function registerModelLane(tools) {
496
503
  : {}),
497
504
  form: result.form,
498
505
  listview: result.listview ? { code: result.listview.code, columnCount: result.listview.columnCount } : undefined,
499
- ruleCodes: (result.rules ?? []).map((rule) => rule.code),
506
+ ruleCodes: Array.isArray(result.rules) ? result.rules.map((rule) => rule.code) : [],
507
+ ...(Array.isArray(result.rules) && result.rules.length > 0
508
+ ? { rules: { truncated: true, totalRules: result.rules.length, nextCommand: `omc call rule.list --args {"schemaCode":"${String(result.schemaCode ?? "")}"}` } }
509
+ : {}),
500
510
  datarules: result.datarules,
501
511
  // r35a-F2:workflow 摘要必须恒有 exists 布尔——compact 投影此前把它丢了,
502
512
  // 真机(走 compact 的分支)于是永远读不到 exists(探针只能靠
@@ -1042,11 +1042,11 @@ export function registerOrganizationLane(tools) {
1042
1042
  name: "org.role.delete",
1043
1043
  lane: LANE,
1044
1044
  scope: SCOPES.roleDelete,
1045
- description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员数、非空即拒绝并给出人数;`force:true` 才会先逐人移出再删。**可用 roleCode 代替 roleId**(org.role.list 的码表只有 code)。",
1045
+ description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员并拒绝;拒绝回执带可直接粘贴的 `org.role.removeUsers` 命令。`force:true` 的语义是**先逐人移出全部成员,再删除角色**(不是强行绕过平台校验)。**可用 roleCode 代替 roleId**(org.role.list 的码表只有 code)。",
1046
1046
  inputSchema: {
1047
1047
  roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
1048
1048
  roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表,如 qc_manager)或名称"),
1049
- force: z.boolean().optional().describe("true = 先移出全部成员再删角色"),
1049
+ force: z.boolean().optional().describe("true = 先逐人调用 removeUsers 移出全部成员,再删角色;不会保留成员"),
1050
1050
  },
1051
1051
  execute: async (session, args) => {
1052
1052
  const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.delete");
@@ -1054,7 +1054,7 @@ export function registerOrganizationLane(tools) {
1054
1054
  if (rows.length > 0 && args.force !== true)
1055
1055
  throw new ToolRefusalError({
1056
1056
  status: "refused", blocker: "role-has-members", roleId, members: rows.length,
1057
- nextCommand: `omc call org.role.delete --args '${JSON.stringify({ roleId, force: true })}'`,
1057
+ nextCommand: `omc call org.role.removeUsers --args '${JSON.stringify({ roleId, ids: rows.map(roleUserId) })}'`,
1058
1058
  message: `角色下还有 ${rows.length} 名成员,平台拒绝删除(errmsg:角色下已经包含用户数据)。先 org.role.removeUsers,或传 force:true 让工具移出后再删。`,
1059
1059
  });
1060
1060
  if (rows.length > 0)
@@ -1517,6 +1517,22 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
1517
1517
  });
1518
1518
  }
1519
1519
  }
1520
+ // F11 (r44): `_input` is the trigger input binding, not a writable model.
1521
+ // QUERY may legitimately use inputParam:"_input"; only UPDATE targets are
1522
+ // rejected here because the platform accepts/publishes them and then throws
1523
+ // at runtime, rolling back the entire rule (including SYSTEM_CREATE).
1524
+ for (const node of nodeRecords) {
1525
+ if (String(node.nodeType ?? "") !== "UPDATE")
1526
+ continue;
1527
+ if (String(node.targetObjectCode ?? "").trim() !== "_input")
1528
+ continue;
1529
+ blockers.push({
1530
+ code: "rule-graph-update-target-input",
1531
+ nodeCode: String(node.nodeCode ?? ""),
1532
+ key: "targetObjectCode",
1533
+ message: 'UPDATE 的 targetObjectCode 不接受 `_input`(1.1.25 运行时 EXCEPTION 并回滚全规则);自更新写本模型 schemaCode;QUERY 的 inputParam 用 `_input` 不受影响',
1534
+ });
1535
+ }
1520
1536
  // Cross-model write-back: SUPPORTED (verified live 2026-09-28). An UPDATE whose
1521
1537
  // `targetObjectCode` is a DIFFERENT model than the rule's own model writes back
1522
1538
  // correctly once TWO prerequisites hold — a 2026-09-27 probe that reported
@@ -0,0 +1,15 @@
1
+ import { assertLaneContract } from "./contract-support.js";
2
+ import { CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION } from "./contract-support.js";
3
+ import { LANES } from "./lane-ids.js";
4
+ const description = "查询规则执行日志;支持时间/规则/状态筛选、按触发行 id 客户端匹配,以及前 3 条日志的节点链。日志异步约 5 秒落库,未见不等于未触发;历史按平台保留天数清理,CODING 在线开发规则不记日志。";
5
+ export const RULELOG_LANE_CONTRACT = Object.freeze({
6
+ lane: LANES.businessrule,
7
+ family: CONTRACT_FAMILY,
8
+ referenceVersion: CONTRACT_REFERENCE_VERSION,
9
+ endpoints: Object.freeze({ search: "ruleLogSearch", activity: "ruleActivityLog" }),
10
+ wire: Object.freeze({ page: 0, size: 20, triggerRows: "inputSummaries[].id", nodeId: "平台 VO 不提供 targetBizObjectId" }),
11
+ evidence: ["TASK-r43a.md §一"],
12
+ tools: Object.freeze({ "rulelog.search": Object.freeze({ scope: "rule execution log search (trigger-time desc, row match client-side)", description }) }),
13
+ });
14
+ export function assertRuleLogContract() { assertLaneContract(RULELOG_LANE_CONTRACT); }
15
+ //# sourceMappingURL=rulelog-contract.js.map
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { ENDPOINTS } from "../endpoints.js";
3
+ import { capabilityArg, environmentArg } from "../args.js";
4
+ import { governedRead } from "../read.js";
5
+ import { defineTool } from "../tool-types.js";
6
+ import { wireCall } from "../wire.js";
7
+ import { RULELOG_LANE_CONTRACT } from "./rulelog-contract.js";
8
+ const path = (template, schemaCode) => template.replace("{schemaCode}", encodeURIComponent(schemaCode));
9
+ const notes = ["日志异步落库约 5 秒,未见不等于未触发", "成功/失败日志按平台配置天数清理,历史有限", "CODING 模式在线开发 Java 规则不记日志"];
10
+ export function registerRuleLogLane(tools) {
11
+ tools.push(defineTool({
12
+ name: "rulelog.search", layer: "low", kind: "read", lane: RULELOG_LANE_CONTRACT.lane,
13
+ scope: RULELOG_LANE_CONTRACT.tools["rulelog.search"].scope,
14
+ description: RULELOG_LANE_CONTRACT.tools["rulelog.search"].description,
15
+ inputSchema: {
16
+ environment: environmentArg, schemaCode: z.string().min(1), bizObjectId: z.string().min(1).optional(),
17
+ since: z.string().optional(), until: z.string().optional(), ruleCode: z.string().optional(), status: z.boolean().optional(),
18
+ includeNodes: z.boolean().optional(), page: z.number().int().min(0).optional(), size: z.number().int().min(1).max(50).optional(), capability: capabilityArg,
19
+ },
20
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
21
+ const requestedPage = args.page ?? 0, requestedSize = args.size ?? 20;
22
+ const fetch = async (page, size) => {
23
+ const body = { page, size };
24
+ for (const [key, value] of [["startTime", args.since], ["endTime", args.until], ["ruleCode", args.ruleCode], ["status", args.status]])
25
+ if (value !== undefined)
26
+ body[key] = value;
27
+ return (await wireCall(session, { path: path(ENDPOINTS.ruleLogSearch, args.schemaCode), method: "POST", body })).data;
28
+ };
29
+ let data;
30
+ let rows;
31
+ let rowFilter;
32
+ if (args.bizObjectId === undefined) {
33
+ data = await fetch(requestedPage, requestedSize);
34
+ rows = Array.isArray(data.content) ? data.content : [];
35
+ }
36
+ else {
37
+ rows = [];
38
+ let scannedElements = 0;
39
+ let scannedPages = 0;
40
+ let found = false;
41
+ for (let p = 0; p < 5; p += 1) {
42
+ const candidate = await fetch(p, 20);
43
+ scannedPages += 1;
44
+ const pageRows = Array.isArray(candidate.content) ? candidate.content : [];
45
+ scannedElements += pageRows.length;
46
+ const hit = pageRows.filter((row) => Array.isArray(row.inputSummaries) && row.inputSummaries.some((v) => v && typeof v === "object" && v.id === args.bizObjectId));
47
+ rows.push(...hit);
48
+ if (hit.length) {
49
+ found = true;
50
+ break;
51
+ }
52
+ }
53
+ data = { totalElements: rows.length, totalPages: scannedPages };
54
+ rowFilter = { bizObjectId: args.bizObjectId, matched: found, scannedPages, scannedElements };
55
+ }
56
+ const logs = await Promise.all(rows.map(async (row, index) => {
57
+ const log = { ruleCode: row.code, name: row.name, triggerType: row.triggerType, status: row.status, triggerTime: row.triggerTime, finishTime: row.finishTime, ...(row.errMsg ? { errMsg: row.errMsg } : {}), ruleVersion: row.ruleVersion, traceId: row.traceId, triggerRows: Array.isArray(row.inputSummaries) ? row.inputSummaries.map((v) => ({ id: v.id, name: v.name })) : [] };
58
+ if (args.includeNodes === true && index < 3) {
59
+ const nodeData = (await wireCall(session, { path: path(ENDPOINTS.ruleActivityLog, args.schemaCode), method: "POST", body: { ruleLogId: row.id } })).data;
60
+ log.nodes = Array.isArray(nodeData) ? nodeData.map((node) => ({ type: node.type, name: node.name, status: node.status, ...(node.targetSchemaCode ? { targetSchemaCode: node.targetSchemaCode } : {}), ...(node.errMsg ? { errMsg: node.errMsg } : {}) })) : [];
61
+ }
62
+ return log;
63
+ }));
64
+ return { index: { schemaCode: args.schemaCode, page: requestedPage, size: requestedSize, totalElements: data.totalElements ?? logs.length, totalPages: data.totalPages ?? 0, ...(rowFilter ? { rowFilter } : {}) }, logs, notes: ["status=true 表示成功,status=false 表示失败", ...notes, ...(args.includeNodes === true ? ["节点 VO 不提供 targetBizObjectId,节点行级 id 不可得;仅前 3 条命中日志拉取节点链"] : [])] };
65
+ }, { target: `rulelog.search:${args.schemaCode}:${JSON.stringify(args)}` }),
66
+ }));
67
+ }
68
+ //# sourceMappingURL=rulelog.js.map
@@ -779,6 +779,55 @@ export function approvalByOriginatorWarnings(template) {
779
779
  message: `所有参与人节点(${activityCodes.join("、")})都指向 {Originator}:该流程会由发起人自审自批;这是允许的单人流程,但请确认该行为符合业务意图。`,
780
780
  }];
781
781
  }
782
+ /**
783
+ * The runtime deliberately treats an empty role resolution as a successful
784
+ * activation owned by the originator. Keep this advisory: the originator
785
+ * may legitimately be a member of the configured role. The platform's
786
+ * participant chain is `ouScope ∩ originator department chain`; when that
787
+ * intersection is empty it silently takes this fallback path.
788
+ */
789
+ export function participantFallbackWarnings(input) {
790
+ if (!input.originator)
791
+ return [];
792
+ const configured = new Map();
793
+ const rows = Array.isArray(input.template?.activities) ? input.template.activities : [];
794
+ for (const row of rows) {
795
+ if (String(row.activityType ?? "") !== "PARTICIPANT")
796
+ continue;
797
+ const code = String(row.activityCode ?? row.code ?? "");
798
+ const participant = typeof row.participant === "string" ? row.participant.trim() : "";
799
+ if (!code || !participant || /^(?:\{Originator\}|Originator)$/i.test(participant))
800
+ continue;
801
+ let role;
802
+ const fn = /FindUserByRole\s*\(\s*[^,]+,\s*(["'])(.*?)\1\s*,/i.exec(participant);
803
+ if (fn?.[2])
804
+ role = fn[2];
805
+ if (!role && participant.startsWith("[")) {
806
+ try {
807
+ const entries = JSON.parse(participant);
808
+ if (Array.isArray(entries)) {
809
+ const candidate = entries.find((entry) => entry && typeof entry === "object" && String(entry.type ?? "") === "2");
810
+ if (candidate)
811
+ role = String(candidate.code ?? candidate.name ?? candidate.id ?? "") || undefined;
812
+ }
813
+ }
814
+ catch { /* advisory only */ }
815
+ }
816
+ if (role)
817
+ configured.set(code, role);
818
+ }
819
+ return input.activities
820
+ .filter((activity) => activity.status === 1 && activity.activityType === "PARTICIPANT" && activity.participantCount > 0)
821
+ .filter((activity) => activity.participants.every((person) => person.id === input.originator))
822
+ .map((activity) => ({
823
+ suspect: true,
824
+ nodeCode: activity.activityCode ?? "?",
825
+ ...(configured.get(activity.activityCode ?? "") ? { configuredRole: configured.get(activity.activityCode ?? "") } : {}),
826
+ note: configured.has(activity.activityCode ?? "")
827
+ ? "角色成员可能不覆盖发起人 OU,平台已退回发起人——核对 org.role.userScope"
828
+ : "参与者与发起人相同;若该节点配置的是角色,可能因成员 scope 不覆盖发起人 OU 而被平台退回发起人——核对 org.role.userScope",
829
+ }));
830
+ }
782
831
  /**
783
832
  * Role participants are filtered by `ouScope ∩ originator's department chain`.
784
833
  * A role whose members all have an empty scope therefore resolves to NOBODY —
@@ -3371,6 +3420,24 @@ export function registerWorkflowLane(tools) {
3371
3420
  // activity that currently holds tasks. No active activity while the
3372
3421
  // instance is still PROCESSING is exactly the silent-stall shape.
3373
3422
  const diagnosis = diagnoseInstanceActivity({ instanceStatus, endStatus: header ? wireString(header.endStatus) : null, originator: header ? wireString(header.originator) : null, activities });
3423
+ // The instance face gives the resolved participant, while the
3424
+ // published template gives the configured role. Compare both faces
3425
+ // here (and only here): data.submit wiring intentionally remains in
3426
+ // runtime-data.ts's owning batch. If the template cannot be read,
3427
+ // participantFallbackWarnings degrades to a participant==originator
3428
+ // hint rather than claiming a role mismatch.
3429
+ let publishedTemplate = null;
3430
+ const workflowCode = header ? wireString(header.workflowCode) ?? wireString(header.workflowCode_) : null;
3431
+ if (workflowCode) {
3432
+ try {
3433
+ const published = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode } });
3434
+ publishedTemplate = published.data && typeof published.data === "object" && !Array.isArray(published.data) ? published.data : null;
3435
+ }
3436
+ catch {
3437
+ publishedTemplate = null;
3438
+ }
3439
+ }
3440
+ const participantFallback = participantFallbackWarnings({ activities, template: publishedTemplate, originator: header ? wireString(header.originator) : null });
3374
3441
  return {
3375
3442
  workflowInstanceId: args.workflowInstanceId,
3376
3443
  ...(instanceStatus ? { instanceStatus } : {}),
@@ -3379,6 +3446,7 @@ export function registerWorkflowLane(tools) {
3379
3446
  activeCount: active.length,
3380
3447
  reachedCount: reached.length,
3381
3448
  activities,
3449
+ ...(participantFallback.length ? { participantFallback } : {}),
3382
3450
  diagnosis,
3383
3451
  };
3384
3452
  }),
@@ -20,6 +20,9 @@ import { argvValue } from "./args.js";
20
20
  import { runToolCall } from "./call.js";
21
21
  import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from "./cli-contract.js";
22
22
  import { writeEnvelope } from "./cli-output.js";
23
+ import { assertSafeInputPath } from "./artifacts.js";
24
+ import { readFileSync } from "node:fs";
25
+ import { resolve } from "node:path";
23
26
  /** Validate the caller's `--fields` payload (fail before any write). */
24
27
  export function validateScaffoldFields(fields) {
25
28
  const problems = [];
@@ -64,7 +67,10 @@ export async function cmdModelScaffold(args) {
64
67
  const flag = (name) => argvValue(args, name);
65
68
  const code = flag("--code") ?? flag("--schema") ?? flag("--schema-code");
66
69
  const name = flag("--name");
67
- const fieldsJson = flag("--fields") ?? flag("--fields-json");
70
+ const fieldsFile = flag("--fields-file");
71
+ const fieldsFlag = flag("--fields");
72
+ const fieldsStdin = fieldsFlag === "-";
73
+ const fieldsJson = fieldsStdin ? readFileSync(0, "utf8") : fieldsFlag ?? flag("--fields-json");
68
74
  const folderId = flag("--folder") ?? flag("--folder-id");
69
75
  const applicationCode = flag("--app");
70
76
  const environment = flag("--env") ?? flag("--environment");
@@ -72,13 +78,13 @@ export async function cmdModelScaffold(args) {
72
78
  const publishForm = !args.includes("--no-form");
73
79
  const forcePublish = args.includes("--force-publish");
74
80
  const usage = [
75
- "用法:omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON 数组>' [--folder <目录id>] [--apply]",
81
+ "用法:omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON 数组>' [--fields-file <路径>|--fields -] [--folder <目录id>] [--apply]",
76
82
  " --fields 每条是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...};子表加 \"subFields\":[...]。",
77
83
  " 子表键名统一用 subFields(field.create 平台键);childFields 仍作别名接受,但 --help 只展示 subFields。",
78
84
  " 默认 dry-run(列出将要做什么);--apply 依次执行 建模型→建字段→建子列→发布字段→发布表单。",
79
85
  " 幂等:模型/字段已存在记为 skipped 继续;有真失败时默认不发布(--force-publish 可强发)。",
80
86
  " 每个子调用使用独立 requestId(避免同 id 不同参数被幂等门拒绝);重入已存在字段记 skipped。",
81
- " 从文件读:--fields-file <配置根内路径>。",
87
+ " --fields-file 从配置根内文件读 JSON 数组;--fields - 从 stdin 读 JSON 数组。",
82
88
  ].join("\n");
83
89
  const invalid = (message) => {
84
90
  writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
@@ -88,11 +94,23 @@ export async function cmdModelScaffold(args) {
88
94
  return invalid("--code 必填");
89
95
  if (!name)
90
96
  return invalid("--name 必填");
91
- if (fieldsJson === undefined)
92
- return invalid("--fields 必填(JSON 数组)");
97
+ if (fieldsFile !== undefined && (fieldsFlag !== undefined || flag("--fields-json") !== undefined))
98
+ return invalid("--fields-file 不能与 --fields/--fields-json 同时使用");
99
+ let inputJson = fieldsJson;
100
+ if (fieldsFile !== undefined) {
101
+ try {
102
+ const root = resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
103
+ inputJson = readFileSync(assertSafeInputPath(root, fieldsFile, "--fields-file"), "utf8");
104
+ }
105
+ catch (error) {
106
+ return invalid(`--fields-file 读取失败:${error instanceof Error ? error.message : String(error)}`);
107
+ }
108
+ }
109
+ if (inputJson === undefined)
110
+ return invalid("--fields 必填(JSON 数组;或使用 --fields-file/--fields -)");
93
111
  let fields;
94
112
  try {
95
- const parsed = JSON.parse(fieldsJson);
113
+ const parsed = JSON.parse(inputJson);
96
114
  if (!Array.isArray(parsed))
97
115
  throw new Error("--fields 必须是 JSON 数组");
98
116
  fields = parsed;
@@ -161,7 +179,7 @@ export async function cmdModelScaffold(args) {
161
179
  status: "dry-run",
162
180
  ...plan,
163
181
  preflight: { checked: fields.length, passed: true },
164
- note: "dry-run 只列计划;已对每个字段跑过本地形状预检(零 wire)。--apply 依次执行 建模型 → 建字段 → 建子列 → 发布字段 → 发布表单。",
182
+ note: "dry-run 只列计划;已对每个字段跑过本地形状预检(零 wire)。dry-run 只校参数与计划,不代表运行时可达/规则健康(模型可见性、规则节点运行时异常都不在探测面)。--apply 依次执行 建模型 → 建字段 → 建子列 → 发布字段 → 发布表单。",
165
183
  }, request));
166
184
  return CLI_EXIT_CODES.ok;
167
185
  }
package/dist/read.js CHANGED
@@ -59,7 +59,11 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
59
59
  // 2026-11-01 预算批 v2(r38 usage 实证):1-4KB 中结果 59 次/135KB≈37% 全内联,
60
60
  // model.inspect 17 次全部卡在旧门下。降到 2048;compact 对配置清单保形为码表
61
61
  // (CODE_LIST_KEYS),完整正文仍在 detailArtifactId。
62
- const large = jsonBytes(result) > 2048;
62
+ // r44fb: keep small row sets in the first envelope even when each row has
63
+ // useful detail (data.list items or rulelog nodes).
64
+ const smallRowSet = (Array.isArray(result.items) && result.items.length <= 10)
65
+ || (Array.isArray(result.logs) && result.logs.length <= 10);
66
+ const large = !smallRowSet && jsonBytes(result) > 2048;
63
67
  const fileName = `${tool.name.replace(/[^a-zA-Z0-9._-]/g, "_")}-${sha256(target).slice(0, 16)}.json`;
64
68
  const artifactRef = writeArtifact(ctx, fileName, detail);
65
69
  const cacheEntry = { environmentFingerprint, targetFingerprint, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
@@ -68,7 +72,7 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
68
72
  // Small raw bodies keep the old inline behaviour; large ones are dieted
69
73
  // (see FULL_INLINE_CEILING_BYTES). The raw body stays fully reachable via
70
74
  // the artifact, so no capability is lost — only the inline copy is skipped.
71
- if (rawBytes <= FULL_INLINE_CEILING_BYTES) {
75
+ if (rawBytes <= FULL_INLINE_CEILING_BYTES || smallRowSet || rawBytes <= 32768) {
72
76
  ctx.readbackCache.set(target, cacheEntry);
73
77
  return { ...detail, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
74
78
  }
@@ -357,7 +357,7 @@ const externalLookupModel = Object.freeze({
357
357
  schemaCode: "<映射模型code>",
358
358
  queryCode: "<映射模型code>",
359
359
  displayField: "<映射模型显示字段>",
360
- mappings: [{ source: "<映射模型.业务字段>", target: "<本模型.落点字段>" }],
360
+ mappings: [{ source: "<目标模型字段>", target: "<本模型字段>" }],
361
361
  },
362
362
  },
363
363
  preconditions: [
@@ -370,7 +370,7 @@ const externalLookupModel = Object.freeze({
370
370
  "下拉框形态在 form.draft 控件层配置;映射表要有数据下拉才有选项。",
371
371
  ],
372
372
  priorToolReferences: [
373
- { tool: "model.create", args: { environment }, purpose: "建映射模型(字段与接口返回一一对应)" },
373
+ { tool: "model.create", args: { environment }, purpose: "建映射模型(字段与接口返回一一对应;mappings 的 source/target 只填裸字段 code,不带模型前缀)" },
374
374
  { tool: "rule.save", args: { environment }, purpose: "数据段规则(三选一,见 conditionalRequirements)" },
375
375
  ],
376
376
  dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "校验 WORK_SHEET options 与 mappings 形状" },
@@ -27,6 +27,19 @@ const onlinejsHighlightScript = Object.freeze({
27
27
  requiresEnvironmentDerivedIds: false,
28
28
  });
29
29
  export const ONLINEJS_METADATA = Object.freeze({
30
- "onlinejs.compose": Object.freeze({ recipes: [onlinejsHighlightScript], relatedTools: ["form.get", "form.draft", "form.publish"] }),
30
+ "onlinejs.compose": Object.freeze({ recipes: [onlinejsHighlightScript, Object.freeze({
31
+ id: "current-user-dept",
32
+ title: "读取当前用户与部门成员(门户已实证请求)",
33
+ status: "offline-valid",
34
+ tool: "onlinejs.compose",
35
+ args: { environment, schemaCode: model, sheetCode: "<默认表单 code == 模型 code>", replace: true, source: "(function(form){ h3form.axios.get(config.apiHost + '/api/organization/user/info', {params:{id:'<userId>'}}).then(function(res){ var dept = {id:res.data.departmentId, name:res.data.departmentName}; return h3form.axios.get(config.apiHost + '/api/organization/department/users', {params:{deptId:dept.id,page:0,size:300,filterType:'admin'}}); }).then(function(res){ var members = res.data.content || []; }); })" },
36
+ preconditions: ["门户取证:GET /api/organization/user/info 回 data.departmentId/departmentName;GET /api/organization/department/users 回 data.content,成员含 id/name"],
37
+ conditionalRequirements: ["这是已实证的门户请求形状;userId、schemaCode、字段使用前替换为真实值", "脚本含引号/较长时用 --args-file"],
38
+ refusalRecovery: [{ code: "onlinejs-source-invalid", action: "完整外壳改 `replace:true`(六元素模板保留,仅整体替换 customScript)" }],
39
+ priorToolReferences: [{ tool: "form.get", args: { environment, schemaCode: model }, purpose: "确认表单已发布与控件编码" }],
40
+ dryRun: { supported: true, command: `omc call onlinejs.compose --env ${environment} --dry-run --args-file compose.json`, notes: "dry-run 校验外壳/合并模式,不写平台" },
41
+ expectedReadback: { readback: { deliveryState: "DELIVERED", published: true, scriptElementPresent: true } },
42
+ requiresEnvironmentDerivedIds: false,
43
+ })], relatedTools: ["form.get", "form.draft", "form.publish"] }),
31
44
  });
32
45
  //# sourceMappingURL=online-js.js.map
package/dist/residue.js CHANGED
@@ -108,10 +108,14 @@ export async function scanResidues(session, prefix, options = {}) {
108
108
  const hasModelResidue = modelFace.some((item) => item.count > 0);
109
109
  const hasOtherResidue = faces.some((item) => item.face !== "model" && item.count > 0);
110
110
  const pendingCascade = !hasModelResidue && hasOtherResidue;
111
+ const nonZeroFaces = faces.filter((item) => item.count > 0);
112
+ const compact = options.allFaces === true ? faces : nonZeroFaces;
111
113
  return {
112
114
  prefix,
113
- faces,
115
+ faces: compact,
116
+ ...(options.allFaces === true ? {} : { byFace: compact }),
114
117
  totalResidues,
118
+ userResidues,
115
119
  clean: userResidues === 0 && !pendingCascade,
116
120
  ...(systemPresetCount > 0 ? { systemPresetCount } : {}),
117
121
  ...(pendingCascade ? { pendingCascade: true } : {}),
@@ -124,7 +128,7 @@ export async function cmdResidue(prefix, options, transport) {
124
128
  try {
125
129
  const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
126
130
  const session = await context.sessions.session(options.env);
127
- return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true }), transport);
131
+ return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true, allFaces: options.allFaces }), transport);
128
132
  }
129
133
  catch (error) {
130
134
  return emitResult({ status: "error", blocker: "residue-scan-failed", message: error instanceof Error ? error.message : String(error) }, transport);
@@ -968,6 +968,10 @@ export async function executeRuleSaveWrite(session, args) {
968
968
  ? "保存≠生效:本次只写了 DRAFT。规则虽 enabled:true,运行时仍跑上一版已发布图——请再 rule.enable(内部 publish + 翻 DRAFT 启用)或 rule.save 传 publish:true 后再 rule.enable,用 data.submit/data.load 核对新图副作用"
969
969
  : "保存≠生效:本次只写了 DRAFT。rule.enable(businessrule/publish 出新版本 + 翻 DRAFT 启用开关)后规则才会分发;或 rule.save 传 publish:true 只发布(启用仍走 rule.enable)";
970
970
  const currentRow = async () => wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data).find((item) => item.code === args.ruleCode);
971
+ // Keep warnings tied to the submitted graph; old node-specific warnings are
972
+ // stale inventory noise after an upsert replaces the graph.
973
+ const graphNodeCodes = new Set(nodes.map((node) => String(node.nodeCode ?? "")).filter(Boolean));
974
+ const currentLintWarnings = lintWarnings.filter((warning) => warning.nodeCode === undefined || graphNodeCodes.has(warning.nodeCode) || warning.code === "rule-graph-system-node-tool-owned");
971
975
  if (args.publish === true) {
972
976
  const row = await currentRow();
973
977
  if (row?.id) {
@@ -1003,6 +1007,9 @@ export async function executeRuleSaveWrite(session, args) {
1003
1007
  // hardcoded true made a draft rule look live and a workflow end-event
1004
1008
  // binding on it was silently dead. Liveness itself is `rule.enable`.
1005
1009
  const published = effective?.status === "ENABLED" || effective?.latestPublished === true || effective?.templateType === "PUBLISH";
1010
+ const effectiveNow = published && enabled;
1011
+ const effectiveBasis = effectiveNow ? "本次图已发布且规则 enabled:true,当前运行面可用" : "本次图未同时满足 publish + enabled;仍需 rule.enable 才是当前运行图";
1012
+ const previousRuleVersion = existing?.ruleVersion;
1006
1013
  return {
1007
1014
  performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", publishRequested: true, ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
1008
1015
  readback: {
@@ -1010,7 +1017,7 @@ export async function executeRuleSaveWrite(session, args) {
1010
1017
  published,
1011
1018
  enabled,
1012
1019
  status: effective?.status,
1013
- ...(lintWarnings.length > 0 ? { lintWarnings } : {}),
1020
+ ...(currentLintWarnings.length > 0 ? { lintWarnings: currentLintWarnings } : {}),
1014
1021
  ...(lintHazards.length > 0 ? { lintHazards } : {}),
1015
1022
  customTrigger: customTriggerApplied,
1016
1023
  // publish only mints the PUBLISH row; the runtime gate
@@ -1024,6 +1031,9 @@ export async function executeRuleSaveWrite(session, args) {
1024
1031
  ...(customTriggerApplied ? {} : { customTriggerHint: "图中没有 CUSTOM_NODE 触发节点:门户按钮/流程按钮调用该规则时会按各自的触发面执行;rule.run 也能执行(它不要求 CUSTOM_NODE),执行时以 BUTTON 触发类型跑同一张图" }),
1025
1032
  id: effective?.id ?? undefined,
1026
1033
  ruleVersion: effective?.ruleVersion,
1034
+ previousRuleVersion,
1035
+ effectiveNow,
1036
+ effectiveBasis,
1027
1037
  latestPublished: effective?.latestPublished,
1028
1038
  ...(existing?.id ? { versionNote } : {}),
1029
1039
  ...(enabled ? {} : { nextAction: "rule.enable(businessrule/publish + businessrule/enable 两步)后规则才会分发并对流程/按钮可见;publish:true 只写入版本" }),
@@ -1031,6 +1041,7 @@ export async function executeRuleSaveWrite(session, args) {
1031
1041
  };
1032
1042
  }
1033
1043
  const effective = await currentRow();
1044
+ const previousRuleVersion = existing?.ruleVersion;
1034
1045
  return {
1035
1046
  performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
1036
1047
  readback: {
@@ -1042,12 +1053,15 @@ export async function executeRuleSaveWrite(session, args) {
1042
1053
  effective: false,
1043
1054
  requiresPublishAction: "rule.enable",
1044
1055
  ...(existing?.defaultRule === true || PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode) ? { presetRule: true } : {}),
1045
- ...(lintWarnings.length > 0 ? { lintWarnings } : {}),
1056
+ ...(currentLintWarnings.length > 0 ? { lintWarnings: currentLintWarnings } : {}),
1046
1057
  ...(lintHazards.length > 0 ? { lintHazards } : {}),
1047
1058
  customTrigger: customTriggerApplied,
1048
1059
  ...(customTriggerApplied ? {} : { customTriggerHint: "图中没有 CUSTOM_NODE 触发节点:门户按钮/流程按钮调用该规则时会按各自的触发面执行;rule.run 也能执行(它不要求 CUSTOM_NODE),执行时以 BUTTON 触发类型跑同一张图" }),
1049
1060
  id: effective?.id ?? undefined,
1050
1061
  ruleVersion: effective?.ruleVersion,
1062
+ previousRuleVersion,
1063
+ effectiveNow: false,
1064
+ effectiveBasis: "本次只写 DRAFT,未发布;rule.enable(或 publish:true 后再 enable)才会让本次图成为当前运行图",
1051
1065
  latestPublished: effective?.latestPublished,
1052
1066
  ...(existing?.id ? { versionNote } : {}),
1053
1067
  nextAction: saveNotEffectiveNextAction,
package/dist/tools.js CHANGED
@@ -13,6 +13,7 @@ import { registerFieldLane } from "./lanes/field.js";
13
13
  import { registerDataRuleLane } from "./lanes/datarule.js";
14
14
  import { registerFormLane } from "./lanes/form.js";
15
15
  import { registerBusinessRuleLane } from "./lanes/businessrule.js";
16
+ import { registerRuleLogLane } from "./lanes/rulelog.js";
16
17
  import { registerRuleLifecycleLane } from "./lanes/rule-lifecycle.js";
17
18
  import { registerRuntimeDataLane } from "./lanes/runtime-data.js";
18
19
  import { registerWorkflowLane } from "./lanes/workflow.js";
@@ -57,6 +58,7 @@ export function buildToolRegistry() {
57
58
  registerDataRuleLane(tools);
58
59
  registerFormLane(tools);
59
60
  registerBusinessRuleLane(tools);
61
+ registerRuleLogLane(tools);
60
62
  registerRuleLifecycleLane(tools);
61
63
  registerRuntimeDataLane(tools);
62
64
  registerWorkflowLane(tools);
package/dist/write.js CHANGED
@@ -93,6 +93,17 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEs
93
93
  if (error instanceof ToolRefusalError)
94
94
  throw error;
95
95
  if (error instanceof WireError) {
96
+ // F3: rule-node runtime failures are masked by the platform as 300004.
97
+ if (error.errcode === 300004) {
98
+ return {
99
+ status: "refused",
100
+ blocker: `wire-${error.kind}`,
101
+ errcode: 300004,
102
+ message: `${error.message}——若 modelPresent:true 且 form 已发布,这个 300004 大概率是业务规则节点运行时异常,不是模型缺失;先查 rulelog 的 EXCEPTION/ROLLBACK,再看最近改动的规则。`,
103
+ nextCommand: "omc help omc-refusals",
104
+ diagnostic: "omc call rulelog.search --args '{\"schemaCode\":\"<model>\",\"includeNodes\":true}';再用 rule.get 检查最近改动规则,隔离法是暂时删/禁规则后重试",
105
+ };
106
+ }
96
107
  // authine-orm Serializer.update/delete throws "Object <id> is not
97
108
  // serialized." when the entity handle was never loaded from the store.
98
109
  // That is a PLATFORM object-state error, not a shape error — reported as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.6.23",
3
+ "version": "0.6.25",
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.23",
58
+ "omc-core": "0.6.25",
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.23"
76
+ "@stalfh233/omc-fake-platform": "0.6.25"
77
77
  }
78
78
  }
@@ -31,8 +31,8 @@ only the section for the capability you need —
31
31
  | 字段 `field` | 字段族 + 子表 + **计算字段** | 字段、属性、子表、明细、下拉、单选、多选、附件、人员、部门、日期、数值、关联、**公式/计算/求和/汇总/合计/回主表** | [字段](#字段-field) |
32
32
  | 默认表单 `form` | 表单布局与发布 | 表单、页面、布局、控件、只读、显示 | [默认表单](#默认表单-form) |
33
33
  | 列表视图 `listview` | 列表表头与列 | 列表、视图、查询列、筛选、表头、甘特 | [列表视图](#列表视图-listview) |
34
- | 字段级规则 `datarule` | 字段级控件规则 | 字段级规则、控件规则、校验规则、计算拼接、显示规则、条件必填/只读/显示 | [字段级规则](#字段级规则-datarule) |
35
- | 业务规则 `businessrule` | 图形规则图 + **定时任务** | 业务规则、触发器、规则图、事件、BRANCH、BIZ_ACTION、**定时任务/定时/每天/周期/调度** | [业务规则](#业务规则-businessrule) |
34
+ | 字段级引擎规则(非表单控件) `datarule` | 字段级引擎读面 | 字段级规则、校验规则、计算拼接、显示规则、条件必填/只读/显示 | [字段级规则](#字段级规则-datarule) |
35
+ | 业务规则 `businessrule` | 图形规则图 + **定时任务** + 审计 | 业务规则、触发器、规则图、事件、BRANCH、BIZ_ACTION、**定时任务/定时/每天/周期/调度**、触发日志审计 | [业务规则](#业务规则-businessrule) |
36
36
  | 规则生命周期 `rule-lifecycle` | 规则启停删 | 启用规则、停用规则、删除规则、规则启停 | [规则生命周期](#规则生命周期-rule-lifecycle) |
37
37
  | 流程模板 `workflow` | 审批流程模板 | 流程、工作流、审批、节点、待办、流转、参与人、审批人、表单操作按钮 | [流程模板](#流程模板-workflow) |
38
38
  | 表单在线 JS `onlinejs` | 表单脚本发布/回读 | 在线js、脚本、联动、onload、onvalidate | [表单在线 JS](#表单在线-js-onlinejs) |
@@ -121,6 +121,8 @@ only the section for the capability you need —
121
121
 
122
122
  ## 字段级规则 `datarule`
123
123
 
124
+ > **字段级引擎规则(非表单控件)**:表单条件必填/显示/只读走 `field.update options`;本面是设计器引擎读面。
125
+
124
126
  - **能做哪些事**:字段级数据规则(bizdatarule)create/update/delete;条件必填(type4)、条件只读(type5)、条件显示(type3)、计算拼接(type2)等,按平台校验器闭环。
125
127
  - **工具**:`datarule.list`、`datarule.create`、`datarule.update`、`datarule.delete`
126
128
  - **recipe**:
@@ -132,7 +134,7 @@ only the section for the capability you need —
132
134
  ## 业务规则 `businessrule`
133
135
 
134
136
  - **能做哪些事**:图形规则图(nodeGraph)——数据事件(Create/Update/Delete)触发、按钮/流程触发、动作节点(UPDATE/CREATE/DELETE/QUERY/ASSIGN/MESSAGE/BIZ_ACTION/GET_LIST…)、BRANCH 条件分支、**定时任务 TIMING_TASK**(每天/周期,SCHEDULER 节点)。
135
- - **工具**:`rule.list`、`rule.get`、`rule.references`(跨规则引用检索:谁引用了服务/模型/字段)、`rule.save`、`rule.node.update`
137
+ - **工具**:`rule.list`、`rule.get`、`rule.references`(跨规则引用检索:谁引用了服务/模型/字段)、`rule.save`、`rule.node.update`、`rulelog.search`(**规则执行日志审计入口**:规则触发没触发/哪行/节点链——data.update 后查 F31)
136
138
  - **recipe**:
137
139
  - `omc describe rule.save --example standard-business-rule-graph`
138
140
  - `omc describe rule.save --example business-rule-branch-conditional`
@@ -160,6 +162,7 @@ only the section for the capability you need —
160
162
 
161
163
  - **能做哪些事**:表单在线 JS 的 compose 六元素骨架、`draftHtmlJson.customScript`、发布与 SHA-256 回读;**读现存脚本**(`onlinejs.get` 抽 customScript innerHTML 原文+SHA-256,不必拉 form.get --full 六份 JSON)(脚本**编写**不在 CLI 范围,遵循项目在线 JS 规范或向用户索取定稿 source)。
162
164
  - **工具**:`onlinejs.get`、`onlinejs.compose`
165
+ - **常用运行时 API(已实证)**:门户可用 `form.getControl(code)`、`form.on('onRendered', fn)` 与控件 `valueChange.subscribe`;当前用户/部门取证走 `h3form.axios.get(config.apiHost + '/api/organization/user/info', {params:{id:userId}})`,响应字段含 `data.departmentId`/`data.departmentName`;部门成员走 `/api/organization/department/users`,列表在 `data.content`,成员含 `id`/`name`。配方:`omc describe onlinejs.compose --example current-user-dept`。
163
166
  - **边界 / 陷阱**:仅表单在线 JS;后端在线开发(`/api/api/programming` Java 源码)是独立 `programming` lane(`programming.get/save/compile/publish`),与在线 JS 不互换。INCREMENTAL 合并对同内容重发幂等;同头注释的整文件修订会被拒(`onlinejs-revision-replace-required`),走 `onlinejs.get` 取回 → 改 → `replace:true`。
164
167
 
165
168
  ## 后端在线开发 `programming`
@@ -11,16 +11,13 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
11
11
 
12
12
  ## 采购申请(主子表 + 合计 + 条件审批 + 列表 + 按钮)
13
13
 
14
- 1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"`
15
- 2. `omc intent schema`(或直接按下方拼装器链)
16
- 3. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '[{"code":"title","name":"标题","propertyType":"SHORT_TEXT"},{"code":"amount","name":"合计","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"<主表code>_details","name":"明细","propertyType":"CHILD_TABLE","subFields":[{"code":"item","name":"品名","propertyType":"SHORT_TEXT"},{"code":"qty","name":"数量","propertyType":"NUMERICAL","options":{"scale":0}},{"code":"price","name":"单价","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"lineAmount","name":"金额","propertyType":"NUMERICAL","options":{"scale":2}}]}]' --apply`
17
- 子表 code 是全局子 schema code,必须全局唯一;不要使用 `details`、`items` 这类通用名,按 `<主表code>_details` 这类带主表前缀的形式命名,避免撞名 40000。
14
+ 1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"` → `omc intent schema`
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 必须全局唯一)
18
16
  4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
19
17
  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`。
20
18
  6. 发起权限:`omc call workflow.addPermission --args '{"workflowCode":"<code>_wf","unitId":"<deptOrRoleId>","unitType":1}'`(这是**谁能发起**,不是行级可见范围)
21
- 7. 列表:`omc call listview.configure --args '{"schemaCode":"<code>","fieldCodes":["title","amount"],"conditions":[{"propertyCode":"assetNo","filterType":"Eq","defaultValue":"<设备编号>"}]}'`
22
- 按设备编号等条件查询时用 `conditions`;形状以 `omc describe listview.configure --param conditions` 为准(至少提供 `propertyCode`,再按字段补 `filterType`/`defaultValue`)。
23
- 8. 表单按钮:`omc call listview.configure --args '{"schemaCode":"<code>","buttons":[{"name":"提交审批","useLocation":"FORM","bindAction":"FLOW"}]}'`(形状以 describe 为准)
19
+ 7. 列表:`listview.configure` 写 `fieldCodes`/`conditions`;条件形状以 `omc describe listview.configure --param conditions` 为准。
20
+ 8. 按钮:`listview.configure` 写 `buttons:[{name,useLocation:"FORM",bindAction:"FLOW"}]`。
24
21
  9. ⚠️ **行级「申请人只看自己」当前工具面不支持**(`workflow.permissions` / `org.role.setUserScope` 都不是行级数据权限)
25
22
 
26
23
  ## 请假审批(线性流)
@@ -36,6 +33,17 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
36
33
  2. `omc call listview.configure --args '{"schemaCode":"vendor","fieldCodes":["name","code"]}'`
37
34
  3. 造数:`omc describe data.submit --example plain-data-submit`
38
35
 
36
+ ## 条件表单 + 关联带出
37
+
38
+ 1. 条件必填/显示/只读写 `field.update options`(不要用 datarule);子表条件列同法,建在父字段 code 下。
39
+ 2. 关联带出映射用**裸 code**(无 `schema.` 前缀);发布链 `field.publish → form.draft rebuild:true → form.publish`,`form.get` 验收。
40
+
41
+ ## 数据事件规则(跨模型联动 + 执行日志)
42
+
43
+ 1. 联动挂预置 `Create`/`Update`(submit 触发 Create、update 触发 Update)→ `rule.enable`;`data.load` 验收目标行。
44
+ 2. 取证:`omc call rulelog.search --args '{"schemaCode":"<模型>","bizObjectId":"<行id>"}'——触发/行/节点链一次问清。
45
+
46
+
39
47
  ## 读法
40
48
 
41
49
  - 默认索引:`omc help omc-playbook`(看节摘要)
@@ -68,6 +68,13 @@ condition/expression/query shape is off. Compare against
68
68
  Escalate to 二开 **only** for a real `capability-out-of-coverage` (§2).
69
69
 
70
70
  `40000 调用数据库发生异常` is the **most opaque** shape code — no detail at all.
71
+
72
+ ### 3d. 300004 during form/submit can be a rule-node failure
73
+
74
+ Model present + form published yet submit says `300004 数据模型不存在`? Often a
75
+ rule-node exception in disguise (r44b): `rulelog.search {schemaCode,includeNodes:true}`
76
+ → `EXCEPTION`/`ROLLBACK` nodes → `rule.get` the changed rule; disable/delete it to
77
+ isolate. Never recreate a healthy model.
71
78
  Line up the model's field-level config first: an `AGGREGATION` FORMULA field whose
72
79
  `aggregation.schemaCode` points at a **child-table schema** publishes fine and then
73
80
  makes every `data.submit`/`data.update` on that model 40000 (r18 2026-10-26 live;