@stalfh233/omc-cli 0.4.71 → 0.4.72
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/artifacts.js +76 -1
- package/dist/bench/evals.js +8 -0
- package/dist/cli.js +45 -2
- package/dist/lanes/rule-graph.js +61 -2
- package/dist/lanes/workflow.js +21 -7
- package/dist/rule-support.js +29 -3
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +2 -0
package/dist/artifacts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
export const CLI_DEFAULT_OUTPUT_BYTES = 64 * 1024;
|
|
5
5
|
export const CLI_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
@@ -13,8 +13,30 @@ function ensureRoot(root) {
|
|
|
13
13
|
mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
14
14
|
return resolved;
|
|
15
15
|
}
|
|
16
|
+
const PRUNE_STAMP = ".prune-stamp";
|
|
17
|
+
const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const PRUNE_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19
|
+
/**
|
|
20
|
+
* Opportunistic hygiene: at most once per day, drop artifacts untouched for a
|
|
21
|
+
* week and enforce the size budget. Hooked into createArtifact so a long-running
|
|
22
|
+
* project never needs a manual sweep; the stamp keeps the scan off the hot path
|
|
23
|
+
* (one readdir per day, not per call).
|
|
24
|
+
*/
|
|
25
|
+
function maybeAutoPrune(resolvedRoot) {
|
|
26
|
+
const stampPath = join(resolvedRoot, PRUNE_STAMP);
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(stampPath) && Date.now() - statSync(stampPath).mtimeMs < PRUNE_INTERVAL_MS)
|
|
29
|
+
return;
|
|
30
|
+
pruneArtifacts(resolvedRoot, { olderThanMs: PRUNE_AGE_MS });
|
|
31
|
+
writeFileSync(stampPath, `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Hygiene is best-effort: never fail a write because cleanup did.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
16
37
|
export function createArtifact(root, content) {
|
|
17
38
|
const resolvedRoot = ensureRoot(root);
|
|
39
|
+
maybeAutoPrune(resolvedRoot);
|
|
18
40
|
const artifactId = randomUUID();
|
|
19
41
|
const path = artifactPath(resolvedRoot, artifactId);
|
|
20
42
|
const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
@@ -116,4 +138,57 @@ export function assertSafeInputPath(root, candidate, label) {
|
|
|
116
138
|
}
|
|
117
139
|
return normalized;
|
|
118
140
|
}
|
|
141
|
+
export function pruneArtifacts(root, options = {}) {
|
|
142
|
+
const resolvedRoot = resolve(root);
|
|
143
|
+
if (!existsSync(resolvedRoot))
|
|
144
|
+
return { removed: 0, freedBytes: 0, kept: 0, keptBytes: 0 };
|
|
145
|
+
const now = options.now ?? Date.now();
|
|
146
|
+
const olderThanMs = options.olderThanMs;
|
|
147
|
+
const maxBytes = options.maxBytes ?? 256 * 1024 * 1024;
|
|
148
|
+
const entries = [];
|
|
149
|
+
let keptBytes = 0;
|
|
150
|
+
let removed = 0;
|
|
151
|
+
let freedBytes = 0;
|
|
152
|
+
const removePair = (path, size) => {
|
|
153
|
+
try {
|
|
154
|
+
rmSync(path, { force: true });
|
|
155
|
+
rmSync(`${path}.sha256`, { force: true });
|
|
156
|
+
removed += 1;
|
|
157
|
+
freedBytes += size;
|
|
158
|
+
}
|
|
159
|
+
catch { /* best effort */ }
|
|
160
|
+
};
|
|
161
|
+
for (const name of readdirSync(resolvedRoot)) {
|
|
162
|
+
if (!/^[a-f0-9-]{36}\.artifact$/u.test(name))
|
|
163
|
+
continue;
|
|
164
|
+
const path = join(resolvedRoot, name);
|
|
165
|
+
let stat;
|
|
166
|
+
try {
|
|
167
|
+
stat = lstatSync(path);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
173
|
+
continue;
|
|
174
|
+
if (olderThanMs !== undefined && now - stat.mtimeMs > olderThanMs) {
|
|
175
|
+
removePair(path, stat.size);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
entries.push({ path, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
179
|
+
keptBytes += stat.size;
|
|
180
|
+
}
|
|
181
|
+
// Budget sweep: oldest first until the store fits.
|
|
182
|
+
if (keptBytes > maxBytes) {
|
|
183
|
+
entries.sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (keptBytes <= maxBytes)
|
|
186
|
+
break;
|
|
187
|
+
removePair(entry.path, entry.size);
|
|
188
|
+
keptBytes -= entry.size;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const kept = entries.filter((entry) => existsSync(entry.path)).length;
|
|
192
|
+
return { removed, freedBytes, kept, keptBytes };
|
|
193
|
+
}
|
|
119
194
|
//# sourceMappingURL=artifacts.js.map
|
package/dist/bench/evals.js
CHANGED
|
@@ -76,6 +76,11 @@ const GOLDEN_CASES = [
|
|
|
76
76
|
async run(harness) {
|
|
77
77
|
await call(harness, "model.create", { name: "金轨迹回填源", code: "omc_mcp_gd2" });
|
|
78
78
|
await call(harness, "model.create", { name: "金轨迹回填目标", code: "omc_mcp_gp" });
|
|
79
|
+
// The platform's rule self-check resolves UPDATE targets against the
|
|
80
|
+
// TARGET model's PUBLISHED fields; an unseeded target would 550018 here
|
|
81
|
+
// and never exercise the DYNAMIC wire this case exists to pin.
|
|
82
|
+
await call(harness, "field.create", { schemaCode: "omc_mcp_gp", code: "latestPeriod", name: "最近申报期", propertyType: "SHORT_TEXT" });
|
|
83
|
+
await call(harness, "field.publish", { schemaCode: "omc_mcp_gp" });
|
|
79
84
|
harness.fake.seedDataRow("omc_mcp_gp", { latestPeriod: "" }, "omc_mcp_gp_row");
|
|
80
85
|
await call(harness, "rule.save", {
|
|
81
86
|
schemaCode: "omc_mcp_gd2",
|
|
@@ -107,6 +112,9 @@ const GOLDEN_CASES = [
|
|
|
107
112
|
async run(harness) {
|
|
108
113
|
await call(harness, "model.create", { name: "金轨迹固定值源", code: "omc_mcp_gf2" });
|
|
109
114
|
await call(harness, "model.create", { name: "金轨迹固定值目标", code: "omc_mcp_gf" });
|
|
115
|
+
await call(harness, "field.create", { schemaCode: "omc_mcp_gf", code: "state", name: "状态", propertyType: "SHORT_TEXT" });
|
|
116
|
+
await call(harness, "field.create", { schemaCode: "omc_mcp_gf", code: "count", name: "次数", propertyType: "NUMERICAL", options: { scale: 0 } });
|
|
117
|
+
await call(harness, "field.publish", { schemaCode: "omc_mcp_gf" });
|
|
110
118
|
harness.fake.seedDataRow("omc_mcp_gf", { state: "old", count: 0 }, "omc_mcp_gf_row");
|
|
111
119
|
await call(harness, "rule.save", {
|
|
112
120
|
schemaCode: "omc_mcp_gf2",
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, requestId } fr
|
|
|
16
16
|
import { configureOutput, humanSummary, stripOutputFlags, writeEnvelope } from "./cli-output.js";
|
|
17
17
|
import { beginUsage, completeUsage, recordUsageOutput, usageReport } from "./usage-log.js";
|
|
18
18
|
import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
|
|
19
|
-
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath } from "./artifacts.js";
|
|
19
|
+
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
|
|
20
20
|
import { resolve } from "node:path";
|
|
21
21
|
import { buildToolRegistry } from "./tools.js";
|
|
22
22
|
import { acquireWriteLock } from "./write-lock.js";
|
|
@@ -437,6 +437,22 @@ function projectJsonPath(value, path) {
|
|
|
437
437
|
}
|
|
438
438
|
return { missed: false, value: current };
|
|
439
439
|
}
|
|
440
|
+
function parseDurationMs(text) {
|
|
441
|
+
const match = /^\s*(\d+)\s*(ms|s|m|h|d|w)?\s*$/u.exec(text);
|
|
442
|
+
if (!match)
|
|
443
|
+
return undefined;
|
|
444
|
+
const amount = Number(match[1]);
|
|
445
|
+
const unit = match[2] ?? "d";
|
|
446
|
+
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : unit === "d" ? 86_400_000 : 604_800_000;
|
|
447
|
+
return amount * factor;
|
|
448
|
+
}
|
|
449
|
+
function formatBytes(bytes) {
|
|
450
|
+
if (bytes < 1024)
|
|
451
|
+
return `${bytes}B`;
|
|
452
|
+
if (bytes < 1024 * 1024)
|
|
453
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
454
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
455
|
+
}
|
|
440
456
|
async function cmdArtifactGet(id, transport, options = {}) {
|
|
441
457
|
try {
|
|
442
458
|
const artifact = readArtifact(artifactRoot(), id);
|
|
@@ -1071,7 +1087,34 @@ async function main() {
|
|
|
1071
1087
|
rollbackCommand.command("apply <ref>").option("--env <environment>").option("--environment <environment>").option("--confirm-production").action(async (ref, options) => {
|
|
1072
1088
|
commandExitCode = await withTransport(options, "rollback apply", (transport) => cmdRollbackApply(ref, { environment: options.env ?? options.environment, confirmProduction: options.confirmProduction === true }, transport));
|
|
1073
1089
|
});
|
|
1074
|
-
command.command("artifact")
|
|
1090
|
+
const artifactCommand = command.command("artifact");
|
|
1091
|
+
artifactCommand.command("prune").option("--older-than <duration>", "删除超过该时长的读回产物(如 7d/12h/30m)").option("--max-bytes <bytes>", "产物目录预算;超出时按最旧优先删除").option("--json").action((options) => {
|
|
1092
|
+
// Machine-global store: oversized readbacks pile up (504 files / 3.4 MB
|
|
1093
|
+
// after one sandbox round). Pruning is deterministic and safe to run at
|
|
1094
|
+
// any time — it only removes this store's own artifact files.
|
|
1095
|
+
const olderThanMs = options.olderThan === undefined ? undefined : parseDurationMs(options.olderThan);
|
|
1096
|
+
if (options.olderThan !== undefined && olderThanMs === undefined) {
|
|
1097
|
+
console.error("用法:omc artifact prune [--older-than 7d] [--max-bytes 268435456] [--json]");
|
|
1098
|
+
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const maxBytes = options.maxBytes === undefined ? undefined : Number(options.maxBytes);
|
|
1102
|
+
if (options.maxBytes !== undefined && (!Number.isFinite(maxBytes) || Number(maxBytes) < 0)) {
|
|
1103
|
+
console.error("--max-bytes 必须是非负整数");
|
|
1104
|
+
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const result = pruneArtifacts(artifactRoot(), {
|
|
1108
|
+
...(olderThanMs !== undefined ? { olderThanMs } : {}),
|
|
1109
|
+
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
1110
|
+
});
|
|
1111
|
+
if (options.json)
|
|
1112
|
+
process.stdout.write(JSON.stringify({ ok: true, ...result }, null, 2) + "\n");
|
|
1113
|
+
else
|
|
1114
|
+
console.error(`已清理 ${result.removed} 个读回产物,释放 ${formatBytes(result.freedBytes)};保留 ${result.kept} 个(${formatBytes(result.keptBytes)})`);
|
|
1115
|
+
commandExitCode = CLI_EXIT_CODES.ok;
|
|
1116
|
+
});
|
|
1117
|
+
artifactCommand.command("get <id>").option("--jq <path>").option("--path-only").option("--summary").option("--keys [a,b]").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
1075
1118
|
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(options.jq ? { jq: options.jq } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}) }));
|
|
1076
1119
|
});
|
|
1077
1120
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -615,6 +615,55 @@ export function graphGeometryNeedsLayout(nodesInput, routesInput) {
|
|
|
615
615
|
const boxes = nodes.map(boxOf);
|
|
616
616
|
return boxes.some((a, ai) => boxes.some((b, bi) => bi > ai && a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height));
|
|
617
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* System persistence nodes belong to the TOOL, never to the caller.
|
|
620
|
+
*
|
|
621
|
+
* `SYSTEM_CREATE/SYSTEM_UPDATE/SYSTEM_DELETE` are the platform's trigger-row
|
|
622
|
+
* persistence nodes: the correct one is a function of the rule code
|
|
623
|
+
* (Create→SYSTEM_CREATE, …) and its position is a function of the action nodes.
|
|
624
|
+
* A caller who hand-writes one can only get it wrong:
|
|
625
|
+
*
|
|
626
|
+
* - wrong type inside a preset rule → data.submit 50000 (live 2026-09-25);
|
|
627
|
+
* - any type inside a CUSTOM rule bound to a workflow event → the end/cancel
|
|
628
|
+
* agenda has no serializable current object and every run throws
|
|
629
|
+
* `Object <id> is not serialized` (live 2026-10-22, r8: 33 occurrences,
|
|
630
|
+
* nothing written, every step still reported DELIVERED);
|
|
631
|
+
* - right type but wrong seat → the merged result is persisted before the
|
|
632
|
+
* action writes it (ASSIGN/BIZ_ACTION must precede the system node).
|
|
633
|
+
*
|
|
634
|
+
* So strip whatever the caller supplied and let `injectDefaultSystemNode` place
|
|
635
|
+
* the one platform default (preset rules only). Routes through the stripped
|
|
636
|
+
* node are bridged pre→post so the chain stays connected.
|
|
637
|
+
*/
|
|
638
|
+
export function stripCallerSystemNodes(nodesInput, routesInput) {
|
|
639
|
+
const nodes = asRecordArray(parseMaybeJson(nodesInput));
|
|
640
|
+
const routes = asRecordArray(parseMaybeJson(routesInput));
|
|
641
|
+
const systemTypes = new Set(["SYSTEM_CREATE", "SYSTEM_UPDATE", "SYSTEM_DELETE"]);
|
|
642
|
+
const stripped = nodes
|
|
643
|
+
.filter((node) => systemTypes.has(String(node.nodeType ?? "")))
|
|
644
|
+
.map((node) => String(node.nodeCode ?? ""));
|
|
645
|
+
if (stripped.length === 0)
|
|
646
|
+
return { nodes, routes, stripped };
|
|
647
|
+
let nextRoutes = routes;
|
|
648
|
+
for (const code of stripped) {
|
|
649
|
+
const incoming = nextRoutes.filter((route) => String(route.postNode ?? "") === code).map((route) => String(route.preNode ?? ""));
|
|
650
|
+
const outgoing = nextRoutes.filter((route) => String(route.preNode ?? "") === code).map((route) => String(route.postNode ?? ""));
|
|
651
|
+
nextRoutes = nextRoutes.filter((route) => String(route.preNode ?? "") !== code && String(route.postNode ?? "") !== code);
|
|
652
|
+
for (const pre of incoming)
|
|
653
|
+
for (const post of outgoing)
|
|
654
|
+
nextRoutes.push({ preNode: pre, postNode: post });
|
|
655
|
+
}
|
|
656
|
+
// Collapse duplicate edges produced by bridging (same endpoints + condition).
|
|
657
|
+
const seen = new Set();
|
|
658
|
+
nextRoutes = nextRoutes.filter((route) => {
|
|
659
|
+
const key = `${String(route.preNode ?? "")}->${String(route.postNode ?? "")}#${JSON.stringify(route.routeCondition ?? null)}`;
|
|
660
|
+
if (seen.has(key))
|
|
661
|
+
return false;
|
|
662
|
+
seen.add(key);
|
|
663
|
+
return true;
|
|
664
|
+
});
|
|
665
|
+
return { nodes: nodes.filter((node) => !systemTypes.has(String(node.nodeType ?? ""))), routes: nextRoutes, stripped };
|
|
666
|
+
}
|
|
618
667
|
/**
|
|
619
668
|
* The single rule-graph normalization used by BOTH the write preflight and the
|
|
620
669
|
* executor, so preview and execute validate the SAME prepared graph (CFG-02
|
|
@@ -632,10 +681,20 @@ export function graphGeometryNeedsLayout(nodesInput, routesInput) {
|
|
|
632
681
|
* in `warnings` (never gating) so the caller can replace it with a domain name.
|
|
633
682
|
*/
|
|
634
683
|
export function prepareRuleGraph(ruleCode, graph, customTrigger, schemaCode) {
|
|
635
|
-
const
|
|
684
|
+
const warnings = [];
|
|
685
|
+
// The caller never owns a persistence node (see stripCallerSystemNodes).
|
|
686
|
+
const stripped = stripCallerSystemNodes(graph.nodes ?? graph.node, graph.routes ?? graph.route);
|
|
687
|
+
for (const nodeCode of stripped.stripped)
|
|
688
|
+
warnings.push({
|
|
689
|
+
code: "rule-graph-system-node-tool-owned",
|
|
690
|
+
nodeCode,
|
|
691
|
+
key: "nodes",
|
|
692
|
+
severity: "warning",
|
|
693
|
+
message: `已忽略调用方传入的系统持久化节点 ${nodeCode}:SYSTEM_CREATE/SYSTEM_UPDATE/SYSTEM_DELETE 由工具按规则类型注入,手写只会错配类型或位置(预置规则错配 → submit 50000;自定义规则/流程事件 → Object is not serialized 且静默不生效)`,
|
|
694
|
+
});
|
|
695
|
+
const injected = injectDefaultSystemNode(ruleCode, stripped.nodes, stripped.routes);
|
|
636
696
|
let finalNodes = injected.nodes;
|
|
637
697
|
let finalRoutes = injected.routes;
|
|
638
|
-
const warnings = [];
|
|
639
698
|
// SCHEDULER ⇒ TIMING_TASK: the platform dispatches a schedule ONLY when the
|
|
640
699
|
// RULE ROW's bizRuleType = 2 (BizRuleScheduling.java:202-205; see
|
|
641
700
|
// lanes/businessrule-contract.ts:42). A caller who authors a SCHEDULER node but
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -913,7 +913,15 @@ function activityCodeOf(record) {
|
|
|
913
913
|
* change still mints a collision-free fresh code. Returns the rewritten
|
|
914
914
|
* activities plus the original→final code map for route remapping.
|
|
915
915
|
*/
|
|
916
|
-
function reconcileCallerActivityCodes(callerActivities, draftActivities) {
|
|
916
|
+
function reconcileCallerActivityCodes(callerActivities, draftActivities, options = {}) {
|
|
917
|
+
// 8.6 pins activityType on an existing code (live 2026-09-23), so a retype
|
|
918
|
+
// there needs a fresh code. AI-Platform 1.1.x does NOT pin it: a raw
|
|
919
|
+
// workflow/update that keeps the code and changes activityType returns
|
|
920
|
+
// errcode 0 and the draft reads back with the new type (live 2026-10-21,
|
|
921
|
+
// 1.1.25, END→PARTICIPANT in place). Minting `_2` there is OMC's own
|
|
922
|
+
// invention and churns every downstream reference (routes, buttons, logs),
|
|
923
|
+
// so only the pinned family mints.
|
|
924
|
+
const mintOnTypeChange = options.mintOnTypeChange !== false;
|
|
917
925
|
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
918
926
|
const used = new Set(draftActivities.map((activity) => String(activity.activityCode ?? "")));
|
|
919
927
|
const codeMap = new Map();
|
|
@@ -941,7 +949,7 @@ function reconcileCallerActivityCodes(callerActivities, draftActivities) {
|
|
|
941
949
|
// the designer's own rename posts exactly that and activityName stays
|
|
942
950
|
// the first-created value). Only a TYPE change still needs a fresh code
|
|
943
951
|
// (activityType is pinned for existing codes).
|
|
944
|
-
if (typeChanged) {
|
|
952
|
+
if (typeChanged && mintOnTypeChange) {
|
|
945
953
|
let suffix = 2;
|
|
946
954
|
while (used.has(`${original}_${suffix}`))
|
|
947
955
|
suffix += 1;
|
|
@@ -1229,7 +1237,7 @@ async function refuseRequiredOnChildTableFields(session, schemaCode, template, t
|
|
|
1229
1237
|
* same edge). This is the "read back, then send the delta" path — without it a
|
|
1230
1238
|
* subset silently REPLACED the whole activity list and dropped nodes.
|
|
1231
1239
|
*/
|
|
1232
|
-
function mergeCallerDelta(template, draft) {
|
|
1240
|
+
function mergeCallerDelta(template, draft, options = {}) {
|
|
1233
1241
|
const { routes: routesRaw, activities: _raw, edges: edgesRaw, ...rest } = template;
|
|
1234
1242
|
const routes = routesRaw ?? edgesRaw;
|
|
1235
1243
|
const draftActivities = Array.isArray(draft.activities) ? draft.activities : [];
|
|
@@ -1258,7 +1266,7 @@ function mergeCallerDelta(template, draft) {
|
|
|
1258
1266
|
// that under the same code). reconcileCallerActivityCodes keeps the code for
|
|
1259
1267
|
// a name change and stamps name_i18n; only a type change still mints a fresh
|
|
1260
1268
|
// code and DROPS the old node (a retype, not a rename).
|
|
1261
|
-
const { activities: reconciledCaller, codeMap } = reconcileCallerActivityCodes(caller, draftActivities);
|
|
1269
|
+
const { activities: reconciledCaller, codeMap } = reconcileCallerActivityCodes(caller, draftActivities, options);
|
|
1262
1270
|
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
1263
1271
|
const originalOf = new Map();
|
|
1264
1272
|
for (const [original, final] of codeMap)
|
|
@@ -1342,7 +1350,7 @@ function mergeCallerDelta(template, draft) {
|
|
|
1342
1350
|
});
|
|
1343
1351
|
return { ...rest, activities: laidOut.activities, rules };
|
|
1344
1352
|
}
|
|
1345
|
-
function normalizeWorkflowTemplate(template, draft) {
|
|
1353
|
+
function normalizeWorkflowTemplate(template, draft, options = {}) {
|
|
1346
1354
|
if (template === undefined || template === null)
|
|
1347
1355
|
return {};
|
|
1348
1356
|
const { routes: routesRaw, activities: rawActivities, edges: edgesRaw, ...rest } = template;
|
|
@@ -1356,7 +1364,7 @@ function normalizeWorkflowTemplate(template, draft) {
|
|
|
1356
1364
|
// The platform cannot rename an existing activityCode (see the reconcile
|
|
1357
1365
|
// comment); give every changed/new node a fresh code, and remap caller routes
|
|
1358
1366
|
// so they follow.
|
|
1359
|
-
const reconciled = rawCaller ? reconcileCallerActivityCodes(rawCaller, draftActivities) : undefined;
|
|
1367
|
+
const reconciled = rawCaller ? reconcileCallerActivityCodes(rawCaller, draftActivities, options) : undefined;
|
|
1360
1368
|
const activities = reconciled?.activities;
|
|
1361
1369
|
const codeMap = reconciled?.codeMap ?? new Map();
|
|
1362
1370
|
const remapCode = (value) => codeMap.get(String(value ?? "")) ?? value;
|
|
@@ -1477,7 +1485,13 @@ async function mergedTemplateBody(session, schemaCode, code, template, merge = f
|
|
|
1477
1485
|
workflowCode: code,
|
|
1478
1486
|
message: `流程 ${code} 不存在(get_draft 401001 缺席)——先 workflow.create 再 update/publish`,
|
|
1479
1487
|
});
|
|
1480
|
-
|
|
1488
|
+
// Type changes are only pinned on the 8.x family (see reconcileCallerActivityCodes).
|
|
1489
|
+
// Unknown versions keep the conservative 8.x behaviour (mint) — writes fail
|
|
1490
|
+
// closed elsewhere, and a spurious `_2` is recoverable while a silent
|
|
1491
|
+
// in-place retype on a pinned platform is not.
|
|
1492
|
+
const version = session.detectedVersion();
|
|
1493
|
+
const mintOnTypeChange = !(version !== undefined && version.startsWith("1."));
|
|
1494
|
+
const normalized = merge ? mergeCallerDelta(template, draft, { mintOnTypeChange }) : normalizeWorkflowTemplate(template, draft, { mintOnTypeChange });
|
|
1481
1495
|
// S29 contract: a PARTICIPANT activity requires sheetCode = the schema's
|
|
1482
1496
|
// sheet, else validate/publish answers 401026 WORKFLOW_ACTIVITY_NO_SHEET. The
|
|
1483
1497
|
// platform's own draft sets each participant's sheetCode to the model code;
|
package/dist/rule-support.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ENDPOINTS } from "./endpoints.js";
|
|
|
4
4
|
import { ToolRefusalError } from "./tool-types.js";
|
|
5
5
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
|
|
6
6
|
import { RULE_BIZ_RULE_TYPE_DESCRIPTION, RULE_CREATE_BODY, RULE_ROUTES_DESCRIPTION, ruleNodeGraphDescription, schedulerSettingDescription, } from "./lanes/businessrule-contract.js";
|
|
7
|
-
import { validateRuleNodeGraph, prepareRuleGraph, PLATFORM_TRIGGER_RULE_CODES } from "./lanes/rule-graph.js";
|
|
7
|
+
import { validateRuleNodeGraph, prepareRuleGraph, PLATFORM_TRIGGER_RULE_CODES, DEFAULT_SYSTEM_NODE_BY_RULE_CODE } from "./lanes/rule-graph.js";
|
|
8
8
|
import { compileMethodConfigPlain } from "./bizservice-config.js";
|
|
9
9
|
import { RESERVED_CODES } from "./code-rules.js";
|
|
10
10
|
/** Business-rule node graph. Platform trigger rules are addressed by ruleCode: Create / Update / Delete / Available / Cancel / GetList / Load. */
|
|
@@ -624,10 +624,36 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
624
624
|
: typeof schedulerInput === "string"
|
|
625
625
|
? schedulerInput
|
|
626
626
|
: JSON.stringify(schedulerInput);
|
|
627
|
-
const nodeJson = typeof nodes === "string" ? nodes : JSON.stringify(nodes);
|
|
628
|
-
const routeJson = typeof routes === "string" ? routes : JSON.stringify(routes);
|
|
629
627
|
const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
|
|
630
628
|
const existing = wireRows(list.data).find((row) => row.code === args.ruleCode);
|
|
629
|
+
// Persistence nodes belong to the PLATFORM, not to us and not to the caller.
|
|
630
|
+
// The preset row already carries its own (Start → Create(SYSTEM_CREATE) →
|
|
631
|
+
// End). Copy that node verbatim instead of fabricating our own: nodeCode,
|
|
632
|
+
// nodeName, isSystemNode and geometry then always match what the designer
|
|
633
|
+
// itself writes. Only its SEAT (relative to the caller's action nodes) is
|
|
634
|
+
// ours, and the fingerprint of a hand-made replica drifting from the
|
|
635
|
+
// platform's is exactly what breaks publish later.
|
|
636
|
+
const systemSpec = DEFAULT_SYSTEM_NODE_BY_RULE_CODE[args.ruleCode];
|
|
637
|
+
if (existing?.id && systemSpec) {
|
|
638
|
+
try {
|
|
639
|
+
const baselineRow = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(existing.id) } })).data;
|
|
640
|
+
const baselineNodes = parseJsonColumn(baselineRow?.node);
|
|
641
|
+
const platformNode = Array.isArray(baselineNodes)
|
|
642
|
+
? baselineNodes.find((node) => String(node.nodeType ?? "") === systemSpec.nodeType)
|
|
643
|
+
: undefined;
|
|
644
|
+
if (platformNode) {
|
|
645
|
+
const index = nodes.findIndex((node) => String(node.nodeType ?? "") === systemSpec.nodeType);
|
|
646
|
+
if (index >= 0)
|
|
647
|
+
nodes[index] = { ...platformNode, isSystemNode: true };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// Best-effort: the documented default keeps the chain valid if the
|
|
652
|
+
// baseline cannot be read (the save itself will report the real error).
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const nodeJson = typeof nodes === "string" ? nodes : JSON.stringify(nodes);
|
|
656
|
+
const routeJson = typeof routes === "string" ? routes : JSON.stringify(routes);
|
|
631
657
|
const schedulerField = schedulerSetting === undefined ? {} : { schedulerSetting };
|
|
632
658
|
// Preset trigger rules (Create/Update/Delete/Load/GetList/Available/Cancel
|
|
633
659
|
// and 流程 code) are platform-managed: their `name` is a platform default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.72",
|
|
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.72",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.72",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -151,7 +151,9 @@ rules:
|
|
|
151
151
|
- **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
|
|
152
152
|
- 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
|
|
153
153
|
- **清理测试数据用批量删除**:`data.delete --args '{"schemaCode":"<code>","bizObjectIds":["id1","id2"]}'` 一次往返删多条(真机 r6:207 次逐条 `data.delete` 占 13.5% 调用)。
|
|
154
|
+
- **系统持久化节点是平台自带的,不要手写**:预置 `Create/Update/Delete` 行本来就带 `SYSTEM_CREATE/SYSTEM_UPDATE/SYSTEM_DELETE`(实测基线 `Start → Create(SYSTEM_CREATE) → End`)。`rule.save` 会丢弃调用方传入的 `SYSTEM_*` 并**从平台基线原样携带**那一个节点(code/name/几何都不由我们猜)。手写的后果:预置规则里类型错配 → `data.submit` 50000;自定义规则/流程事件里带系统节点 → 结束事件无当前对象,`Object <id> is not serialized`,每步仍回 DELIVERED 但什么都没写。只写平台允许配置的业务节点(UPDATE/CREATE/DELETE/BRANCH/ASSIGN/MESSAGE/BIZ_ACTION…)。
|
|
154
155
|
- **取 artifact 先投影**:`artifact get <id> --summary` 看形状 → `--jq '<path>'` 取那一段;只有确实要整份时才裸取(r6:47 次整包拉取 650KB,0 次用投影)。
|
|
156
|
+
- **读回产物会自动清理,也可手动清**:每次大结果落一个 artifact(机器级目录,跨项目累积;实测一轮沙盒 504 个/3.4MB)。`createArtifact` 每 24h 顺手删 7 天未动的;要立刻回收用 `omc artifact prune --older-than 7d --max-bytes 268435456`(默认预算 256MB,超出按最旧优先删),`--json` 回 removed/freedBytes/kept。
|
|
155
157
|
- **核验状态用投影,别重复整条读**:`data.load --args '{"schemaCode":..,"bizObjectId":..,"fields":["sequenceStatus","<业务状态字段>"]}'` 只回指定字段(缺失项在 `missingFields`)——r4 实测同一对象被整条读 74 次;大结果先 `artifact get <id> --summary` → `--keys/--jq`,别整包内联进推理。
|
|
156
158
|
- **流程第一个节点必须是 `{Originator}` 填写节点**:平台 `WorkflowTemplateValidateUtility.validateStartActivity` 对「START 后直接接审批」报 401035「发起节点的参与者只能是{Originator}」,publish 回 errcode 0 但一个 ValidateMessage[] 就把模板挡在门外、不落库(真机 2026-10-21)。OMC 0.4.65 起在 `workflow.update`/`publish` 出门前拒绝(`workflow-first-activity-not-originator`)。
|
|
157
159
|
- **跨模型回写的目标行必须已提交(COMPLETED)**:DRAFT 行不参与目标查询,运行 0 行命中且日志只有「查找范围不存在数据」。单跳关联字段可直接作 DYNAMIC 来源(引擎取关联对象 id);点号路径按子表解析,`关联字段.id` / 两跳路径都不支持。
|