@stalfh233/omc-cli 0.5.16 → 0.6.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/README.md +3 -1
- package/coverage/m1-coverage-manifest-v1.json +38 -0
- package/dist/bench/call-face-eval.js +6 -0
- package/dist/bench/intent-face-eval.js +6 -0
- package/dist/capability-overview.js +19 -3
- package/dist/cli.js +21 -1
- package/dist/contract-resources.js +2 -0
- package/dist/coverage-registry.js +2 -0
- package/dist/endpoints.js +13 -7
- package/dist/lanes/lane-ids.js +1 -0
- package/dist/lanes/online-js-contract.js +4 -4
- package/dist/lanes/programming-contract.js +113 -0
- package/dist/lanes/programming-coverage.js +18 -0
- package/dist/lanes/programming.js +435 -0
- package/dist/programming-workflow.js +793 -0
- package/dist/tools.js +2 -0
- package/docs/omc-introduction.md +6 -4
- package/docs/tool-surface-map.md +2 -1
- package/package.json +3 -3
- package/skills/omc-backend-online-coding.md +211 -0
- package/skills/omc-capabilities.md +10 -2
- package/skills/omc-glossary.md +9 -2
- package/skills/omc-start-here.md +1 -0
- package/skills/omc-tool-selection.md +1 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { ENDPOINTS } from "../endpoints.js";
|
|
4
|
+
import { capabilityArg, environmentArg } from "../args.js";
|
|
5
|
+
import { governedRead } from "../read.js";
|
|
6
|
+
import { writeTool } from "../write.js";
|
|
7
|
+
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
8
|
+
import { wireCall, WireError } from "../wire.js";
|
|
9
|
+
import { PROGRAMMING_LANE_CONTRACT, PROGRAMMING_WIRE } from "./programming-contract.js";
|
|
10
|
+
const LANE = PROGRAMMING_LANE_CONTRACT.lane;
|
|
11
|
+
const TOOLS = PROGRAMMING_LANE_CONTRACT.tools;
|
|
12
|
+
/** metadata.app.CodeType is the NUMERIC index on the wire (8.6.26 source). */
|
|
13
|
+
const CODE_TYPE_BUSINESS_RULE = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Template packages the platform itself generates. 8.6.26 source + the official
|
|
16
|
+
* dev guide say `...engine.service.bizrule.handler`; the live AI-Platform
|
|
17
|
+
* v1.1.25 (8.7-era) deployment generates `...engine.sourcecode.handler`
|
|
18
|
+
* (live smoke 2026-11-03, `programming.get test/Create`). The guard must accept
|
|
19
|
+
* any package the platform emits — refusing the platform's own template is the
|
|
20
|
+
* one false positive that makes the whole lane unusable on a newer build.
|
|
21
|
+
*/
|
|
22
|
+
const TEMPLATE_PACKAGES = new Set([
|
|
23
|
+
"com.authine.cloudpivot.engine.service.bizrule.handler",
|
|
24
|
+
"com.authine.cloudpivot.engine.sourcecode.handler",
|
|
25
|
+
]);
|
|
26
|
+
const TEMPLATE_PACKAGE_HINT = [...TEMPLATE_PACKAGES].join(" 或 ");
|
|
27
|
+
/** SourceUtil.getBusinessRuleSourceCode — the platform's own primary key rule. */
|
|
28
|
+
export function businessRuleSourceCode(schemaCode, ruleCode) {
|
|
29
|
+
return `${schemaCode}${ruleCode}Handler`;
|
|
30
|
+
}
|
|
31
|
+
function sha256(text) {
|
|
32
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
33
|
+
}
|
|
34
|
+
function asRecord(value) {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
36
|
+
}
|
|
37
|
+
function rowsOf(data) {
|
|
38
|
+
return Array.isArray(data) ? data.filter((entry) => asRecord(entry) !== null) : [];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The modern read/template face: POST /get_source. `code` omitted → the
|
|
42
|
+
* platform derives `schemaCode+ruleCode+"Handler"`; an unsaved draft comes back
|
|
43
|
+
* as an in-memory skeleton already filled with the rule-type template
|
|
44
|
+
* (SourceCodeModel.version = null marks "not persisted").
|
|
45
|
+
*/
|
|
46
|
+
async function readSource(session, args) {
|
|
47
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
48
|
+
try {
|
|
49
|
+
const response = await wireCall(session, {
|
|
50
|
+
path: ENDPOINTS.programmingGetSource,
|
|
51
|
+
method: "POST",
|
|
52
|
+
body: {
|
|
53
|
+
codeType: CODE_TYPE_BUSINESS_RULE,
|
|
54
|
+
schemaCode: args.schemaCode,
|
|
55
|
+
businessRuleCode: args.businessRuleCode,
|
|
56
|
+
...(args.businessRuleName !== undefined ? { businessRuleName: args.businessRuleName } : {}),
|
|
57
|
+
...(args.code !== undefined ? { code: args.code } : {}),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const model = asRecord(response.data);
|
|
61
|
+
if (model === null)
|
|
62
|
+
return { present: false, provisioned: false, model: null };
|
|
63
|
+
return { present: true, provisioned: typeof model.version === "number", model };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof WireError && error.kind === "business" && /PROGRAMMING_SOURCE_NONEXISTENT|源码不存在|source.*nonexistent/i.test(error.message))
|
|
67
|
+
return { present: false, provisioned: false, model: null };
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Extract the declared `package` / top-level `class` name from Java source (best effort, textual). */
|
|
72
|
+
function declaredShape(content) {
|
|
73
|
+
const pkg = content.match(/^\s*package\s+([A-Za-z_][\w.]*)\s*;/m);
|
|
74
|
+
// public class Foo / class Foo extends ... (first class declaration wins)
|
|
75
|
+
const cls = content.match(/^\s*(?:public\s+)?(?:final\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/m);
|
|
76
|
+
return { ...(pkg ? { packageName: pkg[1] } : {}), ...(cls ? { className: cls[1] } : {}) };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The template guard (official dev guide: "不要修改在线代码所属的包路径及类名,
|
|
80
|
+
* 否则代码无法正常运行"). Refuse before the write gate so nothing reaches the
|
|
81
|
+
* environment; the class name must equal the platform-owned source code and a
|
|
82
|
+
* declared package must be the handler package.
|
|
83
|
+
*/
|
|
84
|
+
function assertTemplateShape(content, code) {
|
|
85
|
+
const shape = declaredShape(content);
|
|
86
|
+
if (shape.className !== undefined && shape.className !== code) {
|
|
87
|
+
throw new ToolRefusalError({
|
|
88
|
+
status: "refused",
|
|
89
|
+
blocker: "programming-template-class-mismatch",
|
|
90
|
+
message: `源码声明的类名 ${shape.className} 与平台主键 ${code} 不一致。在线开发模板的类名由平台生成(schemaCode+ruleCode+"Handler"),改名后规则无法运行;请保留 ${code}。`,
|
|
91
|
+
expectedClassName: code,
|
|
92
|
+
declaredClassName: shape.className,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (shape.packageName !== undefined && !TEMPLATE_PACKAGES.has(shape.packageName)) {
|
|
96
|
+
throw new ToolRefusalError({
|
|
97
|
+
status: "refused",
|
|
98
|
+
blocker: "programming-template-package-mismatch",
|
|
99
|
+
message: `源码声明的包路径 ${shape.packageName} 不是平台模板的包(${TEMPLATE_PACKAGE_HINT})。官方规范明确包路径不得修改(否则规则无法运行);以 programming.get 返回的模板为准。`,
|
|
100
|
+
expectedPackages: [...TEMPLATE_PACKAGES],
|
|
101
|
+
declaredPackage: shape.packageName,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function listRules(session, schemaCode) {
|
|
106
|
+
const response = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } });
|
|
107
|
+
return rowsOf(response.data);
|
|
108
|
+
}
|
|
109
|
+
async function ruleOf(session, schemaCode, ruleCode) {
|
|
110
|
+
return (await listRules(session, schemaCode)).find((row) => String(row.code ?? row.ruleCode ?? "") === ruleCode);
|
|
111
|
+
}
|
|
112
|
+
async function compileSource(session, code, content) {
|
|
113
|
+
const response = await wireCall(session, { path: ENDPOINTS.programmingCompile, method: "POST", body: { code, content } });
|
|
114
|
+
const result = asRecord(response.data) ?? {};
|
|
115
|
+
const success = result.success === true;
|
|
116
|
+
return {
|
|
117
|
+
success,
|
|
118
|
+
errors: result.errors ?? result.error ?? null,
|
|
119
|
+
diagnostics: {
|
|
120
|
+
...(typeof result.message === "string" ? { message: result.message } : {}),
|
|
121
|
+
...(Array.isArray(result.errorMessages) ? { errorMessages: result.errorMessages } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function sourceRollback() {
|
|
126
|
+
return {
|
|
127
|
+
target: (args) => `programming:${String(args.schemaCode ?? "")}/${String(args.businessRuleCode ?? "")}`,
|
|
128
|
+
read: async (session, args) => {
|
|
129
|
+
const read = await readSource(session, {
|
|
130
|
+
schemaCode: String(args.schemaCode),
|
|
131
|
+
businessRuleCode: String(args.businessRuleCode),
|
|
132
|
+
});
|
|
133
|
+
if (read.model === null)
|
|
134
|
+
return null;
|
|
135
|
+
return { content: read.model.content ?? null, provisioned: read.provisioned };
|
|
136
|
+
},
|
|
137
|
+
inverse: ({ before, args }) => {
|
|
138
|
+
const snapshot = before;
|
|
139
|
+
if (snapshot === null || typeof snapshot.content !== "string")
|
|
140
|
+
return { kind: "irreversible", reason: "before 读取不到既有草稿 content(未落库或读取失败),无法构造等价的回写调用" };
|
|
141
|
+
return {
|
|
142
|
+
kind: "calls",
|
|
143
|
+
calls: [{
|
|
144
|
+
tool: "programming.save",
|
|
145
|
+
args: {
|
|
146
|
+
schemaCode: String(args.schemaCode),
|
|
147
|
+
businessRuleCode: String(args.businessRuleCode),
|
|
148
|
+
content: snapshot.content,
|
|
149
|
+
},
|
|
150
|
+
}],
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function registerProgrammingLane(tools) {
|
|
156
|
+
tools.push(defineTool({
|
|
157
|
+
name: "programming.schemas",
|
|
158
|
+
layer: "low",
|
|
159
|
+
kind: "read",
|
|
160
|
+
lane: LANE,
|
|
161
|
+
scope: TOOLS["programming.schemas"].scope,
|
|
162
|
+
description: TOOLS["programming.schemas"].description,
|
|
163
|
+
inputSchema: {
|
|
164
|
+
environment: environmentArg,
|
|
165
|
+
appCode: z.string().min(1),
|
|
166
|
+
capability: capabilityArg,
|
|
167
|
+
},
|
|
168
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
169
|
+
const response = await wireCall(session, { path: ENDPOINTS.programmingListSchemas, query: { appCode: args.appCode } });
|
|
170
|
+
const schemas = rowsOf(response.data).map((row) => ({ code: String(row.code ?? ""), name: String(row.name ?? "") }));
|
|
171
|
+
return {
|
|
172
|
+
appCode: args.appCode,
|
|
173
|
+
schemas,
|
|
174
|
+
count: schemas.length,
|
|
175
|
+
hint: schemas.length === 0
|
|
176
|
+
? "该应用下没有**已存在在线源码**的模型(平台 list_schemas_by_appcode 只列 listSourceCodes 非空的模型)。要为某条业务规则新建编码源码,用 programming.get(模板生成)+ programming.publish;规则清单用 rule.list。"
|
|
177
|
+
: "programming.sources 可按 schemaCode 列源码;未编码规则用 rule.list 发现后用 programming.get 生成模板。",
|
|
178
|
+
};
|
|
179
|
+
}),
|
|
180
|
+
}));
|
|
181
|
+
tools.push(defineTool({
|
|
182
|
+
name: "programming.sources",
|
|
183
|
+
layer: "low",
|
|
184
|
+
kind: "read",
|
|
185
|
+
lane: LANE,
|
|
186
|
+
scope: TOOLS["programming.sources"].scope,
|
|
187
|
+
description: TOOLS["programming.sources"].description,
|
|
188
|
+
inputSchema: {
|
|
189
|
+
environment: environmentArg,
|
|
190
|
+
schemaCode: z.string().min(1),
|
|
191
|
+
capability: capabilityArg,
|
|
192
|
+
},
|
|
193
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
194
|
+
try {
|
|
195
|
+
const response = await wireCall(session, { path: ENDPOINTS.programmingListSources, query: { schemaCode: args.schemaCode } });
|
|
196
|
+
const sources = rowsOf(response.data).map((row) => ({
|
|
197
|
+
code: String(row.code ?? ""),
|
|
198
|
+
businessRuleCode: String(row.businessRuleCode ?? ""),
|
|
199
|
+
businessRuleName: typeof row.businessRuleName === "string" ? row.businessRuleName : undefined,
|
|
200
|
+
className: typeof row.className === "string" ? row.className : undefined,
|
|
201
|
+
contentBytes: typeof row.content === "string" ? Buffer.byteLength(row.content, "utf8") : undefined,
|
|
202
|
+
contentSha256: typeof row.content === "string" ? sha256(row.content) : undefined,
|
|
203
|
+
version: row.version ?? null,
|
|
204
|
+
published: row.published === true,
|
|
205
|
+
}));
|
|
206
|
+
return { schemaCode: args.schemaCode, present: true, sources, count: sources.length };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// Platform quirk (ProgrammingController.list): one rule missing a draft
|
|
210
|
+
// aborts the WHOLE list with PROGRAMMING_SOURCE_NONEXISTENT.
|
|
211
|
+
if (error instanceof WireError && error.kind === "business" && /PROGRAMMING_SOURCE_NONEXISTENT|源码不存在/i.test(error.message)) {
|
|
212
|
+
return {
|
|
213
|
+
schemaCode: args.schemaCode,
|
|
214
|
+
present: false,
|
|
215
|
+
sources: [],
|
|
216
|
+
count: 0,
|
|
217
|
+
hint: "平台的 list_source_by_schemacode 在任一规则缺草稿时整列报 PROGRAMMING_SOURCE_NONEXISTENT。逐条读取用 programming.get(未落库时返回模板骨架)。",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
}));
|
|
224
|
+
tools.push(defineTool({
|
|
225
|
+
name: "programming.get",
|
|
226
|
+
layer: "low",
|
|
227
|
+
kind: "read",
|
|
228
|
+
lane: LANE,
|
|
229
|
+
scope: TOOLS["programming.get"].scope,
|
|
230
|
+
description: TOOLS["programming.get"].description,
|
|
231
|
+
inputSchema: {
|
|
232
|
+
environment: environmentArg,
|
|
233
|
+
schemaCode: z.string().min(1),
|
|
234
|
+
businessRuleCode: z.string().min(1),
|
|
235
|
+
code: z.string().min(1).optional().describe("源码主键;省略时按 schemaCode+businessRuleCode+\"Handler\" 推导(SourceUtil 规则)"),
|
|
236
|
+
businessRuleName: z.string().optional(),
|
|
237
|
+
full: z.boolean().optional().describe("true = 输出完整 content(默认 content 内联但受通用 2KB diet 影响时会外置 artifact)"),
|
|
238
|
+
capability: capabilityArg,
|
|
239
|
+
},
|
|
240
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
241
|
+
const read = await readSource(session, args);
|
|
242
|
+
const content = typeof read.model?.content === "string" ? read.model.content : null;
|
|
243
|
+
return {
|
|
244
|
+
schemaCode: args.schemaCode,
|
|
245
|
+
businessRuleCode: args.businessRuleCode,
|
|
246
|
+
code: String(read.model?.code ?? args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode)),
|
|
247
|
+
present: read.present,
|
|
248
|
+
provisioned: read.provisioned,
|
|
249
|
+
className: read.model?.className ?? null,
|
|
250
|
+
codeType: read.model?.codeType ?? null,
|
|
251
|
+
version: read.model?.version ?? null,
|
|
252
|
+
published: read.model?.published === true,
|
|
253
|
+
content,
|
|
254
|
+
contentSha256: content === null ? null : sha256(content),
|
|
255
|
+
contentBytes: content === null ? null : Buffer.byteLength(content, "utf8"),
|
|
256
|
+
...(read.provisioned ? {} : { hint: "未落库草稿:content 是平台按规则类型生成的模板骨架(version:null)。改完后 programming.save 落库,programming.publish 生效。" }),
|
|
257
|
+
};
|
|
258
|
+
}, {
|
|
259
|
+
target: `programming.get:${args.schemaCode}/${args.businessRuleCode}`,
|
|
260
|
+
// The source IS the payload of this read: keep it inline rather than let
|
|
261
|
+
// the generic diet replace it with a key count.
|
|
262
|
+
compact: (result) => result,
|
|
263
|
+
}),
|
|
264
|
+
}));
|
|
265
|
+
tools.push(defineTool({
|
|
266
|
+
name: "programming.compile",
|
|
267
|
+
layer: "low",
|
|
268
|
+
kind: "read",
|
|
269
|
+
lane: LANE,
|
|
270
|
+
scope: TOOLS["programming.compile"].scope,
|
|
271
|
+
description: TOOLS["programming.compile"].description,
|
|
272
|
+
inputSchema: {
|
|
273
|
+
environment: environmentArg,
|
|
274
|
+
schemaCode: z.string().min(1),
|
|
275
|
+
businessRuleCode: z.string().min(1),
|
|
276
|
+
content: z.string().optional().describe("要编译的源码;省略时读远端草稿(未落库时用模板骨架)"),
|
|
277
|
+
code: z.string().min(1).optional(),
|
|
278
|
+
capability: capabilityArg,
|
|
279
|
+
},
|
|
280
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
281
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
282
|
+
let content = args.content;
|
|
283
|
+
if (content === undefined) {
|
|
284
|
+
const read = await readSource(session, { schemaCode: args.schemaCode, businessRuleCode: args.businessRuleCode });
|
|
285
|
+
content = typeof read.model?.content === "string" ? read.model.content : undefined;
|
|
286
|
+
}
|
|
287
|
+
if (content === undefined || content.trim() === "")
|
|
288
|
+
return { status: "refused", blocker: "programming-content-required", message: "没有可编译的源码:传 content,或先 programming.save 落库草稿。" };
|
|
289
|
+
const result = await compileSource(session, code, content);
|
|
290
|
+
return {
|
|
291
|
+
schemaCode: args.schemaCode,
|
|
292
|
+
businessRuleCode: args.businessRuleCode,
|
|
293
|
+
code,
|
|
294
|
+
success: result.success,
|
|
295
|
+
errors: result.errors,
|
|
296
|
+
...(result.success ? {} : { hint: "编译失败时规则无法运行(运行时每次执行都重新编译)。按 errors 修正后再 publish。" }),
|
|
297
|
+
};
|
|
298
|
+
}),
|
|
299
|
+
}));
|
|
300
|
+
writeTool(tools, "programming.save", LANE, TOOLS["programming.save"].scope, TOOLS["programming.save"].description, {
|
|
301
|
+
schemaCode: z.string().min(1),
|
|
302
|
+
businessRuleCode: z.string().min(1),
|
|
303
|
+
content: z.string().min(1),
|
|
304
|
+
code: z.string().min(1).optional(),
|
|
305
|
+
businessRuleName: z.string().optional(),
|
|
306
|
+
appCode: z.string().optional(),
|
|
307
|
+
}, async (session, args) => {
|
|
308
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
309
|
+
const response = await wireCall(session, {
|
|
310
|
+
path: ENDPOINTS.programmingSaveSource,
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: {
|
|
313
|
+
code,
|
|
314
|
+
codeType: CODE_TYPE_BUSINESS_RULE,
|
|
315
|
+
schemaCode: args.schemaCode,
|
|
316
|
+
businessRuleCode: args.businessRuleCode,
|
|
317
|
+
...(args.businessRuleName !== undefined ? { businessRuleName: args.businessRuleName } : {}),
|
|
318
|
+
...(args.appCode !== undefined ? { appCode: args.appCode } : {}),
|
|
319
|
+
className: code,
|
|
320
|
+
content: args.content,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const saved = asRecord(response.data);
|
|
324
|
+
const readback = await readSource(session, { schemaCode: args.schemaCode, businessRuleCode: args.businessRuleCode, code });
|
|
325
|
+
const readbackContent = typeof readback.model?.content === "string" ? readback.model.content : null;
|
|
326
|
+
return {
|
|
327
|
+
performed: { errcode: 0, code, version: saved?.version ?? null, contentSha256: sha256(args.content), contentBytes: Buffer.byteLength(args.content, "utf8") },
|
|
328
|
+
readback: {
|
|
329
|
+
present: readback.present,
|
|
330
|
+
provisioned: readback.provisioned,
|
|
331
|
+
version: readback.model?.version ?? null,
|
|
332
|
+
contentMatches: readbackContent === args.content,
|
|
333
|
+
contentSha256: readbackContent === null ? null : sha256(readbackContent),
|
|
334
|
+
published: readback.model?.published === true,
|
|
335
|
+
note: "草稿保存不改变运行时——生效需 programming.publish。",
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}, (args) => {
|
|
339
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
340
|
+
assertTemplateShape(args.content, code);
|
|
341
|
+
}, async (_session, args) => {
|
|
342
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
343
|
+
return {
|
|
344
|
+
code,
|
|
345
|
+
contentSha256: sha256(args.content),
|
|
346
|
+
contentBytes: Buffer.byteLength(args.content, "utf8"),
|
|
347
|
+
note: "dry-run 已执行模板校验(包路径/类名);真写会覆盖远端草稿 content(平台无乐观锁)。",
|
|
348
|
+
};
|
|
349
|
+
}, sourceRollback());
|
|
350
|
+
writeTool(tools, "programming.publish", LANE, TOOLS["programming.publish"].scope, TOOLS["programming.publish"].description, {
|
|
351
|
+
schemaCode: z.string().min(1),
|
|
352
|
+
businessRuleCode: z.string().min(1),
|
|
353
|
+
content: z.string().optional().describe("要发布的源码;省略时用远端草稿(未落库则用模板骨架,会因模板未实现而编译通过但语义为空——通常应传 content)"),
|
|
354
|
+
code: z.string().min(1).optional(),
|
|
355
|
+
businessRuleName: z.string().optional(),
|
|
356
|
+
appCode: z.string().optional(),
|
|
357
|
+
}, async (session, args) => {
|
|
358
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
359
|
+
const rule = await ruleOf(session, args.schemaCode, args.businessRuleCode);
|
|
360
|
+
if (!rule) {
|
|
361
|
+
throw new ToolRefusalError({
|
|
362
|
+
status: "refused",
|
|
363
|
+
blocker: "programming-rule-absent",
|
|
364
|
+
message: `业务规则 ${args.schemaCode}/${args.businessRuleCode} 不存在:publish 必须携带 businessRule 整模型(平台据此把规则切 CODING+enable+发布)。先 rule.create/rule.list 确认规则存在。`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
let content = args.content;
|
|
368
|
+
if (content === undefined) {
|
|
369
|
+
const read = await readSource(session, { schemaCode: args.schemaCode, businessRuleCode: args.businessRuleCode });
|
|
370
|
+
content = typeof read.model?.content === "string" ? read.model.content : undefined;
|
|
371
|
+
}
|
|
372
|
+
if (content === undefined || content.trim() === "")
|
|
373
|
+
throw new ToolRefusalError({
|
|
374
|
+
status: "refused",
|
|
375
|
+
blocker: "programming-content-required",
|
|
376
|
+
message: "没有可发布的源码:传 content,或先 programming.save 落库草稿。",
|
|
377
|
+
});
|
|
378
|
+
assertTemplateShape(content, code);
|
|
379
|
+
// Explicit compile gate: the server compiles again inside publish, but a
|
|
380
|
+
// failed compile must surface its diagnostics BEFORE any write is admitted.
|
|
381
|
+
const compiled = await compileSource(session, code, content);
|
|
382
|
+
if (!compiled.success) {
|
|
383
|
+
throw new ToolRefusalError({
|
|
384
|
+
status: "refused",
|
|
385
|
+
blocker: "programming-compile-failed",
|
|
386
|
+
message: "编译失败,拒绝发布(运行时每次执行都重新编译——坏源码会让该规则的每次业务执行失败)。按 errors 修正后重试。",
|
|
387
|
+
errors: compiled.errors,
|
|
388
|
+
diagnostics: compiled.diagnostics,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const response = await wireCall(session, {
|
|
392
|
+
path: ENDPOINTS.programmingPublish,
|
|
393
|
+
method: "POST",
|
|
394
|
+
body: {
|
|
395
|
+
code,
|
|
396
|
+
codeType: CODE_TYPE_BUSINESS_RULE,
|
|
397
|
+
schemaCode: args.schemaCode,
|
|
398
|
+
businessRuleCode: args.businessRuleCode,
|
|
399
|
+
...(args.businessRuleName !== undefined ? { businessRuleName: args.businessRuleName } : {}),
|
|
400
|
+
...(args.appCode !== undefined ? { appCode: args.appCode } : {}),
|
|
401
|
+
className: code,
|
|
402
|
+
content,
|
|
403
|
+
businessRule: rule,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
// Readback: draft content must equal what we published, and the rule must
|
|
407
|
+
// now be in coding mode + enabled (the platform's publishBusinessRule
|
|
408
|
+
// side effect). published content itself has no wire read endpoint.
|
|
409
|
+
const readbackSource = await readSource(session, { schemaCode: args.schemaCode, businessRuleCode: args.businessRuleCode, code });
|
|
410
|
+
const readbackRule = await ruleOf(session, args.schemaCode, args.businessRuleCode);
|
|
411
|
+
const readbackContent = typeof readbackSource.model?.content === "string" ? readbackSource.model.content : null;
|
|
412
|
+
return {
|
|
413
|
+
performed: { errcode: 0, code, contentSha256: sha256(content), contentBytes: Buffer.byteLength(content, "utf8"), compiled: true },
|
|
414
|
+
readback: {
|
|
415
|
+
present: readbackSource.present,
|
|
416
|
+
contentMatches: readbackContent === content,
|
|
417
|
+
developmentMode: readbackRule?.developmentMode ?? readbackRule?.development_mode ?? null,
|
|
418
|
+
enabled: readbackRule?.enabled ?? null,
|
|
419
|
+
publishStatus: readbackRule?.publishStatus ?? null,
|
|
420
|
+
publishedFlag: readbackSource.model?.published === true,
|
|
421
|
+
note: "运行时执行的是 published 最新版;published 内容无 wire 读取端点,回滚靠本地 git + 重新 publish。",
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}, (args) => {
|
|
425
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
426
|
+
if (args.content !== undefined)
|
|
427
|
+
assertTemplateShape(args.content, code);
|
|
428
|
+
}, async (_session, args) => ({
|
|
429
|
+
code: args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode),
|
|
430
|
+
contentSha256: args.content === undefined ? null : sha256(args.content),
|
|
431
|
+
note: "dry-run 已执行模板校验;真写会 compile→publish(插新 published 版本行并把规则切 CODING+enable)。",
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
export { PROGRAMMING_WIRE };
|
|
435
|
+
//# sourceMappingURL=programming.js.map
|