@stalfh233/omc-cli 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +304 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../../endpoints.js";
|
|
2
|
+
import { wireCall, WireError } from "../../wire.js";
|
|
3
|
+
import { isScalarString, pushError } from "../validation.js";
|
|
4
|
+
import { toPlain } from "../yaml.js";
|
|
5
|
+
import { PLATFORM_TRIGGER_RULE_CODES } from "../../lanes/rule-graph.js";
|
|
6
|
+
const WORKFLOW_KEYS = new Set(["code", "name", "publish", "template", "bindRules"]);
|
|
7
|
+
const BINDING_KEYS = new Set(["handler", "ruleCodes"]);
|
|
8
|
+
/** 8.6.26 ActivityType enum (mirrors lanes/workflow.ts) — an illegal type is refused at apply, so catch it offline. */
|
|
9
|
+
const WORKFLOW_ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
|
|
10
|
+
/**
|
|
11
|
+
* Offline shape check of a declared workflow template.
|
|
12
|
+
*
|
|
13
|
+
* `workflow.update` reads activities ONLY from `template.activities`, but reads
|
|
14
|
+
* the topology from `routes`/`rules`/`edges`. A template that declares routes
|
|
15
|
+
* but no activities (bench42: the agent wrote `{nodes, edges}` — a guessed
|
|
16
|
+
* shape) therefore passes `intent validate`, and only fails at apply with
|
|
17
|
+
* `workflow.update: rules[0] references an unknown activity` (all activity codes
|
|
18
|
+
* resolve to the empty set) — aborting the whole `intent.apply` run after dozens
|
|
19
|
+
* of successful steps. Mirror the lane's accepted spellings here so the mistake
|
|
20
|
+
* is caught offline with an actionable message.
|
|
21
|
+
*/
|
|
22
|
+
function validateWorkflowTemplate(template, base, ctx) {
|
|
23
|
+
const rawActivities = template.activities;
|
|
24
|
+
const activityList = Array.isArray(rawActivities)
|
|
25
|
+
? rawActivities
|
|
26
|
+
: rawActivities && typeof rawActivities === "object"
|
|
27
|
+
? Object.keys(rawActivities).sort((a, b) => Number(a) - Number(b)).map((key) => rawActivities[key])
|
|
28
|
+
: undefined;
|
|
29
|
+
if (activityList === undefined) {
|
|
30
|
+
const hint = template.nodes !== undefined || template.edges !== undefined ? "(检测到 nodes/edges——本 DSL 用 activities/routes)" : "";
|
|
31
|
+
pushError(ctx, "intent-workflow-activities-required", `workflow.template 必须声明 activities(非空活动列表)${hint};workflow.update 只从 activities 读节点,路由 preNode/postNode 必须指向其中的活动 code`, `${base}.activities`, "value");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (activityList.length === 0) {
|
|
35
|
+
pushError(ctx, "intent-workflow-activities-empty", "workflow.template.activities 不能为空", `${base}.activities`, "value");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const codes = new Set();
|
|
39
|
+
activityList.forEach((activity, index) => {
|
|
40
|
+
if (!activity || typeof activity !== "object") {
|
|
41
|
+
pushError(ctx, "intent-workflow-activity-invalid", `activities[${index}] 必须是映射({id|activityCode, type|activityType})`, `${base}.activities[${index}]`, "value");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const row = activity;
|
|
45
|
+
const code = row.activityCode ?? row.id;
|
|
46
|
+
if (typeof code !== "string" || code.trim().length === 0)
|
|
47
|
+
pushError(ctx, "intent-workflow-activity-code-invalid", `activities[${index}] 缺 code(id 或 activityCode)`, `${base}.activities[${index}]`, "value");
|
|
48
|
+
else
|
|
49
|
+
codes.add(code.trim());
|
|
50
|
+
const type = row.activityType ?? row.type;
|
|
51
|
+
if (typeof type !== "string" || !WORKFLOW_ACTIVITY_TYPES.has(type))
|
|
52
|
+
pushError(ctx, "intent-workflow-activity-type-invalid", `activities[${index}] 的 type 必须是 8.6.26 ActivityType(${[...WORKFLOW_ACTIVITY_TYPES].join("/")};START/END/PARTICIPANT),得到 ${JSON.stringify(type)}`, `${base}.activities[${index}].type`, "value");
|
|
53
|
+
});
|
|
54
|
+
const rawRoutes = template.routes ?? template.rules ?? template.edges;
|
|
55
|
+
if (rawRoutes === undefined)
|
|
56
|
+
return;
|
|
57
|
+
if (!Array.isArray(rawRoutes)) {
|
|
58
|
+
pushError(ctx, "intent-workflow-routes-invalid", "workflow.template.routes 必须是列表", `${base}.routes`, "value");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
rawRoutes.forEach((route, index) => {
|
|
62
|
+
if (!route || typeof route !== "object") {
|
|
63
|
+
pushError(ctx, "intent-workflow-route-invalid", `routes[${index}] 必须是映射({preNode, postNode})`, `${base}.routes[${index}]`, "value");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const row = route;
|
|
67
|
+
const pre = row.preActivityCode ?? row.preNode ?? row.pre;
|
|
68
|
+
const post = row.postActivityCode ?? row.postNode ?? row.post;
|
|
69
|
+
if (typeof pre !== "string" || !codes.has(pre))
|
|
70
|
+
pushError(ctx, "intent-workflow-route-activity-unknown", `routes[${index}].preNode "${String(pre ?? "")}" 不是任何已声明活动 code(已知:${[...codes].join("/")})`, `${base}.routes[${index}]`, "value");
|
|
71
|
+
if (typeof post !== "string" || !codes.has(post))
|
|
72
|
+
pushError(ctx, "intent-workflow-route-activity-unknown", `routes[${index}].postNode "${String(post ?? "")}" 不是任何已声明活动 code(已知:${[...codes].join("/")})`, `${base}.routes[${index}]`, "value");
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Event-handler key -> draft field (mirrors workflow-contract, kept local to avoid a lane import cycle). */
|
|
76
|
+
const HANDLER_FIELDS = Object.freeze({ start: "startEventHandler", cancel: "cancelEventHandler", activate: "activateEventHandler", end: "endEventHandler" });
|
|
77
|
+
const HANDLER_NAMES = Object.keys(HANDLER_FIELDS);
|
|
78
|
+
/**
|
|
79
|
+
* The rule codes bound per handler in a workflow draft. Only a handler carrying
|
|
80
|
+
* `bizActionType:"BIZ_RULE"` counts: the same code under another action type is
|
|
81
|
+
* NOT a rule binding, and must not make the compiler skip the bind.
|
|
82
|
+
*/
|
|
83
|
+
function bindingsOf(template) {
|
|
84
|
+
const result = {};
|
|
85
|
+
if (!template)
|
|
86
|
+
return result;
|
|
87
|
+
for (const [handler, field] of Object.entries(HANDLER_FIELDS)) {
|
|
88
|
+
const face = template[field];
|
|
89
|
+
const isRuleHandler = face?.bizActionType === "BIZ_RULE";
|
|
90
|
+
result[handler] = isRuleHandler && Array.isArray(face?.bizActions) ? face.bizActions.filter((value) => typeof value === "string") : [];
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
function absent(error) {
|
|
95
|
+
return error instanceof WireError && error.kind === "business" && error.errcode === 401001;
|
|
96
|
+
}
|
|
97
|
+
async function getOrNull(session, path, workflowCode) {
|
|
98
|
+
try {
|
|
99
|
+
return ((await wireCall(session, { path, query: { workflowCode } })).data ?? null);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (absent(error))
|
|
103
|
+
return null;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function publishedOf(template) {
|
|
108
|
+
return template !== null && (template.templateType === "PUBLISH" || template.published === true);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The platform returns `propertyPermissions` (and nested `subPropertyPermissions`)
|
|
112
|
+
* in a NON-deterministic order — two reads of an unchanged workflow differ only
|
|
113
|
+
* by array order. The baseline fingerprint hashes the raw template, so an
|
|
114
|
+
* unordered permission array made every replan hash differently and the apply
|
|
115
|
+
* drift gate answered `intent-baseline-drifted` forever (dogfood bench50: three
|
|
116
|
+
* refused applies on an unchanged environment). Permissions are keyed by
|
|
117
|
+
* `propertyCode`, so order carries no semantics — sort them before storing.
|
|
118
|
+
*/
|
|
119
|
+
function sortPermissions(value) {
|
|
120
|
+
if (!Array.isArray(value))
|
|
121
|
+
return value;
|
|
122
|
+
return value
|
|
123
|
+
.map((entry) => {
|
|
124
|
+
if (!entry || typeof entry !== "object")
|
|
125
|
+
return entry;
|
|
126
|
+
const row = entry;
|
|
127
|
+
return row.subPropertyPermissions !== undefined ? { ...row, subPropertyPermissions: sortPermissions(row.subPropertyPermissions) } : row;
|
|
128
|
+
})
|
|
129
|
+
.sort((a, b) => String(a?.propertyCode ?? "").localeCompare(String(b?.propertyCode ?? "")));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Deep-walk the template and canonicalize EVERY `propertyPermissions` /
|
|
133
|
+
* `subPropertyPermissions` array by `propertyCode` — the platform emits them
|
|
134
|
+
* unordered on `activities[]`, `originateActivity`, and any other activity
|
|
135
|
+
* shape, and each one flips the baseline fingerprint independently.
|
|
136
|
+
*/
|
|
137
|
+
function canonicalizePermissions(value) {
|
|
138
|
+
if (Array.isArray(value))
|
|
139
|
+
return value.map(canonicalizePermissions);
|
|
140
|
+
if (value === null || typeof value !== "object")
|
|
141
|
+
return value;
|
|
142
|
+
const record = value;
|
|
143
|
+
const canonical = {};
|
|
144
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
145
|
+
if (key === "propertyPermissions" || key === "subPropertyPermissions")
|
|
146
|
+
canonical[key] = sortPermissions(entry);
|
|
147
|
+
else
|
|
148
|
+
canonical[key] = canonicalizePermissions(entry);
|
|
149
|
+
}
|
|
150
|
+
return canonical;
|
|
151
|
+
}
|
|
152
|
+
function canonicalizeWorkflowTemplate(template) {
|
|
153
|
+
if (!template)
|
|
154
|
+
return undefined;
|
|
155
|
+
return canonicalizePermissions(template);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Canonical participant form for equivalence. The intent accepts several
|
|
159
|
+
* spellings — the keyword string `{Originator}`, a structured object
|
|
160
|
+
* `{type: "Originator", name: …}`, a role object array, or a FindUserByRole
|
|
161
|
+
* function string. The platform normalizes a bare originator to the keyword
|
|
162
|
+
* `{Originator}`, so an intent that declares the object form would otherwise
|
|
163
|
+
* report perpetual drift (bench57). Normalize both sides the same way: a lone
|
|
164
|
+
* originator (keyword, or object whose only meaningful key is type=Originator)
|
|
165
|
+
* becomes the keyword; everything else is compared as canonical key-sorted JSON.
|
|
166
|
+
*/
|
|
167
|
+
function canonicalParticipant(participant) {
|
|
168
|
+
if (typeof participant === "string") {
|
|
169
|
+
const trimmed = participant.trim();
|
|
170
|
+
if (/^originator$/i.test(trimmed))
|
|
171
|
+
return "{Originator}";
|
|
172
|
+
return trimmed;
|
|
173
|
+
}
|
|
174
|
+
if (participant && typeof participant === "object" && !Array.isArray(participant)) {
|
|
175
|
+
const record = participant;
|
|
176
|
+
const keys = Object.keys(record);
|
|
177
|
+
const typeValue = record.type ?? record.participantType;
|
|
178
|
+
if (keys.every((key) => key === "type" || key === "participantType" || key === "name" || key === "i18nMessage" || key === "label") && typeof typeValue === "string" && /^originator$/i.test(typeValue))
|
|
179
|
+
return "{Originator}";
|
|
180
|
+
const sorted = Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
181
|
+
return JSON.stringify(sorted);
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(participant))
|
|
184
|
+
return JSON.stringify(participant);
|
|
185
|
+
return participant;
|
|
186
|
+
}
|
|
187
|
+
function workflowProjection(template) {
|
|
188
|
+
if (!template)
|
|
189
|
+
return { activities: [], rules: [] };
|
|
190
|
+
const activities = Array.isArray(template.activities) ? template.activities.map((activity) => {
|
|
191
|
+
const row = activity;
|
|
192
|
+
return { code: String(row.activityCode ?? row.id ?? ""), type: String(row.activityType ?? row.type ?? ""), ...(row.participant !== undefined ? { participant: canonicalParticipant(row.participant) } : {}) };
|
|
193
|
+
}) : [];
|
|
194
|
+
const rawRules = Array.isArray(template.rules) ? template.rules : Array.isArray(template.routes) ? template.routes : [];
|
|
195
|
+
const rules = rawRules.map((rule) => {
|
|
196
|
+
const row = rule;
|
|
197
|
+
return {
|
|
198
|
+
pre: String(row.preActivityCode ?? row.preNode ?? row.pre ?? ""),
|
|
199
|
+
post: String(row.postActivityCode ?? row.postNode ?? row.post ?? ""),
|
|
200
|
+
...(Array.isArray(row.points) ? { points: row.points } : {}),
|
|
201
|
+
// Branch condition (round7): keep it in the projection so readback
|
|
202
|
+
// equivalence can see a changed/absent ruleConditionGroup, not just the
|
|
203
|
+
// endpoints. An edge with the same pre->post but a different condition is
|
|
204
|
+
// a different route.
|
|
205
|
+
...(row.ruleConditionGroup !== undefined ? { ruleConditionGroup: row.ruleConditionGroup } : {}),
|
|
206
|
+
...(row.childMatchType !== undefined ? { childMatchType: row.childMatchType } : {}),
|
|
207
|
+
...(row.formula !== undefined ? { formula: row.formula } : {}),
|
|
208
|
+
...(row.utilizeElse !== undefined ? { utilizeElse: row.utilizeElse } : {}),
|
|
209
|
+
...(row.alternativeFlag !== undefined ? { alternativeFlag: row.alternativeFlag } : {}),
|
|
210
|
+
...(row.text !== undefined ? { text: row.text } : {}),
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
return { activities, rules };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The platform cannot rename an existing activity in place: `workflow.update`
|
|
217
|
+
* mints a fresh code (`<code>_2`, `<code>_3`, …) when a caller node's type/name
|
|
218
|
+
* differs from the stored draft (see reconcileCallerActivityCodes). An intent
|
|
219
|
+
* that declares `dept_approve` therefore converges to a live `dept_approve_2`
|
|
220
|
+
* after a re-update, and a strict code compare reports perpetual
|
|
221
|
+
* `workflow-template-mismatch` (bench57). Match on the base code so a
|
|
222
|
+
* platform-minted suffix does not read as drift; type/participant/condition
|
|
223
|
+
* checks still gate real differences.
|
|
224
|
+
*/
|
|
225
|
+
function stripRenamedSuffix(code) {
|
|
226
|
+
return String(code ?? "").replace(/_\d+$/, "");
|
|
227
|
+
}
|
|
228
|
+
function findActivity(byCode, byBase, code) {
|
|
229
|
+
return byCode.get(String(code)) ?? byBase.get(stripRenamedSuffix(code));
|
|
230
|
+
}
|
|
231
|
+
function workflowMatches(live, desired) {
|
|
232
|
+
if (!desired)
|
|
233
|
+
return true;
|
|
234
|
+
const actual = workflowProjection(live);
|
|
235
|
+
const expected = workflowProjection(desired);
|
|
236
|
+
const actualActivities = new Map(actual.activities.map((row) => [String(row.code), row]));
|
|
237
|
+
const actualByBase = new Map(actual.activities.map((row) => [stripRenamedSuffix(row.code), row]));
|
|
238
|
+
for (const row of expected.activities) {
|
|
239
|
+
const current = findActivity(actualActivities, actualByBase, row.code);
|
|
240
|
+
if (!current || current.type !== row.type || (row.participant !== undefined && current.participant !== row.participant))
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const edgeKey = (pre, post) => `${stripRenamedSuffix(pre)}->${stripRenamedSuffix(post)}`;
|
|
244
|
+
const actualRules = new Map(actual.rules.map((row) => [edgeKey(row.pre, row.post), row]));
|
|
245
|
+
for (const row of expected.rules) {
|
|
246
|
+
const current = actualRules.get(edgeKey(row.pre, row.post));
|
|
247
|
+
if (!current)
|
|
248
|
+
return false;
|
|
249
|
+
if (row.points !== undefined && JSON.stringify(current.points) !== JSON.stringify(row.points))
|
|
250
|
+
return false;
|
|
251
|
+
// A declared branch condition must be present and equivalent. Only keys the
|
|
252
|
+
// desired template declares are compared, so the platform's own defaults on
|
|
253
|
+
// the live row (popupType/utilizeElse) do not read as drift.
|
|
254
|
+
for (const key of ["ruleConditionGroup", "childMatchType", "formula", "utilizeElse", "alternativeFlag", "text"])
|
|
255
|
+
if (row[key] !== undefined && JSON.stringify(current[key] ?? null) !== JSON.stringify(row[key]))
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Actionable diff for a `workflow-template-mismatch`: which expected activities
|
|
262
|
+
* are missing / have a different type or participant, and which expected edges
|
|
263
|
+
* are missing. The platform adds its own nodes (e.g. the 填写申请单 originate
|
|
264
|
+
* activity), so ADDITIONS are not reported — only what the declared template
|
|
265
|
+
* expects and does not find. bench53 spent 3 diagnosis rounds on a bare
|
|
266
|
+
* "活动/连线不等价".
|
|
267
|
+
*/
|
|
268
|
+
function workflowMismatchDetail(live, desired) {
|
|
269
|
+
if (!desired)
|
|
270
|
+
return "";
|
|
271
|
+
const actual = workflowProjection(live);
|
|
272
|
+
const expected = workflowProjection(desired);
|
|
273
|
+
const actualActivities = new Map(actual.activities.map((row) => [String(row.code), row]));
|
|
274
|
+
const actualByBase = new Map(actual.activities.map((row) => [stripRenamedSuffix(row.code), row]));
|
|
275
|
+
const issues = [];
|
|
276
|
+
for (const row of expected.activities) {
|
|
277
|
+
const current = findActivity(actualActivities, actualByBase, row.code);
|
|
278
|
+
if (!current)
|
|
279
|
+
issues.push(`缺活动 ${row.code}(${row.type})`);
|
|
280
|
+
else if (current.type !== row.type)
|
|
281
|
+
issues.push(`活动 ${row.code} 类型 ${current.type}≠期望 ${row.type}`);
|
|
282
|
+
else if (row.participant !== undefined && current.participant !== row.participant)
|
|
283
|
+
issues.push(`活动 ${row.code} 参与者 ${JSON.stringify(current.participant)}≠期望 ${JSON.stringify(row.participant)}`);
|
|
284
|
+
}
|
|
285
|
+
const edgeKey = (pre, post) => `${stripRenamedSuffix(pre)}->${stripRenamedSuffix(post)}`;
|
|
286
|
+
const actualRules = new Map(actual.rules.map((row) => [edgeKey(row.pre, row.post), row]));
|
|
287
|
+
for (const row of expected.rules) {
|
|
288
|
+
const current = actualRules.get(edgeKey(row.pre, row.post));
|
|
289
|
+
if (!current)
|
|
290
|
+
issues.push(`缺连线 ${row.pre}→${row.post}`);
|
|
291
|
+
else {
|
|
292
|
+
if (row.points !== undefined && JSON.stringify(current.points) !== JSON.stringify(row.points))
|
|
293
|
+
issues.push(`连线 ${row.pre}→${row.post} 坐标不同`);
|
|
294
|
+
for (const key of ["ruleConditionGroup", "childMatchType", "formula", "utilizeElse", "alternativeFlag", "text"])
|
|
295
|
+
if (row[key] !== undefined && JSON.stringify(current[key] ?? null) !== JSON.stringify(row[key]))
|
|
296
|
+
issues.push(`连线 ${row.pre}→${row.post} 的 ${key} 不同`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return issues.length > 0 ? issues.slice(0, 8).join(";") : "平台已补全/规范化,无声明面差异";
|
|
300
|
+
}
|
|
301
|
+
/** Parse the optional `bindRules` list into normalized bindings. */
|
|
302
|
+
function readBindings(seqNode, base, ctx) {
|
|
303
|
+
if (seqNode === undefined || (seqNode.kind === "scalar" && seqNode.value === null))
|
|
304
|
+
return [];
|
|
305
|
+
if (seqNode.kind !== "seq") {
|
|
306
|
+
pushError(ctx, "intent-workflow-bindrules-invalid", "workflow.bindRules 必须是列表", base, "value");
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
const bindings = [];
|
|
310
|
+
seqNode.items.forEach((item, index) => {
|
|
311
|
+
const itemBase = `${base}[${index}]`;
|
|
312
|
+
if (item.kind !== "map") {
|
|
313
|
+
pushError(ctx, "intent-workflow-binding-not-a-map", "bindRules 项必须是映射(只接受 handler / ruleCodes)", itemBase, "value");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
for (const entry of item.entries)
|
|
317
|
+
if (!BINDING_KEYS.has(entry.key))
|
|
318
|
+
pushError(ctx, "intent-key-unknown", `bindRules 项不支持键 "${entry.key}"(只接受 handler / ruleCodes)`, `${itemBase}.${entry.key}`);
|
|
319
|
+
const handlerNode = item.entries.find((entry) => entry.key === "handler")?.value;
|
|
320
|
+
const codesNode = item.entries.find((entry) => entry.key === "ruleCodes")?.value;
|
|
321
|
+
const handler = isScalarString(handlerNode) ? handlerNode.value.trim() : "";
|
|
322
|
+
if (!HANDLER_NAMES.includes(handler))
|
|
323
|
+
pushError(ctx, "intent-workflow-binding-handler-invalid", `bindRules[].handler 必须是 ${HANDLER_NAMES.join(" / ")}(得到 "${handler}")`, `${itemBase}.handler`, "value");
|
|
324
|
+
const validCodes = codesNode?.kind === "seq" && codesNode.items.length > 0 && codesNode.items.every((entry) => isScalarString(entry) && entry.value.trim().length > 0);
|
|
325
|
+
if (!validCodes)
|
|
326
|
+
pushError(ctx, "intent-workflow-binding-codes-invalid", "bindRules[].ruleCodes 必须是非空字符串列表", `${itemBase}.ruleCodes`, "value");
|
|
327
|
+
if (HANDLER_NAMES.includes(handler) && validCodes)
|
|
328
|
+
bindings.push({ handler: handler, ruleCodes: codesNode.items.map((entry) => entry.value.trim()) });
|
|
329
|
+
});
|
|
330
|
+
return bindings;
|
|
331
|
+
}
|
|
332
|
+
export const workflowCapability = {
|
|
333
|
+
id: "workflow",
|
|
334
|
+
title: "流程模板",
|
|
335
|
+
requires: ["model"],
|
|
336
|
+
operations: ["create", "update", "publish"],
|
|
337
|
+
validate(node, draft, ctx) {
|
|
338
|
+
const workflowNode = node.entries.find((entry) => entry.key === "workflow")?.value;
|
|
339
|
+
if (workflowNode === undefined || (workflowNode.kind === "scalar" && workflowNode.value === null))
|
|
340
|
+
return;
|
|
341
|
+
if (workflowNode.kind !== "map") {
|
|
342
|
+
pushError(ctx, "intent-workflow-not-a-map", "workflow 必须是映射", `${ctx.base}.workflow`, "value");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
for (const entry of workflowNode.entries)
|
|
346
|
+
if (!WORKFLOW_KEYS.has(entry.key))
|
|
347
|
+
pushError(ctx, "intent-key-unknown", `workflow 不支持键 "${entry.key}"(只接受 code / name / publish / template / bindRules)`, `${ctx.base}.workflow.${entry.key}`);
|
|
348
|
+
const codeNode = workflowNode.entries.find((entry) => entry.key === "code")?.value;
|
|
349
|
+
const nameNode = workflowNode.entries.find((entry) => entry.key === "name")?.value;
|
|
350
|
+
const publishNode = workflowNode.entries.find((entry) => entry.key === "publish")?.value;
|
|
351
|
+
const templateNode = workflowNode.entries.find((entry) => entry.key === "template")?.value;
|
|
352
|
+
const bindRulesNode = workflowNode.entries.find((entry) => entry.key === "bindRules")?.value;
|
|
353
|
+
if (!isScalarString(codeNode) || codeNode.value.trim().length === 0)
|
|
354
|
+
pushError(ctx, "intent-workflow-code-invalid", "workflow.code 必须是非空字符串", `${ctx.base}.workflow.code`, "value");
|
|
355
|
+
if (!isScalarString(nameNode) || nameNode.value.trim().length === 0)
|
|
356
|
+
pushError(ctx, "intent-workflow-name-invalid", "workflow.name 必须是非空字符串", `${ctx.base}.workflow.name`, "value");
|
|
357
|
+
if (publishNode === undefined || publishNode.kind !== "scalar" || typeof publishNode.value !== "boolean")
|
|
358
|
+
pushError(ctx, "intent-workflow-publish-invalid", "workflow.publish 必须是布尔", `${ctx.base}.workflow.publish`, "value");
|
|
359
|
+
if (templateNode !== undefined && templateNode.kind !== "map")
|
|
360
|
+
pushError(ctx, "intent-workflow-template-invalid", "workflow.template 必须是映射", `${ctx.base}.workflow.template`, "value");
|
|
361
|
+
if (publishNode?.kind === "scalar" && publishNode.value === true && templateNode === undefined)
|
|
362
|
+
pushError(ctx, "intent-workflow-template-required", "workflow.publish:true 必须同时声明 template(流程发布需要完整模板)", `${ctx.base}.workflow.template`, "value");
|
|
363
|
+
const bindRules = readBindings(bindRulesNode, `${ctx.base}.workflow.bindRules`, ctx);
|
|
364
|
+
// Workflow event handlers can only bind CUSTOM rules — the platform's rule
|
|
365
|
+
// picker does not offer preset trigger rows (Create/Update/…, which already
|
|
366
|
+
// run on the data event). Catch it offline so apply never tries (bench59).
|
|
367
|
+
for (const binding of bindRules)
|
|
368
|
+
for (const boundCode of binding.ruleCodes)
|
|
369
|
+
if (PLATFORM_TRIGGER_RULE_CODES.has(boundCode))
|
|
370
|
+
pushError(ctx, "intent-workflow-binding-preset-rule", `workflow.bindRules 绑定了平台预置触发规则 "${boundCode}"——流程事件规则选择器只提供自定义规则。要「流程事件触发某逻辑」,请新建自定义规则(自定义 code)承载动作节点再绑定;或把动作直接追加到预置 Create/Update 规则。`, `${ctx.base}.workflow.bindRules`, "value");
|
|
371
|
+
if (templateNode?.kind === "map")
|
|
372
|
+
validateWorkflowTemplate(toPlain(templateNode), `${ctx.base}.workflow.template`, ctx);
|
|
373
|
+
if (isScalarString(codeNode) && isScalarString(nameNode) && publishNode?.kind === "scalar" && typeof publishNode.value === "boolean") {
|
|
374
|
+
draft.workflow = {
|
|
375
|
+
code: codeNode.value.trim(),
|
|
376
|
+
name: nameNode.value.trim(),
|
|
377
|
+
publish: publishNode.value,
|
|
378
|
+
...(templateNode?.kind === "map" ? { template: toPlain(templateNode) } : {}),
|
|
379
|
+
...(bindRules.length > 0 ? { bindRules } : {}),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
async read(session, intent, draft) {
|
|
384
|
+
const workflow = intent.workflow;
|
|
385
|
+
if (!workflow)
|
|
386
|
+
return;
|
|
387
|
+
const draftTemplate = await getOrNull(session, ENDPOINTS.workflowGetDraft, workflow.code);
|
|
388
|
+
const publishedTemplate = await getOrNull(session, ENDPOINTS.workflowGetPublished, workflow.code);
|
|
389
|
+
const published = publishedOf(publishedTemplate);
|
|
390
|
+
draft.workflow = {
|
|
391
|
+
present: draftTemplate !== null || publishedTemplate !== null,
|
|
392
|
+
published,
|
|
393
|
+
template: canonicalizeWorkflowTemplate((intent.workflow?.publish ? publishedTemplate ?? draftTemplate : draftTemplate ?? publishedTemplate) ?? undefined),
|
|
394
|
+
// Bindings are read from the DRAFT: workflow.bindRule merges into the draft
|
|
395
|
+
// then publishes, so the draft is the authoritative binding face.
|
|
396
|
+
bindings: bindingsOf(draftTemplate),
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
plan(intent, baseline, emit) {
|
|
400
|
+
const workflow = intent.workflow;
|
|
401
|
+
if (!workflow)
|
|
402
|
+
return;
|
|
403
|
+
const current = baseline.workflow;
|
|
404
|
+
const resource = `workflow:${intent.code}.${workflow.code}`;
|
|
405
|
+
const missing = current?.present !== true;
|
|
406
|
+
const templateChanged = workflow.template !== undefined && !workflowMatches(current?.template, workflow.template);
|
|
407
|
+
if (missing) {
|
|
408
|
+
emit.change("workflow.create", { schemaCode: intent.code, name: workflow.name, code: workflow.code }, resource, "workflow-absent");
|
|
409
|
+
if (workflow.template !== undefined)
|
|
410
|
+
emit.change("workflow.update", { schemaCode: intent.code, code: workflow.code, template: workflow.template }, resource, "workflow-template-declared");
|
|
411
|
+
}
|
|
412
|
+
else if (templateChanged) {
|
|
413
|
+
emit.change("workflow.update", { schemaCode: intent.code, code: workflow.code, template: workflow.template }, resource, "workflow-template-drift");
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
emit.skip(resource, "workflow-present");
|
|
417
|
+
}
|
|
418
|
+
if (workflow.publish && (missing || templateChanged || current?.published !== true))
|
|
419
|
+
emit.change("workflow.publish", { schemaCode: intent.code, code: workflow.code, ...(workflow.template !== undefined ? { template: workflow.template } : {}) }, resource, current?.published === true ? "workflow-template-drift" : "workflow-not-published");
|
|
420
|
+
// Rule bindings: ensure-present. `workflow.bindRule` merges, so we add only
|
|
421
|
+
// the codes missing from the draft and never touch undeclared ones. Runs
|
|
422
|
+
// after create/update/publish because the workflow must exist first.
|
|
423
|
+
for (const binding of workflow.bindRules ?? []) {
|
|
424
|
+
const bindingResource = `workflow-binding:${intent.code}.${workflow.code}.${binding.handler}`;
|
|
425
|
+
const liveCodes = current?.bindings?.[binding.handler] ?? [];
|
|
426
|
+
const missingCodes = binding.ruleCodes.filter((code) => !liveCodes.includes(code));
|
|
427
|
+
if (missingCodes.length > 0)
|
|
428
|
+
emit.change("workflow.bindRule", { schemaCode: intent.code, workflowCode: workflow.code, ruleCodes: missingCodes, handler: binding.handler, publish: workflow.publish }, bindingResource, "workflow-rule-not-bound");
|
|
429
|
+
else
|
|
430
|
+
emit.skip(bindingResource, "workflow-rule-bound");
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
decode(baseline, draft) {
|
|
434
|
+
if (baseline.workflow)
|
|
435
|
+
draft.workflow = { present: baseline.workflow.present, published: baseline.workflow.published, template: baseline.workflow.template, bindings: baseline.workflow.bindings };
|
|
436
|
+
},
|
|
437
|
+
compare(intent, live, emit) {
|
|
438
|
+
const workflow = intent.workflow;
|
|
439
|
+
if (!workflow)
|
|
440
|
+
return;
|
|
441
|
+
const current = live.workflow;
|
|
442
|
+
const resource = `workflow:${intent.code}.${workflow.code}`;
|
|
443
|
+
if (!current?.present) {
|
|
444
|
+
emit.difference("workflow-missing", resource, `流程 ${intent.code}.${workflow.code} 不存在`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (workflow.template !== undefined && !workflowMatches(current.template, workflow.template))
|
|
448
|
+
emit.difference("workflow-template-mismatch", resource, `流程 ${intent.code}.${workflow.code} 的活动/连线不等价:${workflowMismatchDetail(current.template, workflow.template)}`);
|
|
449
|
+
if (workflow.publish && !current.published)
|
|
450
|
+
emit.difference("workflow-not-published", resource, `流程 ${intent.code}.${workflow.code} 未发布`);
|
|
451
|
+
for (const binding of workflow.bindRules ?? []) {
|
|
452
|
+
const liveCodes = current.bindings?.[binding.handler] ?? [];
|
|
453
|
+
if (binding.ruleCodes.some((code) => !liveCodes.includes(code)))
|
|
454
|
+
emit.difference("workflow-rule-not-bound", `workflow-binding:${intent.code}.${workflow.code}.${binding.handler}`, `流程 ${intent.code}.${workflow.code} 的 ${binding.handler} 未绑定全部声明规则`);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ToolRefusalError } from "../tool-types.js";
|
|
2
|
+
/** The refusal every assembler and capability shares (exact historical shape). */
|
|
3
|
+
export function refuse(blocker, message, extra = {}) {
|
|
4
|
+
throw new ToolRefusalError({ status: "refused", blocker, message, ...extra });
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { assertSafeInputPath } from "../artifacts.js";
|
|
4
|
+
import { CLI_ARGS_MAX_BYTES } from "../call.js";
|
|
5
|
+
import { INTENT_VERSION, parseIntent, YamlSyntaxError, IntentInvalidError } from "./dsl.js";
|
|
6
|
+
/**
|
|
7
|
+
* Ticket 117 — the `omc intent` command family (Spec 107 usage face).
|
|
8
|
+
* validate is fully offline; plan/apply/readback land in tickets 118-120.
|
|
9
|
+
*/
|
|
10
|
+
export const INTENT_COMPILER_VERSION = "omc-intent-compiler/1";
|
|
11
|
+
export function readIntentFile(root, path) {
|
|
12
|
+
const safePath = assertSafeInputPath(root, path, "intent file");
|
|
13
|
+
const content = readFileSync(safePath, "utf8");
|
|
14
|
+
if (Buffer.byteLength(content) > CLI_ARGS_MAX_BYTES)
|
|
15
|
+
throw new Error(`intent file exceeds ${CLI_ARGS_MAX_BYTES} bytes`);
|
|
16
|
+
return content;
|
|
17
|
+
}
|
|
18
|
+
/** Validate an intent file's text offline (zero environment contact). */
|
|
19
|
+
export function validateIntentText(text) {
|
|
20
|
+
try {
|
|
21
|
+
const { intent } = parseIntent(text);
|
|
22
|
+
return { status: "ok", valid: true, errors: [], intent };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof YamlSyntaxError)
|
|
26
|
+
return {
|
|
27
|
+
status: "error",
|
|
28
|
+
valid: false,
|
|
29
|
+
errors: [{ code: "yaml-syntax-error", message: error.message, path: "", line: error.line, column: error.column }],
|
|
30
|
+
};
|
|
31
|
+
if (error instanceof IntentInvalidError)
|
|
32
|
+
return { status: "error", valid: false, errors: error.errors };
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The intent-face capability block surfaced by `omc commands --json`. */
|
|
37
|
+
export function intentCapabilityBlock() {
|
|
38
|
+
return {
|
|
39
|
+
intentVersion: INTENT_VERSION,
|
|
40
|
+
compiler: INTENT_COMPILER_VERSION,
|
|
41
|
+
commands: ["intent validate <file> [--show-intent]", "intent plan <file> --env <env> (大 plan 自动外置,回 planFile;用 intent apply <planFile>)", "intent apply <planFile|plan.json> [--no-verify]", "intent readback <file> --env <env>"],
|
|
42
|
+
lifecycle: "model -> field create (incl. CHILD_TABLE subFields) -> field publish -> form draft/publish -> listview -> businessrule (rule.save + rule.enable/disable); apply confirms the final readback by default",
|
|
43
|
+
declarativeCapabilities: { model: "partial", field: "partial", businessrule: "partial", datarule: "partial", workflow: "partial", onlinejs: "partial", bizservice: "partial", report: "partial" },
|
|
44
|
+
absence: "models[].absent:true deletes a whole model; fields[]/listview/datarules[] take absent:true too. Only declared-absent resources are deleted; riskClass becomes destructive and plan.cleanup lists them.",
|
|
45
|
+
workflowBindings: "models[].workflow.bindRules: [{handler: start|cancel|activate|end, ruleCodes:[...]}] ensures the rules are bound (workflow.bindRule MERGES; undeclared bindings are ignored).",
|
|
46
|
+
servicesNote: "top-level services[] registers a new service (optional one method) or patches an existing one; adding a method to an existing service has no declarative tool yet.",
|
|
47
|
+
reportsNote: "top-level reports[] creates/configures a report page + BI dashboard (global/attributes/charts); charts diff by uuid, removeUnlistedCharts:true deletes undeclared charts. Only declared reports are touched.",
|
|
48
|
+
applyVerification: {
|
|
49
|
+
default: "on",
|
|
50
|
+
semantics: "after a DELIVERED apply, decode the authoritative live state back into intent shape; equivalent=true -> verification.status=ok; otherwise status=refused + blocker intent-readback-mismatch (exit 12)",
|
|
51
|
+
optOut: "--no-verify",
|
|
52
|
+
},
|
|
53
|
+
fieldFamilies: [
|
|
54
|
+
"SHORT_TEXT",
|
|
55
|
+
"LONG_TEXT",
|
|
56
|
+
"NUMERICAL",
|
|
57
|
+
"DATE",
|
|
58
|
+
"LOGICAL",
|
|
59
|
+
"SELECTION",
|
|
60
|
+
"ATTACHMENT",
|
|
61
|
+
"CHILD_TABLE",
|
|
62
|
+
"WORK_SHEET",
|
|
63
|
+
"ADDRESS",
|
|
64
|
+
"MULT_WORK_SHEET",
|
|
65
|
+
"RADIO",
|
|
66
|
+
"CHECKBOX",
|
|
67
|
+
"DROPDOWN_BOX",
|
|
68
|
+
"DROPDOWN_MULTI_BOX",
|
|
69
|
+
"RELEVANCE_DATA",
|
|
70
|
+
"HYPER_LINK",
|
|
71
|
+
"TIME",
|
|
72
|
+
"STAFF_SELECTOR",
|
|
73
|
+
"STAFF_MULTI_SELECTOR",
|
|
74
|
+
"DEPARTMENT_SELECTOR",
|
|
75
|
+
"DEPARTMENT_MULTI_SELECTOR",
|
|
76
|
+
"FORMULA",
|
|
77
|
+
],
|
|
78
|
+
constraints: [
|
|
79
|
+
"ensure-present semantics: resources outside the intent are never deleted or modified",
|
|
80
|
+
"type drift on an existing field is refused (use the omc call escape hatch)",
|
|
81
|
+
"CHILD_TABLE fields declare their child columns as subFields[{code,name,type[,choices]}]; child families are scalars only (no nested child table / relation / formula)",
|
|
82
|
+
"a rule declares actions (compact UPDATE) or nodeGraph, never both; non-UPDATE node types use nodeGraph",
|
|
83
|
+
"escape hatch: uncovered capabilities stay available through omc call or a raw nodeGraph",
|
|
84
|
+
],
|
|
85
|
+
example: 'intentVersion: 1\nmodels:\n - code: eval_m1\n name: 评测建模\n fields:\n - code: f_title\n name: 标题\n type: SHORT_TEXT\n - code: items\n name: 明细\n type: CHILD_TABLE\n subFields:\n - code: item_name\n name: 项目\n type: SHORT_TEXT\n form:\n publish: true\n',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function intentConfigRoot() {
|
|
89
|
+
return resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical value serialization for intent equivalence checks. Two values that
|
|
3
|
+
* differ only by key order, or by whether a JSON column is still a string or
|
|
4
|
+
* already parsed, must compare equal — otherwise an already-converged
|
|
5
|
+
* environment would report perpetual drift.
|
|
6
|
+
*
|
|
7
|
+
* `undefined` and `null` deliberately share one canonical form so an omitted
|
|
8
|
+
* optional field equals a platform-supplied null/default.
|
|
9
|
+
*/
|
|
10
|
+
export function canonicalValue(value) {
|
|
11
|
+
if (value === undefined || value === null)
|
|
12
|
+
return "null";
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(canonicalValue).join(",")}]`;
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(value);
|
|
18
|
+
if (parsed !== null && typeof parsed === "object")
|
|
19
|
+
return canonicalValue(parsed);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A plain scalar string (URL, name, code) stays a scalar.
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "object") {
|
|
27
|
+
return `{${Object.entries(value)
|
|
28
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
29
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalValue(entry)}`)
|
|
30
|
+
.join(",")}}`;
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
/** True when two values are equivalent under `canonicalValue`. */
|
|
35
|
+
export function sameValue(left, right) {
|
|
36
|
+
return canonicalValue(left) === canonicalValue(right);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Equivalence for an optional field: a missing value equals the platform's
|
|
40
|
+
* `false` default in EITHER direction. The real platform stores `childRule:false`
|
|
41
|
+
* on live rows while an intent omits the key, so `(false, undefined)` must equal
|
|
42
|
+
* as well as `(undefined, false)` — otherwise a converged environment reports
|
|
43
|
+
* perpetual drift (a real-platform-only case the fake hid by omitting the key).
|
|
44
|
+
*/
|
|
45
|
+
export function optionalSame(left, right) {
|
|
46
|
+
const leftAbsent = left === undefined || left === null;
|
|
47
|
+
const rightAbsent = right === undefined || right === null;
|
|
48
|
+
if (leftAbsent && rightAbsent)
|
|
49
|
+
return true;
|
|
50
|
+
if (leftAbsent && right === false)
|
|
51
|
+
return true;
|
|
52
|
+
if (rightAbsent && left === false)
|
|
53
|
+
return true;
|
|
54
|
+
return sameValue(left, right);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=compare.js.map
|