@stalfh233/omc-cli 0.5.10 → 0.5.12

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.
@@ -256,7 +256,7 @@ export function classifyResult(result) {
256
256
  const suffix = candidates.length > 0 ? `;did you mean: ${candidates.join("、")}` : ";用 `omc commands --json --compact --lane <lane>` 或 `omc next` 定位工具";
257
257
  return { status: "error", error: withNext({ code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
258
258
  }
259
- if (result.blocker === "tool-input-invalid")
259
+ if (result.blocker === "tool-input-invalid" || result.blocker === "tool-input-unknown-keys")
260
260
  return {
261
261
  status: "error",
262
262
  error: { ...withNext({ code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }), nextCommand: `omc describe ${String(result.tool ?? "<tool>")}` },
package/dist/cli.js CHANGED
@@ -42,7 +42,7 @@ import { absencePointer } from "./capability-absences.js";
42
42
  import { intentConfigRoot, readIntentFile, validateIntentText } from "./intent/cli.js";
43
43
  import { intentSchemaBlock, intentExample, intentExampleIds } from "./intent/schema.js";
44
44
  import { planIntent, readbackIntent, applyIntent, intentWarnings } from "./intent/plan-apply.js";
45
- import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills } from "./skills.js";
45
+ import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills, suggestHelpTopics } from "./skills.js";
46
46
  import { parseToolInput, ToolInputError } from "./tool-types.js";
47
47
  import { WireError, wireCall } from "./wire.js";
48
48
  import { DEFAULT_PULL_CONCURRENCY, GraphIndexAbsentError, GraphPullLockHeldError, MAX_PULL_CONCURRENCY, impactGraph, publishGraphPull, queryGraphReferences, readGraphStatus, searchGraphRules, } from "./graph.js";
@@ -204,7 +204,7 @@ async function cmdDoctor(store, environment, serviceCode) {
204
204
  const errcode = error instanceof WireError ? error.errcode : undefined;
205
205
  if (errcode === 109001 || /许可证|license/i.test(error.message)) {
206
206
  ok = false;
207
- steps.push(`许可证 FAIL 已过期(平台拒绝 apppackage/list:${String(errcode ?? "")})——续期或更换环境;否则几乎全部读面与所有写面都会被 109001 拒绝`);
207
+ steps.push(`许可证 FAIL 已过期(平台拒绝 apppackage/list:${String(errcode ?? "")})——续期或更换环境。注意:109001 是**分面拦截**不是全环境不可用(r40 实证:报表/集成/部分读面被拒,模型/字段/数据面仍可交付)——不要因本项 FAIL 一刀切停步,按需逐面探测并用最小写验证`);
208
208
  }
209
209
  else {
210
210
  ok = false;
@@ -1361,7 +1361,8 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
1361
1361
  if (singleSection !== undefined) {
1362
1362
  const sliced = readSkillSection(topic, singleSection);
1363
1363
  if (sliced === undefined) {
1364
- const message = `未知 topic "${topic}";可用 topic:${topics.map((entry) => entry.name).join("、")}(命令清单见 omc help commands)${commandPointer}`;
1364
+ const suggestions = suggestHelpTopics(topic);
1365
+ const message = `未知 topic "${topic}";可用 topic:${topics.map((entry) => entry.name).join("、")}(命令清单见 omc help commands)${commandPointer}${suggestions.length > 0 ? `;你可能要找:${suggestions.join("、")}` : ""}`;
1365
1366
  console.error(`help: ${message}`);
1366
1367
  return emitInvalid(transport, { message });
1367
1368
  }
@@ -1382,7 +1383,8 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
1382
1383
  }
1383
1384
  const document = readSkill(topic);
1384
1385
  if (document === undefined) {
1385
- const message = `未知 topic "${topic}";可用 topic:${topics.map((entry) => entry.name).join("、")}(命令清单见 omc help commands)${commandPointer}`;
1386
+ const suggestions = suggestHelpTopics(topic);
1387
+ const message = `未知 topic "${topic}";可用 topic:${topics.map((entry) => entry.name).join("、")}(命令清单见 omc help commands)${commandPointer}${suggestions.length > 0 ? `;你可能要找:${suggestions.join("、")}` : ""}`;
1386
1388
  console.error(`help: ${message}`);
1387
1389
  return emitInvalid(transport, { message });
1388
1390
  }
@@ -1491,6 +1491,32 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
1491
1491
  const relaxEnums = options.relaxEnums === true;
1492
1492
  if (nodeRecords.length !== nodes.length)
1493
1493
  blockers.push({ code: "rule-graph-node-not-object", message: "nodes 中存在非对象元素" });
1494
+ // r42 头号返工根因(维护者同踩):filterCondition 的目标查找条件键是
1495
+ // mainConditions(FilterCondition 模型,List<List<TableCondition>>),不是
1496
+ // 触发条件/BRANCH 出口的 conditionGroups——错键被平台静默反序列化为空,
1497
+ // 引擎零循环恒报「查找范围不存在数据」,形似"跨模型 UPDATE 不可用"(曾据
1498
+ // 此误判平台边界,反编译 FilterCondition.mainConditions + mainConditions 正形
1499
+ // 真机回写成功双重实证后纠正)。fail closed:错键当场拒绝。
1500
+ for (const node of nodeRecords) {
1501
+ const nodeType = String(node.nodeType ?? "");
1502
+ if (nodeType !== "UPDATE" && nodeType !== "DELETE" && nodeType !== "CREATE" && nodeType !== "ASSIGN" && nodeType !== "QUERY")
1503
+ continue;
1504
+ const fc = node.filterCondition;
1505
+ if (fc === undefined || fc === null || typeof fc !== "object" || Array.isArray(fc))
1506
+ continue;
1507
+ const record = fc;
1508
+ if ("mainConditions" in record)
1509
+ continue;
1510
+ const wrongKeys = ["conditionGroups", "conditions", "mainTableConditions", "filterGroups", "targetConditionGroups"].filter((key) => key in record);
1511
+ if (wrongKeys.length > 0 || Object.keys(record).length === 0) {
1512
+ blockers.push({
1513
+ code: "rule-graph-filter-condition-wrong-key",
1514
+ nodeCode: String(node.nodeCode ?? ""),
1515
+ key: "filterCondition",
1516
+ message: `${nodeType} "${String(node.nodeCode ?? "")}" 的 filterCondition 键写错(${wrongKeys.join("、") || "空对象"})——目标查找条件用 mainConditions: [[{targetSchemaDataItem, ruleConditionType, ruleValueType, value|currentSchemaDataItem}]];conditionGroups 是触发条件/BRANCH 出口的键,放这里会被平台静默忽略 → 引擎恒报「查找范围不存在数据」(形似跨模型回写不可用)。形状见 omc describe rule.save --example business-rule-branch-conditional`,
1517
+ });
1518
+ }
1519
+ }
1494
1520
  // Cross-model write-back: SUPPORTED (verified live 2026-09-28). An UPDATE whose
1495
1521
  // `targetObjectCode` is a DIFFERENT model than the rule's own model writes back
1496
1522
  // correctly once TWO prerequisites hold — a 2026-09-27 probe that reported
@@ -1423,6 +1423,23 @@ async function refuseRequiredOnChildTableFields(session, schemaCode, template, t
1423
1423
  * same edge). This is the "read back, then send the delta" path — without it a
1424
1424
  * subset silently REPLACED the whole activity list and dropped nodes.
1425
1425
  */
1426
+ function refusePartialBtnConfigList(activities) {
1427
+ // r32 实证(backlog 清算批):btnConfigList 局部行=整行替换——只传 {code, remark}
1428
+ // 会把 enabled 静默置 false、commentPermission 置 null(驳回意见必填丢失)。
1429
+ // fail-closed:按钮行不带显式 enabled 一律拒绝,指路 hazards 的整行回写法。
1430
+ for (const activity of activities) {
1431
+ const rows = activity.btnConfigList;
1432
+ if (!Array.isArray(rows))
1433
+ continue;
1434
+ const partial = rows.filter((row) => row !== null && typeof row === "object" && !("enabled" in row));
1435
+ if (partial.length > 0)
1436
+ throw new ToolRefusalError({
1437
+ status: "refused",
1438
+ blocker: "workflow-btnconfig-partial-row",
1439
+ message: `workflow.update: 节点 ${String(activity.activityCode ?? activity.id ?? "")} 的 btnConfigList 存在不带 enabled 的按钮行(${partial.map((row) => String(row.code ?? "?")).join("、")})——该平台按**整行替换**处理,缺省键会被静默重置(enabled→false、commentPermission→null)。先 workflow.get_published/get_draft 取该按钮整行,改目标属性后整条回写(见 omc help omc-write-hazards --section 按钮)。`,
1440
+ });
1441
+ }
1442
+ }
1426
1443
  function mergeCallerDelta(template, draft, options = {}) {
1427
1444
  const { routes: routesRaw, rules: rulesRaw, activities: _raw, edges: edgesRaw, ...rest } = template;
1428
1445
  // `routes` and `edges` are the recipe spellings; `rules` is the platform DTO
@@ -1486,6 +1503,7 @@ function mergeCallerDelta(template, draft, options = {}) {
1486
1503
  return { ...activity, ...override };
1487
1504
  });
1488
1505
  const newActivities = [...callerByOriginal.values()];
1506
+ refusePartialBtnConfigList([...mergedActivities, ...newActivities]);
1489
1507
  // A caller-only code that arrived WITHOUT a type has no draft row to inherit
1490
1508
  // from — refuse it here (the lint allows type-less permission-shape rows
1491
1509
  // precisely because they are expected to match an existing node).
@@ -1595,6 +1613,8 @@ function normalizeWorkflowTemplate(template, draft, options = {}) {
1595
1613
  // comment); give every changed/new node a fresh code, and remap caller routes
1596
1614
  // so they follow.
1597
1615
  const reconciled = rawCaller ? reconcileCallerActivityCodes(rawCaller, draftActivities, options) : undefined;
1616
+ if (rawCaller !== undefined)
1617
+ refusePartialBtnConfigList(rawCaller);
1598
1618
  const activities = reconciled?.activities;
1599
1619
  const codeMap = reconciled?.codeMap ?? new Map();
1600
1620
  const remapCode = (value) => codeMap.get(String(value ?? "")) ?? value;
package/dist/skills.js CHANGED
@@ -48,6 +48,57 @@ export function listSkills() {
48
48
  return { name, description };
49
49
  });
50
50
  }
51
+ /**
52
+ * r43 后 backlog:`omc help 编号` 曾零命中(高频中文业务词不是 topic 名)。
53
+ * 双路建议:①精选同义词表(高频业务词→topic,r24–r43 探针实测检索失败样本)
54
+ * ②全 topic 节标题子串扫描(自维护——节名改了建议跟着变)。
55
+ */
56
+ const HELP_TOPIC_SYNONYMS = Object.freeze({
57
+ 编号: ["omc-capabilities"],
58
+ 序列号: ["omc-capabilities"],
59
+ 单号: ["omc-capabilities"],
60
+ 流水号: ["omc-capabilities"],
61
+ 自动编号: ["omc-capabilities"],
62
+ 审批: ["omc-five-piece-flow"],
63
+ 流程: ["omc-five-piece-flow"],
64
+ 回写: ["omc-business-configuration"],
65
+ 跨模型: ["omc-business-configuration"],
66
+ 规则: ["omc-business-configuration"],
67
+ 字典: ["omc-business-configuration"],
68
+ 权限: ["omc-business-configuration"],
69
+ 视图: ["omc-write-hazards"],
70
+ 过滤: ["omc-write-hazards"],
71
+ 条件: ["omc-write-hazards"],
72
+ 删除: ["omc-write-hazards"],
73
+ 清理: ["omc-write-hazards"],
74
+ 子表: ["omc-write-hazards"],
75
+ 表单: ["omc-write-hazards"],
76
+ 验收: ["omc-acceptance-criteria"],
77
+ 入门: ["omc-start-here"],
78
+ 拒绝: ["omc-refusals"],
79
+ });
80
+ export function suggestHelpTopics(query) {
81
+ if (query === undefined || query.length === 0)
82
+ return [];
83
+ const hits = new Set(HELP_TOPIC_SYNONYMS[query] ?? []);
84
+ for (const skill of listSkills()) {
85
+ if (skill.name.includes(query) || (skill.description ?? "").includes(query))
86
+ hits.add(skill.name);
87
+ }
88
+ // 节标题扫描:`omc help <topic> --section "<heading>"` 直达
89
+ for (const name of listSkills().map((skill) => skill.name)) {
90
+ const outline = readSkillOutline(name);
91
+ if (outline === undefined)
92
+ continue;
93
+ for (const section of outline.sections) {
94
+ if (section.heading.includes(query)) {
95
+ hits.add(`omc help ${name} --section "${section.heading}"`);
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ return [...hits].slice(0, 5);
101
+ }
51
102
  /** Read one methodology topic for `omc help <topic>`. */
52
103
  export function readSkill(name) {
53
104
  if (!/^[a-z0-9-]+$/.test(name))
@@ -91,6 +91,9 @@ function applyInputAliases(tool, input) {
91
91
  continue;
92
92
  normalized ??= { ...input };
93
93
  normalized[canonical] = input[hit];
94
+ // 同义词键就地删除:归一后的输入只留规范键——写面未知键 fail-closed
95
+ // (r43 后)不把已消费的同义词误报成未知键。
96
+ delete normalized[hit];
94
97
  }
95
98
  return normalized ?? input;
96
99
  }
@@ -111,8 +114,53 @@ function keyRenameHint(issuePath, input, tool) {
111
114
  // "你传的是 schemaCode", which pointed at the wrong key.
112
115
  return `(该对象在本工具里的键是 ${key},你传的是 ${synonyms.join("/")};键名对照见 omc-tool-selection 的 Key-name table)`;
113
116
  }
117
+ /** Cheap closest-key suggestion: exact→prefix→shared-start bigrams, threshold keeps it honest. */
118
+ function closestSchemaKey(key, schemaKeys) {
119
+ if (schemaKeys.has(key))
120
+ return key;
121
+ let best;
122
+ let bestScore = 0;
123
+ for (const candidate of schemaKeys) {
124
+ const lower = key.toLowerCase();
125
+ const candLower = candidate.toLowerCase();
126
+ let score = 0;
127
+ if (candLower.startsWith(lower) || lower.startsWith(candLower))
128
+ score = 0.9;
129
+ else if (candLower.includes(lower) || lower.includes(candLower))
130
+ score = 0.7;
131
+ else {
132
+ let shared = 0;
133
+ for (let i = 0; i + 2 <= lower.length; i++)
134
+ if (candLower.includes(lower.slice(i, i + 2)))
135
+ shared++;
136
+ score = lower.length > 2 ? shared / (lower.length - 1) : 0;
137
+ }
138
+ if (score > bestScore) {
139
+ bestScore = score;
140
+ best = candidate;
141
+ }
142
+ }
143
+ return bestScore >= 0.4 ? best : undefined;
144
+ }
114
145
  export function parseToolInput(tool, input) {
115
146
  const normalized = applyInputAliases(tool, input);
147
+ // r30 引出、r42/r43 再证:静默丢弃错键是头号返工源(dictionary.save items
148
+ // 未知键、跨模型 filterCondition 错键同族)。写面 fail-closed——未知顶层键
149
+ // 当场拒绝并给最接近的合法键提示;读面保持宽松(探查性调用常见无害附加)。
150
+ if (tool.kind === "write") {
151
+ const schemaKeys = new Set(Object.keys(tool.inputSchema));
152
+ const unknown = Object.keys(normalized).filter((key) => !schemaKeys.has(key));
153
+ if (unknown.length > 0) {
154
+ const suggestions = unknown.map((key) => `${key}→${closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
155
+ throw new ToolInputError({
156
+ status: "error",
157
+ blocker: "tool-input-unknown-keys",
158
+ tool: tool.name,
159
+ message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。最接近的合法键:${suggestions.join(";")}。参数表:omc describe ${tool.name}`,
160
+ issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: `unknown key; closest: ${closestSchemaKey(key, schemaKeys) ?? "none"}` })),
161
+ });
162
+ }
163
+ }
116
164
  const result = z.object(tool.inputSchema).safeParse(normalized);
117
165
  if (result.success)
118
166
  return result.data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,9 +52,9 @@
52
52
  "access": "public"
53
53
  },
54
54
  "dependencies": {
55
- "@stalfh233/omc-fake-platform": "0.5.10",
55
+ "@stalfh233/omc-fake-platform": "0.5.12",
56
56
  "commander": "14.0.3",
57
- "omc-core": "0.5.10",
57
+ "omc-core": "0.5.12",
58
58
  "proper-lockfile": "4.1.2",
59
59
  "write-file-atomic": "6.0.0",
60
60
  "zod": "^3.23.8",
@@ -122,6 +122,7 @@ rules:
122
122
 
123
123
  - **每个 filter 项必须有且只有一个条件来源**:`from`(动态 EQ)或一个比较键(`equals`/`notEquals`/`gt`/`gte`/`lt`/`lte` + 标量)。缺来源 `intent-rule-leaf-source-missing` 拒绝;**绝不**静默成 `EQ null`。
124
124
  - 动作节点的条件分「定位数据」与「执行门」两类,**别混**(详见下「规则图的触发条件」)。
125
+ - **跨模型 UPDATE 目标查找键=`filterCondition.mainConditions`**(`[[{targetSchemaDataItem, ruleConditionType, ruleValueType, value|currentSchemaDataItem}]]`,非触发条件的 conditionGroups——错键静默反序列化为空,引擎恒报「查找范围不存在数据」形似跨模型不可用,r42 曾误判;校验 `rule-graph-filter-condition-wrong-key` 已拦)。连坑:叶取值键 `value` 非 fixedValue;`rule.node.update` 只写 DRAFT,改完须 `rule.enable`/`--publish` 重发布才生效。
125
126
  - **跨模型写(`targetObjectCode`≠规则模型)平台支持、可运行时生效**(2026-09-28 真机复核)。两个前提:① 承载它的规则必须真的触发——`Create` 规则在提交时触发;`Update` 规则只在 `data.update` 触发,**submit 不执行它**(放进 `Update` 是典型静默空转,lint 警告 `rule-graph-cross-model-update-trigger-inert`;也可绑定流程事件如 `end`)。② 目标行必须已提交——目标查询不匹配 DRAFT 行,`data.save` 未 submit 的行不回写。
126
127
  - 关联字段:`type: WORK_SHEET` + `relativeCode: <目标模型>` + `options.displayField`,与 `field.create` 行形状一致。(键名是 `relativeCode`,wire 键 `relativeSchemaCode`,别混。)
127
128
  - 节点显示名用 activity 级的 `name`(`participant.name` 只作参与者说明,平台不认作节点名;`{type: Originator, name: 部门负责人}` 会被工具提升为节点名)。
@@ -55,7 +55,7 @@ omc help omc-write-hazards
55
55
  - **BIZ_ACTION 判成功只看传输**:REST 适配器解析响应后直接返回,**不校验 `errcode`**。扩展接口回 `{"errcode":50000}` + HTTP 200 会被当成节点成功(日志 `biz action node execute successful`)。要阻断必须走表单 `onValidate` 或数据规则,不能靠业务返回码。
56
56
  - **`rule.run`/自定义规则不回写出参**(r26 实证 2 轮):出参合并只发生在预置 Update 规则的执行上下文;`rule.run` 手动触发与自定义规则不把服务出参写回本模型——出参落表要走「BIZ_ACTION 节点 outputMappings + 数据事件触发」,验收用 data.load 看字段而非 rule.run 回执。
57
57
  - **出参回写三前提(r28b 8 组实验定案,单点总纲)**:① 响应 JSON 根级平铺出参键 ② 预置 Create/Update 规则上下文 ③ data.submit/data.update 触发(data.save 不触发)。三者缺一出参恒 null 且节点日志仍 successful——外部接口回信封的(`{code,message,data:[...]}`),先落平铺整形端点(平台扩展)再配 outputMappings,不要反复换规则形状试。
58
- - **布尔出参映射到文本字段 → ClassCastException 50000**(r28b 实证):服务出参 `bizPropertyType:LOGICAL` 的值映射到 SHORT_TEXT 字段会让规则执行炸 50000——布尔出参只能映射 LOGICAL 字段;要落文本先在扩展侧转字符串。
58
+ - **布尔出参映射到文本字段 → ClassCastException 50000**(r28b):LOGICAL 出参映射 SHORT_TEXT 即炸——布尔只映射 LOGICAL 字段,落文本先在扩展侧转字符串。
59
59
  - **审批提交的 formData 带子表数组 = 追加行**(r28b 实证):approval submit 的 formData 里带子表数组会被平台当「追加行」处理,审批回执只回传主表字段——审批链路里要改子表行,用 data.update 单独写,不要塞进 formData。
60
60
  - **workItemId 不在 instanceActivity 里**(r28b 实证):流程实例活动查询不返回 workItemId;办理待办要的 workItemId 从 `search_workitems?state=PENDING` 的 `content[].id` 取。
61
61
  - **模型创建/删除非原子,会有半态残留**(r28b 实证 ~20min):model.create 在表单层撞错(如「表单已存在」)时 schema 可能已半落库——refused 后换名重试前先 model.list 核对半态并清理;同理模型删除后平台读面不可见但库里可能留僵尸行(引擎报 `appFunction not exist` 就是它在作祟),迁移/复用编码前先核对。
@@ -193,13 +193,15 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
193
193
  - **`transferDepartment` 迁人不迁作用域**(r33 真机):换部门后 `role.userScope` 仍指旧部门——迁移后复核成员作用域。
194
194
  - **`user.delete` 是软删**(r33 真机):回 `deleted:true` 但 `user.info` 仍 `exists:true`(DISABLE)——验证口径用列表/状态,别称物理清除。
195
195
  - **「名称重复 200001」≠ 失败**(r33 真机):多为首次已建成、重发才撞重复——先读回确认首建结果再决定。
196
- - **删了的东西≠删干净(孤儿三连,1101 三例)**:①删模型留收藏+h_biz_* 行→日志刷 `appFunction not exist`;②规则死服务 A1→submit 500015;③流程模板删后绑定残留(h_workflow_relative_object/header 指死 code)→getWorkflow 50000。都是删上游留下游引用、报错在别处——先查孤儿引用,维护者按表清。
197
- - **两套条件 DSL(r38/r39)**:listview.configure `conditions`={propertyCode, filterType, **defaultValue**}(值键叫 defaultValue);data.list `filter`={op, field, value}。互抄必错——形状各查 `--param conditions`/`--param filter`。
198
- - **表单读回以控件清单为准,不看布尔位(r40)**:form.get 的 `published` 布尔会 false→true 抖动;draftViewJson 可含已删字段悬空引用(rebuild 自清)。判定布局依据=`publishedControlCodes` 内容;configure_form 回执已点名 `missingFieldCodes`。
199
- - **submit 被拒仍可能已落草稿(r40)**:`data.save --submit:true` 遇许可证/权限拒绝时平台可能在拒答前已写草稿——拒后 data.list 对账 total,残留 data.delete 单清。
200
- - **空聚合≠模型存在(r40)**:rule.list/datarule.list 对不存在模型静默回空——「0 条规则」≠模型在(mcc_contract 从不存在却一路答空)。rule.list 已修(空时探真身标 `modelPresent:false`);datarule.list 同陷阱——判存在性一律 model.inspect。
201
- - **form.publish(rebuild) 后立即 submit 可能丢字段(r41 疑似竞态)**:链内演示行 `order_no` 落 null,同形状延迟复现正常。落演示行后 `data.load` 抽验非选项字段,缺值 data.save 单补,不重跑整链。
196
+ - **删了的东西≠删干净(孤儿三连,1101 三例)**:①删模型留收藏/h_biz_* 行→日志 appFunction not exist;②规则死服务 A1→submit 500015;③流程模板删后绑定残留→getWorkflow 50000。都是报错在别处——先查孤儿引用。
197
+ - **两套条件 DSL(r38/r39)**:listview `conditions`={propertyCode, filterType, defaultValue};data.list `filter`={op, field, value}。互抄必错。
198
+ - **表单读回以控件清单为准(r40)**:`published` 布尔会抖动、draftViewJson 可含悬空引用(rebuild 自清)——判定依据=publishedControlCodes 内容;configure_form 回执已点名 missingFieldCodes。
199
+ - **submit 被拒仍可能已落草稿(r40)**:许可证/权限拒绝可能拒答前已写草稿——拒后对账 total,残留单清。
200
+ - **空聚合≠模型存在(r40)**:rule.list 对不存在模型静默回空(已修,空时标 modelPresent:false);datarule.list 响亮拒 601003。判存在性用 model.inspect。
201
+ - **form.publish(rebuild) 后立即 submit 可能丢字段(r41 疑似竞态)**:链内演示行 order_no 落 null,延迟复现正常。落行后 data.load 抽验,缺值单补。
202
202
  - **data.list 选项列投影可为 null(r40/r41)**:`all:true`/queryCode 下 RADIO 读 null,`data.load`/`filter` 才有值——验证选项字段以 data.load 为权威。
203
+ - **流程绑定行不可 data.save 直改(r42)**:COMPLETED 行直改打回 PROCESSING、值不落、可能岔行——走提交流程。连续 submit 撞限流 100005——批量先 workitem.list 取 workItemId。
204
+ - **带流程模型 data.submit 必停审批位(r43)**:批量初始化滞留待办——流程加条件分支按单据类型直达结束。SEQNO 不回收。
203
205
  - **读回形状≠可写形状**(r38):`listview.get` 读回条件带 `null` 字段,原样回写被拒——读回是输出形态,可写面有自己的契约;null 归一(false/省略)或只回写要改的键。
204
206
  - **data.list 的 columns 收窄可能静默空**(r38):非视图列字段投影 null;甚至 columns+分页 total>0 而 items 0——判"无数据"先去 columns 复查或用 data.load。
205
207