@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,364 @@
|
|
|
1
|
+
import { businessRefusal, malformedEnvelope, NOT_HANDLED } from "./state.js";
|
|
2
|
+
const PENDING_STATES = new Set(["NOTSTARTED", "INPROGRESS"]);
|
|
3
|
+
const ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
|
|
4
|
+
const POINT = /^-?\d+(?:\.\d+)?,\s-?\d+(?:\.\d+)?$/;
|
|
5
|
+
/**
|
|
6
|
+
* Live 8.6.26 fidelity (2026-09-17 probe): workflow/validate and /publish
|
|
7
|
+
* answer HTTP 200 + errcode 0 and put the truth in `data` as
|
|
8
|
+
* ValidateMessage[]. The originate (first PARTICIPANT) node's `participant`
|
|
9
|
+
* must be the platform keyword `"{Originator}"`; a plain value ("admin")
|
|
10
|
+
* answers `401035 <activityName> ERROR` and the flow never goes live.
|
|
11
|
+
* `"{Creator}"` on a non-originate participant node is a `401062` WARN that
|
|
12
|
+
* does not block.
|
|
13
|
+
*/
|
|
14
|
+
function validateWorkflowParticipants(activities) {
|
|
15
|
+
if (!Array.isArray(activities))
|
|
16
|
+
return [];
|
|
17
|
+
const rows = activities;
|
|
18
|
+
const participants = rows.filter((activity) => String(activity?.activityType ?? "") === "PARTICIPANT");
|
|
19
|
+
const messages = [];
|
|
20
|
+
const originate = participants[0];
|
|
21
|
+
if (originate && String(originate.participant ?? "") !== "{Originator}")
|
|
22
|
+
messages.push({ errCode: 401035, errContent: originate.activityName ?? originate.activityCode ?? "", errMsg: null, errorLevel: "ERROR" });
|
|
23
|
+
for (const activity of participants.slice(1)) {
|
|
24
|
+
if (String(activity.participant ?? "") === "{Creator}")
|
|
25
|
+
messages.push({ errCode: 401062, errContent: activity.activityName ?? activity.activityCode ?? "", errMsg: null, errorLevel: "WARN" });
|
|
26
|
+
}
|
|
27
|
+
return messages;
|
|
28
|
+
}
|
|
29
|
+
function invalidRenderShape(body) {
|
|
30
|
+
if (!Array.isArray(body.activities))
|
|
31
|
+
return "activities";
|
|
32
|
+
const codes = new Set();
|
|
33
|
+
for (const activity of body.activities) {
|
|
34
|
+
const record = activity;
|
|
35
|
+
const code = String(record?.activityCode ?? "").trim();
|
|
36
|
+
const type = String(record?.activityType ?? "").trim();
|
|
37
|
+
if (!code || codes.has(code) || !ACTIVITY_TYPES.has(type))
|
|
38
|
+
return "activities";
|
|
39
|
+
codes.add(code);
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(body.rules))
|
|
42
|
+
return "rules";
|
|
43
|
+
for (const rule of body.rules) {
|
|
44
|
+
const record = rule;
|
|
45
|
+
const pre = String(record?.preActivityCode ?? "");
|
|
46
|
+
const post = String(record?.postActivityCode ?? "");
|
|
47
|
+
const points = record?.points;
|
|
48
|
+
if (!codes.has(pre) || !codes.has(post) || !Array.isArray(points) || points.length === 0 || points.some((point) => typeof point !== "string" || !POINT.test(point)))
|
|
49
|
+
return "rules";
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Spawn the two work items a workflow start produces: the originator fill item
|
|
55
|
+
* (already FINISHED — the start submit is the fill) and the pending approval
|
|
56
|
+
* item that `workitem.list` must surface for `data.submit`.
|
|
57
|
+
*/
|
|
58
|
+
export function startWorkflowInstance(state, input) {
|
|
59
|
+
const sequence = state.workItems.size + 1;
|
|
60
|
+
const instanceId = `wf-${sequence}`;
|
|
61
|
+
const fillId = `wi-${sequence}`;
|
|
62
|
+
const approveId = `wi-${sequence + 1}`;
|
|
63
|
+
state.workItems.set(fillId, {
|
|
64
|
+
id: fillId,
|
|
65
|
+
instanceId,
|
|
66
|
+
activityCode: "fill",
|
|
67
|
+
activityName: "填写申请单",
|
|
68
|
+
participant: "tester",
|
|
69
|
+
state: "FINISHED",
|
|
70
|
+
workflowCode: input.workflowCode,
|
|
71
|
+
schemaCode: input.schemaCode,
|
|
72
|
+
bizObjectId: input.bizObjectId,
|
|
73
|
+
});
|
|
74
|
+
state.workItems.set(approveId, {
|
|
75
|
+
id: approveId,
|
|
76
|
+
instanceId,
|
|
77
|
+
activityCode: "approve",
|
|
78
|
+
activityName: "审批",
|
|
79
|
+
participant: "tester",
|
|
80
|
+
state: "INPROGRESS",
|
|
81
|
+
workflowCode: input.workflowCode,
|
|
82
|
+
schemaCode: input.schemaCode,
|
|
83
|
+
bizObjectId: input.bizObjectId,
|
|
84
|
+
});
|
|
85
|
+
return { workflowInstanceId: instanceId, workItemId: approveId };
|
|
86
|
+
}
|
|
87
|
+
/** Complete a pending work item (approval submit). Returns false if unknown. */
|
|
88
|
+
export function completeWorkItem(state, workItemId) {
|
|
89
|
+
const item = state.workItems.get(workItemId);
|
|
90
|
+
if (!item)
|
|
91
|
+
return false;
|
|
92
|
+
item.state = "FINISHED";
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Portal draft-save shape (finishStart=FALSE): one instance in DRAFT with a
|
|
97
|
+
* single PENDING start work item. Submitting that item (startWorkflowDraft's
|
|
98
|
+
* workItemId) is what advances the instance — see startWorkflowInstance for
|
|
99
|
+
* the finishStart=TRUE twin.
|
|
100
|
+
*/
|
|
101
|
+
export function startWorkflowDraft(state, input) {
|
|
102
|
+
const sequence = state.workItems.size + 1;
|
|
103
|
+
const instanceId = `wf-${sequence}`;
|
|
104
|
+
const startId = `wi-${sequence}`;
|
|
105
|
+
state.workItems.set(startId, {
|
|
106
|
+
id: startId,
|
|
107
|
+
instanceId,
|
|
108
|
+
activityCode: "fill",
|
|
109
|
+
activityName: "填写申请单",
|
|
110
|
+
participant: "tester",
|
|
111
|
+
state: "INPROGRESS",
|
|
112
|
+
workflowCode: input.workflowCode,
|
|
113
|
+
schemaCode: input.schemaCode,
|
|
114
|
+
bizObjectId: input.bizObjectId,
|
|
115
|
+
isStart: true,
|
|
116
|
+
});
|
|
117
|
+
return { workflowInstanceId: instanceId, workItemId: startId };
|
|
118
|
+
}
|
|
119
|
+
/** After the START work item is submitted, the next (approval) activity spawns a pending todo. */
|
|
120
|
+
export function spawnApprovalWorkItem(state, input) {
|
|
121
|
+
const approveId = `wi-${state.workItems.size + 1}`;
|
|
122
|
+
state.workItems.set(approveId, {
|
|
123
|
+
id: approveId,
|
|
124
|
+
instanceId: input.instanceId,
|
|
125
|
+
activityCode: "approve",
|
|
126
|
+
activityName: "审批",
|
|
127
|
+
participant: "tester",
|
|
128
|
+
state: "INPROGRESS",
|
|
129
|
+
workflowCode: input.workflowCode,
|
|
130
|
+
schemaCode: input.schemaCode,
|
|
131
|
+
bizObjectId: input.bizObjectId,
|
|
132
|
+
});
|
|
133
|
+
return approveId;
|
|
134
|
+
}
|
|
135
|
+
function toWorkItemVO(item) {
|
|
136
|
+
return {
|
|
137
|
+
id: item.id,
|
|
138
|
+
instanceId: item.instanceId,
|
|
139
|
+
activityCode: item.activityCode,
|
|
140
|
+
activityName: item.activityName,
|
|
141
|
+
state: item.state,
|
|
142
|
+
workflowCode: item.workflowCode,
|
|
143
|
+
schemaCode: item.schemaCode,
|
|
144
|
+
participant: item.participant,
|
|
145
|
+
participantName: item.participantName ?? item.participant,
|
|
146
|
+
originator: item.originator,
|
|
147
|
+
sequenceNo: item.sequenceNo,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** W7-S29-greenfield lane: workflow template authoring + runtime work-item query. Twin of src/lanes/workflow.ts. */
|
|
151
|
+
export const workflowRoute = (ctx, input) => {
|
|
152
|
+
const q = input.query ?? {};
|
|
153
|
+
const body = (input.body ?? {});
|
|
154
|
+
switch (input.path) {
|
|
155
|
+
case "/api/api/workflow/create": {
|
|
156
|
+
const code = String(body.workflowCode ?? body.code ?? "");
|
|
157
|
+
const name = String(body.workflowName ?? body.name ?? code);
|
|
158
|
+
const schemaCode = String(body.schemaCode ?? "");
|
|
159
|
+
// Ticket 76 live fidelity: creating a workflow when the SCHEMA already
|
|
160
|
+
// has one answers an envelope with NO numeric errcode (live:
|
|
161
|
+
// "envelope for /api/api/workflow/create has no numeric errcode") — the
|
|
162
|
+
// tool must decode it as a duplicate-create, not a malformed failure.
|
|
163
|
+
if ([...ctx.state.workflows.values()].some((wf) => wf.schemaCode === schemaCode) && !ctx.state.workflows.has(code))
|
|
164
|
+
return malformedEnvelope({ workflowCode: code, workflowName: name, schemaCode, exist: true });
|
|
165
|
+
if (!ctx.state.workflows.has(code)) {
|
|
166
|
+
// Ticket 112 platform fidelity: the real create auto-generates a
|
|
167
|
+
// full draft (START → PARTICIPANT 填写申请单 → END with canvas rules;
|
|
168
|
+
// WorkflowTemplateController update @ApiImplicitParam example) — the
|
|
169
|
+
// fake seeds the same platform-shaped template so the merge path has
|
|
170
|
+
// real activity boxes and rule shapes to echo.
|
|
171
|
+
ctx.state.workflows.set(code, {
|
|
172
|
+
code,
|
|
173
|
+
name,
|
|
174
|
+
schemaCode,
|
|
175
|
+
template: {
|
|
176
|
+
workflowCode: code,
|
|
177
|
+
name,
|
|
178
|
+
workflowName: name,
|
|
179
|
+
schemaCode,
|
|
180
|
+
// Live create seeds templateType DRAFT ("SAVE" is not a
|
|
181
|
+
// WorkflowTemplateStatus member — S29 contract).
|
|
182
|
+
templateType: "DRAFT",
|
|
183
|
+
activities: [
|
|
184
|
+
{ activityCode: "Activity1", activityType: "START", activityName: "开始", x: 327, y: 36, width: 158, height: 40 },
|
|
185
|
+
{ activityCode: "Activity2", activityType: "PARTICIPANT", activityName: "填写申请单", participant: "{Originator}", x: 327, y: 116, width: 158, height: 40 },
|
|
186
|
+
{ activityCode: "Activity3", activityType: "END", activityName: "结束", x: 327, y: 196, width: 158, height: 40 },
|
|
187
|
+
],
|
|
188
|
+
rules: [
|
|
189
|
+
{ preActivityCode: "Activity1", postActivityCode: "Activity2", points: ["406, 76", "406, 116"] },
|
|
190
|
+
{ preActivityCode: "Activity2", postActivityCode: "Activity3", points: ["406, 156", "406, 196"] },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
published: null,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return { code };
|
|
197
|
+
}
|
|
198
|
+
case "/api/api/workflow/isworkflow": {
|
|
199
|
+
// WorkflowTemplateController.isWorkFlow(schemaCode): any header for the
|
|
200
|
+
// schema answers true.
|
|
201
|
+
const schemaCode = String(q.schemaCode ?? "");
|
|
202
|
+
return [...ctx.state.workflows.values()].some((wf) => wf.schemaCode === schemaCode);
|
|
203
|
+
}
|
|
204
|
+
case "/api/api/workflow/getWorkflow": {
|
|
205
|
+
// WorkflowTemplateController.getWorkflow(schemaCode): the published
|
|
206
|
+
// template for the schema (carries workflowCode), or null when the model
|
|
207
|
+
// has none. Used to auto-start a workflow-backed data.submit.
|
|
208
|
+
const schemaCode = String(q.schemaCode ?? "");
|
|
209
|
+
const wf = [...ctx.state.workflows.values()].find((candidate) => candidate.schemaCode === schemaCode);
|
|
210
|
+
if (!wf)
|
|
211
|
+
return null;
|
|
212
|
+
const template = (wf.published ?? wf.template);
|
|
213
|
+
return { ...template, workflowCode: wf.code };
|
|
214
|
+
}
|
|
215
|
+
case "/api/api/workflow/get_header": {
|
|
216
|
+
const wf = ctx.state.workflows.get(String(q.workflowCode ?? ""));
|
|
217
|
+
// Round-2 live fidelity (BUG-05): these endpoints answer 401001
|
|
218
|
+
// 流程不存在! for an ABSENT workflow rather than a null payload.
|
|
219
|
+
if (!wf)
|
|
220
|
+
return businessRefusal(401001, "流程不存在!");
|
|
221
|
+
return { code: wf.code, name: wf.name };
|
|
222
|
+
}
|
|
223
|
+
case "/api/api/workflow/get_draft": {
|
|
224
|
+
const wf = ctx.state.workflows.get(String(q.workflowCode ?? ""));
|
|
225
|
+
if (!wf)
|
|
226
|
+
return businessRefusal(401001, "流程不存在!");
|
|
227
|
+
return wf.template;
|
|
228
|
+
}
|
|
229
|
+
case "/api/api/workflow/get_published": {
|
|
230
|
+
const wf = ctx.state.workflows.get(String(q.workflowCode ?? ""));
|
|
231
|
+
// Live fidelity: like get_header/get_draft, an ABSENT workflow answers
|
|
232
|
+
// 401001 流程不存在! rather than a null payload; the tool degrades that
|
|
233
|
+
// to exists:false. Ticket 77: an UNPUBLISHED workflow still answers the
|
|
234
|
+
// DRAFT shape here — data presence is not publish evidence. The
|
|
235
|
+
// published face carries the templateType PUBLISH marker.
|
|
236
|
+
if (!wf)
|
|
237
|
+
return businessRefusal(401001, "流程不存在!");
|
|
238
|
+
return wf.published ?? wf.template ?? null;
|
|
239
|
+
}
|
|
240
|
+
case "/api/api/workflow/update": {
|
|
241
|
+
const code = String(body.workflowCode ?? q.workflowCode ?? "");
|
|
242
|
+
const wf = ctx.state.workflows.get(code);
|
|
243
|
+
if (!wf)
|
|
244
|
+
return null;
|
|
245
|
+
// Ticket 112 platform fidelity (WorkflowTemplateUtil.java:90-171): an
|
|
246
|
+
// illegal templateType ("BusinessProcess" is not a
|
|
247
|
+
// WorkflowTemplateStatus member — valueOf at :136) or an activities
|
|
248
|
+
// element without a type makes the converter swallow the exception
|
|
249
|
+
// into a PARTIAL model; the save chain then fails with errcode 50000.
|
|
250
|
+
if (body.templateType === "BusinessProcess")
|
|
251
|
+
return businessRefusal(50000, "服务异常");
|
|
252
|
+
if (Array.isArray(body.activities) &&
|
|
253
|
+
body.activities.some((activity) => {
|
|
254
|
+
const record = activity;
|
|
255
|
+
const type = record != null && typeof record === "object" ? (record.type ?? record.activityType) : undefined;
|
|
256
|
+
return type === undefined || type === null || String(type).trim() === "";
|
|
257
|
+
}))
|
|
258
|
+
return businessRefusal(50000, "服务异常");
|
|
259
|
+
if (body.activities !== undefined && invalidRenderShape(body))
|
|
260
|
+
return businessRefusal(50000, "服务异常");
|
|
261
|
+
// Platform fidelity (live 2026-09-23; bench12/13/15): an existing
|
|
262
|
+
// activityCode cannot be RENAMED or RETYPED — the stored name/type win.
|
|
263
|
+
// A caller that wants a different name/type must send it under a fresh
|
|
264
|
+
// code (the tool reconciles this before the wire); re-sending the same
|
|
265
|
+
// code with a changed name is a silent no-op on the platform, so the fake
|
|
266
|
+
// must reproduce it rather than store the body verbatim.
|
|
267
|
+
const stored = wf.template.activities;
|
|
268
|
+
const storedByCode = new Map((Array.isArray(stored) ? stored : []).map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
269
|
+
const pinned = Array.isArray(body.activities)
|
|
270
|
+
? {
|
|
271
|
+
...body,
|
|
272
|
+
activities: body.activities.map((activity) => {
|
|
273
|
+
const prior = storedByCode.get(String(activity.activityCode ?? ""));
|
|
274
|
+
return prior
|
|
275
|
+
? { ...activity, activityName: prior.activityName, i18nMessage: prior.i18nMessage, activityType: prior.activityType }
|
|
276
|
+
: activity;
|
|
277
|
+
}),
|
|
278
|
+
}
|
|
279
|
+
: body;
|
|
280
|
+
wf.template = { ...pinned };
|
|
281
|
+
return { code };
|
|
282
|
+
}
|
|
283
|
+
case "/api/api/workflow/validate":
|
|
284
|
+
case "/api/api/workflow/publish": {
|
|
285
|
+
const code = String(body.workflowCode ?? q.workflowCode ?? "");
|
|
286
|
+
const wf = ctx.state.workflows.get(code);
|
|
287
|
+
if (!wf)
|
|
288
|
+
return null;
|
|
289
|
+
if (body.activities !== undefined && invalidRenderShape(body))
|
|
290
|
+
return businessRefusal(50000, "服务异常");
|
|
291
|
+
// Live 8.6.26 fidelity (2026-09-17): validate/publish REFUSE with HTTP
|
|
292
|
+
// 200 + errcode 0 and the truth in `data` as ValidateMessage[]
|
|
293
|
+
// ({errCode, errContent, errorLevel}). The originate (first PARTICIPANT)
|
|
294
|
+
// node's participant must be the platform keyword "{Originator}" — a
|
|
295
|
+
// plain user/role value answers 401035 ERROR while errcode stays 0, and
|
|
296
|
+
// the flow never goes live. `{Creator}` on a non-originate node is a WARN
|
|
297
|
+
// (401062) that does not block.
|
|
298
|
+
const activities = Array.isArray(body.activities) ? body.activities : wf.template.activities;
|
|
299
|
+
const validation = validateWorkflowParticipants(activities);
|
|
300
|
+
if (input.path === "/api/api/workflow/validate")
|
|
301
|
+
return validation;
|
|
302
|
+
if (validation.some((message) => message.errorLevel === "ERROR"))
|
|
303
|
+
return validation;
|
|
304
|
+
// Ticket 77: the published face carries the PUBLISH marker (live readback
|
|
305
|
+
// evidence: publish state is identified by templateType:PUBLISH /
|
|
306
|
+
// version fields, never by data presence).
|
|
307
|
+
wf.published = { ...body, templateType: "PUBLISH", workflowVersion: (wf.published ? Number(wf.published.workflowVersion ?? 1) : 0) + 1 };
|
|
308
|
+
return { code, workflowVersion: wf.published.workflowVersion };
|
|
309
|
+
}
|
|
310
|
+
case "/api/api/workflow/delete":
|
|
311
|
+
ctx.state.workflows.delete(String(body.workflowCode ?? q.workflowCode ?? q.code ?? ""));
|
|
312
|
+
return null;
|
|
313
|
+
case "/api/api/runtime/workflow/search_workitems": {
|
|
314
|
+
// The real endpoint returns the caller's pending todos (biz_workitem only
|
|
315
|
+
// holds pending rows); finished items move to the finished store. The
|
|
316
|
+
// server's filterWorkflowInstanceId is a NOT-equal exclusion, so it is
|
|
317
|
+
// deliberately not honored here — exact instance matching is client-side.
|
|
318
|
+
let rows = [...ctx.state.workItems.values()].filter((item) => PENDING_STATES.has(item.state));
|
|
319
|
+
if (q.workflowCode)
|
|
320
|
+
rows = rows.filter((item) => item.workflowCode === q.workflowCode);
|
|
321
|
+
if (q.schemaCode)
|
|
322
|
+
rows = rows.filter((item) => item.schemaCode === q.schemaCode);
|
|
323
|
+
const page = Number(q.page ?? 0);
|
|
324
|
+
const size = Number(q.size ?? 20);
|
|
325
|
+
const start = page * size;
|
|
326
|
+
return {
|
|
327
|
+
totalElements: rows.length,
|
|
328
|
+
totalPages: size > 0 ? Math.ceil(rows.length / size) : 0,
|
|
329
|
+
content: rows.slice(start, start + size).map(toWorkItemVO),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case "/api/api/runtime/workflow/getBizObjectByWfInstanceId": {
|
|
333
|
+
const instanceId = String(q.workflowInstanceId ?? "");
|
|
334
|
+
const item = [...ctx.state.workItems.values()].find((candidate) => candidate.instanceId === instanceId);
|
|
335
|
+
if (!item)
|
|
336
|
+
return null;
|
|
337
|
+
return { id: item.bizObjectId, schemaCode: item.schemaCode };
|
|
338
|
+
}
|
|
339
|
+
case "/api/api/organization/role/list":
|
|
340
|
+
// Real shape: groups with `children` roles (see real-env observation).
|
|
341
|
+
return [
|
|
342
|
+
{ id: "grp-1", name: "默认", code: "SYS_1000000", children: [{ id: "role-lead", name: "部门负责人", code: "SYS260630023816197", roleType: "SYS", corpId: "corp-1" }] },
|
|
343
|
+
{ id: "role-finance", name: "财务负责人", code: "SYS260515031207258", roleType: "SYS", corpId: "corp-1" },
|
|
344
|
+
];
|
|
345
|
+
case "/api/api/organization/department/tree":
|
|
346
|
+
return { departmentList: [{ id: "dept-1", name: "总所", code: "1", corpId: "corp-1", parentId: null }, { id: "dept-2", name: "业务部", code: "1_1", corpId: "corp-1", parentId: "dept-1" }] };
|
|
347
|
+
case "/api/api/organization/user/search": {
|
|
348
|
+
// UserController.search: @RequestBody {wd,page,size} → PageVO<UserVO>.
|
|
349
|
+
const wd = String(body.wd ?? "");
|
|
350
|
+
if (!wd)
|
|
351
|
+
return businessRefusal(10005, "关键字不能为空");
|
|
352
|
+
const users = [
|
|
353
|
+
{ id: "user-1", username: "18900000001", name: "付文彬", departmentId: "dept-1", userId: "18900000001", status: "ENABLE", deleted: false, corpId: "corp-1" },
|
|
354
|
+
{ id: "user-2", username: "18900000002", name: "李四", departmentId: "dept-2", userId: "18900000002", status: "ENABLE", deleted: false, corpId: "corp-1" },
|
|
355
|
+
].filter((user) => user.name.includes(wd) || user.username.includes(wd));
|
|
356
|
+
const page = Number(body.page ?? 0);
|
|
357
|
+
const size = Number(body.size ?? 100);
|
|
358
|
+
return { totalElements: users.length, totalPages: size > 0 ? Math.ceil(users.length / size) : 0, content: users.slice(page * size, page * size + size) };
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
return NOT_HANDLED;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { FIELD_PROPERTY_TYPES } from "./field-families.js";
|
|
2
|
+
import { assertFormConditions, assertNumericalScale, assertFieldOptionsAllowed, buildChildTableSubSchema, buildFieldOptions, ensureRelationDisplayFieldOptions, normalizeFieldPropertyType, relationDisplayFieldCode, } from "./field-families.js";
|
|
3
|
+
import { nameI18nJson } from "./field-families.js";
|
|
4
|
+
import { decodeJsonObjectColumn } from "./json-column.js";
|
|
5
|
+
import { ToolRefusalError } from "./tool-types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Fail-closed relation-target gate (round17 dogfood R3). A relation field whose
|
|
8
|
+
* options lack `schemaCode` is ACCEPTED by the platform at create, but then
|
|
9
|
+
* `field.publish` fails with an opaque `errcode 300010 数据模型编码不能为空!` and
|
|
10
|
+
* the column is unusable. Backfill `options.schemaCode` from the top-level
|
|
11
|
+
* `relativeCode` when present (they name the same target); refuse when neither
|
|
12
|
+
* carries a target, naming the fix instead of letting the model be poisoned.
|
|
13
|
+
*/
|
|
14
|
+
function assertRelationTargetPresent(propertyType, options, relativeCode, code) {
|
|
15
|
+
if (propertyType !== FIELD_PROPERTY_TYPES.WORK_SHEET && propertyType !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
16
|
+
return;
|
|
17
|
+
const optionCode = typeof options.schemaCode === "string" ? options.schemaCode.trim() : "";
|
|
18
|
+
if (optionCode)
|
|
19
|
+
return;
|
|
20
|
+
// Accept the target under any of the synonyms callers naturally reach for
|
|
21
|
+
// (top-level relativeCode; options.relativeCode) — they all name the target
|
|
22
|
+
// model. The platform's authoritative relation key is options.schemaCode.
|
|
23
|
+
const candidates = [relativeCode, options.relativeCode, options.relativeSchemaCode]
|
|
24
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
25
|
+
.filter((value) => value.length > 0);
|
|
26
|
+
const target = candidates[0];
|
|
27
|
+
if (target) {
|
|
28
|
+
options.schemaCode = target;
|
|
29
|
+
if (typeof options.queryCode !== "string" || options.queryCode.trim() === "")
|
|
30
|
+
options.queryCode = target;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
throw new ToolRefusalError({
|
|
34
|
+
status: "refused",
|
|
35
|
+
blocker: "field-relation-target-missing",
|
|
36
|
+
code,
|
|
37
|
+
message: "关联字段缺少目标模型:options.schemaCode 与顶层 relativeCode 都为空。平台建字段时接受它,但 field.publish 会以 300010『数据模型编码不能为空』拒绝,之后该列不可用。请传 relativeCode:'<目标模型code>'(工具会自动补全 options.schemaCode/queryCode),或在 options 里直接给 schemaCode",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Normalize one field change and run every reproduced write gate exactly once. */
|
|
41
|
+
export function prepareFieldChange(input) {
|
|
42
|
+
const propertyType = normalizeFieldPropertyType({ propertyType: input.propertyType });
|
|
43
|
+
const isChildTable = propertyType === FIELD_PROPERTY_TYPES.CHILD_TABLE;
|
|
44
|
+
if (isChildTable) {
|
|
45
|
+
const subSchema = buildChildTableSubSchema({ code: input.code, subFields: input.subFields });
|
|
46
|
+
return { propertyType, isChildTable, options: {}, subSchema };
|
|
47
|
+
}
|
|
48
|
+
const raw = buildFieldOptions({
|
|
49
|
+
propertyType: input.propertyType,
|
|
50
|
+
...(input.choices ? { choices: input.choices } : {}),
|
|
51
|
+
...(input.options ? { options: input.options } : {}),
|
|
52
|
+
});
|
|
53
|
+
assertRelationTargetPresent(propertyType, raw, input.relativeCode, input.code);
|
|
54
|
+
// Gates run here so every entry point (direct, orchestrator, intent) shares them.
|
|
55
|
+
assertFieldOptionsAllowed(propertyType, raw, input.schemaCode);
|
|
56
|
+
assertNumericalScale(propertyType, raw);
|
|
57
|
+
assertFormConditions(raw);
|
|
58
|
+
const options = ensureRelationDisplayFieldOptions(propertyType, raw);
|
|
59
|
+
const displayCode = relationDisplayFieldCode(propertyType, options);
|
|
60
|
+
return { propertyType, isChildTable, options, ...(displayCode ? { displayCode } : {}) };
|
|
61
|
+
}
|
|
62
|
+
/** The single platform wire body for a field create. */
|
|
63
|
+
export function fieldChangeWireBody(input, prepared) {
|
|
64
|
+
return {
|
|
65
|
+
name: input.name,
|
|
66
|
+
name_i18n: nameI18nJson(input.name),
|
|
67
|
+
code: input.code,
|
|
68
|
+
schemaCode: input.schemaCode,
|
|
69
|
+
propertyType: prepared.propertyType,
|
|
70
|
+
propertyEmpty: input.propertyEmpty ?? false,
|
|
71
|
+
...(prepared.isChildTable
|
|
72
|
+
? { defaultValue: null, subSchema: prepared.subSchema }
|
|
73
|
+
: { defaultValue: "", ...(prepared.displayCode ? { relativePropertyCode: prepared.displayCode } : {}), options: JSON.stringify(prepared.options) }),
|
|
74
|
+
...(input.relativeCode ? { relativeCode: input.relativeCode } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function prepareFieldUpdate(input) {
|
|
78
|
+
const recordType = Number(input.base.type ?? input.base.propertyType);
|
|
79
|
+
const isChildTable = recordType === FIELD_PROPERTY_TYPES.CHILD_TABLE;
|
|
80
|
+
if (isChildTable && (input.options !== undefined || input.choices !== undefined || input.subFields !== undefined))
|
|
81
|
+
throw new ToolRefusalError({
|
|
82
|
+
status: "refused",
|
|
83
|
+
blocker: "child-table-update-unsupported",
|
|
84
|
+
schemaCode: input.schemaCode,
|
|
85
|
+
propertyCode: input.code,
|
|
86
|
+
message: `子表字段 ${input.schemaCode}.${input.code} 不接受 options/choices/subFields。子表列就是 schemaCode=${input.code} 上的普通字段:增列用 field.create --args '{"schemaCode":"${input.code}","code":"<子列>",...}',改列/删列用 field.update/field.delete(同 schemaCode),改完对父模型 field.publish 即发布`,
|
|
87
|
+
});
|
|
88
|
+
const currentOptions = decodeJsonObjectColumn(input.base.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
|
|
89
|
+
const nextOptions = isChildTable
|
|
90
|
+
? currentOptions
|
|
91
|
+
: ensureRelationDisplayFieldOptions(recordType, {
|
|
92
|
+
...currentOptions,
|
|
93
|
+
...buildFieldOptions({
|
|
94
|
+
propertyType: recordType,
|
|
95
|
+
...(input.choices ? { choices: input.choices } : {}),
|
|
96
|
+
...(input.options ? { options: input.options } : {}),
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (!isChildTable && !input.options && !input.choices)
|
|
100
|
+
Object.assign(nextOptions, currentOptions);
|
|
101
|
+
if (!isChildTable) {
|
|
102
|
+
// An update that touches a relation's options must keep a resolvable target:
|
|
103
|
+
// backfill schemaCode from relativeCode (top-level or already stored) so
|
|
104
|
+
// field.publish cannot later 300010 on this field.
|
|
105
|
+
assertRelationTargetPresent(recordType, nextOptions, input.relativeCode ?? (typeof input.base.relativeCode === "string" ? input.base.relativeCode : undefined), input.code);
|
|
106
|
+
assertFieldOptionsAllowed(recordType, nextOptions, input.schemaCode);
|
|
107
|
+
if (input.options !== undefined || input.choices !== undefined)
|
|
108
|
+
assertNumericalScale(recordType, nextOptions);
|
|
109
|
+
if (input.options !== undefined)
|
|
110
|
+
assertFormConditions(input.options);
|
|
111
|
+
}
|
|
112
|
+
// Ticket 61: an explicit displayField wins; otherwise preserve the stored
|
|
113
|
+
// top-level relativePropertyCode (the runtime reads it, not options).
|
|
114
|
+
const displayCode = relationDisplayFieldCode(recordType, nextOptions)
|
|
115
|
+
?? (typeof input.base.relativePropertyCode === "string" && input.base.relativePropertyCode.trim() ? input.base.relativePropertyCode.trim() : undefined);
|
|
116
|
+
const name = input.name ?? (typeof input.base.name === "string" ? input.base.name : undefined);
|
|
117
|
+
const nameI18n = input.name_i18n !== undefined
|
|
118
|
+
? (typeof input.name_i18n === "string" ? input.name_i18n : JSON.stringify(input.name_i18n))
|
|
119
|
+
: input.name !== undefined
|
|
120
|
+
? nameI18nJson(input.name)
|
|
121
|
+
: (typeof input.base.name_i18n === "string" ? input.base.name_i18n : undefined);
|
|
122
|
+
return { propertyType: recordType, isChildTable, options: nextOptions, ...(displayCode !== undefined ? { displayCode } : {}), ...(name !== undefined ? { name } : {}), ...(nameI18n !== undefined ? { nameI18n } : {}) };
|
|
123
|
+
}
|
|
124
|
+
/** The single field-update wire body: the stored row spread with changed keys. */
|
|
125
|
+
export function fieldUpdateWireBody(input, prepared) {
|
|
126
|
+
return {
|
|
127
|
+
...input.base,
|
|
128
|
+
schemaCode: input.schemaCode,
|
|
129
|
+
code: input.code,
|
|
130
|
+
...(prepared.isChildTable ? {} : { options: JSON.stringify(prepared.options) }),
|
|
131
|
+
...(prepared.displayCode !== undefined ? { relativePropertyCode: prepared.displayCode } : {}),
|
|
132
|
+
...(prepared.name !== undefined ? { name: prepared.name } : {}),
|
|
133
|
+
...(prepared.nameI18n !== undefined ? { name_i18n: prepared.nameI18n } : {}),
|
|
134
|
+
...(input.relativeCode ? { relativeCode: input.relativeCode } : {}),
|
|
135
|
+
...(input.propertyEmpty !== undefined ? { propertyEmpty: input.propertyEmpty } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function canonical(value) {
|
|
139
|
+
if (Array.isArray(value))
|
|
140
|
+
return value.map(canonical);
|
|
141
|
+
if (value && typeof value === "object") {
|
|
142
|
+
const record = value;
|
|
143
|
+
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonical(record[key])]));
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function parseStoredOptions(value) {
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(value);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
/** Canonical, storage-independent view used to decide "same field, no change". */
|
|
159
|
+
export function fieldSemanticDescriptor(row) {
|
|
160
|
+
const type = Number(row.type ?? row.propertyType);
|
|
161
|
+
const subSchema = (row.subSchema ?? null);
|
|
162
|
+
const subFields = subSchema?.properties
|
|
163
|
+
? Object.values(subSchema.properties).map((property) => ({
|
|
164
|
+
code: String(property.code ?? ""),
|
|
165
|
+
type: Number.isNaN(Number(property.type ?? property.propertyType)) ? null : Number(property.type ?? property.propertyType),
|
|
166
|
+
options: canonical(parseStoredOptions(property.options)),
|
|
167
|
+
}))
|
|
168
|
+
: [];
|
|
169
|
+
return {
|
|
170
|
+
code: String(row.code ?? row.propertyCode ?? ""),
|
|
171
|
+
name: (row.name ?? null),
|
|
172
|
+
type: Number.isNaN(type) ? null : type,
|
|
173
|
+
options: canonical(parseStoredOptions(row.options)),
|
|
174
|
+
subFields: [...subFields].sort((left, right) => left.code.localeCompare(right.code)),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** True when a stored field already matches the requested change (name/type/options/subFields). */
|
|
178
|
+
export function sameFieldChange(stored, input) {
|
|
179
|
+
const descriptor = fieldSemanticDescriptor(stored);
|
|
180
|
+
if (descriptor.code !== input.code)
|
|
181
|
+
return false;
|
|
182
|
+
if (descriptor.name !== input.name)
|
|
183
|
+
return false;
|
|
184
|
+
const prepared = prepareFieldChange(input);
|
|
185
|
+
if (descriptor.type !== prepared.propertyType)
|
|
186
|
+
return false;
|
|
187
|
+
if (prepared.isChildTable) {
|
|
188
|
+
const wanted = prepared.subSchema?.properties;
|
|
189
|
+
const wantedFields = wanted
|
|
190
|
+
? Object.values(wanted).map((property) => ({
|
|
191
|
+
code: String(property.code ?? ""),
|
|
192
|
+
type: Number(property.type ?? property.propertyType),
|
|
193
|
+
options: canonical(parseStoredOptions(property.options)),
|
|
194
|
+
})).sort((left, right) => left.code.localeCompare(right.code))
|
|
195
|
+
: [];
|
|
196
|
+
return JSON.stringify(descriptor.subFields) === JSON.stringify(wantedFields);
|
|
197
|
+
}
|
|
198
|
+
return JSON.stringify(descriptor.options) === JSON.stringify(canonical(prepared.options));
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=field-change.js.map
|