@stalfh233/omc-cli 0.4.42 → 0.4.43

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.
@@ -36,8 +36,8 @@ function fieldRollbackSpec(toolName) {
36
36
  const row = before;
37
37
  const options = row.options !== undefined ? decodeJsonColumn(row.options, JSON_COLUMN_PASSTHROUGH) : undefined;
38
38
  if (toolName === "field.delete")
39
- return { kind: "calls", calls: [{ tool: "field.create", args: { schemaCode, code, name: String(row.name ?? code), propertyType: String(row.propertyType ?? row.type), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode !== undefined ? { relativeCode: row.relativeCode } : {}), ...(row.subFields !== undefined ? { subFields: row.subFields } : {}) } }] };
40
- return { kind: "calls", calls: [{ tool: "field.update", args: { schemaCode, code, ...(row.name !== undefined ? { name: row.name } : {}), ...(row.name_i18n !== undefined ? { name_i18n: row.name_i18n } : {}), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode !== undefined ? { relativeCode: row.relativeCode } : {}), ...(row.subFields !== undefined ? { subFields: row.subFields } : {}), ...(row.propertyEmpty !== undefined ? { propertyEmpty: row.propertyEmpty } : {}) } }] };
39
+ return { kind: "calls", calls: [{ tool: "field.create", args: { schemaCode, code, name: String(row.name ?? code), propertyType: String(row.propertyType ?? row.type), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode != null ? { relativeCode: row.relativeCode } : {}), ...(row.subFields !== undefined ? { subFields: row.subFields } : {}) } }] };
40
+ return { kind: "calls", calls: [{ tool: "field.update", args: { schemaCode, code, ...(row.name !== undefined ? { name: row.name } : {}), ...(row.name_i18n != null ? { name_i18n: row.name_i18n } : {}), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode != null ? { relativeCode: row.relativeCode } : {}), ...(row.subFields !== undefined ? { subFields: row.subFields } : {}), ...(row.propertyEmpty !== undefined ? { propertyEmpty: row.propertyEmpty } : {}) } }] };
41
41
  },
42
42
  };
43
43
  }
@@ -967,6 +967,23 @@ async function backfillDesignerDefaults(session, schemaCode, body, callerTemplat
967
967
  }
968
968
  }
969
969
  }
