@stalfh233/omc-cli 0.6.3 → 0.6.5

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/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) => {
@@ -50,6 +50,23 @@ const KEY_SYNONYMS = Object.freeze({
50
50
  code: ["schemaCode", "sheetCode", "workflowCode"],
51
51
  ruleCode: ["code"],
52
52
  });
53
+ /**
54
+ * r25 沙盒实证(2026-11-04):探针给 `configure_form` 传了 `publish:true`,
55
+ * 得到「未知键 publish。最接近的合法键:publish→(无相近键)」——写面 fail-closed
56
+ * 是对的,但**意图没被接住**:配置与发布本来就是两步,用户真正要的是
57
+ * `form.publish`。同类还有「配完字段就发布」(`field.publish`)与
58
+ * 「配完列表视图」(无需发布)。这里按工具给出可执行的两步指引,
59
+ * 比「无相近键」有用得多;代价是探针少花一轮(21→22 实测)。
60
+ */
61
+ const TWO_STEP_GUIDANCE = Object.freeze({
62
+ configure_form: Object.freeze({
63
+ publish: "表单的配置与发布是两步:本工具只写草稿,发布用 `omc call form.publish --args '{\"schemaCode\":\"<模型>\",\"sheetCode\":\"<槽位>\"}'`(也可在本工具的成功回执后紧接着跑)。",
64
+ publishNow: "同上:本工具不负责发布,用 `omc call form.publish`。",
65
+ }),
66
+ configure_model: Object.freeze({
67
+ publish: "模型/字段的配置与发布是两步:字段建好后用 `omc call field.publish --args '{\"schemaCode\":\"<模型>\"}'` 发布;模型本身不需要单独发布。",
68
+ }),
69
+ });
53
70
  /**
54
71
  * Per-tool input aliases: a synonym the caller may send INSTEAD of the canonical
55
72
  * key. Applied in `parseToolInput` BEFORE validation (fill canonical from an
@@ -152,11 +169,17 @@ export function parseToolInput(tool, input) {
152
169
  const unknown = Object.keys(normalized).filter((key) => !schemaKeys.has(key));
153
170
  if (unknown.length > 0) {
154
171
  const suggestions = unknown.map((key) => `${key}→${closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
172
+ // r25 实证:`publish→(无相近键)` 等于没说——配置与发布是两步,用户要的是
173
+ // 另一个工具。有语义指引时优先给可执行的下一步。
174
+ const twoStep = unknown
175
+ .map((key) => TWO_STEP_GUIDANCE[tool.name]?.[key])
176
+ .filter((hint) => typeof hint === "string" && hint.length > 0);
155
177
  throw new ToolInputError({
156
178
  status: "error",
157
179
  blocker: "tool-input-unknown-keys",
158
180
  tool: tool.name,
159
- message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。最接近的合法键:${suggestions.join(";")}。参数表:omc describe ${tool.name}`,
181
+ message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `最接近的合法键:${suggestions.join(";")}。参数表:omc describe ${tool.name}`}`,
182
+ ...(twoStep.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : {}),
160
183
  issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: `unknown key; closest: ${closestSchemaKey(key, schemaKeys) ?? "none"}` })),
161
184
  });
162
185
  }
@@ -164,12 +187,30 @@ export function parseToolInput(tool, input) {
164
187
  const result = z.object(tool.inputSchema).safeParse(normalized);
165
188
  if (result.success)
166
189
  return result.data;
190
+ // T3(GitLab #17,2026-11-04):封闭取值此前只能靠报错试出来(`dir:"desc"`
191
+ // → zod 原生 "Expected 'ASC' | 'DESC'",而 filter op 的枚举又只在拒绝文案里)。
192
+ // 把「这个参数到底收哪些值」直接放进错误信封:zod 的 invalid_enum_value 自带
193
+ // options;schema 表达不了的(paramEnums 声明的 filter.op)用声明值兜底。
194
+ const acceptedValues = {};
195
+ for (const issue of result.error.issues) {
196
+ const path = issue.path.join(".") || "args";
197
+ const options = issue.options;
198
+ if (issue.code === "invalid_enum_value" && Array.isArray(options) && options.length > 0)
199
+ acceptedValues[path] = options.map((value) => String(value));
200
+ }
201
+ for (const [key, values] of Object.entries(tool.paramEnums ?? {})) {
202
+ if (acceptedValues[key] === undefined)
203
+ acceptedValues[key] = values.split("|");
204
+ }
205
+ const firstPath = result.error.issues[0]?.path.join(".") || undefined;
167
206
  throw new ToolInputError({
168
207
  status: "error",
169
208
  blocker: "tool-input-invalid",
170
209
  tool: tool.name,
171
210
  message: result.error.issues.map((issue) => `${issue.path.join(".") || "args"}: ${issue.message}${keyRenameHint(issue.path, normalized, tool) ?? ""}`).join("; "),
172
211
  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) } : {}) })),
212
+ ...(Object.keys(acceptedValues).length > 0 ? { acceptedValues } : {}),
213
+ ...(firstPath !== undefined ? { nextCommand: `omc describe ${tool.name} --param ${firstPath.split(".")[0]}` } : {}),
173
214
  });
174
215
  }
175
216
  //# 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. */
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",
3
+ "version": "0.6.5",
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.3",
55
+ "@stalfh233/omc-fake-platform": "0.6.5",
56
56
  "commander": "14.0.3",
57
- "omc-core": "0.6.3",
57
+ "omc-core": "0.6.5",
58
58
  "proper-lockfile": "4.1.2",
59
59
  "write-file-atomic": "6.0.0",
60
60
  "zod": "^3.23.8",
@@ -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`(防乱码)——**对输出做内容检索前先加 `--utf8`(单次明文)或先解 JSON,"grep 搜不到中文"≠"没有配置"**(静默假阴性坑)。stdout 恒为纯 JSON、人读进度在 stderr——脚本解析勿 `2>&1` 合流。
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 硬来**。