@stalfh233/omc-cli 0.0.0-stage → 0.4.10

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.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. package/skills/omc-write-hazards.md +87 -0
@@ -0,0 +1,493 @@
1
+ import { ENDPOINTS } from "../../endpoints.js";
2
+ import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "../../json-column.js";
3
+ import { wireCall, wireRows } from "../../wire.js";
4
+ import { countConditionLeaves, prepareRuleGraph, RULE_IDENTITY_FILTER, validateRuleNodeGraph } from "../../lanes/rule-graph.js";
5
+ import { canonicalJson } from "../dsl.js";
6
+ import { isScalarString, pushError } from "../validation.js";
7
+ import { toPlain } from "../yaml.js";
8
+ /** Keys a rule item accepts. */
9
+ const RULE_KEYS = new Set(["code", "name", "enabled", "bizRuleType", "customTrigger", "actions", "nodeGraph"]);
10
+ /** Keys a compact action accepts. */
11
+ const ACTION_KEYS = new Set(["type", "target", "filter", "when", "set"]);
12
+ /** Keys a compact filter/assignment item accepts. */
13
+ const LEAF_KEYS = new Set(["field", "from", "equals", "notEquals", "gt", "gte", "lt", "lte", "value"]);
14
+ /**
15
+ * Compact-condition operator keys → the platform's `ruleConditionType`. `equals`
16
+ * existed alone before round 14 (FIXED EQ); the comparison operators were added
17
+ * because a threshold rule (`amount > 5000`) is otherwise INEXPRESSIBLE on the
18
+ * compact face and forced callers into a hand-built `nodeGraph` (bench37: ~46 KB
19
+ * of validate/retry churn from exactly that). `from` stays the DYNAMIC (EQ) form.
20
+ */
21
+ const CONDITION_OPERATORS = Object.freeze({
22
+ equals: "EQ",
23
+ notEquals: "NEQ",
24
+ gt: "GT",
25
+ gte: "GTEQ",
26
+ lt: "LT",
27
+ lte: "LTEQ",
28
+ });
29
+ /** Geometry/display keys ignored when comparing a synthesized graph to the live one. */
30
+ const GEOMETRY_KEYS = new Set(["x", "y", "width", "height", "points", "originHeight", "originWidth", "edit"]);
31
+ function stripGeometry(value) {
32
+ if (Array.isArray(value))
33
+ return value.map(stripGeometry);
34
+ if (value && typeof value === "object") {
35
+ const out = {};
36
+ for (const [key, entry] of Object.entries(value))
37
+ if (!GEOMETRY_KEYS.has(key))
38
+ out[key] = stripGeometry(entry);
39
+ return out;
40
+ }
41
+ return value;
42
+ }
43
+ /**
44
+ * Canonicalize a route for digesting: strip geometry and apply the same
45
+ * defaults `ensureRouteShape` applies to the wire (`routeCondition` absent ->
46
+ * true, `matchType` absent/empty -> "ANY"), so an equivalent live route that
47
+ * omits those keys is not reported as drift. An explicit `false`/custom value
48
+ * is preserved.
49
+ */
50
+ function canonicalRoute(route) {
51
+ const stripped = stripGeometry(route);
52
+ if (!stripped || typeof stripped !== "object" || Array.isArray(stripped))
53
+ return stripped;
54
+ const out = { ...stripped };
55
+ if (out.routeCondition === undefined || out.routeCondition === null)
56
+ out.routeCondition = true;
57
+ if (out.matchType === undefined || out.matchType === null || String(out.matchType).trim() === "")
58
+ out.matchType = "ANY";
59
+ return out;
60
+ }
61
+ /**
62
+ * Semantic digest of a rule graph: geometry/display-only keys removed, nodes
63
+ * sorted by nodeCode, routes defaulted and sorted by their canonical form.
64
+ * Stable across the platform's cosmetic normalization (layout, route ordering,
65
+ * omitted route defaults), so readback compares intent, not canvas pixels.
66
+ */
67
+ export function ruleGraphDigest(nodes, routes) {
68
+ const nodeList = Array.isArray(nodes)
69
+ ? [...nodes].sort((a, b) => String(a?.nodeCode ?? "").localeCompare(String(b?.nodeCode ?? "")))
70
+ : [];
71
+ const routeList = (Array.isArray(routes) ? routes : []).map(canonicalRoute).sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
72
+ return canonicalJson({ nodes: stripGeometry(nodeList) ?? [], routes: routeList });
73
+ }
74
+ function conditionLeaf(condition) {
75
+ const raw = condition;
76
+ if (condition.from !== undefined)
77
+ return { targetSchemaDataItem: condition.field, ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: condition.from };
78
+ const operator = Object.keys(CONDITION_OPERATORS).find((key) => raw[key] !== undefined) ?? "equals";
79
+ return { targetSchemaDataItem: condition.field, ruleConditionType: CONDITION_OPERATORS[operator], ruleValueType: "FIXED", value: raw[operator] ?? null };
80
+ }
81
+ /**
82
+ * A `when` leaf compiles to the node's EXECUTION GATE (`dataCondition`), whose
83
+ * leaves use the platform's `Condition`/`DataFilterCondition` shape: the left
84
+ * value is `currentSchemaDataItem` (the input/current-data field) and the
85
+ * operator is `ruleDataConditionType` — NOT `filterCondition`'s
86
+ * `targetSchemaDataItem` + `ruleConditionType`. Verified against 8.6.26
87
+ * `DataCondition.java` (conditionGroup: List<List<DataFilterCondition>>),
88
+ * `DataFilterCondition.java` (ruleDataConditionType) and `Condition.java`
89
+ * (currentSchemaDataItem / ruleValueType / value).
90
+ */
91
+ function gateConditionLeaf(condition) {
92
+ const raw = condition;
93
+ const operator = Object.keys(CONDITION_OPERATORS).find((key) => raw[key] !== undefined) ?? "equals";
94
+ return { currentSchemaDataItem: condition.field, ruleDataConditionType: CONDITION_OPERATORS[operator], ruleValueType: "FIXED", value: raw[operator] ?? null };
95
+ }
96
+ function assignment(action) {
97
+ if (action.from !== undefined)
98
+ return { targetDataItem: action.field, ruleValueType: "DYNAMIC", value: "", ruleActionType: "EQUALS", currentDataItemValue: action.from };
99
+ return { targetDataItem: action.field, ruleValueType: "FIXED", value: action.value ?? null, ruleActionType: "EQUALS" };
100
+ }
101
+ /**
102
+ * Synthesize the raw node graph from compact actions: a linear
103
+ * Start → UPDATE_1..N → End chain. The platform's own normalization
104
+ * (prepareRuleGraph: preset system node / custom trigger / layout / route
105
+ * geometry) is applied on top, exactly as `rule.save` will apply it again.
106
+ */
107
+ function graphFromActions(actions, modelCode) {
108
+ const nodes = [{ nodeCode: "Start", nodeName: "开始", nodeType: "START" }];
109
+ actions.forEach((action, index) => {
110
+ const filter = action.filter ?? [];
111
+ const when = action.when ?? [];
112
+ // `filter` is the row LOCATOR (filterCondition). An omitted filter on a
113
+ // SAME-MODEL action defaults to the self-identity match (`id = id`) — the
114
+ // "update the current record" intent. A cross-model action must name its
115
+ // locator explicitly; an empty one is refused fail-closed by lintRuleGraph
116
+ // (empty filterCondition = unconditional whole-target-table update).
117
+ const filterCondition = filter.length > 0
118
+ ? { mainConditions: [filter.map(conditionLeaf)], sheetConditions: [] }
119
+ : action.target === modelCode
120
+ ? RULE_IDENTITY_FILTER
121
+ : { mainConditions: [], sheetConditions: [] };
122
+ nodes.push({
123
+ nodeCode: `Update_${index + 1}`,
124
+ nodeName: "数据更新",
125
+ nodeType: "UPDATE",
126
+ targetObjectCode: action.target,
127
+ inputParam: "_input",
128
+ updateNodeType: "UPDATE_MODEL",
129
+ execFormMapping: false,
130
+ // `when` is the node EXECUTION GATE (`dataCondition`): the node runs only
131
+ // when the input data satisfies it. Distinct from `filter` (which rows).
132
+ ...(when.length > 0 ? { dataCondition: { conditionGroup: [when.map(gateConditionLeaf)] } } : {}),
133
+ filterCondition,
134
+ dataActions: action.set.map(assignment),
135
+ });
136
+ });
137
+ nodes.push({ nodeCode: "End", nodeName: "结束", nodeType: "END" });
138
+ const routes = nodes.slice(0, -1).map((node, index) => ({ preNode: String(node.nodeCode), postNode: String(nodes[index + 1].nodeCode) }));
139
+ return { nodes, routes };
140
+ }
141
+ /**
142
+ * The graph the compiler expects on the platform for a declared rule.
143
+ * `modelCode` (the declaring model) lets a compact same-model action default its
144
+ * locator to self-identity; it is optional because a raw `nodeGraph` needs no
145
+ * synthesis (and callers that only compare digests still pass it).
146
+ */
147
+ export function expectedRuleGraph(rule, modelCode = "") {
148
+ const raw = rule.nodeGraph ?? graphFromActions(rule.actions ?? [], modelCode);
149
+ return prepareRuleGraph(rule.code, { ...raw, ...(rule.bizRuleType !== undefined ? { bizRuleType: rule.bizRuleType } : {}) }, rule.customTrigger, modelCode || undefined).graph;
150
+ }
151
+ /**
152
+ * Offline dry-lint of a declared rule's PREPARED graph. Runs the exact validator
153
+ * `rule.save` runs at apply time, so `intent validate`/`plan` fail with the same
154
+ * blockers before any approval or wire (2026-09-27: a CREATE node missing
155
+ * `originatorValueType` passed plan and only failed at apply). Pure — no
156
+ * environment contact. Shape errors are already reported by the readers, so a
157
+ * prepare throw is swallowed rather than double-reported.
158
+ */
159
+ function lintRuleGraph(rule, ctx, base, schemaCode) {
160
+ try {
161
+ const graph = expectedRuleGraph(rule, schemaCode);
162
+ for (const blocker of validateRuleNodeGraph(graph, rule.code, schemaCode)) {
163
+ // Warning-severity findings (e.g. cross-model-unverified, defaulted keys)
164
+ // ride the payload but never gate — same rule the tool preflight applies.
165
+ if (blocker.severity === "warning")
166
+ continue;
167
+ pushError(ctx, blocker.code, blocker.message, `${base}.${rule.nodeGraph ? "nodeGraph" : "actions"}`, "value");
168
+ }
169
+ // Cross-model UPDATE with an empty filter is fail-closed on the declarative
170
+ // face: the platform semantic is "find the target rows matching these
171
+ // conditions" (empty ⇒ update the WHOLE target table). The tool's own lint
172
+ // rates this a warning (legal at the wire), but for a target model OTHER than
173
+ // the rule's own, an unconditional update is almost certainly the "write back
174
+ // to the related row" intent with the locate-filter forgotten — bench52/53
175
+ // saved+publish passed and the runtime silently wrote nothing, costing a full
176
+ // end-to-end cycle each. Make the declarative face require the filter.
177
+ for (const raw of graph.nodes ?? []) {
178
+ if (String(raw.nodeType ?? "") !== "UPDATE")
179
+ continue;
180
+ const target = typeof raw.targetObjectCode === "string" ? raw.targetObjectCode : "";
181
+ const crossModel = target.length > 0 && target !== schemaCode && !target.startsWith(`${schemaCode}.`);
182
+ if (crossModel && countConditionLeaves(raw.filterCondition) === 0)
183
+ pushError(ctx, "intent-rule-cross-model-filter-required", `跨模型 UPDATE 节点 ${String(raw.nodeCode ?? "")}(目标 ${target})的 filterCondition 为空——空条件=无条件下更新目标模型整表。回写关联行的规则必须给出定位条件,例如 filter: [{field: id, from: <本模型保存目标 id 的字段>}]`, `${base}.${rule.nodeGraph ? "nodeGraph" : "actions"}`, "value");
184
+ }
185
+ }
186
+ catch {
187
+ /* shape errors already reported */
188
+ }
189
+ }
190
+ /** The exact `rule.save` args for a declared rule (nodeGraph is the prepared graph). */
191
+ function ruleSaveArgs(schemaCode, rule) {
192
+ return {
193
+ schemaCode,
194
+ ruleCode: rule.code,
195
+ name: rule.name,
196
+ nodeGraph: expectedRuleGraph(rule, schemaCode),
197
+ ...(rule.customTrigger !== undefined ? { customTrigger: rule.customTrigger } : {}),
198
+ };
199
+ }
200
+ /**
201
+ * Read a compact condition/assignment leaf off a YAML map.
202
+ *
203
+ * Fail-closed (round 14): before, a leaf with `field` but no `from`/`equals`
204
+ * silently became `{..., value: null}` and the platform normalized it to
205
+ * "field EQ null" — a rule that saves + publishes fine but never matches
206
+ * (bench37: an agent wrote a threshold intent, got `EQ null`, and burned ~46 KB
207
+ * of validate/apply retries chasing it). A condition now needs EXACTLY ONE of
208
+ * `from` (DYNAMIC EQ) or an operator key; an assignment needs `from` XOR `value`.
209
+ * Zero or ambiguous sources are a semantic error, never a silent null.
210
+ */
211
+ function readLeaf(leaf, base, ctx, kind) {
212
+ for (const entry of leaf.entries)
213
+ if (!LEAF_KEYS.has(entry.key))
214
+ pushError(ctx, "intent-key-unknown", `${kind} 不支持键 "${entry.key}"(条件接受 field / from / equals / notEquals / gt / gte / lt / lte;赋值接受 field / from / value)`, `${base}.${entry.key}`);
215
+ const fieldNode = leaf.entries.find((entry) => entry.key === "field")?.value;
216
+ const fromNode = leaf.entries.find((entry) => entry.key === "from")?.value;
217
+ if (!isScalarString(fieldNode))
218
+ pushError(ctx, "intent-rule-leaf-field-invalid", `${kind} 缺 field(非空字符串)`, `${base}.field`, "value");
219
+ if (fromNode !== undefined && !isScalarString(fromNode))
220
+ pushError(ctx, "intent-rule-leaf-from-invalid", `${kind}.from 必须是非空字符串(触发字段 code)`, `${base}.from`, "value");
221
+ const literalKeys = kind === "condition" ? Object.keys(CONDITION_OPERATORS) : ["value"];
222
+ const present = literalKeys.filter((key) => leaf.entries.some((entry) => entry.key === key));
223
+ for (const key of present) {
224
+ const node = leaf.entries.find((entry) => entry.key === key).value;
225
+ const value = node.kind === "scalar" ? node.value : undefined;
226
+ if (value !== undefined && value !== null && typeof value === "object")
227
+ pushError(ctx, "intent-rule-leaf-literal-invalid", `${kind}.${key} 必须是标量`, `${base}.${key}`, "value");
228
+ }
229
+ const sources = present.length + (fromNode !== undefined ? 1 : 0);
230
+ if (sources === 0)
231
+ pushError(ctx, "intent-rule-leaf-source-missing", kind === "condition"
232
+ ? `filter 项缺条件来源:给 from: <字段>(DYNAMIC)或一个比较键(equals/notEquals/gt/gte/lt/lte: <值>)。空来源会被平台归一化成 "EQ null",规则永不命中`
233
+ : `set 项缺赋值来源:给 value: <值>(FIXED)或 from: <字段>(DYNAMIC)`, base, "value");
234
+ else if (sources > 1)
235
+ pushError(ctx, "intent-rule-leaf-source-ambiguous", `${kind} 只能有一个来源(from 或 ${literalKeys.join("/")} 之一),得到 ${sources} 个`, base, "value");
236
+ if (!isScalarString(fieldNode))
237
+ return null;
238
+ if (sources !== 1)
239
+ return { field: fieldNode.value.trim() };
240
+ if (fromNode !== undefined)
241
+ return { field: fieldNode.value.trim(), from: fromNode.value.trim() };
242
+ const operator = present[0];
243
+ const literalNode = leaf.entries.find((entry) => entry.key === operator).value;
244
+ const literal = literalNode.kind === "scalar" ? literalNode.value : null;
245
+ return { field: fieldNode.value.trim(), [operator]: literal };
246
+ }
247
+ function readActions(actionsNode, base, ctx, modelCode) {
248
+ if (actionsNode.kind !== "seq") {
249
+ pushError(ctx, "intent-rule-actions-not-a-list", `actions 必须是列表(得到 ${actionsNode.kind})`, `${base}.actions`, "value");
250
+ return [];
251
+ }
252
+ const actions = [];
253
+ actionsNode.items.forEach((actionNode, index) => {
254
+ const actionBase = `${base}.actions[${index}]`;
255
+ if (actionNode.kind !== "map") {
256
+ pushError(ctx, "intent-rule-action-not-a-map", "action 必须是映射", actionBase, "value");
257
+ return;
258
+ }
259
+ for (const entry of actionNode.entries)
260
+ if (!ACTION_KEYS.has(entry.key))
261
+ pushError(ctx, "intent-key-unknown", `action 不支持键 "${entry.key}"(只接受 type / target / filter / when / set)`, `${actionBase}.${entry.key}`);
262
+ const typeNode = actionNode.entries.find((entry) => entry.key === "type")?.value;
263
+ const targetNode = actionNode.entries.find((entry) => entry.key === "target")?.value;
264
+ const filterNode = actionNode.entries.find((entry) => entry.key === "filter")?.value;
265
+ const whenNode = actionNode.entries.find((entry) => entry.key === "when")?.value;
266
+ const setNode = actionNode.entries.find((entry) => entry.key === "set")?.value;
267
+ const type = isScalarString(typeNode) ? typeNode.value.trim() : "";
268
+ if (type !== "UPDATE")
269
+ pushError(ctx, "intent-rule-action-type-unsupported", `action.type 目前只支持 "UPDATE"(得到 "${type}")。action.filter 是「定位要操作哪些数据」(filterCondition,改哪条),action.when 是「输入数据满足条件才执行」(dataCondition,要不要执行)——两者是两码事。非 UPDATE 节点(CREATE/DELETE/BIZ_ACTION/MESSAGE…)用 nodeGraph 透传。`, `${actionBase}.type`, "value");
270
+ if (!isScalarString(targetNode) && targetNode !== undefined && !(targetNode.kind === "scalar" && targetNode.value === null))
271
+ pushError(ctx, "intent-rule-action-target-invalid", `第 ${index + 1} 个动作的 target 必须是字符串(目标模型 code);同模型动作可省略 target,默认当前模型 ${modelCode}`, `${actionBase}.target`, "value");
272
+ // `target` defaults to the rule's OWN model: the common case is a same-model
273
+ // update, and forcing the code to be repeated is redundant friction (bench61
274
+ // feedback). Cross-model writes still name the other model explicitly.
275
+ const targetCode = isScalarString(targetNode) ? targetNode.value.trim() : modelCode;
276
+ const readList = (listNode, listKey) => {
277
+ if (listNode === undefined || (listNode.kind === "scalar" && listNode.value === null))
278
+ return [];
279
+ if (listNode.kind !== "seq") {
280
+ pushError(ctx, "intent-rule-action-list-invalid", `action.${listKey} 必须是列表`, `${actionBase}.${listKey}`, "value");
281
+ return [];
282
+ }
283
+ const rows = [];
284
+ listNode.items.forEach((item, itemIndex) => {
285
+ if (item.kind !== "map") {
286
+ pushError(ctx, "intent-rule-action-item-invalid", `action.${listKey} 项必须是映射`, `${actionBase}.${listKey}[${itemIndex}]`, "value");
287
+ return;
288
+ }
289
+ const row = readLeaf(item, `${actionBase}.${listKey}[${itemIndex}]`, ctx, listKey === "set" ? "assignment" : "condition");
290
+ if (row)
291
+ rows.push(row);
292
+ });
293
+ return rows;
294
+ };
295
+ const filter = readList(filterNode, "filter");
296
+ const when = readList(whenNode, "when");
297
+ const set = readList(setNode, "set");
298
+ // The gate (`when`) compiles to `dataCondition`, whose leaf has no DYNAMIC
299
+ // "compare against another field" form in this compact face (the platform's
300
+ // DataFilterCondition DYNAMIC value shape is not the same as filter's). Fail
301
+ // closed rather than emit a leaf the engine misreads; nodeGraph can carry it.
302
+ when.forEach((row, whenIndex) => {
303
+ if (row.from !== undefined)
304
+ pushError(ctx, "intent-rule-when-from-unsupported", `action.when 不支持 from(执行门只支持与字面量比较:equals/notEquals/gt/gte/lt/lte);需要与另一字段比较时用 nodeGraph 传 dataCondition`, `${actionBase}.when[${whenIndex}].from`, "value");
305
+ });
306
+ if (type === "UPDATE") {
307
+ // Never DROP a declared action. Before round 15, an action with an empty
308
+ // filter or set was silently discarded, so the rule compiled with no
309
+ // UPDATE node and validate/plan passed — bench52/53 lost a whole end-to-end
310
+ // cycle to a cross-model writeback whose filter was omitted and whose
311
+ // action therefore never existed. An empty `set` is always an error; an
312
+ // empty `filter` compiles and is gated by lintRuleGraph (cross-model).
313
+ if (set.length === 0)
314
+ pushError(ctx, "intent-rule-action-set-required", "action.set 不能为空(UPDATE 至少要有一个赋值;平台 UPDATE 节点必须有 dataActions)", `${actionBase}.set`, "value");
315
+ actions.push({
316
+ type: "UPDATE",
317
+ target: targetCode,
318
+ filter: filter,
319
+ ...(whenNode !== undefined && !(whenNode.kind === "scalar" && whenNode.value === null) ? { when: when } : {}),
320
+ set: set,
321
+ });
322
+ }
323
+ });
324
+ return actions;
325
+ }
326
+ /**
327
+ * The business-rule capability. Rules are model-scoped and declared as either
328
+ * compact `actions` (the compiler synthesizes the nodeGraph) or a raw
329
+ * `nodeGraph` escape hatch. It requires the field capability because a rule
330
+ * binds declared fields. Liveness is `rule.enable` (publish + enable), never
331
+ * `rule.save` alone.
332
+ */
333
+ export const businessruleCapability = {
334
+ id: "businessrule",
335
+ title: "业务规则",
336
+ requires: ["field"],
337
+ operations: ["save", "enable", "disable"],
338
+ validate(node, draft, ctx) {
339
+ const rulesNode = node.entries.find((entry) => entry.key === "rules")?.value;
340
+ if (rulesNode === undefined || (rulesNode.kind === "scalar" && rulesNode.value === null))
341
+ return;
342
+ if (rulesNode.kind !== "seq") {
343
+ pushError(ctx, "intent-rules-not-a-list", `rules 必须是列表(得到 ${rulesNode.kind})`, `${ctx.base}.rules`, "value");
344
+ return;
345
+ }
346
+ const seen = new Set();
347
+ rulesNode.items.forEach((ruleNode, index) => {
348
+ const base = `${ctx.base}.rules[${index}]`;
349
+ if (ruleNode.kind !== "map") {
350
+ pushError(ctx, "intent-rule-not-a-map", "规则项必须是映射", base, "value");
351
+ return;
352
+ }
353
+ for (const entry of ruleNode.entries)
354
+ if (!RULE_KEYS.has(entry.key))
355
+ pushError(ctx, "intent-key-unknown", `规则不支持键 "${entry.key}"(只接受 code / name / enabled / actions / nodeGraph / bizRuleType / customTrigger)`, `${base}.${entry.key}`);
356
+ const codeNode = ruleNode.entries.find((entry) => entry.key === "code")?.value;
357
+ const nameNode = ruleNode.entries.find((entry) => entry.key === "name")?.value;
358
+ const enabledNode = ruleNode.entries.find((entry) => entry.key === "enabled")?.value;
359
+ const bizRuleTypeNode = ruleNode.entries.find((entry) => entry.key === "bizRuleType")?.value;
360
+ const customTriggerNode = ruleNode.entries.find((entry) => entry.key === "customTrigger")?.value;
361
+ const actionsNode = ruleNode.entries.find((entry) => entry.key === "actions")?.value;
362
+ const nodeGraphNode = ruleNode.entries.find((entry) => entry.key === "nodeGraph")?.value;
363
+ let code = "";
364
+ if (!isScalarString(codeNode))
365
+ pushError(ctx, "intent-rule-code-invalid", "规则 code 必须是非空字符串", `${base}.code`, "value");
366
+ else {
367
+ code = codeNode.value.trim();
368
+ if (seen.has(code))
369
+ pushError(ctx, "intent-rule-code-duplicate", `规则 code 在模型内重复:${code}`, `${base}.code`, "value");
370
+ seen.add(code);
371
+ }
372
+ if (!isScalarString(nameNode))
373
+ pushError(ctx, "intent-rule-name-invalid", "规则 name 必须是非空字符串", `${base}.name`, "value");
374
+ let enabled = false;
375
+ if (enabledNode !== undefined) {
376
+ if (enabledNode.kind !== "scalar" || typeof enabledNode.value !== "boolean")
377
+ pushError(ctx, "intent-rule-enabled-invalid", "rule.enabled 必须是布尔", `${base}.enabled`, "value");
378
+ else
379
+ enabled = enabledNode.value;
380
+ }
381
+ let bizRuleType;
382
+ if (bizRuleTypeNode !== undefined) {
383
+ if (bizRuleTypeNode.kind !== "scalar" || typeof bizRuleTypeNode.value !== "number")
384
+ pushError(ctx, "intent-rule-bizruletype-invalid", "rule.bizRuleType 必须是数字", `${base}.bizRuleType`, "value");
385
+ else
386
+ bizRuleType = bizRuleTypeNode.value;
387
+ }
388
+ let customTrigger;
389
+ if (customTriggerNode !== undefined) {
390
+ if (customTriggerNode.kind !== "scalar" || typeof customTriggerNode.value !== "boolean")
391
+ pushError(ctx, "intent-rule-customtrigger-invalid", "rule.customTrigger 必须是布尔", `${base}.customTrigger`, "value");
392
+ else
393
+ customTrigger = customTriggerNode.value;
394
+ }
395
+ const hasActions = actionsNode !== undefined && !(actionsNode.kind === "scalar" && actionsNode.value === null);
396
+ const hasNodeGraph = nodeGraphNode !== undefined && !(nodeGraphNode.kind === "scalar" && nodeGraphNode.value === null);
397
+ if (hasActions && hasNodeGraph)
398
+ pushError(ctx, "intent-rule-actions-and-nodegraph", "规则不能同时声明 actions 与 nodeGraph(二选一)", base, "value");
399
+ if (!hasActions && !hasNodeGraph)
400
+ pushError(ctx, "intent-rule-body-missing", "规则必须声明 actions(紧凑)或 nodeGraph(透传)之一", base, "value");
401
+ const actions = hasActions && actionsNode ? readActions(actionsNode, base, ctx, draft.code) : undefined;
402
+ let nodeGraph;
403
+ if (hasNodeGraph && nodeGraphNode) {
404
+ if (nodeGraphNode.kind !== "map")
405
+ pushError(ctx, "intent-rule-nodegraph-invalid", "rule.nodeGraph 必须是映射", `${base}.nodeGraph`, "value");
406
+ else
407
+ nodeGraph = toPlain(nodeGraphNode);
408
+ }
409
+ if (code.length === 0 || !isScalarString(nameNode))
410
+ return;
411
+ const rule = {
412
+ code,
413
+ name: nameNode.value,
414
+ enabled,
415
+ ...(bizRuleType !== undefined ? { bizRuleType } : {}),
416
+ ...(customTrigger !== undefined ? { customTrigger } : {}),
417
+ ...(actions ? { actions } : {}),
418
+ ...(nodeGraph ? { nodeGraph } : {}),
419
+ };
420
+ lintRuleGraph(rule, ctx, base, draft.code);
421
+ draft.rules.push(rule);
422
+ });
423
+ },
424
+ async read(session, intent, draft) {
425
+ if (!intent.rules?.length)
426
+ return;
427
+ const rows = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: draft.code } })).data);
428
+ for (const row of rows) {
429
+ const code = String(row.code ?? "");
430
+ if (!code)
431
+ continue;
432
+ const declared = intent.rules.find((rule) => rule.code === code);
433
+ if (!declared) {
434
+ draft.rules.push({ code, present: true, enabled: row.enabled === true, nodes: undefined, routes: undefined });
435
+ continue;
436
+ }
437
+ const record = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: String(row.id) } })).data;
438
+ draft.rules.push({
439
+ code,
440
+ present: true,
441
+ enabled: row.enabled === true,
442
+ nodes: record ? decodeJsonColumn(record.node, JSON_COLUMN_NULL_COALESCE) : undefined,
443
+ routes: record ? decodeJsonColumn(record.route, JSON_COLUMN_NULL_COALESCE) : undefined,
444
+ });
445
+ }
446
+ },
447
+ plan(intent, baseline, emit) {
448
+ for (const rule of intent.rules ?? []) {
449
+ const live = (baseline.rules ?? []).find((entry) => entry.code === rule.code);
450
+ const expected = expectedRuleGraph(rule, intent.code);
451
+ const resource = `rule:${intent.code}.${rule.code}`;
452
+ const graphChanged = live === undefined || live.nodes === undefined || ruleGraphDigest(live.nodes, live.routes) !== ruleGraphDigest(expected.nodes, expected.routes);
453
+ if (live === undefined)
454
+ emit.change("rule.save", ruleSaveArgs(intent.code, rule), resource, "rule-absent");
455
+ else if (graphChanged)
456
+ emit.change("rule.save", ruleSaveArgs(intent.code, rule), resource, "rule-graph-drift");
457
+ else
458
+ emit.skip(resource, "rule-present");
459
+ if (rule.enabled) {
460
+ if (graphChanged || live?.enabled !== true)
461
+ emit.change("rule.enable", { schemaCode: intent.code, ruleCode: rule.code }, resource, live === undefined ? "rule-created-disabled" : "rule-not-enabled");
462
+ }
463
+ else if (live?.enabled === true) {
464
+ emit.change("rule.disable", { schemaCode: intent.code, ruleCode: rule.code }, resource, "rule-enabled-unexpected");
465
+ }
466
+ }
467
+ },
468
+ decode(baseline, draft) {
469
+ draft.rules = (baseline.rules ?? []).map((rule) => ({ code: rule.code, present: rule.present, enabled: rule.enabled, nodes: rule.nodes, routes: rule.routes }));
470
+ },
471
+ compare(intent, live, emit) {
472
+ const declared = new Set((intent.rules ?? []).map((rule) => rule.code));
473
+ for (const rule of intent.rules ?? []) {
474
+ const current = (live.rules ?? []).find((entry) => entry.code === rule.code);
475
+ const resource = `rule:${intent.code}.${rule.code}`;
476
+ if (current?.present !== true) {
477
+ emit.difference("rule-missing", resource, `规则 ${intent.code}.${rule.code} 不存在`);
478
+ continue;
479
+ }
480
+ if (rule.enabled && !current.enabled)
481
+ emit.difference("rule-not-enabled", resource, `规则 ${intent.code}.${rule.code} 未启用(enabled 应为 true)`);
482
+ if (!rule.enabled && current.enabled)
483
+ emit.difference("rule-enabled-unexpected", resource, `规则 ${intent.code}.${rule.code} 已启用但意图声明为未启用`);
484
+ const expected = expectedRuleGraph(rule, intent.code);
485
+ if (ruleGraphDigest(current.nodes, current.routes) !== ruleGraphDigest(expected.nodes, expected.routes))
486
+ emit.difference("rule-graph-mismatch", resource, `规则 ${intent.code}.${rule.code} 的节点图与意图不等价`);
487
+ }
488
+ for (const current of live.rules ?? [])
489
+ if (!declared.has(current.code))
490
+ emit.ignored(`rule:${intent.code}.${current.code}`);
491
+ },
492
+ };
493
+ //# sourceMappingURL=businessrule.js.map