970
+ /**
971
+ * Rename pairs surfaced in the readback (live r51 2026-10-14): a rename mints
972
+ * `<original>_<n>` and DROPS the original code — re-sending the OLD code then
973
+ * reads as a brand-new node and APPENDS it, silently growing the flow. The
974
+ * readback names the pairs so the caller switches to the new code.
975
+ */
976
+ function renamedActivityPairs(template, data) {
977
+ const callerCodes = new Set((callerActivities(template) ?? []).map((row) => activityCodeOf(row)).filter((code) => code !== ""));
978
+ const bodyCodes = new Set((Array.isArray(data.activities) ? data.activities : []).map((row) => String(row.activityCode ?? "")));
979
+ const pairs = [];
980
+ for (const code of bodyCodes) {
981
+ const match = /^(.+)_\d+$/.exec(code);
982
+ if (match && callerCodes.has(match[1]) && !bodyCodes.has(match[1]))
983
+ pairs.push({ from: match[1], to: code });
984
+ }
985
+ return pairs;
986
+ }
970
987
  /**
971
988
  * Live 2026-10-14: a workflow node's propertyPermissions ACCEPTS
972
989
  * `required:true` on a CHILD_TABLE (子表) property at the wire level, but the
@@ -1633,7 +1650,8 @@ export function registerWorkflowLane(tools) {
1633
1650
  throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.update returned no draft" });
1634
1651
  validateWorkflowRender(data, "workflow.update readback");
1635
1652
  const participantWarnings = participantReferenceWarnings(data);
1636
- return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : 0, templateType: data.templateType ?? null, ...(participantWarnings.length ? { participantWarnings } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
1653
+ const renamedActivities = renamedActivityPairs(template, data);
1654
+ return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : 0, templateType: data.templateType ?? null, ...(participantWarnings.length ? { participantWarnings } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "改名会铸新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
1637
1655
  }, (args) => {
1638
1656
  // Zero-wire refusal (ticket 77): fires in semanticGate BEFORE the
1639
1657
  // governed pipeline — including the rollback before-capture read.
@@ -8,12 +8,12 @@ const onlinejsHighlightScript = Object.freeze({
8
8
  environment,
9
9
  schemaCode: model,
10
10
  sheetCode: "<默认表单 code == 模型 code>",
11
- mode: "replace",
11
+ replace: true,
12
12
  source: "(function(form){ var amt = form.getControl('amountCode'); var party = form.getControl('targetCode'); function refresh(v){ party.options.style = Number(v) > 100000 ? 'color: red;' : ''; } form.on('onRendered', function(){ refresh(amt.getValue()); amt.valueChange.subscribe(function(change){ refresh(change.value); }); }); })",
13
13
  },
14
14
  preconditions: [
15
15
  "控件编码先 model.inspect/field.list 确认(示例 amountCode/targetCode 换成真实字段 code)",
16
- "完整 `(function(form){…})` 生命周期外壳必须 mode:'replace'(merge 模式收外壳会被拒并指路 replace——r38 实证)",
16
+ "完整 `(function(form){…})` 生命周期外壳必须配 `replace:true`(默认 INCREMENTAL merge 收外壳会被拒并指路 replace——r38 实证)",
17
17
  ],
18
18
  conditionalRequirements: [
19
19
  "事件模型:`form.on('onRendered', …)` 初始化 + `<控件>.valueChange.subscribe(function(change){ change.value })` 监听;改样式用 `control.options.style`",
@@ -23,7 +23,7 @@ const onlinejsHighlightScript = Object.freeze({
23
23
  priorToolReferences: [{ tool: "form.get", args: { environment, schemaCode: model }, purpose: "确认表单已发布与控件编码" }],
24
24
  dryRun: { supported: true, command: `omc call onlinejs.compose --env ${environment} --dry-run --args-file compose.json`, notes: "dry-run 校验外壳/合并模式,不写平台" },
25
25
  expectedReadback: { readback: { deliveryState: "DELIVERED", published: true, scriptElementPresent: true, htmlRenderCompat: { draft: { ok: true }, published: { ok: true } } } },
26
- refusalRecovery: [{ code: "merge-mode-requires-shell", action: "完整外壳改 mode:'replace'(六元素模板保留,仅整体替换 customScript)" }],
26
+ refusalRecovery: [{ code: "onlinejs-source-invalid", action: "完整外壳改 `replace:true`(六元素模板保留,仅整体替换 customScript)" }],
27
27
  requiresEnvironmentDerivedIds: false,
28
28
  });
29
29
  export const ONLINEJS_METADATA = Object.freeze({
@@ -59,6 +59,28 @@ export function deepDiff(before, current, path = "", limit = 40, acc = []) {
59
59
  walk(before, current, path);
60
60
  return { changes: acc, total: acc.length };
61
61
  }
62
+ /**
63
+ * Drop null/undefined values from inverse-call args, recursively (live r55
64
+ * 2026-10-14): the wire rows carry explicit nulls (`relativeCode:null`) and
65
+ * `!== undefined` guards let them through — the target tool's input schema then
66
+ * rejects the replay as input-invalid (exit 10) and the restore stops. A null
67
+ * in the before row means "no value", and for every replayable config tool an
68
+ * ABSENT key is the same meaning with a schema the tool accepts.
69
+ */
70
+ export function pruneNullish(value) {
71
+ if (Array.isArray(value))
72
+ return value.map(pruneNullish);
73
+ if (value !== null && typeof value === "object") {
74
+ const out = {};
75
+ for (const [key, item] of Object.entries(value)) {
76
+ if (item === null || item === undefined)
77
+ continue;
78
+ out[key] = pruneNullish(item);
79
+ }
80
+ return out;
81
+ }
82
+ return value;
83
+ }
62
84
  /**
63
85
  * Canonical projection for restore verification: a replayed restore will
64
86
  * NEVER be byte-equal to the before snapshot — the platform stamps
package/dist/rollback.js CHANGED
@@ -5,13 +5,20 @@ import { SessionManager } from "./session-manager.js";
5
5
  import { makeToolContext, buildToolRegistry } from "./tools.js";
6
6
  import { runToolCall } from "./call.js";
7
7
  import { readJournal, resolveJournalEntry, laterWritesOnTarget } from "./journal.js";
8
- import { canonicalForVerify, deepDiff } from "./rollback-spec.js";
8
+ import { canonicalForVerify, deepDiff, pruneNullish } from "./rollback-spec.js";
9
9
  import { sha256 } from "./support.js";
10
10
  import { emitTransportResult } from "./command-transport.js";
11
11
  /** Pure runtime assembly: an injected context (tests) or the default one. */
12
12
  function runtime(context) {
13
13
  return context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
14
14
  }
15
+ /** Nullish-prune every call arg set in an inverse plan (see pruneNullish). */
16
+ function pruneInversePlan(plan) {
17
+ const pruneCalls = (calls) => calls.map((call) => ({ tool: call.tool, args: pruneNullish(call.args) }));
18
+ if (plan.kind === "calls")
19
+ return { ...plan, calls: pruneCalls(plan.calls) };
20
+ return plan.partial ? { ...plan, partial: pruneCalls(plan.partial) } : plan;
21
+ }
15
22
  function resolvedEnvironment(environment) {
16
23
  return new ConfigStore(defaultPaths()).resolveEnvironment(environment).name;
17
24
  }
@@ -94,17 +101,31 @@ async function buildPlan(ref, environmentOption, context) {
94
101
  currentFingerprint,
95
102
  diff: deepDiff(before, current),
96
103
  laterWrites,
97
- inverse: spec.inverse({ before, args: artifactArgs, readback: (artifact.readback ?? {}) }),
104
+ // Every inverse call's args are nullish-pruned centrally: wire-row nulls
105
+ // must never reach a tool's input schema (r55 field.update relativeCode:null).
106
+ inverse: pruneInversePlan(spec.inverse({ before, args: artifactArgs, readback: (artifact.readback ?? {}) })),
98
107
  },
