@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,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