@stalfh233/omc-cli 0.6.16 → 0.6.18
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/batch-call.js +39 -5
- package/dist/capability-overview.js +2 -2
- package/dist/cli.js +25 -13
- package/dist/compute.js +1 -1
- package/dist/field-change.js +7 -1
- package/dist/field-families.js +102 -1
- package/dist/graph.js +142 -24
- package/dist/lanes/app.js +17 -4
- package/dist/lanes/field.js +118 -5
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +57 -30
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +41 -9
- package/dist/lanes/organization.js +9 -2
- package/dist/lanes/programming.js +67 -14
- package/dist/lanes/runtime-data.js +177 -20
- package/dist/lanes/workflow-contract.js +5 -1
- package/dist/lanes/workflow.js +104 -3
- package/dist/mcp-graph.js +45 -15
- package/dist/programming-workflow.js +16 -3
- package/dist/read-shapes.js +3 -3
- package/dist/rollback.js +54 -19
- package/dist/rule-when.js +42 -12
- package/dist/skills.js +1 -1
- package/dist/workflow-support.js +2 -1
- package/dist/write.js +6 -2
- package/package.json +3 -3
package/dist/mcp-graph.js
CHANGED
|
@@ -58,24 +58,27 @@ const EXPLORE_TOOL = {
|
|
|
58
58
|
name: "omc_graph_explore",
|
|
59
59
|
description: [
|
|
60
60
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
61
|
-
"输入 query
|
|
62
|
-
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{rules}}。",
|
|
61
|
+
"输入 query 可为业务名(实体 name+code 子串)或规则 code+name 词首;也可直接给字段/模型/服务 code。kind 可选 field|model|service,不给则三种都查。",
|
|
62
|
+
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
|
+
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset 继续调用。",
|
|
63
64
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
64
65
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
65
66
|
].join(" "),
|
|
66
67
|
inputSchema: {
|
|
67
68
|
type: "object",
|
|
68
69
|
properties: {
|
|
69
|
-
query: { type: "string", description: "
|
|
70
|
+
query: { type: "string", description: "业务名(实体 name+code 子串)、字段/模型/服务 code,或规则检索词(规则 code+name 词首匹配)" },
|
|
70
71
|
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
71
72
|
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
73
|
+
offset: { type: "integer", minimum: 0, description: "引用/实体结果起始偏移;truncated 时使用 nextOffset" },
|
|
74
|
+
limit: { type: "integer", minimum: 1, maximum: MATCH_CAP, description: "每类结果最多返回多少条(默认 50)" },
|
|
72
75
|
},
|
|
73
76
|
required: ["query"],
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
const STATUS_TOOL = {
|
|
77
80
|
name: "omc_graph_status",
|
|
78
|
-
description: "索引完整性与新鲜度:environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、gaps
|
|
81
|
+
description: "索引完整性与新鲜度:environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、ageMinutes、staleAfterMinutes、dbPath、coverage.applicationCodes、gaps。超过阈值会给 warning;回答前先看一眼,索引缺失会给出 pull 指引。",
|
|
79
82
|
inputSchema: { type: "object", properties: {} },
|
|
80
83
|
};
|
|
81
84
|
export function graphToolDefinitions() {
|
|
@@ -87,8 +90,9 @@ function toolResult(payload, isError = false) {
|
|
|
87
90
|
function toolError(message) {
|
|
88
91
|
return toolResult({ status: "error", message }, true);
|
|
89
92
|
}
|
|
90
|
-
function cap(rows) {
|
|
91
|
-
|
|
93
|
+
function cap(rows, offset = 0, limit = MATCH_CAP) {
|
|
94
|
+
const page = rows.slice(offset, offset + limit);
|
|
95
|
+
return { rows: page, truncated: offset + page.length < rows.length, offset, limit, ...(offset + page.length < rows.length ? { nextOffset: offset + page.length } : {}), total: rows.length };
|
|
92
96
|
}
|
|
93
97
|
/** Shared handler core — exported so tests drive it without a stdio pair. */
|
|
94
98
|
export async function executeGraphTool(name, rawArgs) {
|
|
@@ -111,33 +115,59 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
111
115
|
return toolError(`索引属于环境「${index.environmentName}」;要查「${args.env.trim()}」请先 omc graph pull --env ${args.env.trim()} 重建索引`);
|
|
112
116
|
}
|
|
113
117
|
const kinds = kind === undefined ? [...KINDS] : [kind];
|
|
118
|
+
const offset = typeof args.offset === "number" && Number.isSafeInteger(args.offset) && args.offset >= 0 ? args.offset : 0;
|
|
119
|
+
const limit = typeof args.limit === "number" && Number.isSafeInteger(args.limit) && args.limit > 0 && args.limit <= MATCH_CAP ? args.limit : MATCH_CAP;
|
|
114
120
|
const references = {};
|
|
115
121
|
const impact = {};
|
|
116
122
|
for (const one of kinds) {
|
|
117
123
|
const ref = queryGraphReferences(one, query);
|
|
118
|
-
const capped = cap(ref.matches);
|
|
119
|
-
|
|
124
|
+
const capped = cap(ref.matches, offset, limit);
|
|
125
|
+
// r35a-F4c(真机 2026-11-06):**MCP 面是第三个消费者**——CLI 的 references
|
|
126
|
+
// 与 impact 都带 coverage 了,这里若不显式透传,MCP 用户看到的仍是
|
|
127
|
+
// 「matches:[] + 一句旧 note」,照样会把「表单没扫」误读成「没人引用」。
|
|
128
|
+
references[one] = {
|
|
129
|
+
count: ref.matches.length,
|
|
130
|
+
rows: capped.rows,
|
|
131
|
+
offset: capped.offset,
|
|
132
|
+
limit: capped.limit,
|
|
133
|
+
...(capped.truncated ? { truncated: true, nextOffset: capped.nextOffset, note: `共 ${capped.total} 条,已给 ${capped.rows.length} 条,offset=${capped.offset} 取下一段(调用时传 offset=${capped.nextOffset}、limit=${capped.limit})` } : {}),
|
|
134
|
+
...(ref.coverage ? { coverage: ref.coverage } : {}),
|
|
135
|
+
...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}),
|
|
136
|
+
};
|
|
120
137
|
if (ref.matches.length > 0 || kind !== undefined) {
|
|
121
138
|
const imp = impactGraph(one, query);
|
|
122
139
|
impact[one] = {
|
|
123
140
|
totals: imp.totals,
|
|
124
|
-
rules: cap(imp.rules),
|
|
125
|
-
datarules: cap(imp.datarules),
|
|
126
|
-
listviews: cap(imp.listviews),
|
|
141
|
+
rules: cap(imp.rules, offset, limit),
|
|
142
|
+
datarules: cap(imp.datarules, offset, limit),
|
|
143
|
+
listviews: cap(imp.listviews, offset, limit),
|
|
144
|
+
...(imp.coverage ? { coverage: imp.coverage } : {}),
|
|
127
145
|
};
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
|
-
const search = searchGraphRules(query);
|
|
148
|
+
const search = searchGraphRules(query, limit, offset);
|
|
149
|
+
const resolvedEntity = search.matches.model.items[0] ? { kind: "model", schemaCode: search.matches.model.items[0].schemaCode, code: search.matches.model.items[0].schemaCode } :
|
|
150
|
+
search.matches.field.items[0] ? { kind: "field", schemaCode: search.matches.field.items[0].schemaCode, code: search.matches.field.items[0].code } :
|
|
151
|
+
search.matches.service.items[0] ? { kind: "service", schemaCode: "", code: search.matches.service.items[0].code } :
|
|
152
|
+
search.rules[0] ? { kind: "rule", schemaCode: search.rules[0].schemaCode, code: search.rules[0].ruleCode } : undefined;
|
|
153
|
+
const searchNote = "实体检索:业务名/实体 name+code 使用子串匹配。\n规则检索:规则 code+name 使用词首匹配(中文子串不命中)。";
|
|
154
|
+
const entityTruncated = Object.values(search.matches).some((match) => match.truncated === true);
|
|
155
|
+
const paginationNote = entityTruncated ? `\n结果已截断:共 N 条时请使用对应 match 的 nextOffset,并在下一次调用传 offset=nextOffset、limit=${limit} 取下一段。` : "";
|
|
131
156
|
return toolResult({
|
|
132
157
|
status: "ok",
|
|
133
158
|
query,
|
|
159
|
+
found: resolvedEntity !== undefined,
|
|
160
|
+
...(resolvedEntity ? { resolved: resolvedEntity } : {}),
|
|
134
161
|
...(kind !== undefined ? { kind } : {}),
|
|
135
162
|
index,
|
|
136
163
|
references,
|
|
137
164
|
impact,
|
|
138
|
-
search: { count: search.rules.length, rules: search.rules },
|
|
139
|
-
|
|
140
|
-
note:
|
|
165
|
+
search: { count: search.rules.length, rules: search.rules, matches: search.matches, coverage: search.coverage },
|
|
166
|
+
searchNote: `${searchNote}${paginationNote}`,
|
|
167
|
+
...(search.note ? { searchNote: `${searchNote}\n${search.note}` } : {}),
|
|
168
|
+
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
|
169
|
+
// 探针据此差点误删被表单引用的字段)。改为指向结构化 coverage,不再靠散文。
|
|
170
|
+
note: `零命中只代表 references[].coverage.scanned 面没有命中;本索引覆盖应用:${index.coverage.applicationCodes.join("、") || "(未知)"},不代表其他应用没有结果;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响`,
|
|
141
171
|
});
|
|
142
172
|
}
|
|
143
173
|
return toolError(`未知工具:${name}`);
|
|
@@ -17,6 +17,7 @@ import { wireCall, WireError, wireRows } from "./wire.js";
|
|
|
17
17
|
* endpoint — see .scratch/backend-online-coding/recon.md §8).
|
|
18
18
|
*
|
|
19
19
|
* Commands:
|
|
20
|
+
* ⚠ test 会 publish 并切编码模式(不可逆)
|
|
20
21
|
* pull remote → tree + manifest (environment-stamped)
|
|
21
22
|
* status local edits vs manifest, plus optional remote drift (--deep)
|
|
22
23
|
* apply changed files → programming.save (→ --publish)
|
|
@@ -801,18 +802,30 @@ export async function cmdProgrammingTest(options) {
|
|
|
801
802
|
const targetSchema = String(dataSpec.schema ?? schemaCode);
|
|
802
803
|
const filter = (dataSpec.filter ?? {});
|
|
803
804
|
const expect = (dataSpec.expect ?? {});
|
|
805
|
+
// data.list is only a locator here. Its projection is intentionally not
|
|
806
|
+
// used as evidence: omitted columns are returned as null and used to turn
|
|
807
|
+
// a correct rule into a false failure (F11).
|
|
804
808
|
const listed = await callTool("data.list", {
|
|
805
809
|
environment: session.environmentName,
|
|
806
810
|
schemaCode: targetSchema,
|
|
807
811
|
...filterOf(filter),
|
|
808
|
-
displayFields:
|
|
812
|
+
displayFields: ["bizObjectId", "id"],
|
|
809
813
|
}, context);
|
|
810
814
|
const rows = (listed.rows ?? listed.items ?? []);
|
|
811
|
-
const
|
|
815
|
+
const loadedRows = [];
|
|
816
|
+
for (const row of rows) {
|
|
817
|
+
const id = String(row.bizObjectId ?? row.id ?? "");
|
|
818
|
+
if (!id)
|
|
819
|
+
continue;
|
|
820
|
+
const loaded = await callTool("data.load", { environment: session.environmentName, schemaCode: targetSchema, bizObjectId: id, full: true }, context);
|
|
821
|
+
if (loaded.exists === true && loaded.data && typeof loaded.data === "object")
|
|
822
|
+
loadedRows.push(loaded.data);
|
|
823
|
+
}
|
|
824
|
+
const matched = loadedRows.filter((row) => matchExpectation(row, expect).ok);
|
|
812
825
|
const ok = matched.length > 0;
|
|
813
826
|
if (!ok)
|
|
814
827
|
assertsFailed += 1;
|
|
815
|
-
assertReports.push({ schema: targetSchema, filter, expect, rows:
|
|
828
|
+
assertReports.push({ schema: targetSchema, filter, expect, rows: loadedRows.length, ok, ...(ok ? {} : { sample: loadedRows.slice(0, 3) }) });
|
|
816
829
|
}
|
|
817
830
|
report.asserts = assertReports;
|
|
818
831
|
// 5. Cleanup — only rows the fixture names (seed filter), never a blanket key.
|
package/dist/read-shapes.js
CHANGED
|
@@ -71,7 +71,7 @@ const READ_SHAPES = {
|
|
|
71
71
|
keys: ["bizObjectId", "exists", "data:{...}"],
|
|
72
72
|
},
|
|
73
73
|
"app.list": {
|
|
74
|
-
keys: ["applications:array<{code,name,groupId
|
|
74
|
+
keys: ["applications:array<{code,name,groupId?,modelCount}>", "applicationsCount", "omitted"],
|
|
75
75
|
notes: ["applicationsCount + omitted 与 discover 同构;omitted:false 表示内联即全集"],
|
|
76
76
|
},
|
|
77
77
|
"model.list": {
|
|
@@ -85,8 +85,8 @@ const READ_SHAPES = {
|
|
|
85
85
|
},
|
|
86
86
|
"model.inspect": {
|
|
87
87
|
// Slim/compact face (window diet): field inventory is `fieldCodes`, not fields[].
|
|
88
|
-
keys: ["schemaCode", "fieldCodes", "form", "listview", "ruleCodes", "datarules", "workflow", "hint"],
|
|
89
|
-
notes: ["slim 面键是 fieldCodes(不是 fields[].code
|
|
88
|
+
keys: ["schemaCode", "applicationCode", "fieldCodes", "form", "listview", "ruleCodes", "datarules", "workflow", "hint"],
|
|
89
|
+
notes: ["slim 面键是 fieldCodes(不是 fields[].code);all:true 时 fields/artifact 行含 options"],
|
|
90
90
|
},
|
|
91
91
|
"field.list": {
|
|
92
92
|
keys: ["fields:array<{code,name,type,...}>", "modelPresent?"],
|
package/dist/rollback.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readJournal, resolveJournalEntry, laterWritesOnTarget } from "./journal
|
|
|
8
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
|
+
import { acquireWriteLock } from "./write-lock.js";
|
|
11
12
|
/** Pure runtime assembly: an injected context (tests) or the default one. */
|
|
12
13
|
function runtime(context) {
|
|
13
14
|
return context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
@@ -191,25 +192,53 @@ export async function applyRollback(ref, options, context) {
|
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
const results = [];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
195
|
+
// The inverse plan is intentionally kept as the seam for future inverse
|
|
196
|
+
// extensions (for example onlinejs.compose remove:true): all new restore
|
|
197
|
+
// shapes still become governed calls here, without changing the evidence
|
|
198
|
+
// or lock protocol.
|
|
199
|
+
const waitMs = options.waitMs ?? 0;
|
|
200
|
+
const deadline = Date.now() + waitMs;
|
|
201
|
+
let lock;
|
|
202
|
+
for (;;) {
|
|
203
|
+
lock = await acquireWriteLock(entry.environment);
|
|
204
|
+
if (lock.held)
|
|
205
|
+
break;
|
|
206
|
+
if (Date.now() >= deadline) {
|
|
207
|
+
return {
|
|
208
|
+
status: "refused",
|
|
209
|
+
blocker: "write-lock-held",
|
|
210
|
+
rollbackOf: { seq: entry.seq, tool: entry.tool },
|
|
211
|
+
waitMs,
|
|
212
|
+
lockOwner: lock.owner ?? null,
|
|
213
|
+
message: `回滚写锁被占用;已等待 ${waitMs}ms。可用 --wait <ms> 有界排队后重试`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(200, Math.max(0, deadline - Date.now()))));
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
for (const [index, call] of plan.inverse.calls.entries()) {
|
|
220
|
+
const { exitCode, result } = await runToolCall(call.tool, {
|
|
221
|
+
args: { ...call.args, environment: entry.environment, ...(options.confirmProduction ? { confirmProduction: true } : {}) },
|
|
222
|
+
requestId: `omc-rollback-${entry.seq}-${index + 1}`,
|
|
223
|
+
context: ctx,
|
|
224
|
+
print: () => undefined,
|
|
225
|
+
});
|
|
226
|
+
results.push({
|
|
227
|
+
tool: call.tool,
|
|
228
|
+
exitCode,
|
|
229
|
+
status: result.status,
|
|
230
|
+
...(result.deliveryState ? { deliveryState: result.deliveryState } : {}),
|
|
231
|
+
...(result.summary ? { summary: result.summary } : {}),
|
|
232
|
+
// r55: exit-code-only results hid the reason a restore call failed
|
|
233
|
+
// (input-invalid showed as a bare "status=error"). Surface the shape.
|
|
234
|
+
...(exitCode === 0 ? {} : { blocker: result.blocker ?? null, message: result.message ?? null, ...(result.issues !== undefined ? { issues: result.issues } : {}), ...(result.diagnostic !== undefined ? { diagnostic: result.diagnostic } : {}) }),
|
|
235
|
+
});
|
|
236
|
+
if (exitCode !== 0)
|
|
237
|
+
return { status: "stopped", rollbackOf: { seq: entry.seq, tool: entry.tool }, step: index + 1, results, message: `恢复调用 ${call.tool} 未成功(status=${result.status});已执行的调用已各自记 journal,可对已完成步骤再 plan` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await lock.release();
|
|
213
242
|
}
|
|
214
243
|
// Authoritative verify: re-read the target and compare against the
|
|
215
244
|
// canonicalized before. "restored" tolerates the write pipeline
|
|
@@ -232,6 +261,12 @@ export async function applyRollback(ref, options, context) {
|
|
|
232
261
|
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target },
|
|
233
262
|
calls: results,
|
|
234
263
|
verify,
|
|
264
|
+
// Stable evidence is present on both the restored and mismatch paths.
|
|
265
|
+
// `recoveredTo` deliberately identifies the original journal target; the
|
|
266
|
+
// replay writes are separately journaled and can be rolled back again.
|
|
267
|
+
restoredSha256: restoredSha,
|
|
268
|
+
matchesBefore: verify === "restored",
|
|
269
|
+
recoveredTo: { seq: entry.seq, ts: entry.ts },
|
|
235
270
|
...(verify === "mismatch" ? { beforeSha, restoredSha, message: "恢复调用已完成但读回与 before 存在值冲突(非默认显式化):立即人工核对目标现态(omc rollback plan <seq> 的 diff 可复看差异)" } : {}),
|
|
236
271
|
hint: "本次恢复的每次调用都走了完整写闸门并已记 journal;恢复本身可再回滚(rollback plan <新 seq>)",
|
|
237
272
|
};
|
package/dist/rule-when.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* 编译目标只用**设计器验证过**的形状(前台生效约束,2026-10-16 用户裁决:后端
|
|
12
12
|
* 接口大量不拦截,save/publish 通过≠门户生效):
|
|
13
13
|
* - 预置触发行 Create/Update(设计器原生行,非自定义)
|
|
14
|
-
* - 单 UPDATE 节点:
|
|
14
|
+
* - 单 UPDATE 节点:dataCondition(输入数据满足条件时执行)+
|
|
15
|
+
* filterCondition(默认按当前输入行定位)+ dataActions(赋值)
|
|
15
16
|
* - FIXED 字面量 / DYNAMIC 字段引用 / EXPRESSION 公式(含 DATEDIF 语法,均真机验证)
|
|
16
17
|
* - 布局/路由交给 prepareRuleGraph 注入(points 画布兼容,Ticket 109)
|
|
17
18
|
*
|
|
@@ -23,6 +24,16 @@ import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contr
|
|
|
23
24
|
import { writeEnvelope } from "./cli-output.js";
|
|
24
25
|
import { argvValue } from "./args.js";
|
|
25
26
|
const WHEN_OPS = { "=": "EQ", "==": "EQ", "!=": "NEQ", ">": "GT", "<": "LT", ">=": "GTEQ", "<=": "LTEQ", "~=": "CT", "!~": "NCT" };
|
|
27
|
+
function parseWhen(raw) {
|
|
28
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|~=|!~|==|!=|=|>|<)\s*([\s\S]+)$/.exec(raw.trim());
|
|
29
|
+
if (!match)
|
|
30
|
+
throw new Error(`--when "${raw}" 解析失败:形如 字段=值 / 字段!=值 / 字段>=数字 / 字段~=文本(包含)`);
|
|
31
|
+
const [, field, op, rawValue] = match;
|
|
32
|
+
if (rawValue.startsWith("="))
|
|
33
|
+
throw new Error(`--when "${raw}" 疑似操作符手误(值以 = 开头):检查是否多打了等号`);
|
|
34
|
+
const value = /^-?\d+(\.\d+)?$/.test(rawValue.trim()) ? Number(rawValue.trim()) : rawValue.trim();
|
|
35
|
+
return { field, op, value };
|
|
36
|
+
}
|
|
26
37
|
/** 编译 --when/--then DSL 为设计器验证形状的规则图(导出供测试断言)。 */
|
|
27
38
|
export function composeRuleWhenGraph(options) {
|
|
28
39
|
if (options.when.length === 0)
|
|
@@ -33,17 +44,14 @@ export function composeRuleWhenGraph(options) {
|
|
|
33
44
|
for (const raw of options.when) {
|
|
34
45
|
// 最长操作符优先:>= <= ~= !~ == != > <(操作符两侧允许空格——r34 探针
|
|
35
46
|
// 实测 `dr_price > 1000` 因空格被拒,属于自然书写)
|
|
36
|
-
const
|
|
37
|
-
if (!match)
|
|
38
|
-
throw new Error(`--when "${raw}" 解析失败:形如 字段=值 / 字段!=值 / 字段>=数字 / 字段~=文本(包含)`);
|
|
39
|
-
const [, field, op, rawValue] = match;
|
|
40
|
-
if (rawValue.startsWith("="))
|
|
41
|
-
throw new Error(`--when "${raw}" 疑似操作符手误(值以 = 开头):检查是否多打了等号`);
|
|
47
|
+
const { field, op, value } = parseWhen(raw);
|
|
42
48
|
const opCode = WHEN_OPS[op];
|
|
43
49
|
if (!opCode)
|
|
44
50
|
throw new Error(`--when 操作符 "${op}" 不受支持`);
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
// DataCondition uses DataFilterCondition, not TableCondition:
|
|
52
|
+
// currentSchemaDataItem is the input field and the operator wire key is
|
|
53
|
+
// ruleDataConditionType (ruleConditionType is @JsonIgnore here).
|
|
54
|
+
leaves.push({ currentSchemaDataItem: field, ruleDataConditionType: opCode, ruleValueType: "FIXED", value });
|
|
47
55
|
}
|
|
48
56
|
const actions = [];
|
|
49
57
|
for (const raw of options.then) {
|
|
@@ -70,7 +78,11 @@ export function composeRuleWhenGraph(options) {
|
|
|
70
78
|
nodes: [
|
|
71
79
|
{ nodeCode: "Start", nodeType: "START" },
|
|
72
80
|
{ nodeCode: "UpdateData1", nodeType: "UPDATE", targetObjectCode: options.schema, inputParam: "_input",
|
|
73
|
-
filterCondition
|
|
81
|
+
// UPDATE.filterCondition selects rows; it is not an execution gate.
|
|
82
|
+
// Keep the trigger row as the default target and put --when on the
|
|
83
|
+
// platform's real execution-gate field (dataCondition).
|
|
84
|
+
dataCondition: { conditionGroup: [leaves] },
|
|
85
|
+
filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] },
|
|
74
86
|
dataActions: actions },
|
|
75
87
|
{ nodeCode: "End", nodeType: "END" },
|
|
76
88
|
],
|
|
@@ -130,9 +142,27 @@ export async function cmdRuleWhen(args) {
|
|
|
130
142
|
catch (error) {
|
|
131
143
|
return invalid(error instanceof Error ? error.message : String(error));
|
|
132
144
|
}
|
|
133
|
-
const
|
|
145
|
+
const rowScope = {
|
|
146
|
+
mode: "current-row",
|
|
147
|
+
matchedRows: null,
|
|
148
|
+
sampleIds: [],
|
|
149
|
+
note: "当前行由运行时 _input.id 确定;保存面没有具体行 id,无法预先 count,按 fail-open 继续保存",
|
|
150
|
+
};
|
|
151
|
+
// print: () => undefined —— stdout 必须恰好一个 JSON 信封(repo 契约)。默认 print 是
|
|
152
|
+
// console.log,会把工具进度行打到信封前面:任何按行解析 stdout 的管道(agent、
|
|
153
|
+
// 重放脚本)第一行就 json.loads 失败(0.6.17 重放 R8 真机撞上)。approval-flow
|
|
154
|
+
// 等 composer 都覆写了,这里漏了;compute.ts 同病同修。
|
|
155
|
+
const { result, exitCode } = await runToolCall("rule.save", { args: createArgs, requestId: request, environment, print: () => undefined });
|
|
156
|
+
// `rule.save` owns the wire write; enrich this high-level command's receipt
|
|
157
|
+
// with the impact face it can prove from the compiled graph. The generated
|
|
158
|
+
// id=id locator is current-row scope, so no count query is needed and the
|
|
159
|
+
// answer is fail-open rather than inventing a table-wide count.
|
|
160
|
+
const scopedResult = {
|
|
161
|
+
...result,
|
|
162
|
+
rowScope,
|
|
163
|
+
};
|
|
134
164
|
const classified = classifyResult(result);
|
|
135
|
-
writeEnvelope(envelope(classified.status,
|
|
165
|
+
writeEnvelope(envelope(classified.status, scopedResult, request, classified.error));
|
|
136
166
|
if (apply && classified.status === "ok")
|
|
137
167
|
process.stderr.write("[omc] 前台生效验证清单(后端通过≠门户生效):1) omc call rule.enable 启用;2) 造一条命中 --when 的测试数据提交;3) 门户打开该记录核对 --then 字段已变更;4) 再造一条不命中的数据确认不误改;5) 设计器打开本规则确认节点/条件显示正常。\n");
|
|
138
168
|
return exitCode;
|
package/dist/skills.js
CHANGED
|
@@ -109,7 +109,7 @@ const SYNTHETIC_SKILLS = Object.freeze({
|
|
|
109
109
|
"# 批量写",
|
|
110
110
|
"",
|
|
111
111
|
"通用批量入口是 `omc call <tool> --args-list '<json数组>'`,逐条执行且默认不中断。",
|
|
112
|
-
"先做零写入形状校验:`omc call data.save --args-list '<
|
|
112
|
+
"先做零写入形状校验:`omc call data.save --args-list '<json数组>' --preflight`;回执 `shapeOnly:true`,只校验 JSON 形状与 schema 必填键,不做业务语义校验;业务校验要真写一次才知道。大数组请用 shell 变量传入(`--args-list` 暂不读取文件或 stdin)。",
|
|
113
113
|
"支持 `data.save`、`data.delete`、`field.create` 等同一工具逐条批量调用;没有专用 `data.import`,导入数据就是逐条 `data.save`。",
|
|
114
114
|
"失败项回 `row`(1 基)、`index`(0 基)和尽力推断的字段/原因;成功项回输入中的 `code` 等业务标识。",
|
|
115
115
|
"",
|
package/dist/workflow-support.js
CHANGED
|
@@ -173,13 +173,14 @@ export function diagnoseInstanceActivity(input) {
|
|
|
173
173
|
const originator = input.originator ?? null;
|
|
174
174
|
const fallbackSuspect = originator !== null &&
|
|
175
175
|
active.some((activity) => activity.participantCount > 0 && activity.participants.every((person) => person.id === originator));
|
|
176
|
+
const unresolved = active.filter((activity) => activity.activityType === "PARTICIPANT" && activity.participantCount === 0).map((activity) => activity.activityCode ?? "?");
|
|
176
177
|
return {
|
|
177
178
|
code: "has-active-activity",
|
|
178
179
|
message: `当前激活节点:${active.map((activity) => `${activity.activityCode}(${activity.activityName ?? ""})`).join("、")}`,
|
|
179
180
|
checks: fallbackSuspect
|
|
180
181
|
? ["若这些节点本应流转给角色或他人,现在却落在发起人身上:参与人解析为空时平台会退回发起人(1.1.25 实测),流程看似推进实为错派。核对参与人写法(参与者对象数组 / FindUserByRole(org, roleCode[, order]))与角色成员。"]
|
|
181
182
|
: [],
|
|
182
|
-
warnings: fallbackSuspect ? ["participant-fallback-to-originator"] : [],
|
|
183
|
+
warnings: [...(fallbackSuspect ? ["participant-fallback-to-originator"] : []), ...unresolved.map((activityCode) => `participant-unresolved:${activityCode}`)],
|
|
183
184
|
};
|
|
184
185
|
}
|
|
185
186
|
/**
|
package/dist/write.js
CHANGED
|
@@ -143,7 +143,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEs
|
|
|
143
143
|
const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
|
|
144
144
|
return { status: executed.status, steps: executed.steps, failedStep: executed.failedStep, ...executed.envelope, ...(args.requestId ? { requestId: args.requestId } : {}), artifactFingerprint: artifactRef.detailSha256 };
|
|
145
145
|
}
|
|
146
|
-
const { performed, readback, steps, halfSuccess, irreversible } = executed;
|
|
146
|
+
const { performed, readback, steps, halfSuccess, irreversible, rollbackReceipt, runtimeVerification } = executed;
|
|
147
147
|
// Readback gate (live 2026-10-17): a tool that reports `readback.present:false`
|
|
148
148
|
// has NOT proven its own write — the platform accepted the request but the
|
|
149
149
|
// object is absent from the authoritative face. "Called without error" must
|
|
@@ -204,17 +204,21 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEs
|
|
|
204
204
|
return {
|
|
205
205
|
status: "ok",
|
|
206
206
|
deliveryState,
|
|
207
|
+
...((typeof performed.applicationCode === "string" ? { applicationCode: performed.applicationCode } : typeof readback.applicationCode === "string" ? { applicationCode: readback.applicationCode } : {})),
|
|
207
208
|
...(readOnlyEscapeNote !== undefined ? { readOnlyEscape: readOnlyEscapeNote } : {}),
|
|
208
209
|
performed,
|
|
209
210
|
readback,
|
|
210
211
|
...(readbackWarning ? { readbackWarning } : {}),
|
|
211
212
|
...(steps !== undefined ? { steps } : {}),
|
|
212
213
|
...(halfSuccess !== undefined ? { halfSuccess } : {}),
|
|
214
|
+
...(runtimeVerification !== undefined ? { runtimeVerification } : {}),
|
|
213
215
|
...(typeof irreversible === "string" ? { irreversible } : {}),
|
|
214
216
|
// r30a F1: first write → replay:"none"; duplicate only on true replay.
|
|
215
217
|
...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name, "none") } : {}),
|
|
216
218
|
...(productionConfirmed ? { productionConfirmed: true } : {}),
|
|
217
|
-
...(rollbackRef
|
|
219
|
+
...(rollbackRef
|
|
220
|
+
? { rollback: { ...(rollbackReceipt ?? {}), seq: rollbackRef.seq, ...(rollbackReceipt ? {} : { hint: rollbackRef.hint }) } }
|
|
221
|
+
: {}),
|
|
218
222
|
summary: writeSummary(tool.name, args, { performed, readback }, "done"),
|
|
219
223
|
detailFile: artifactRef.detailFile,
|
|
220
224
|
detailSha256: artifactRef.detailSha256,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"commander": "14.0.3",
|
|
58
|
-
"omc-core": "0.6.
|
|
58
|
+
"omc-core": "0.6.18",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
60
|
"write-file-atomic": "6.0.0",
|
|
61
61
|
"zod": "^3.23.8",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"directory": "packages/omc-cli"
|
|
74
74
|
},
|
|
75
75
|
"optionalDependencies": {
|
|
76
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
76
|
+
"@stalfh233/omc-fake-platform": "0.6.18"
|
|
77
77
|
}
|
|
78
78
|
}
|