99
108
  };
100
109
  }
101
110
  export async function planRollback(ref, options, context) {
102
111
  const loaded = await buildPlan(ref, options.environment, context);
103
112
  const { entry, plan } = loaded;
113
+ // Preview values are for READING — full before templates run to tens of KB
114
+ // and used to blow the envelope budget (live r51: plan had to be fetched
115
+ // back from an artifact). Truncate previews; `apply` executes the full
116
+ // parameters regardless.
117
+ const preview = (value) => {
118
+ if (value === undefined || value === null)
119
+ return value;
120
+ const text = typeof value === "string" ? value : JSON.stringify(value);
121
+ return text !== undefined && text.length > 200 ? `${text.slice(0, 200)}…(省略 ${text.length - 200}B,apply 用完整参数)` : value;
122
+ };
123
+ const script = plan.inverse.kind === "calls" ? plan.inverse.calls.map((call) => JSON.stringify({ tool: call.tool, args: { ...call.args, environment: entry.environment } })) : [];
124
+ const scriptOmitted = script.join("").length > 6000;
104
125
  return {
105
126
  status: "ok",
106
127
  rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target, ts: entry.ts },
107
- diff: { changedPaths: plan.diff.changes, total: plan.diff.total },
128
+ diff: { changedPaths: plan.diff.changes.map((change) => ({ path: change.path, before: preview(change.before), current: preview(change.current) })), total: plan.diff.total },
108
129
  ...(plan.laterWrites.length > 0
109
130
  ? { warning: `该目标在此写之后还有 ${plan.laterWrites.length} 次写(seq ${plan.laterWrites.map((w) => w.seq).join(", ")});本次回滚会用 before 参数覆盖这些后续变更`, laterWrites: plan.laterWrites.map((w) => ({ seq: w.seq, tool: w.tool, ts: w.ts })) }
110
131
  : {}),
@@ -113,7 +134,9 @@ export async function planRollback(ref, options, context) {
113
134
  kind: "calls",
114
135
  count: plan.inverse.calls.length,
115
136
  ...(plan.inverse.notes ? { notes: plan.inverse.notes } : {}),
116
- script: plan.inverse.calls.map((call) => JSON.stringify({ tool: call.tool, args: { ...call.args, environment: entry.environment } })),
137
+ ...(scriptOmitted
138
+ ? { scriptOmitted: true, scriptHint: "恢复调用参数较大(含完整 before 模板)已省略——直接 omc rollback apply <seq> 执行即用完整参数" }
139
+ : { script }),
117
140
  }
118
141
  : { kind: "irreversible", reason: plan.inverse.reason, ...(plan.inverse.partial ? { partial: plan.inverse.partial } : {}) },
119
142
  hint: "预览不写入;确认后 omc rollback apply <seq>(恢复走完整写闸门,回滚本身也会记 journal,可再回滚)",
@@ -137,7 +160,16 @@ export async function applyRollback(ref, options, context) {
137
160
  context: ctx,
138
161
  print: () => undefined,
139
162
  });
140
- results.push({ tool: call.tool, exitCode, status: result.status, ...(result.deliveryState ? { deliveryState: result.deliveryState } : {}), ...(result.summary ? { summary: result.summary } : {}) });
163
+ results.push({
164
+ tool: call.tool,
165
+ exitCode,
166
+ status: result.status,
167
+ ...(result.deliveryState ? { deliveryState: result.deliveryState } : {}),
168
+ ...(result.summary ? { summary: result.summary } : {}),
169
+ // r55: exit-code-only results hid the reason a restore call failed
170
+ // (input-invalid showed as a bare "status=error"). Surface the shape.
171
+ ...(exitCode === 0 ? {} : { blocker: result.blocker ?? null, message: result.message ?? null, ...(result.issues !== undefined ? { issues: result.issues } : {}), ...(result.diagnostic !== undefined ? { diagnostic: result.diagnostic } : {}) }),
172
+ });
141
173
  if (exitCode !== 0)
142
174
  return { status: "stopped", rollbackOf: { seq: entry.seq, tool: entry.tool }, step: index + 1, results, message: `恢复调用 ${call.tool} 未成功(status=${result.status});已执行的调用已各自记 journal,可对已完成步骤再 plan` };
143
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.42",
3
+ "version": "0.4.43",
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.42",
53
+ "@stalfh233/omc-fake-platform": "0.4.43",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.42",
55
+ "omc-core": "0.4.43",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -42,6 +42,7 @@ node "$CLI" help omc-five-piece-flow
42
42
 
43
43
  ## 模型与字段
44
44
 
45
+ - **code 长度平台硬限**:模型 code ≤26 字符、字段 code ≤28(`business-code-invalid` 出门校验;长业务名先起短 code,长名放 name)。
45
46
  - 模型层字段默认可空;必填、只读、可见性在表单和流程节点配置。
46
47
  - `propertyEmpty` 默认 `false`,不要把业务必填硬编码在数据模型。
47
48
  - 关联单选优先读目标模型已发布字段,明确显示字段;无映射不要写空数组字符串。