@stalfh233/omc-cli 0.4.98 → 0.4.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +223 -9
- package/dist/field-families.js +27 -0
- package/dist/goal-routes.js +127 -57
- package/dist/lanes/report.js +21 -5
- package/dist/lanes/runtime-data-contract.js +1 -1
- package/dist/lanes/runtime-data.js +30 -0
- package/dist/recipes/field.js +1 -1
- package/dist/recipes/runtime-data.js +3 -1
- package/dist/refusal-guidance.js +207 -0
- package/dist/skills.js +89 -21
- package/dist/tools.js +35 -0
- package/dist/usage-log.js +21 -2
- package/dist/write.js +2 -2
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +1 -1
- package/skills/omc-start-here.md +2 -1
- package/skills/omc-write-hazards.md +17 -9
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape-family refusals: the payload was malformed in a way the CLI can see
|
|
3
|
+
* offline. Each cites the recipe that carries the correct shape.
|
|
4
|
+
*/
|
|
5
|
+
const SHAPE_GATES = Object.freeze({
|
|
6
|
+
"relation-single-array-shape": {
|
|
7
|
+
whatWentWrong: "单选关联字段(propertyType 9)传了数组",
|
|
8
|
+
correctShape: "单选关联只接受 id 字符串或 {id} 对象:{\"字段\":\"<关联行id>\"} 或 {\"字段\":{\"id\":\"<id>\"}};多选关联(11)/人员(50)/组织(51)才用数组",
|
|
9
|
+
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
10
|
+
doNot: "不要对单选关联继续传数组重试——平台稳定回 50000,不是偶发",
|
|
11
|
+
},
|
|
12
|
+
"submit-date-shape": {
|
|
13
|
+
whatWentWrong: "日期字段的值形状不对(毫秒时间戳会落库 NULL)",
|
|
14
|
+
correctShape: "字符串且匹配字段 format:日期字段 \"YYYY-MM-DD\";日期时间字段 ISO8601 带时区 \"2026-01-01T10:00:00+08:00\"",
|
|
15
|
+
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
16
|
+
doNot: "不要传毫秒数(如 1790000000000)——不报错但落库 NULL,读回才发现",
|
|
17
|
+
},
|
|
18
|
+
"submit-child-rows-missing-id": {
|
|
19
|
+
whatWentWrong: "更新已有记录时,子表行没带 id",
|
|
20
|
+
correctShape: "更新时子表行必须带平台 id(新增行才省略);无 id 的行会被当**新增**插入,不判重",
|
|
21
|
+
command: undefined,
|
|
22
|
+
recipeCommand: { tool: "data.update", recipe: "plain-data-submit" },
|
|
23
|
+
doNot: "不要用同一批无 id 子行重试 update——每次都会再插一遍(r17 实测 2 行变 8 行)",
|
|
24
|
+
},
|
|
25
|
+
"submit-approval-agree-misplaced": {
|
|
26
|
+
whatWentWrong: "agree 放在了 approval 对象里,平台只读顶层 agree",
|
|
27
|
+
correctShape: "通过/驳回用顶层参数:{\"agree\": true|false, \"approvalNote\": \"意见\"};approval 对象只是评论载体(content/relUsers)",
|
|
28
|
+
recipeCommand: { tool: "data.submit", recipe: "workflow-backed-data-submit" },
|
|
29
|
+
doNot: "approval.agree 会被静默忽略且仍按通过处理(r18b 真机:agree:false 照样 APPROVE)——原地重试必然得到同样的错误结果",
|
|
30
|
+
},
|
|
31
|
+
"submit-approval-opinion-misplaced": {
|
|
32
|
+
whatWentWrong: "opinion 放在了 approval 对象里,平台不读它",
|
|
33
|
+
correctShape: "审批意见用顶层 approvalNote;approval.content 是评论内容,不是审批意见",
|
|
34
|
+
recipeCommand: { tool: "data.submit", recipe: "workflow-backed-data-submit" },
|
|
35
|
+
doNot: "改 approval.opinion 重试无意义——字段被静默忽略",
|
|
36
|
+
},
|
|
37
|
+
"aggregation-self-schema-cycle": {
|
|
38
|
+
whatWentWrong: "聚合目标是自己的子表——平台 publishFormula 必然判环(checkCycleOfAggregation:1032),无条件 305003",
|
|
39
|
+
correctShape: "聚合 schemaCode 指向**另一个模型**的主表标量列;要子表合计先物化到来源主表再聚合",
|
|
40
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
41
|
+
doNot: "同模型子表聚合是平台死路(8.6.26 与 1.1.25 逐行相同),改 payload 无用;也不要删平台自动生成的 t100 CALCULATE_RULE 镜像行",
|
|
42
|
+
},
|
|
43
|
+
"aggregation-target-is-child-schema": {
|
|
44
|
+
whatWentWrong: "聚合指向了子表 schema",
|
|
45
|
+
correctShape: "aggregation.schemaCode 必须是来源模型主表;数据项只能是来源主表的标量列",
|
|
46
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
47
|
+
doNot: "聚合指向子表会让目标模型整个不可写(40000 无明细)且匹配恒 0——先删这个字段配置再继续",
|
|
48
|
+
},
|
|
49
|
+
"aggregation-data-item-not-scalar": {
|
|
50
|
+
whatWentWrong: "聚合数据项不是来源主表的标量列",
|
|
51
|
+
correctShape: "跨模型汇总的数据项只能是来源主表标量列;子表列先在来源模型物化成主表字段",
|
|
52
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
53
|
+
doNot: "直接把子表列写进数据项不会报配置错误——回算静默恒 0,先回来源模型物化",
|
|
54
|
+
},
|
|
55
|
+
"aggregation-large-missing-k": {
|
|
56
|
+
whatWentWrong: "LARGE/SMALL 聚合缺 k(前 N 名的 N)",
|
|
57
|
+
correctShape: "algorithmOptions 里带 k(正整数);LARGE=前 k 大,SMALL=前 k 小",
|
|
58
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
59
|
+
doNot: "缺 k 时平台抛 NumberFormatException 掩码成 50000——重试同参数无用",
|
|
60
|
+
},
|
|
61
|
+
"aggregation-union-result-type": {
|
|
62
|
+
whatWentWrong: "UNION(文本拼接)的 resultType 越界",
|
|
63
|
+
correctShape: "UNION 的 resultType 只能 0 或 1(拼接分隔行为),不是数值型枚举",
|
|
64
|
+
recipeCommand: { tool: "field.create", recipe: "formula-aggregation-cross-model" },
|
|
65
|
+
doNot: "写 2 及以上平台直接 50000——改回 0/1 再发布",
|
|
66
|
+
},
|
|
67
|
+
"rule-graph-validation-failed": {
|
|
68
|
+
whatWentWrong: "规则图形状没过写前校验",
|
|
69
|
+
correctShape: "nodeGraph:nodes(Start/动作/End,系统节点标 isSystemNode)+ routes(preNode→postNode);条件分支用 BRANCH 节点",
|
|
70
|
+
recipeCommand: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
71
|
+
},
|
|
72
|
+
"rule-graph-routes-empty": {
|
|
73
|
+
whatWentWrong: "规则图缺 routes(节点连线)",
|
|
74
|
+
correctShape: "每条 route {preNode, postNode, routeCondition:true};Start→首节点→…→End 必须连通",
|
|
75
|
+
recipeCommand: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
76
|
+
},
|
|
77
|
+
"rule-graph-bizaction-mapping-field-missing": {
|
|
78
|
+
whatWentWrong: "BIZ_ACTION 节点的输入映射缺 serviceMethodParameterCode 或 bizCode",
|
|
79
|
+
correctShape: "inputMappings 每条:serviceMethodParameterCode(服务方法参数)↔ bizCode(模型字段 code),codeType 按类型",
|
|
80
|
+
recipeCommand: { tool: "rule.save", recipe: "rule-cross-model-create" },
|
|
81
|
+
missingInputs: [
|
|
82
|
+
{ name: "serviceCode/serviceMethodCode", source: "omc call bizservice.list --args '{}'(已注册的二开服务)" },
|
|
83
|
+
{ name: "bizCode(模型字段)", source: "omc call field.list --args '{\"schemaCode\":\"<模型>\",\"all\":true}'" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
"rule-graph-bizmethod-unresolved": {
|
|
87
|
+
whatWentWrong: "规则图引用的二开服务方法不存在或未注册",
|
|
88
|
+
correctShape: "methodMapping.schemaCode/serviceCode/serviceMethodCode 必须是已注册服务的真实 code",
|
|
89
|
+
recipeCommand: { tool: "rule.save", recipe: "rule-cross-model-create" },
|
|
90
|
+
missingInputs: [
|
|
91
|
+
{ name: "serviceCode/serviceMethodCode", source: "omc call bizservice.list --args '{}';方法级详情用 bizservice.get" },
|
|
92
|
+
],
|
|
93
|
+
doNot: "不要凭记忆拼写服务 code——静默不匹配时规则保存成功但运行为空",
|
|
94
|
+
},
|
|
95
|
+
"field-relation-target-missing": {
|
|
96
|
+
whatWentWrong: "关联字段的目标模型缺失或不可解析",
|
|
97
|
+
correctShape: "关联字段 options 里目标模型 schemaCode 必须已存在且已发布",
|
|
98
|
+
missingInputs: [
|
|
99
|
+
{ name: "目标模型 schemaCode", source: "omc call model.list --args '{}'" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Platform/permission/environment families: honest "fix this first" with NO
|
|
105
|
+
* retry command — a command that would fail again is worse than none.
|
|
106
|
+
*/
|
|
107
|
+
const FIX_FIRST_GATES = Object.freeze({
|
|
108
|
+
"workflow-start-permission-denied": "40009 有三个独立同码原因:① 发起人角色没挂该流程的发起权限(ouScope ∩ 发起人部门链,全空会静默回退发起人);② 流程未启用/未发布;③ 模型未绑定流程。先 omc call workflow.get_published 核对 ②,再查角色 ouScope——不要反复换参数重试 data.submit",
|
|
109
|
+
"environment-write-disabled": "当前环境 profile 不可写:换可写环境,或让维护者在配置里开 writable——CLI 侧没有绕过开关",
|
|
110
|
+
"production-confirmation-required": "生产环境的写需要显式 confirmProduction:true——确认这次写入真的要落生产再加该参数,不要靠重试",
|
|
111
|
+
"app-not-published": "目标应用未发布:先 workflow/form/field 各面发布(或 app 级发布),再重试运行面操作",
|
|
112
|
+
"submit-workitem-not-mine": "该 workItem 不在当前登录人的待办里:用 omc call workitem.list --args '{\"status\":\"PENDING\"}' 拿**自己的** workItemId,不要用别人的 id 提交",
|
|
113
|
+
"capability-out-of-coverage": "需求不在覆盖面内:这份拒绝就是答案——用 omc capabilities 核对运行时清单,不要换相近工具伪造实现",
|
|
114
|
+
"version-family-evidence-missing": "平台版本证据缺失,写被关闭:先 omc doctor / discover_environment 取版本证据,非 8.6/1.1.25 家族按边界处理",
|
|
115
|
+
});
|
|
116
|
+
/** `tool-input-invalid` and friends: point at the refused tool's own describe. */
|
|
117
|
+
function inputInvalidGuidance(toolName) {
|
|
118
|
+
if (!toolName)
|
|
119
|
+
return undefined;
|
|
120
|
+
return {
|
|
121
|
+
whatWentWrong: "入参没过工具的 schema 校验(缺必填或类型不对)",
|
|
122
|
+
correctShape: "必填参数与类型见 command 指向的 describe(firstCall 自带最小可跑形状)",
|
|
123
|
+
command: `omc describe ${toolName}`,
|
|
124
|
+
doNot: "不要盲目重试——同参数必然同拒绝",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* r19 p14 — the report BI boundary (`/dashboard/v1/*`). A deployment without
|
|
129
|
+
* the report service answers every BI call with a gateway 502 (live p9
|
|
130
|
+
* evidence); that is an ENVIRONMENT boundary, not a transient failure, so the
|
|
131
|
+
* teaching says which faces still work and to stop retrying. Matched from the
|
|
132
|
+
* message/detail text so both the refused and the frozen (uncertain) envelope
|
|
133
|
+
* paths can carry it.
|
|
134
|
+
*/
|
|
135
|
+
function dashboardBoundaryGuidance() {
|
|
136
|
+
return {
|
|
137
|
+
whatWentWrong: "报表 BI 通道(/dashboard/v1/*)不可达——报表服务未启动(本机有此服务;网关 502,p9 实证)",
|
|
138
|
+
fixFirst: "报表能力暂不优先支持(维护者裁决,先隐藏):不要重试同一子面。平台层仍可用:report.tree / report.update / report.get 存在性判定;确需 BI 详情/数据源/图表/配置时,先启动本机报表服务再深进",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function mentionsDashboardChannel(payload) {
|
|
142
|
+
return JSON.stringify({ m: payload.message, d: payload.detail, r: payload.reason }).includes("/dashboard/v1/");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the authored recipe command against the registry: the guidance must
|
|
146
|
+
* never cite a recipe the registry does not ship (that would be a second,
|
|
147
|
+
* contradictory shape source). Falls back to the tool's describe; if even the
|
|
148
|
+
* tool is unknown, the command is omitted.
|
|
149
|
+
*/
|
|
150
|
+
function resolveCommand(authored, registry) {
|
|
151
|
+
if (!authored.recipeCommand || !registry)
|
|
152
|
+
return undefined;
|
|
153
|
+
const tool = registry.find((entry) => entry.name === authored.recipeCommand.tool);
|
|
154
|
+
const recipe = tool?.executionMetadata?.recipes.find((entry) => entry.id === authored.recipeCommand.recipe);
|
|
155
|
+
if (tool && recipe)
|
|
156
|
+
return `omc describe ${tool.name} --example ${recipe.id}`;
|
|
157
|
+
if (tool)
|
|
158
|
+
return `omc describe ${tool.name}`;
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
export function refusalRecovery(blocker, payload, options = {}) {
|
|
162
|
+
// A refusal that already teaches (write-lock-held carries lock recovery) is
|
|
163
|
+
// not overwritten — single owner per envelope.
|
|
164
|
+
if (payload.recovery !== undefined)
|
|
165
|
+
return payload.recovery;
|
|
166
|
+
// r19 p14: the report BI boundary is matched from the wire path in the
|
|
167
|
+
// message/detail, so both refused and frozen (uncertain) envelopes teach it.
|
|
168
|
+
if (mentionsDashboardChannel(payload))
|
|
169
|
+
return dashboardBoundaryGuidance();
|
|
170
|
+
const shape = SHAPE_GATES[blocker];
|
|
171
|
+
if (shape) {
|
|
172
|
+
const command = resolveCommand(shape, options.registry);
|
|
173
|
+
const { recipeCommand: _drop, ...rest } = shape;
|
|
174
|
+
return { ...rest, ...(command !== undefined ? { command } : {}) };
|
|
175
|
+
}
|
|
176
|
+
const fixFirst = FIX_FIRST_GATES[blocker];
|
|
177
|
+
if (fixFirst)
|
|
178
|
+
return { whatWentWrong: String(payload.message ?? blocker).slice(0, 160), fixFirst };
|
|
179
|
+
if (blocker === "tool-input-invalid")
|
|
180
|
+
return inputInvalidGuidance(options.toolName);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Attach guidance to a refused result at the `runToolCall` choke point.
|
|
185
|
+
* Idempotent, refuses to overwrite, and leaves non-refused results untouched.
|
|
186
|
+
*/
|
|
187
|
+
export function attachRefusalRecovery(result, toolName, registry) {
|
|
188
|
+
// r19 p14: `/dashboard/v1/*` failures surface as UNCERTAIN (frozen) — the
|
|
189
|
+
// http-status wire kind is classified "uncertain" by design, and frozen
|
|
190
|
+
// envelopes carry `reason`/`detail` but NO `blocker` field. The dashboard
|
|
191
|
+
// boundary is definitive (gateway 502, report service not started), so those
|
|
192
|
+
// frozen envelopes get the teaching too; every other uncertain result stays
|
|
193
|
+
// untouched (never relabel network jitter as an environment boundary).
|
|
194
|
+
const frozenWithBoundary = result.status === "uncertain" && mentionsDashboardChannel(result);
|
|
195
|
+
if (result.status !== "refused" && !frozenWithBoundary)
|
|
196
|
+
return result;
|
|
197
|
+
if (result.recovery !== undefined)
|
|
198
|
+
return result;
|
|
199
|
+
const blocker = typeof result.blocker === "string" ? result.blocker : "";
|
|
200
|
+
if (!blocker && !frozenWithBoundary)
|
|
201
|
+
return result;
|
|
202
|
+
const recovery = refusalRecovery(blocker, result, { registry, toolName });
|
|
203
|
+
if (!recovery)
|
|
204
|
+
return result;
|
|
205
|
+
return { ...result, recovery };
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=refusal-guidance.js.map
|
package/dist/skills.js
CHANGED
|
@@ -81,6 +81,22 @@ function scanHeadings(lines) {
|
|
|
81
81
|
}
|
|
82
82
|
return found;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Slice one heading's body using the documented rule: a section runs until the
|
|
86
|
+
* next heading of the same or a higher level. Shared by `--section` and search
|
|
87
|
+
* so a search hit can announce the exact bytes `--section` would return.
|
|
88
|
+
*/
|
|
89
|
+
function sliceSectionText(lines, start, level) {
|
|
90
|
+
let end = lines.length;
|
|
91
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
92
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
93
|
+
if (match && match[1].length <= level) {
|
|
94
|
+
end = index;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.slice(start, end).join("\n").trim() + "\n";
|
|
99
|
+
}
|
|
84
100
|
/** Return one Markdown heading and its nested content without loading the full topic into the envelope. */
|
|
85
101
|
export function readSkillSection(name, requested) {
|
|
86
102
|
const document = readSkill(name);
|
|
@@ -121,16 +137,7 @@ export function readSkillSection(name, requested) {
|
|
|
121
137
|
if (start < 0)
|
|
122
138
|
return { document, section: "", headings };
|
|
123
139
|
const startEntry = scanned.find((entry) => entry.index === start);
|
|
124
|
-
|
|
125
|
-
let end = lines.length;
|
|
126
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
127
|
-
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
128
|
-
if (match && match[1].length <= level) {
|
|
129
|
-
end = index;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return { document, section: lines.slice(start, end).join("\n").trim() + "\n", headings };
|
|
140
|
+
return { document, section: sliceSectionText(lines, startEntry.index, startEntry.level), headings };
|
|
134
141
|
}
|
|
135
142
|
/**
|
|
136
143
|
* The "shape" of a topic: every heading with the bytes it would cost to read.
|
|
@@ -147,17 +154,7 @@ export function readSkillOutline(name) {
|
|
|
147
154
|
const lines = document.content.split(/\r?\n/);
|
|
148
155
|
const headings = scanHeadings(lines);
|
|
149
156
|
const sections = headings.map((entry) => {
|
|
150
|
-
|
|
151
|
-
// Same slicing rule as readSkillSection: a section runs until the next
|
|
152
|
-
// heading of the same or a higher level, so reported bytes match the read.
|
|
153
|
-
for (let index = entry.index + 1; index < lines.length; index += 1) {
|
|
154
|
-
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
155
|
-
if (match && match[1].length <= entry.level) {
|
|
156
|
-
end = index;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const body = `${lines.slice(entry.index, end).join("\n").trim()}\n`;
|
|
157
|
+
const body = sliceSectionText(lines, entry.index, entry.level);
|
|
161
158
|
return { heading: entry.heading, level: entry.level, bytes: Buffer.byteLength(body, "utf8") };
|
|
162
159
|
});
|
|
163
160
|
const bytes = Buffer.byteLength(document.content, "utf8");
|
|
@@ -170,4 +167,75 @@ export function readSkillOutline(name) {
|
|
|
170
167
|
const visible = indexLevel === undefined ? sections : sections.filter((entry) => entry.level === indexLevel);
|
|
171
168
|
return { document, bytes, sections: visible.length > 0 ? visible : sections };
|
|
172
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Cross-topic full-text search (`omc help --search`).
|
|
172
|
+
*
|
|
173
|
+
* Why this exists: r18 spent 173.8 KB re-dumping five whole topics twice to grep
|
|
174
|
+
* for `40000`, because `omc help` is a bookshelf (one heading index per topic)
|
|
175
|
+
* and there was no way to ask a question. A search that returns only
|
|
176
|
+
* topic/heading would still cost a second call, so a hit carries its section
|
|
177
|
+
* body and the exact bytes `--section` would return.
|
|
178
|
+
*
|
|
179
|
+
* Matching is deliberately literal: case-insensitive substring, whitespace
|
|
180
|
+
* splits terms, `all` requires every term in the same section and `any`
|
|
181
|
+
* requires one. No stemming, no fuzzy matching — an agent searching an exact
|
|
182
|
+
* error code must never be steered to a similar-looking section.
|
|
183
|
+
*/
|
|
184
|
+
export function searchSkills(query, options) {
|
|
185
|
+
const terms = query.trim().toLowerCase().split(/\s+/).filter((term) => term.length > 0);
|
|
186
|
+
if (terms.length === 0)
|
|
187
|
+
return { matches: [], total: 0 };
|
|
188
|
+
const found = [];
|
|
189
|
+
for (const entry of listSkills()) {
|
|
190
|
+
const document = readSkill(entry.name);
|
|
191
|
+
if (!document)
|
|
192
|
+
continue;
|
|
193
|
+
const lines = document.content.split(/\r?\n/);
|
|
194
|
+
const scanned = scanHeadings(lines);
|
|
195
|
+
// Same visible levels as the index: a whole-document H1 must not swallow
|
|
196
|
+
// every H2 hit into one giant match.
|
|
197
|
+
const levels = [...new Set(scanned.map((item) => item.level))].sort((left, right) => left - right);
|
|
198
|
+
const singleRoot = levels.length > 1 && scanned.filter((item) => item.level === levels[0]).length === 1;
|
|
199
|
+
const indexLevel = singleRoot ? levels[1] : levels[0];
|
|
200
|
+
const visible = scanned.filter((heading) => heading.level === indexLevel);
|
|
201
|
+
for (const [ordinal, heading] of visible.entries()) {
|
|
202
|
+
if (indexLevel === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
const section = sliceSectionText(lines, heading.index, heading.level);
|
|
205
|
+
const lower = section.toLowerCase();
|
|
206
|
+
const hitTerms = terms.filter((term) => lower.includes(term));
|
|
207
|
+
if (hitTerms.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
if (options.mode === "all" && hitTerms.length < terms.length)
|
|
210
|
+
continue;
|
|
211
|
+
const matchLines = [];
|
|
212
|
+
for (let index = heading.index; index < lines.length && matchLines.length < 3; index += 1) {
|
|
213
|
+
const text = lines[index];
|
|
214
|
+
const lineTerms = hitTerms.filter((term) => text.toLowerCase().includes(term));
|
|
215
|
+
if (lineTerms.length === 0)
|
|
216
|
+
continue;
|
|
217
|
+
if (matchLines.length > 0 && /^(#{1,6})\s+/.test(text))
|
|
218
|
+
continue;
|
|
219
|
+
matchLines.push({ line: index + 1, text: text.trim(), terms: lineTerms });
|
|
220
|
+
}
|
|
221
|
+
const headingHit = hitTerms.some((term) => heading.heading.toLowerCase().includes(term));
|
|
222
|
+
const bytes = Buffer.byteLength(section, "utf8");
|
|
223
|
+
found.push({
|
|
224
|
+
topic: document.name,
|
|
225
|
+
heading: heading.heading,
|
|
226
|
+
sectionOrdinal: ordinal + 1,
|
|
227
|
+
matchLines,
|
|
228
|
+
sectionBytes: bytes,
|
|
229
|
+
section,
|
|
230
|
+
// Heading hits beat body hits; more distinct terms beat fewer; smaller
|
|
231
|
+
// sections beat larger ones (cheaper to carry, usually more specific).
|
|
232
|
+
score: (headingHit ? 1000 : 0) + hitTerms.length * 100 + Math.max(0, 100 - Math.round(bytes / 64)),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
found.sort((left, right) => right.score - left.score
|
|
237
|
+
|| left.topic.localeCompare(right.topic)
|
|
238
|
+
|| left.sectionOrdinal - right.sectionOrdinal);
|
|
239
|
+
return { matches: found.slice(0, Math.max(1, options.limit)), total: found.length };
|
|
240
|
+
}
|
|
173
241
|
//# sourceMappingURL=skills.js.map
|
package/dist/tools.js
CHANGED
|
@@ -21,6 +21,8 @@ import { registerListViewLane } from "./lanes/list-view.js";
|
|
|
21
21
|
import { registerReportLane } from "./lanes/report.js";
|
|
22
22
|
import { registerApplyChangesLane } from "./lanes/apply-changes.js";
|
|
23
23
|
import { executionMetadataFor } from "./execution-metadata.js";
|
|
24
|
+
import { attachRefusalRecovery } from "./refusal-guidance.js";
|
|
25
|
+
import { ToolRefusalError } from "./tool-types.js";
|
|
24
26
|
export { ToolRefusalError, ToolUncertainError } from "./tool-types.js";
|
|
25
27
|
export { makeToolContext };
|
|
26
28
|
/**
|
|
@@ -59,6 +61,39 @@ export function buildToolRegistry() {
|
|
|
59
61
|
if (metadata)
|
|
60
62
|
tool.executionMetadata = metadata;
|
|
61
63
|
}
|
|
64
|
+
// r19 ticket 06 — 拒绝即教学,收口在最深的一层:每个 handler 返回/抛出的
|
|
65
|
+
// refused 结果在这里统一附加 recovery(同源配方命令、正确形状、不要做什么)。
|
|
66
|
+
// 收口在 registry 而不是 CLI 出口,是因为 runToolCall、测试 harness、MCP
|
|
67
|
+
// client、组装器全部经过 handler——一处附加、处处生效。已自带 recovery 的
|
|
68
|
+
// 拒绝(write-lock-held)不会被覆盖(attachRefusalRecovery 幂等)。
|
|
69
|
+
for (const tool of tools) {
|
|
70
|
+
const inner = tool.handler;
|
|
71
|
+
const name = tool.name;
|
|
72
|
+
tool.handler = async (ctx, args, self) => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await inner(ctx, args, self);
|
|
75
|
+
if (result && typeof result === "object") {
|
|
76
|
+
const status = result.status;
|
|
77
|
+
// attachRefusalRecovery decides for itself: refused envelopes get
|
|
78
|
+
// guidance, uncertain ones only when the dashboard boundary is
|
|
79
|
+
// pinned in the frozen detail (r19 p14).
|
|
80
|
+
if (status === "refused" || status === "uncertain") {
|
|
81
|
+
return attachRefusalRecovery(result, name, tools);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const payload = error.payload;
|
|
88
|
+
if (payload && typeof payload === "object" && payload.status === "refused") {
|
|
89
|
+
const enriched = attachRefusalRecovery(payload, name, tools);
|
|
90
|
+
if (enriched !== payload)
|
|
91
|
+
throw new ToolRefusalError(enriched);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
62
97
|
return tools;
|
|
63
98
|
}
|
|
64
99
|
export { loadCoverageManifest };
|
package/dist/usage-log.js
CHANGED
|
@@ -34,6 +34,21 @@ const USAGE_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
34
34
|
* Memos still read `logPath()` only — a fallback log is append-only telemetry,
|
|
35
35
|
* never a source of "already did this" answers.
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* r19 ticket 02: explicit opt-out for the WRITE side of the ledger.
|
|
39
|
+
*
|
|
40
|
+
* A maintainer script or an investigation subprocess that runs read-only
|
|
41
|
+
* discovery faces inside a sandbox used to append to that round's
|
|
42
|
+
* `.omc/usage.jsonl` — the r18 probe was read as 469 calls where the agent
|
|
43
|
+
* made 184 (19 polling reads + 105 investigation reads). "What did this round
|
|
44
|
+
* cost" must mean "what did the AGENT spend": `OMC_NO_USAGE=1` mutes this
|
|
45
|
+
* invocation's records (project log, user fallback, explicit OMC_USAGE_LOG —
|
|
46
|
+
* the switch beats them all). Only the write entry points consult this guard;
|
|
47
|
+
* reading (`omc usage`, idempotency memos) is untouched.
|
|
48
|
+
*/
|
|
49
|
+
function recordingDisabled() {
|
|
50
|
+
return process.env.OMC_NO_USAGE === "1";
|
|
51
|
+
}
|
|
37
52
|
function recordLogPath() {
|
|
38
53
|
const explicit = process.env.OMC_USAGE_LOG;
|
|
39
54
|
if (explicit)
|
|
@@ -74,6 +89,8 @@ function appendRecord(path, record) {
|
|
|
74
89
|
}
|
|
75
90
|
/** Append a `script-call` record: one line of `omc script` = one inner tool call. */
|
|
76
91
|
export function recordScriptCall(input) {
|
|
92
|
+
if (recordingDisabled())
|
|
93
|
+
return;
|
|
77
94
|
const target = recordLogPath();
|
|
78
95
|
if (!target)
|
|
79
96
|
return;
|
|
@@ -209,7 +226,7 @@ function commandOf(argv) {
|
|
|
209
226
|
for (let index = 2; index < argv.length && values.length < 2; index += 1) {
|
|
210
227
|
const token = argv[index];
|
|
211
228
|
if (token.startsWith("--")) {
|
|
212
|
-
if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top"].includes(token))
|
|
229
|
+
if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top", "--search", "--search-mode", "--limit"].includes(token))
|
|
213
230
|
index += 1;
|
|
214
231
|
continue;
|
|
215
232
|
}
|
|
@@ -227,7 +244,7 @@ function facesOf(argv) {
|
|
|
227
244
|
// `omc artifact get <id> --jq <path>` showed up as a bare `artifact get` with
|
|
228
245
|
// empty argKeys, so the cost report could not tell a 200 B projection from a
|
|
229
246
|
// 44 KB pull — precisely the read path these flags exist to make cheap.
|
|
230
|
-
const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--out", "--quiet", "--jq", "--summary", "--keys", "--path-only", "--extract", "--extract-keys", "--records"];
|
|
247
|
+
const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--search", "--search-mode", "--out", "--quiet", "--jq", "--summary", "--keys", "--path-only", "--extract", "--extract-keys", "--records"];
|
|
231
248
|
return faces.filter((face) => argv.includes(face));
|
|
232
249
|
}
|
|
233
250
|
const safeArgumentKeys = new Set(["schemaCode", "propertyCode", "code", "workflowCode", "ruleCode", "sheetCode", "id", "dataRuleType", "propertyType", "published", "handler"]);
|
|
@@ -270,6 +287,8 @@ function summarizeArgs(argv) {
|
|
|
270
287
|
return summary;
|
|
271
288
|
}
|
|
272
289
|
export function beginUsage(argv) {
|
|
290
|
+
if (recordingDisabled())
|
|
291
|
+
return;
|
|
273
292
|
const target = recordLogPath();
|
|
274
293
|
if (!target)
|
|
275
294
|
return;
|
package/dist/write.js
CHANGED
|
@@ -124,7 +124,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
124
124
|
const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
|
|
125
125
|
return { status: executed.status, steps: executed.steps, failedStep: executed.failedStep, ...executed.envelope, ...(args.requestId ? { requestId: args.requestId } : {}), artifactFingerprint: artifactRef.detailSha256 };
|
|
126
126
|
}
|
|
127
|
-
const { performed, readback, steps } = executed;
|
|
127
|
+
const { performed, readback, steps, halfSuccess } = executed;
|
|
128
128
|
// Readback gate (live 2026-10-17): a tool that reports `readback.present:false`
|
|
129
129
|
// has NOT proven its own write — the platform accepted the request but the
|
|
130
130
|
// object is absent from the authoritative face. "Called without error" must
|
|
@@ -177,7 +177,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
177
177
|
});
|
|
178
178
|
rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
|
|
179
179
|
}
|
|
180
|
-
return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
|
|
180
|
+
return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(halfSuccess !== undefined ? { halfSuccess } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.100",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.100",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.100",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -156,7 +156,7 @@ rules:
|
|
|
156
156
|
- **`queryConditions` 不能为空**:它是反向过滤器的唯一输入,空数组时回算日志照打但目标字段恒 0(r17 实测)。
|
|
157
157
|
- **聚合目标必须是来源主表的标量列**(r18 2026-10-26 真机):`aggregation.schemaCode` 指向**子表 schema** 时能发布,但目标模型从此整个不可写(`data.submit`/`data.update` 全回 40000 无明细),且子表无父单单号列、匹配恒 0;`dataItemCode` 写 `<子表>.<子列>` 则 publish 回 309004。要子表合计:先在来源主模型物化一个数值字段,再聚合该标量。
|
|
158
158
|
- **DYNAMIC 条件的两个 code 分属两侧,写反了静默 0**:`propertyCode` = 来源模型字段,`currentPropertyCode` = 目标模型字段;语义「来源行的 X 等于本目标行的 Y」。
|
|
159
|
-
- **`propertyType`/`currentPropertyType`(BizPropertyType)与 `operatorType`(OperatorType)必须写整数**——两者都是 `@JsonCreator get(int)`:propertyType 0=SHORT_TEXT 2=NUMERICAL 3=DATE 9=WORK_SHEET…;operatorType 1=EQ 2=NEQ 3=GT 4=IT(小于)5=GTEQ 6=ITEQ(小于等于)9=IS_NULL 10=IS_NOT_NULL…。写名字会被 `ObjectMapperUtils` 抛 json processing exception,再被死循环校验掩码成 305003(工具接受名字并自动转码)。`calculateType` 相反,是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION
|
|
159
|
+
- **`propertyType`/`currentPropertyType`(BizPropertyType)与 `operatorType`(OperatorType)必须写整数**——两者都是 `@JsonCreator get(int)`:propertyType 0=SHORT_TEXT 2=NUMERICAL 3=DATE 9=WORK_SHEET…;operatorType 1=EQ 2=NEQ 3=GT 4=IT(小于)5=GTEQ 6=ITEQ(小于等于)9=IS_NULL 10=IS_NOT_NULL…。写名字会被 `ObjectMapperUtils` 抛 json processing exception,再被死循环校验掩码成 305003(工具接受名字并自动转码)。`calculateType` 相反,是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION);LARGE/SMALL 必须带 k(`LARGE({来源.列},2)`),UNION 是文本拼接、resultType 必须是 0/1。
|
|
160
160
|
- **回算时机(r17b 2026-10-26 完整行为图谱)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠);② 来源行 submit/update/**delete** 触发 `RollUpEventListener`→`FormulaPropertySupport` 事件回算(FIXED+DYNAMIC 混用、多组 OR、COUNTIF/MAX、子表行级条件都验过);③ ⚠️ 事件回算**只在目标模型发布后几分钟内可靠**,之后静默停止(无报错)——`field.publish --force` 目标模型恢复。发布后平台自动生成 t100 `CALCULATE_RULE` 镜像行,别删。r17 真机:目标行 srcNo=C1、来源 C1 行 amt=100 → 读 100;再提交一条 C1/amt=5 → 读 105。
|
|
161
161
|
- **普通数值字段 + 计算规则(type2)不能替代**:REST 路径永不执行(`CalculateHandler.java:50` 的 `forceExecuteCalculateRule` 从不置真),门户里才算。
|
|
162
162
|
- **DYNAMIC 来源不要写点号**:`dataActions` 的 `currentDataItemValue` 写 `items.sealNo` 会被引擎当成**子表列集合**(DatasetImpl),标量目标收到集合 → 运行期 `submit/update` 报 50000(`WorkSheetBizField.fillClientContent: Incorrect value type`)。单跳关联字段直接写字段 code(引擎取 `.id`);子表列只能用于公式/聚合。`rule.save` 已写前拒绝(`rule-graph-create-dynamic-source-is-child-collection`)。
|
package/skills/omc-start-here.md
CHANGED
|
@@ -26,7 +26,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
26
26
|
- `build`/`modify` → 按阶梯内联的**写工具 + 必填参数**执行。
|
|
27
27
|
- `scout`(能不能/怎么写/是否支持)→ 先探边界(`omc-capability-scouting`),别直接写。若 `next` 返回 **`boundary`**(`match.kind:"boundary"`,如 `ocr-e-signature`/`release-prod`/`inline-coding`),说明需求**不在覆盖内**(`write:false`):如实报告、用 `omc capabilities` 核对,**不要**用相近能力伪造实现。
|
|
28
28
|
阶梯步骤已内联工具必填参数(`route.ladder[].tools`/`readTools`)与命中 recipe 的 `describe <tool> --example <id>`;据此直接执行——**不要再逐能力 `commands --lane`**,也不要先扫 `commands --json`/全部 `contract`/全部 `describe --full`。
|
|
29
|
-
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB
|
|
29
|
+
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB,读一次就留在后续每一轮窗口)。**带着具体问题(错误码/跨能力语义/某个词)直接 `omc help --search "<词>"`**——跨全部 topic 全文检索,命中节的正文随结果内联返回,一次调用就是答案;**严禁自己写 `for t in …; do omc help $t --all | grep …` 循环**(r18 实测 12 调用/176KB,检索一次 8KB 搞定)。
|
|
30
30
|
2. 写前只需读 `omc-config-draft-review`(人审门)与 `omc-five-piece-flow`(发现→草稿→权限→执行→回读);`omc-business-configuration` 按命中的能力分节读(见 `next` 的 `readSections`),不要整篇预取。
|
|
31
31
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|
|
32
32
|
4. 只有工具不在 `next` 命中能力内、或要看 purpose/枚举时,才补一次 `commands --json --compact --lane <lane>`。非显然形状(规则图、流程模板、表单布局、RESTFUL)才单独 `describe --example`。
|
|
@@ -50,6 +50,7 @@ omc script - --env <env> <<< '{"tool":"model.list"}' # 多调链:NDJSON 每
|
|
|
50
50
|
omc capability <selected-id> # only the selected capability
|
|
51
51
|
omc describe <tool> --example <recipe-id> # recipe first; default describe otherwise
|
|
52
52
|
omc help <topic> --section "<heading>" # one methodology section, not all
|
|
53
|
+
omc help --search "40000" # full-text search across all topics (answer in one call)
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
Discovery precedes every write. 预算档位见 `omc-tool-selection` 的「Discovery 成本档位」;裸 `omc commands` 是有界索引,全量目录须显式 `commands --json`。
|
|
@@ -97,6 +97,16 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
97
97
|
|
|
98
98
|
- **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish` 各失败一次)。读可并行,写不要。
|
|
99
99
|
|
|
100
|
+
## 审批提交的真实形状(r18b 2026-10-26 真机,1.1.25)
|
|
101
|
+
|
|
102
|
+
| 参数 | 位置 | 语义 |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `agree` | **顶层** | true=通过 / false=驳回;**缺省=true**(驳回后实例仍 PROCESSING,别当成功) |
|
|
105
|
+
| `approval.content` / `relUsers` / `resources` | approval 对象 | 意见 / 下一步审批人 / 附件 |
|
|
106
|
+
| `approval.agree`、`approval.opinion` | — | **静默忽略**(真机:agree:false 仍 APPROVE 且走完流程;opinion 不落库)。OMC 已写前拒绝 |
|
|
107
|
+
|
|
108
|
+
链:`data.save`(draft→workItemId) → `data.submit`(顶层 agree) → 下一级待办。验收看 `sequenceStatus`。
|
|
109
|
+
|
|
100
110
|
## 计算规则(datarule)的取值时机
|
|
101
111
|
|
|
102
112
|
- **字段计算规则(`datarule.create` `dataRuleType:2`)在 REST 路径上不会执行、也不会回填**(r17 2026-10-25 复核,AI-Platform 1.1.25):
|
|
@@ -112,15 +122,13 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
112
122
|
**回算可靠性(重要)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠);② 来源行 submit/update/delete 的事件回算**只在目标模型发布后几分钟内可靠**,之后静默停止(无报错,源变了目标不更新)——重新 `field.publish --force` 目标模型恢复。验收:发布后立刻改来源行并读目标字段;长期对账别依赖事件回算。
|
|
113
123
|
- **同模型(含自己的子表)聚合不可用**:`field.publish` 必抛 305003 —— `publishFormula` 把相对码改写成子表父模型(= 本模型),
|
|
114
124
|
`checkCycleOfAggregation:1032` 判成环。与 payload 无关,8.6.26 与 1.1.25 代码相同。OMC 已写前拒绝 `aggregation-self-schema-cycle`。
|
|
115
|
-
-
|
|
116
|
-
`
|
|
117
|
-
`
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`aggregation-
|
|
122
|
-
- **`40000` 的无明细掩码怎么查**:先怀疑字段级配置(上面的聚合/公式),用「删/禁一个公式字段再写一次」二分,
|
|
123
|
-
不要从表单/权限开始查。OMC 现在会把 40000 转成 `runtime-write-db-exception` 并列出该模型的公式字段。
|
|
125
|
+
- **聚合必须指向来源主表的标量列**(r18/r18b 真机):`schemaCode` 指向**子表 schema** 能发布,但目标模型从此整个不可写
|
|
126
|
+
(写入一律 `40000`,无明细),且子表无父单单号列、匹配恒 0;`dataItemCode` 写 `<子表>.<子列>` 则 publish 回 309004。
|
|
127
|
+
要子表合计:先在来源主表物化一个数值字段,再聚合该标量。写前拒绝:`aggregation-target-is-child-schema` / `aggregation-data-item-not-scalar`。
|
|
128
|
+
- **`40000` 无明细时**先怀疑字段级配置(上面的聚合、公式序列化),用「删/禁一个公式字段再写一次」二分,别从表单/权限查。
|
|
129
|
+
OMC 会转成 `runtime-write-db-exception` 并列出该模型的公式字段。
|
|
130
|
+
- **`LARGE/SMALL` 必须带 k**:`LARGE({来源.列},2)`(缺 k → NumberFormatException → 写入 50000);
|
|
131
|
+
**`UNION` 是文本拼接**,顶层 `resultType` 必须 0/1(写 2 会 50000)。拒绝:`aggregation-large-missing-k` / `aggregation-union-result-type`。
|
|
124
132
|
- **区别:`FORMULA`(计算公式,`propertyType:100`)字段的 EXPRESSION 模式仍只在前端算** —— API 写入不回填。
|
|
125
133
|
|
|
126
134
|
## 规则与业务集成
|