@stalfh233/omc-cli 0.6.2 → 0.6.4
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/README.md +1 -1
- package/dist/call.js +10 -1
- package/dist/capability-absences.js +15 -2
- package/dist/capability-overview.js +1 -1
- package/dist/cli-contract.js +23 -4
- package/dist/cli-output.js +28 -10
- package/dist/cli.js +127 -16
- package/dist/command-transport.js +7 -1
- package/dist/config-store.js +17 -0
- package/dist/discovery.js +32 -3
- package/dist/endpoints.js +5 -0
- package/dist/governed-execution.js +26 -1
- package/dist/lanes/b2-registration-contract.js +1 -1
- package/dist/lanes/b2-registration.js +205 -4
- package/dist/lanes/businessrule-contract.js +5 -1
- package/dist/lanes/businessrule.js +38 -3
- package/dist/lanes/form.js +20 -0
- package/dist/lanes/model.js +20 -4
- package/dist/lanes/rule-lifecycle.js +12 -2
- package/dist/lanes/runtime-data.js +153 -15
- package/dist/path-projection.js +32 -8
- package/dist/read.js +62 -9
- package/dist/recipes/businessrule.js +83 -1
- package/dist/rule-support.js +61 -1
- package/dist/support.js +12 -0
- package/dist/tool-types.js +18 -0
- package/dist/wire.js +35 -1
- package/dist/write.js +7 -5
- package/package.json +3 -3
- package/skills/omc-start-here.md +1 -1
package/dist/rule-support.js
CHANGED
|
@@ -838,6 +838,34 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
838
838
|
// SCHEDULER rules keep the schedule on the rule row (schedulerSetting), not
|
|
839
839
|
// on the node: normalize object → JSON string exactly like node/route.
|
|
840
840
|
const schedulerInput = preparedGraph.schedulerSetting ?? preparedGraph.scheduler;
|
|
841
|
+
// A7 写前 gate(2026-11-03 issue,8.1.108 实测):dataSourceType:"BIZ_SERVICE" 的
|
|
842
|
+
// 调度必须携带 methodMapping{serviceCode,serviceMethodCode,methodCode}。缺
|
|
843
|
+
// methodCode(应用编码)时平台 create 容忍、update 回**无明细 50000**,且 enable
|
|
844
|
+
// 只翻 status 不翻 enabled(实体残缺装不起调度链)。在本地拒绝,不发到 wire。
|
|
845
|
+
const schedulerForGate = typeof schedulerInput === "string" ? parseJsonColumn(schedulerInput) : schedulerInput;
|
|
846
|
+
if (schedulerForGate !== null && typeof schedulerForGate === "object" && !Array.isArray(schedulerForGate)
|
|
847
|
+
&& String(schedulerForGate.dataSourceType ?? "") === "BIZ_SERVICE") {
|
|
848
|
+
const mapping = schedulerForGate.methodMapping;
|
|
849
|
+
const missing = [];
|
|
850
|
+
if (mapping === null || typeof mapping !== "object" || Array.isArray(mapping)) {
|
|
851
|
+
missing.push("methodMapping{serviceCode,serviceMethodCode,methodCode}");
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
for (const key of ["serviceCode", "serviceMethodCode", "methodCode"]) {
|
|
855
|
+
const value = mapping[key];
|
|
856
|
+
if (typeof value !== "string" || !value.trim())
|
|
857
|
+
missing.push(`methodMapping.${key}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (missing.length > 0) {
|
|
861
|
+
throw new ToolRefusalError({
|
|
862
|
+
status: "refused",
|
|
863
|
+
blocker: "rule-scheduler-bizservice-mapping",
|
|
864
|
+
message: `schedulerSetting.dataSourceType=BIZ_SERVICE 但 ${missing.join("、")} 缺失(methodCode 是应用编码 appCode,不是方法 code——serviceCode+serviceMethodCode 定位方法,methodCode 定位应用)。缺 methodCode 的实体平台 create 容忍、update 回无明细 50000,且 enable 只翻 status 不翻 enabled。可先 rule.get 一条同族生产规则对照 methodMapping 形状。`,
|
|
865
|
+
missing,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
841
869
|
const schedulerSetting = schedulerInput === undefined || schedulerInput === null
|
|
842
870
|
? undefined
|
|
843
871
|
: typeof schedulerInput === "string"
|
|
@@ -885,8 +913,40 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
885
913
|
// Only CUSTOM rule codes take the caller's name. `existing` already carries
|
|
886
914
|
// the platform name, so the spread preserves it.
|
|
887
915
|
const isPresetRule = PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode);
|
|
916
|
+
// A5 写前 gate(2026-11-03 issue):平台设计器只接受 中文/字母/数字、≤50 字符的
|
|
917
|
+
// 规则名。违规名 create 能落库(平台不拦),但设计器打开即报「规则名称仅支持
|
|
918
|
+
// 输入中文、字母和数字」且无法编辑——产出一个 UI 不可维护的 landmine。
|
|
919
|
+
// 只约束自定义规则(预置触发行的 name 属平台,见上方 isPresetRule 分支)。
|
|
920
|
+
if (!isPresetRule && typeof args.name === "string" && !/^[\u4e00-\u9fa5A-Za-z0-9]{1,50}$/.test(args.name)) {
|
|
921
|
+
throw new ToolRefusalError({
|
|
922
|
+
status: "refused",
|
|
923
|
+
blocker: "rule-name-invalid",
|
|
924
|
+
message: `规则 name「${args.name.slice(0, 60)}」不符合平台约束:仅支持中文/字母/数字,长度 1-50(设计器校验;违规名虽能落库,但设计器无法再编辑该规则)。建议改为纯中文或字母数字命名。`,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
// A8(2026-11-03 issue,8.1.108 实测):`options` / `extensionSetting` 缺席或空串
|
|
928
|
+
// 时平台落 "",8.1 族实体校验要求结构化的默认值——update 回无明细 50000。写前
|
|
929
|
+
// 归一:空串/缺席补平台默认结构(与设计器保存行为一致)。
|
|
930
|
+
const normalizeRuleJsonColumn = (value, fallback) => {
|
|
931
|
+
if (typeof value === "string" && value.trim())
|
|
932
|
+
return value;
|
|
933
|
+
if (value !== null && typeof value === "object")
|
|
934
|
+
return JSON.stringify(value);
|
|
935
|
+
return fallback;
|
|
936
|
+
};
|
|
888
937
|
const performed = existing?.id
|
|
889
|
-
? await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: {
|
|
938
|
+
? await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: {
|
|
939
|
+
...existing,
|
|
940
|
+
...(isPresetRule ? {} : { name: args.name }),
|
|
941
|
+
options: normalizeRuleJsonColumn(existing.options, '{"ruleVariable":[]}'),
|
|
942
|
+
extensionSetting: normalizeRuleJsonColumn(existing.extensionSetting, "{}"),
|
|
943
|
+
node: nodeJson,
|
|
944
|
+
route: routeJson,
|
|
945
|
+
bizRuleType,
|
|
946
|
+
...schedulerField,
|
|
947
|
+
enabled: existing.enabled === true,
|
|
948
|
+
status: existing.status ?? "DISABLED",
|
|
949
|
+
} })
|
|
890
950
|
: await wireCall(session, { path: ENDPOINTS.businessRuleCreate, method: "POST", body: { code: args.ruleCode, name: args.name, schemaCode: args.schemaCode, bizRuleType, node: nodeJson, route: routeJson, schedulerSetting: schedulerSetting ?? null, ...RULE_CREATE_BODY } });
|
|
891
951
|
// Ticket 62: a bare save only writes the DRAFT working row — the designer
|
|
892
952
|
// and the runtime read the PUBLISHED version, so "saved" ≠ "visible". An
|
package/dist/support.js
CHANGED
|
@@ -118,6 +118,18 @@ export function applicationScopeHint(scope, noun) {
|
|
|
118
118
|
}
|
|
119
119
|
return `本次只扫描了应用 ${scope.code}(source=${scope.source})——零命中只代表该应用没有${noun};跨应用请先 app.list 取 applicationCode。`;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* T4(GitLab #17):同一条作用域事实,但**非零结果也返回**。调用方看到
|
|
123
|
+
* 「只有 3 个模型」时无从判断是租户就这么少、还是被应用边界裁掉了;
|
|
124
|
+
* model.list 之前只在零命中时才说作用域,于是这个裁剪完全不可见。
|
|
125
|
+
*/
|
|
126
|
+
export function applicationScopeNotice(scope) {
|
|
127
|
+
const count = scope.applicationCount ?? 1;
|
|
128
|
+
const where = scope.source === "first-tenant-application" && count > 1
|
|
129
|
+
? `租户首个应用 ${scope.code}(租户共 ${count} 个应用)`
|
|
130
|
+
: `应用 ${scope.code}`;
|
|
131
|
+
return `本次只在${where}内扫描(source=${scope.source})——结果被应用边界裁剪,不代表租户全集;跨应用先 app.list 取 applicationCode 再传 applicationCode。`;
|
|
132
|
+
}
|
|
121
133
|
export function flattenTreeRows(rows) {
|
|
122
134
|
const flat = [];
|
|
123
135
|
const visit = (row) => {
|
package/dist/tool-types.js
CHANGED
|
@@ -164,12 +164,30 @@ export function parseToolInput(tool, input) {
|
|
|
164
164
|
const result = z.object(tool.inputSchema).safeParse(normalized);
|
|
165
165
|
if (result.success)
|
|
166
166
|
return result.data;
|
|
167
|
+
// T3(GitLab #17,2026-11-04):封闭取值此前只能靠报错试出来(`dir:"desc"`
|
|
168
|
+
// → zod 原生 "Expected 'ASC' | 'DESC'",而 filter op 的枚举又只在拒绝文案里)。
|
|
169
|
+
// 把「这个参数到底收哪些值」直接放进错误信封:zod 的 invalid_enum_value 自带
|
|
170
|
+
// options;schema 表达不了的(paramEnums 声明的 filter.op)用声明值兜底。
|
|
171
|
+
const acceptedValues = {};
|
|
172
|
+
for (const issue of result.error.issues) {
|
|
173
|
+
const path = issue.path.join(".") || "args";
|
|
174
|
+
const options = issue.options;
|
|
175
|
+
if (issue.code === "invalid_enum_value" && Array.isArray(options) && options.length > 0)
|
|
176
|
+
acceptedValues[path] = options.map((value) => String(value));
|
|
177
|
+
}
|
|
178
|
+
for (const [key, values] of Object.entries(tool.paramEnums ?? {})) {
|
|
179
|
+
if (acceptedValues[key] === undefined)
|
|
180
|
+
acceptedValues[key] = values.split("|");
|
|
181
|
+
}
|
|
182
|
+
const firstPath = result.error.issues[0]?.path.join(".") || undefined;
|
|
167
183
|
throw new ToolInputError({
|
|
168
184
|
status: "error",
|
|
169
185
|
blocker: "tool-input-invalid",
|
|
170
186
|
tool: tool.name,
|
|
171
187
|
message: result.error.issues.map((issue) => `${issue.path.join(".") || "args"}: ${issue.message}${keyRenameHint(issue.path, normalized, tool) ?? ""}`).join("; "),
|
|
172
188
|
issues: result.error.issues.map((issue) => ({ path: issue.path, code: issue.code, message: issue.message, ...(keyRenameHint(issue.path, normalized, tool) ? { hint: keyRenameHint(issue.path, normalized, tool) } : {}) })),
|
|
189
|
+
...(Object.keys(acceptedValues).length > 0 ? { acceptedValues } : {}),
|
|
190
|
+
...(firstPath !== undefined ? { nextCommand: `omc describe ${tool.name} --param ${firstPath.split(".")[0]}` } : {}),
|
|
173
191
|
});
|
|
174
192
|
}
|
|
175
193
|
//# sourceMappingURL=tool-types.js.map
|
package/dist/wire.js
CHANGED
|
@@ -10,13 +10,21 @@ export class WireError extends Error {
|
|
|
10
10
|
kind;
|
|
11
11
|
errcode;
|
|
12
12
|
diagnostic;
|
|
13
|
+
hint;
|
|
13
14
|
constructor(kind, message, errcode,
|
|
14
15
|
/** Round-2 T-06: diagnosable context for malformed/http-status envelopes. */
|
|
15
|
-
diagnostic
|
|
16
|
+
diagnostic,
|
|
17
|
+
/**
|
|
18
|
+
* P1-4(GitLab #18):调用方可在抛出前附加一条**不改判定**的排查提示
|
|
19
|
+
* (例如「同一 schemaCode 去掉 filter 能读 → 是过滤形状问题」)。信封照实
|
|
20
|
+
* 保留 wire-business/errcode,只多一行可执行线索。
|
|
21
|
+
*/
|
|
22
|
+
hint) {
|
|
16
23
|
super(message);
|
|
17
24
|
this.kind = kind;
|
|
18
25
|
this.errcode = errcode;
|
|
19
26
|
this.diagnostic = diagnostic;
|
|
27
|
+
this.hint = hint;
|
|
20
28
|
}
|
|
21
29
|
}
|
|
22
30
|
/** Redact common secret keys and truncate — the malformed-envelope preview must be safe to log. */
|
|
@@ -232,6 +240,32 @@ export async function wireCall(session, call) {
|
|
|
232
240
|
return result;
|
|
233
241
|
}
|
|
234
242
|
/** List rows from the standard list envelope (array or {data:[]}). */
|
|
243
|
+
/**
|
|
244
|
+
* 版本族端点备选链(2026-11-03 issue A1,8.1.108 实测):部分端点只存在于
|
|
245
|
+
* 某个版本族——8.6 的 `runtime/business_rule/list_by_schemacode` 在 8.1 族不存在
|
|
246
|
+
* (404),8.1 用 `app/businessrule/list_by_schemacode`。按序尝试 candidates,
|
|
247
|
+
* **仅 HTTP 404(端点缺席)下探下一个**:401/500/transport/业务码拒绝原样抛出,
|
|
248
|
+
* 不吞真错误。全部 404 时重抛最后一个。
|
|
249
|
+
*/
|
|
250
|
+
export async function wireCallWith404Fallback(session, candidates, onFallback) {
|
|
251
|
+
let lastError;
|
|
252
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
253
|
+
const call = candidates[index];
|
|
254
|
+
try {
|
|
255
|
+
return await wireCall(session, call);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const endpointAbsent = error instanceof WireError && error.kind === "http-status" && error.diagnostic?.httpStatus === 404;
|
|
259
|
+
if (!endpointAbsent)
|
|
260
|
+
throw error;
|
|
261
|
+
lastError = error;
|
|
262
|
+
const next = candidates[index + 1];
|
|
263
|
+
if (next && onFallback)
|
|
264
|
+
onFallback(call, next);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
throw lastError;
|
|
268
|
+
}
|
|
235
269
|
export function wireRows(value) {
|
|
236
270
|
const rows = Array.isArray(value)
|
|
237
271
|
? value
|
package/dist/write.js
CHANGED
|
@@ -34,7 +34,7 @@ import { WireError } from "./wire.js";
|
|
|
34
34
|
* → session → version-family gate (fail closed without evidence) → production
|
|
35
35
|
* confirmation → write lease → execute with authoritative readback.
|
|
36
36
|
*/
|
|
37
|
-
async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
37
|
+
async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEscape) {
|
|
38
38
|
const requestFingerprint = args.requestId ? sha256({ requestId: args.requestId, args: plannableArgsRaw(args) }) : undefined;
|
|
39
39
|
// The shared admission owns the gate sequence; this face only supplies its
|
|
40
40
|
// plan and its dry-run / replay payload shapes.
|
|
@@ -43,6 +43,8 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
43
43
|
confirmProduction: args.confirmProduction,
|
|
44
44
|
dryRun: args.dryRun,
|
|
45
45
|
capability: args.capability,
|
|
46
|
+
args,
|
|
47
|
+
...(readOnlyEscape !== undefined ? { readOnlyEscape } : {}),
|
|
46
48
|
coverage: { lane: args.capability ?? tool.lane, scope: tool.scope ?? "" },
|
|
47
49
|
plan: (session) => planFor(ctx, args.environment, tool.name, args),
|
|
48
50
|
// A face may supply a session-aware dry-run projection (e.g. model.delete's
|
|
@@ -53,7 +55,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
53
55
|
});
|
|
54
56
|
if (outcome.kind === "settled")
|
|
55
57
|
return outcome.result;
|
|
56
|
-
const { session, plan, productionConfirmed } = outcome.admission;
|
|
58
|
+
const { session, plan, productionConfirmed, readOnlyEscape: readOnlyEscapeNote } = outcome.admission;
|
|
57
59
|
return ctx.lease.run(session.environmentName, `${tool.name}:${randomUUID().slice(0, 8)}`, async () => {
|
|
58
60
|
// Rollback before-capture: the pre-write state of the declared target is
|
|
59
61
|
// fetched INSIDE the lease (serialized with the write itself) and stored
|
|
@@ -180,7 +182,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
180
182
|
});
|
|
181
183
|
rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
|
|
182
184
|
}
|
|
183
|
-
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 };
|
|
185
|
+
return { status: "ok", deliveryState, ...(readOnlyEscapeNote !== undefined ? { readOnlyEscape: readOnlyEscapeNote } : {}), 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 };
|
|
184
186
|
});
|
|
185
187
|
}
|
|
186
188
|
/**
|
|
@@ -223,7 +225,7 @@ function findCompleted(root, requestId, tool, environment, requestFingerprint) {
|
|
|
223
225
|
return undefined;
|
|
224
226
|
}
|
|
225
227
|
/** Low-level write registration factory: shared by every lane adapter. */
|
|
226
|
-
export function writeTool(tools, name, lane, scope, description, inputSchema, execute, preflight, dryRunPayload, rollback) {
|
|
228
|
+
export function writeTool(tools, name, lane, scope, description, inputSchema, execute, preflight, dryRunPayload, rollback, readOnlyEscape) {
|
|
227
229
|
const rawExecute = execute;
|
|
228
230
|
const semanticGate = (args) => {
|
|
229
231
|
if (name === "field.create")
|
|
@@ -280,7 +282,7 @@ export function writeTool(tools, name, lane, scope, description, inputSchema, ex
|
|
|
280
282
|
inputSchema: { environment: environmentArg, capability: capabilityArg, confirmProduction: confirmProductionArg, requestId: z.string().min(1).optional(), dryRun: z.boolean().optional(), ...inputSchema },
|
|
281
283
|
handler: async (ctx, args, self) => {
|
|
282
284
|
semanticGate(args);
|
|
283
|
-
return governedWrite(ctx, self, args, (session) => rawExecute(session, args), dryRunPayload);
|
|
285
|
+
return governedWrite(ctx, self, args, (session) => rawExecute(session, args), dryRunPayload, readOnlyEscape);
|
|
284
286
|
},
|
|
285
287
|
}),
|
|
286
288
|
// Ticket 46: the batch entry drives the SAME executor/gates, so a batch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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.6.
|
|
55
|
+
"@stalfh233/omc-fake-platform": "0.6.4",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.6.
|
|
57
|
+
"omc-core": "0.6.4",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|
package/skills/omc-start-here.md
CHANGED
|
@@ -12,7 +12,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
12
12
|
|
|
13
13
|
## 最小路径(先读这段就能开始)
|
|
14
14
|
|
|
15
|
-
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。读命令输出默认把中文转义为 `\uXXXX
|
|
15
|
+
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**这是有意为之**:stdout 恒为「恰好一个合法 JSON envelope」,在任何代码页下都能被 `JSON.parse`/`ConvertFrom-Json` 还原(GitLab #17 T2 实证:曾让 `--utf8` 关掉转义,GBK 管道把信封结构解码坏、`ConvertFrom-Json` 中途失败)。`--utf8` 只让**人类诊断文本**(stderr/散文)按 UTF-8 原文输出;要连 stdout 也是中文明文,必须先让管道按 UTF-8 解码(`chcp 65001`)——只设 `OMC_ASCII_STDOUT=0` 是关写出端转义、不改管道解码,在 cp936 管道下会把信封解码坏。对输出做内容检索时**先解 JSON 再检索**(或 `--jq` 取值),"grep 搜不到中文"≠"没有配置"(静默假阴性坑)。stdout 恒为纯 JSON、人读进度在 stderr——脚本解析勿 `2>&1` 合流。
|
|
16
16
|
|
|
17
17
|
0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true`——命中这些形态先看 `--help`,比 recipe 手拼省 10+ 轮。
|
|
18
18
|
0c. **清理一个模型用 `omc model-purge --code <x> [--purge-instances] --apply`**——按依赖倒序(实例 → 模型级联带走流程/视图/字段/表单),**不要手工逐层删**:删部门/字段留下缓存或孤儿,会让后续调用出现「假 40009 / 假孤儿 / 全局待办 50000」这类幽灵故障(r12–r14 三次实测)。若返回 `unremovableInstances`(业务对象已删的停滞实例,平台 `delete_instance` NPE),按返回的 `maintainerAction` 让维护者清 `biz_workflow_instance`;**不要改用 model.delete 硬来**。
|