@stalfh233/omc-cli 0.6.16 → 0.6.17
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/dist/batch-call.js +19 -4
- package/dist/capability-overview.js +2 -2
- package/dist/cli.js +4 -1
- package/dist/compute.js +1 -1
- package/dist/field-families.js +38 -1
- package/dist/graph.js +113 -19
- package/dist/lanes/field.js +5 -1
- package/dist/lanes/model.js +7 -2
- package/dist/lanes/organization.js +9 -2
- package/dist/lanes/runtime-data.js +25 -5
- package/dist/lanes/workflow-contract.js +5 -1
- package/dist/lanes/workflow.js +104 -3
- package/dist/mcp-graph.js +15 -4
- package/dist/rule-when.js +42 -12
- package/dist/skills.js +1 -1
- package/dist/workflow-support.js +2 -1
- package/package.json +3 -3
package/dist/batch-call.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* per-tool preflight apply exactly as they do for a single call.
|
|
20
20
|
*/
|
|
21
21
|
import { refusalText } from "./cli-contract.js";
|
|
22
|
+
import { createArtifact } from "./artifacts.js";
|
|
23
|
+
import { artifactFetchHint, artifactRoot } from "./command-transport.js";
|
|
22
24
|
import { runScript } from "./script.js";
|
|
23
25
|
function inputBusinessKey(args) {
|
|
24
26
|
const keys = ["code", "name", "bizObjectId", "id"];
|
|
@@ -153,6 +155,12 @@ export async function runBatchCall(options) {
|
|
|
153
155
|
};
|
|
154
156
|
});
|
|
155
157
|
const entriesInline = entriesSummary.slice(0, ENTRIES_INLINE_LIMIT);
|
|
158
|
+
const warnings = batchWarnings(options.argsList);
|
|
159
|
+
const warningsInline = warnings.slice(0, ENTRIES_INLINE_LIMIT);
|
|
160
|
+
const detailOmitted = entriesSummary.length > entriesInline.length || warnings.length > warningsInline.length;
|
|
161
|
+
const detailArtifact = detailOmitted
|
|
162
|
+
? createArtifact(artifactRoot(), JSON.stringify({ entries: entriesSummary, warnings }, null, 2) + "\n")
|
|
163
|
+
: undefined;
|
|
156
164
|
return {
|
|
157
165
|
exitCode: failed > 0 ? 1 : 0,
|
|
158
166
|
result: {
|
|
@@ -166,10 +174,17 @@ export async function runBatchCall(options) {
|
|
|
166
174
|
// 每条一行摘要(index/ok/bizObjectId)——不是完整信封,但足以回答
|
|
167
175
|
// 「第 3 条建成了吗、它的 id 是什么」。失败项另有 failures 带完整原因。
|
|
168
176
|
entries: entriesInline,
|
|
169
|
-
...(
|
|
170
|
-
|
|
171
|
-
? {
|
|
172
|
-
|
|
177
|
+
...(warnings.length > 0 ? {
|
|
178
|
+
warnings: warningsInline,
|
|
179
|
+
...(warnings.length > warningsInline.length ? { warningsOmitted: warnings.length - warningsInline.length } : {}),
|
|
180
|
+
} : {}),
|
|
181
|
+
...(entriesSummary.length > entriesInline.length ? { entriesOmitted: entriesSummary.length - entriesInline.length } : {}),
|
|
182
|
+
...(detailArtifact !== undefined ? {
|
|
183
|
+
detailArtifactId: detailArtifact.artifactId,
|
|
184
|
+
sha256: detailArtifact.sha256,
|
|
185
|
+
fetch: artifactFetchHint(detailArtifact.artifactId),
|
|
186
|
+
entriesHint: `条目或预检警告超过 ${ENTRIES_INLINE_LIMIT} 条,只内联前 ${ENTRIES_INLINE_LIMIT} 条;完整结果见 detailArtifactId`,
|
|
187
|
+
} : {}),
|
|
173
188
|
// Only failures carry full per-entry detail: echoing 8 full success
|
|
174
189
|
// envelopes would rebuild exactly the wall of text this command exists
|
|
175
190
|
// to remove.
|
|
@@ -123,7 +123,7 @@ const SURFACES = [
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
id: "workflow",
|
|
126
|
-
title: "流程模板",
|
|
126
|
+
title: "流程模板 + 组织与权限",
|
|
127
127
|
lane: LANES.workflow,
|
|
128
128
|
description: "流程模板生命周期:get_draft/get_published、create、update、publish、bindRule、delete、workitem.list。**含组织/部门/角色/权限范围(org.* 工具)**:部门树、角色组/角色、用户搜索、成员与管理范围(userScope)——流程参与人/可见性的候选来源。",
|
|
129
129
|
operations: ["create", "update", "publish", "bindRule", "delete"],
|
|
@@ -193,7 +193,7 @@ const CAPABILITY_ALIASES = Object.freeze({
|
|
|
193
193
|
businessrule: ["business-rules", "B1-businessrule"],
|
|
194
194
|
"rule-lifecycle": ["rule-lifecycles", "S25-rule-lifecycle"],
|
|
195
195
|
// r28h F1/F7: org.* lives under workflow; aliases let "我要管部门/组织" hit it.
|
|
196
|
-
workflow: ["workflows", "W7-S29-greenfield", "org", "organization", "
|
|
196
|
+
workflow: ["workflows", "W7-S29-greenfield", "org", "organization", "permission", "role", "权限", "角色", "组织"],
|
|
197
197
|
onlinejs: ["online-js"],
|
|
198
198
|
programming: ["backend-online-coding", "programming-online-dev", "在线开发", "在线编码"],
|
|
199
199
|
bizservice: ["bizservices", "B2-registration"],
|
package/dist/cli.js
CHANGED
|
@@ -1670,7 +1670,10 @@ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
|
1670
1670
|
function cmdGraphStatus(transport) {
|
|
1671
1671
|
try {
|
|
1672
1672
|
const status = readGraphStatus();
|
|
1673
|
-
|
|
1673
|
+
// r31a-F16:这里是**第四个**白名单式重建(前三次:compact() 投影、references
|
|
1674
|
+
// 覆盖声明、mcp-graph 的 explore)。白名单每漏一个键就等于没修,且不会被任何
|
|
1675
|
+
// 单测发现——所以改成展开,新增字段自动透传。
|
|
1676
|
+
return emitResult({ status: "ok", ...status }, transport);
|
|
1674
1677
|
}
|
|
1675
1678
|
catch (error) {
|
|
1676
1679
|
if (error instanceof GraphIndexAbsentError)
|
package/dist/compute.js
CHANGED
|
@@ -189,7 +189,7 @@ export async function cmdCompute(args) {
|
|
|
189
189
|
let result;
|
|
190
190
|
let exitCode;
|
|
191
191
|
try {
|
|
192
|
-
({ result, exitCode } = await runToolCall("datarule.create", { args: createArgs, requestId: request, environment }));
|
|
192
|
+
({ result, exitCode } = await runToolCall("datarule.create", { args: createArgs, requestId: request, environment, print: () => undefined }));
|
|
193
193
|
}
|
|
194
194
|
catch (error) {
|
|
195
195
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/field-families.js
CHANGED
|
@@ -64,7 +64,7 @@ export function relationDisplayFieldCode(type, options) {
|
|
|
64
64
|
const displayField = options.displayField;
|
|
65
65
|
return typeof displayField === "string" && displayField.trim() ? displayField.trim() : undefined;
|
|
66
66
|
}
|
|
67
|
-
export const choicesArg = z.array(z.string()).optional().describe('allowed values for text-choice families
|
|
67
|
+
export const choicesArg = z.array(z.string()).optional().describe('allowed values for text-choice families, e.g. ["正常","异常","待复检"]。注意:SELECTION(5) 是人员/部门混合选择器,不接受 choices;文本单选族随版本不同,拒绝信息会列当前 live 集');
|
|
68
68
|
function jsonString() {
|
|
69
69
|
return z.string().refine((value) => {
|
|
70
70
|
if (value === "")
|
|
@@ -830,6 +830,43 @@ export function assertFieldFamilyAllowed(args) {
|
|
|
830
830
|
// works if the preflight gate is the same gate the write uses.
|
|
831
831
|
assertNumericalScale(normalized, options);
|
|
832
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Reject the platform's accept-then-drop org-selector+choices shapes before write.
|
|
835
|
+
*
|
|
836
|
+
* 三个组织选择器族同语义(SELECTION 混合 / STAFF_SELECTOR 人员 / DEPARTMENT_SELECTOR
|
|
837
|
+
* 部门):choices 都会被平台静默丢弃,字段建成后写任何 choices 值都 50000 无明细。
|
|
838
|
+
* r37(2026-11-07)之前 SELECTION 走的是 execute 路径的 capability-out-of-coverage
|
|
839
|
+
* (citation.reason 才说不接受 choices),STAFF/DEPARTMENT 走 live 集拒绝——同一语义
|
|
840
|
+
* 三个族两种 blocker。统一收敛到这里:同语义 = 同 blocker(含 options.choices 形态,
|
|
841
|
+
* 那是 F14 真机撞出来的:字段落库、field.list 还回显 choices、运行面才炸)。
|
|
842
|
+
*/
|
|
843
|
+
const ORG_SELECTOR_TYPES = new Set([
|
|
844
|
+
FIELD_PROPERTY_TYPES.SELECTION,
|
|
845
|
+
FIELD_PROPERTY_TYPES.STAFF_SELECTOR,
|
|
846
|
+
FIELD_PROPERTY_TYPES.DEPARTMENT_SELECTOR,
|
|
847
|
+
]);
|
|
848
|
+
export function assertSelectionChoicesAllowed(args) {
|
|
849
|
+
const options = args.options;
|
|
850
|
+
const hasTopLevelChoices = args.choices !== undefined;
|
|
851
|
+
const hasOptionsChoices = options !== null && typeof options === "object" && !Array.isArray(options)
|
|
852
|
+
&& Object.prototype.hasOwnProperty.call(options, "choices");
|
|
853
|
+
if (!hasTopLevelChoices && !hasOptionsChoices)
|
|
854
|
+
return;
|
|
855
|
+
const raw = args.propertyType;
|
|
856
|
+
const type = typeof raw === "number" && Number.isInteger(raw)
|
|
857
|
+
? raw
|
|
858
|
+
: typeof raw === "string" && raw in FIELD_PROPERTY_TYPES
|
|
859
|
+
? FIELD_PROPERTY_TYPES[raw]
|
|
860
|
+
: undefined;
|
|
861
|
+
if (type === undefined || !ORG_SELECTOR_TYPES.has(type))
|
|
862
|
+
return;
|
|
863
|
+
const family = typeof raw === "string" ? raw : String(type);
|
|
864
|
+
throw new ToolRefusalError({
|
|
865
|
+
status: "refused",
|
|
866
|
+
blocker: "field-choices-family-mismatch",
|
|
867
|
+
message: `${family} 是组织选择器(人员/部门),choices 会被平台静默丢弃;字段虽会建成,但写入任何 choices 值都会返回 50000 且无明细。选人/部门请使用不带 choices 的本族字段(值走引用);文本单选族随版本不同,请先看 omc describe field.create --param propertyType,拒绝信息会列当前 live 集。`,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
833
870
|
function refuseFieldOptions(reason, message) {
|
|
834
871
|
const citation = {
|
|
835
872
|
code: "capability-out-of-coverage",
|
package/dist/graph.js
CHANGED
|
@@ -46,10 +46,18 @@ function nodeSqlite() {
|
|
|
46
46
|
* staleness is handled by reporting pulledAt + identity, refreshed by an
|
|
47
47
|
* explicit re-pull (no incremental watermarks, no write-back hooks).
|
|
48
48
|
*/
|
|
49
|
-
const GRAPH_SCHEMA_VERSION = "
|
|
49
|
+
const GRAPH_SCHEMA_VERSION = "5";
|
|
50
50
|
const STALE_LOCK_MS = 15 * 60 * 1000;
|
|
51
51
|
export const DEFAULT_PULL_CONCURRENCY = 16;
|
|
52
52
|
export const MAX_PULL_CONCURRENCY = 32;
|
|
53
|
+
/** 索引超过这个天数就提示刷新(配置类查询,1 天足够新)。 */
|
|
54
|
+
export const STALE_INDEX_DAYS = 1;
|
|
55
|
+
export function indexAgeDays(pulledAtIso, now = Date.now()) {
|
|
56
|
+
const pulled = Date.parse(pulledAtIso);
|
|
57
|
+
if (Number.isNaN(pulled))
|
|
58
|
+
return 0;
|
|
59
|
+
return Math.max(0, Math.floor((now - pulled) / 86_400_000));
|
|
60
|
+
}
|
|
53
61
|
export class GraphIndexAbsentError extends Error {
|
|
54
62
|
constructor(message) {
|
|
55
63
|
super(message);
|
|
@@ -179,13 +187,39 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
179
187
|
// zero-guard) wedge the scheduler.
|
|
180
188
|
const scope = await resolveApplicationScope(session, undefined).catch(() => undefined);
|
|
181
189
|
const applicationCode = scope?.code ?? "";
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
const gaps = [];
|
|
191
|
+
const treeRows = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
192
|
+
const modelEntities = treeRows.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code !== "").map((row) => ({ schemaCode: String(row.code), name: String(row.name ?? row.code) })).sort((a, b) => a.schemaCode.localeCompare(b.schemaCode));
|
|
193
|
+
const schemaCodes = modelEntities.map((row) => row.schemaCode);
|
|
194
|
+
const fields = [];
|
|
195
|
+
const services = [];
|
|
196
|
+
await Promise.all(schemaCodes.map(async (schemaCode) => {
|
|
197
|
+
try {
|
|
198
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
const code = String(row.code ?? row.propertyCode ?? "");
|
|
201
|
+
if (code)
|
|
202
|
+
fields.push({ schemaCode, code, name: String(row.name ?? row.propertyName ?? code) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
gaps.push({ kind: "field-list", ref: schemaCode, note: error instanceof Error ? error.message : String(error) });
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
209
|
+
try {
|
|
210
|
+
await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList });
|
|
211
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceListByCategoryId, query: { categoryId: "" } })).data);
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
const code = String(row.code ?? "");
|
|
214
|
+
if (code)
|
|
215
|
+
services.push({ code, name: String(row.name ?? code) });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
gaps.push({ kind: "service-list", ref: "all", note: error instanceof Error ? error.message : String(error) });
|
|
220
|
+
}
|
|
186
221
|
const rules = [];
|
|
187
222
|
const edges = [];
|
|
188
|
-
const gaps = [];
|
|
189
223
|
let parseFailures = 0;
|
|
190
224
|
let dataruleCount = 0;
|
|
191
225
|
let viewCount = 0;
|
|
@@ -375,6 +409,9 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
375
409
|
},
|
|
376
410
|
rules,
|
|
377
411
|
edges,
|
|
412
|
+
models: modelEntities,
|
|
413
|
+
fields: fields.sort((a, b) => `${a.schemaCode}/${a.code}`.localeCompare(`${b.schemaCode}/${b.code}`)),
|
|
414
|
+
services: services.sort((a, b) => a.code.localeCompare(b.code)),
|
|
378
415
|
gaps,
|
|
379
416
|
scanned: { models: schemaCodes.length, rules: rules.length, datarules: dataruleCount, views: viewCount, parseFailures },
|
|
380
417
|
timings: { listMs, readMs, totalMs: Date.now() - totalStart },
|
|
@@ -489,7 +526,10 @@ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCU
|
|
|
489
526
|
via TEXT NOT NULL
|
|
490
527
|
);
|
|
491
528
|
CREATE INDEX edges_lookup ON edges(kind, code);
|
|
492
|
-
|
|
529
|
+
CREATE TABLE gaps (kind TEXT NOT NULL, ref TEXT NOT NULL, note TEXT);
|
|
530
|
+
CREATE TABLE models (schema_code TEXT PRIMARY KEY, name TEXT NOT NULL);
|
|
531
|
+
CREATE TABLE fields (schema_code TEXT NOT NULL, code TEXT NOT NULL, name TEXT NOT NULL);
|
|
532
|
+
CREATE TABLE services (code TEXT PRIMARY KEY, name TEXT NOT NULL);
|
|
493
533
|
`);
|
|
494
534
|
const insertMeta = db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)");
|
|
495
535
|
// `applicationResolution` is an object; String(value) would persist
|
|
@@ -506,12 +546,21 @@ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCU
|
|
|
506
546
|
const insertGap = db.prepare("INSERT INTO gaps (kind, ref, note) VALUES (?, ?, ?)");
|
|
507
547
|
for (const gap of data.gaps)
|
|
508
548
|
insertGap.run(gap.kind, gap.ref, gap.note ?? null);
|
|
549
|
+
const insertModel = db.prepare("INSERT INTO models (schema_code, name) VALUES (?, ?)");
|
|
550
|
+
for (const row of data.models)
|
|
551
|
+
insertModel.run(row.schemaCode, row.name);
|
|
552
|
+
const insertField = db.prepare("INSERT INTO fields (schema_code, code, name) VALUES (?, ?, ?)");
|
|
553
|
+
for (const row of data.fields)
|
|
554
|
+
insertField.run(row.schemaCode, row.code, row.name);
|
|
555
|
+
const insertService = db.prepare("INSERT INTO services (code, name) VALUES (?, ?)");
|
|
556
|
+
for (const row of data.services)
|
|
557
|
+
insertService.run(row.code, row.name);
|
|
509
558
|
// FTS5 mirrors rules (code+name) for `graph search`; one rebuild after
|
|
510
559
|
// the bulk insert is cheaper than per-row triggers on a wholesale rebuild.
|
|
511
560
|
db.exec(`
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
561
|
+
CREATE VIRTUAL TABLE rules_fts USING fts5(rule_code, name, content='rules', content_rowid='rowid');
|
|
562
|
+
INSERT INTO rules_fts(rules_fts) VALUES('rebuild');
|
|
563
|
+
`);
|
|
515
564
|
db.exec("COMMIT;");
|
|
516
565
|
}
|
|
517
566
|
finally {
|
|
@@ -544,6 +593,11 @@ function openGraphIndex() {
|
|
|
544
593
|
throw new GraphIndexAbsentError(`索引 schema 版本不匹配(${meta.schemaVersion} ≠ ${GRAPH_SCHEMA_VERSION});重跑 omc graph pull`);
|
|
545
594
|
const ruleCount = db.prepare("SELECT COUNT(*) AS n FROM rules").get().n;
|
|
546
595
|
const modelCount = db.prepare("SELECT COUNT(DISTINCT schema_code) AS n FROM rules").get().n;
|
|
596
|
+
const entityCounts = {
|
|
597
|
+
models: db.prepare("SELECT COUNT(*) AS n FROM models").get().n,
|
|
598
|
+
fields: db.prepare("SELECT COUNT(*) AS n FROM fields").get().n,
|
|
599
|
+
services: db.prepare("SELECT COUNT(*) AS n FROM services").get().n,
|
|
600
|
+
};
|
|
547
601
|
const edgeRows = db.prepare("SELECT kind, COUNT(*) AS n FROM edges GROUP BY kind").all();
|
|
548
602
|
const edges = {};
|
|
549
603
|
for (const row of edgeRows)
|
|
@@ -555,6 +609,8 @@ function openGraphIndex() {
|
|
|
555
609
|
byKind[row.kind] = row.n;
|
|
556
610
|
gapCount += row.n;
|
|
557
611
|
}
|
|
612
|
+
const pulledAtIso = meta.pulledAtIso ?? "";
|
|
613
|
+
const ageDays = indexAgeDays(pulledAtIso);
|
|
558
614
|
return {
|
|
559
615
|
db,
|
|
560
616
|
summary: {
|
|
@@ -564,10 +620,17 @@ function openGraphIndex() {
|
|
|
564
620
|
// such key, so this stays optional rather than failing the read.
|
|
565
621
|
...(meta.applicationResolution ? { applicationResolution: parseApplicationResolution(meta.applicationResolution) } : {}),
|
|
566
622
|
identityFingerprint: meta.identityFingerprint ?? "",
|
|
567
|
-
pulledAtIso
|
|
623
|
+
pulledAtIso,
|
|
568
624
|
freshness: gapCount === 0 ? "complete" : "partial",
|
|
625
|
+
ageDays,
|
|
626
|
+
// r31a-F16:旧索引 + complete 标签 = 静默错答。给可直接照抄的刷新命令,
|
|
627
|
+
// 并明确「complete 只指扫描无缺口」。
|
|
628
|
+
...(ageDays >= STALE_INDEX_DAYS
|
|
629
|
+
? { staleWarning: `索引是 ${ageDays} 天前的快照(pulledAt ${pulledAtIso});freshness:"complete" 只表示扫描无缺口、不表示最新。先 \`omc graph pull --env ${meta.environmentName ?? "<env>"}\` 再查,或给结论加时效限定` }
|
|
630
|
+
: {}),
|
|
569
631
|
models: modelCount,
|
|
570
632
|
rules: ruleCount,
|
|
633
|
+
entities: entityCounts,
|
|
571
634
|
edges,
|
|
572
635
|
gaps: { count: gapCount, byKind },
|
|
573
636
|
},
|
|
@@ -578,6 +641,17 @@ function openGraphIndex() {
|
|
|
578
641
|
throw error instanceof GraphIndexAbsentError ? error : new GraphIndexAbsentError(`索引不可读(schema 不匹配或文件损坏):${error instanceof Error ? error.message : String(error)};重跑 omc graph pull`);
|
|
579
642
|
}
|
|
580
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* r35a-F4 / r35a-F4b(真机 2026-11-06):探针拿 `matches:[]` 差点把一个**被表单引用**
|
|
646
|
+
* 的字段判成「可安全删除」,紧接着发现 `graph impact` 的 `totals` 全 0 也有同样误读风险。
|
|
647
|
+
* 三个面(CLI references / CLI impact / MCP explore)必须共用同一份声明——
|
|
648
|
+
* 分散定义就会被漏掉一处(MCP 面第一版就漏了)。
|
|
649
|
+
*/
|
|
650
|
+
export const GRAPH_COVERAGE = Object.freeze({
|
|
651
|
+
scanned: Object.freeze(["businessrule", "datarule", "listview"]),
|
|
652
|
+
notScanned: Object.freeze(["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"]),
|
|
653
|
+
note: "零命中/全零 totals 只代表 scanned 面没有命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响",
|
|
654
|
+
});
|
|
581
655
|
/**
|
|
582
656
|
* Zero-hit disambiguation (2026-10-31 capability-test friction, hit twice in
|
|
583
657
|
* one session): querying `impact field mcc_supplier` returns an empty result
|
|
@@ -611,11 +685,7 @@ export function queryGraphReferences(kind, code) {
|
|
|
611
685
|
// 字段判成「可安全删除」——note 只说了「不含规则条件叶子/流程绑定」,从没
|
|
612
686
|
// 明说表单与流程权限表不在扫描范围。散文字段不承担安全契约:这里给结构化
|
|
613
687
|
// coverage,让「没人引用」与「这一面没扫」在机器可读层面可分。
|
|
614
|
-
const coverage =
|
|
615
|
-
scanned: ["businessrule", "datarule", "listview"],
|
|
616
|
-
notScanned: ["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"],
|
|
617
|
-
note: "matches:[] 只代表 scanned 面零命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除",
|
|
618
|
-
};
|
|
688
|
+
const coverage = GRAPH_COVERAGE;
|
|
619
689
|
return {
|
|
620
690
|
kind,
|
|
621
691
|
code,
|
|
@@ -643,7 +713,14 @@ export function readGraphStatus() {
|
|
|
643
713
|
const { db, summary } = openGraphIndex();
|
|
644
714
|
try {
|
|
645
715
|
const gaps = db.prepare("SELECT kind, ref, note FROM gaps ORDER BY kind, ref LIMIT 50").all();
|
|
646
|
-
|
|
716
|
+
const dbPath = graphDbPath();
|
|
717
|
+
return {
|
|
718
|
+
index: summary,
|
|
719
|
+
dbBytes: statSync(dbPath).size,
|
|
720
|
+
dbPath,
|
|
721
|
+
dbId: sha256(dbPath).slice(0, 12),
|
|
722
|
+
gaps,
|
|
723
|
+
};
|
|
647
724
|
}
|
|
648
725
|
finally {
|
|
649
726
|
db.close();
|
|
@@ -661,6 +738,11 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
661
738
|
try {
|
|
662
739
|
const tokens = term.trim().split(/\s+/).filter((token) => token !== "");
|
|
663
740
|
let matches = [];
|
|
741
|
+
const entityMatches = {
|
|
742
|
+
model: { count: 0, total: 0, items: [] },
|
|
743
|
+
field: { count: 0, total: 0, items: [] },
|
|
744
|
+
service: { count: 0, total: 0, items: [] },
|
|
745
|
+
};
|
|
664
746
|
if (tokens.length > 0) {
|
|
665
747
|
const quoted = tokens.map((token, index) => `"${token.replaceAll('"', '""')}"${index === tokens.length - 1 ? "*" : ""}`).join(" ");
|
|
666
748
|
const rows = db
|
|
@@ -683,13 +765,24 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
683
765
|
refs,
|
|
684
766
|
};
|
|
685
767
|
});
|
|
768
|
+
const searchEntity = (countSql, rowsSql, params, map) => {
|
|
769
|
+
const total = Number(db.prepare(countSql).get(...params).n);
|
|
770
|
+
const rows = db.prepare(rowsSql).all(...params, limit);
|
|
771
|
+
return { count: rows.length, total, items: rows.map(map), ...(rows.length < total ? { truncated: true } : {}) };
|
|
772
|
+
};
|
|
773
|
+
entityMatches.model = searchEntity("SELECT COUNT(*) AS n FROM models WHERE instr(name, ?) > 0 OR instr(schema_code, ?) > 0", "SELECT schema_code, name FROM models WHERE instr(name, ?) > 0 OR instr(schema_code, ?) > 0 ORDER BY schema_code LIMIT ?", [term.trim(), term.trim()], (row) => ({ schemaCode: row.schema_code, name: row.name }));
|
|
774
|
+
entityMatches.field = searchEntity("SELECT COUNT(*) AS n FROM fields WHERE instr(name, ?) > 0 OR instr(code, ?) > 0", "SELECT schema_code, code, name FROM fields WHERE instr(name, ?) > 0 OR instr(code, ?) > 0 ORDER BY schema_code, code LIMIT ?", [term.trim(), term.trim()], (row) => ({ schemaCode: row.schema_code, code: row.code, name: row.name }));
|
|
775
|
+
entityMatches.service = searchEntity("SELECT COUNT(*) AS n FROM services WHERE instr(name, ?) > 0 OR instr(code, ?) > 0", "SELECT code, name FROM services WHERE instr(name, ?) > 0 OR instr(code, ?) > 0 ORDER BY code LIMIT ?", [term.trim(), term.trim()], (row) => ({ code: row.code, name: row.name }));
|
|
686
776
|
}
|
|
687
777
|
const gapped = summary.gaps.count > 0;
|
|
688
778
|
return {
|
|
689
779
|
term,
|
|
690
780
|
rules: matches,
|
|
781
|
+
matches: entityMatches,
|
|
691
782
|
index: summary,
|
|
692
|
-
|
|
783
|
+
coverage: { scanned: ["rule.code", "rule.name", "model.code", "model.name", "field.code", "field.name", "service.code", "service.name"], notScanned: ["rule.body", "form", "workflow.permissions"], note: "检索覆盖规则与实体的 code/name;规则体、表单与流程权限未扫描" },
|
|
784
|
+
...(term.trim() !== "" && !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(term.trim()) ? { note: "疑似业务词;本工具同时检索实体 name/code 与规则 code/name,引用明细请使用匹配到的 code 再查 references/impact" } : {}),
|
|
785
|
+
...(matches.length === 0 && entityMatches.model.total === 0 && entityMatches.field.total === 0 && entityMatches.service.total === 0
|
|
693
786
|
? {
|
|
694
787
|
note: tokens.length === 0
|
|
695
788
|
? "检索词为空"
|
|
@@ -767,11 +860,12 @@ export function impactGraph(kind, code) {
|
|
|
767
860
|
listviews: listViewHits.slice(0, 20).map((hit) => ({ schemaCode: hit.schema_code, viewCode: hit.view_code ?? "", via: hit.via })),
|
|
768
861
|
totals: { rules: hop1.length, datarules: dataruleHits.length, listviews: listViewHits.length, touchedServices: services.size, touchedModels: models.size, touchedFields: fields.size },
|
|
769
862
|
index: summary,
|
|
863
|
+
coverage: GRAPH_COVERAGE,
|
|
770
864
|
...(hop1.length === 0 && dataruleHits.length === 0 && listViewHits.length === 0
|
|
771
865
|
? {
|
|
772
866
|
note: (gapped
|
|
773
867
|
? "零命中,但索引存在扫描缺口——不可作为「无影响」证明;先 omc graph pull 重建再查"
|
|
774
|
-
: "
|
|
868
|
+
: "零命中=仅 scanned 面(规则/数据规则/视图)没有影响;form(表单控件)与 workflow.permissions 未扫描,见 coverage.notScanned——不可据此判定「删除无影响」") +
|
|
775
869
|
(zeroHitKindHint(db, kind, code, "impact") ?? ""),
|
|
776
870
|
}
|
|
777
871
|
: {}),
|
package/dist/lanes/field.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
3
|
import { fieldChangeWireBody, fieldUpdateWireBody, prepareFieldChange, prepareFieldUpdate } from "../field-change.js";
|
|
4
|
-
import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, childColumnsOf, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, } from "../field-families.js";
|
|
4
|
+
import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, childColumnsOf, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, assertFieldFamilyAllowed, assertSelectionChoicesAllowed, } from "../field-families.js";
|
|
5
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
6
6
|
import { governedRead } from "../read.js";
|
|
7
7
|
import { writeTool } from "../write.js";
|
|
@@ -442,6 +442,10 @@ export function registerFieldLane(tools) {
|
|
|
442
442
|
},
|
|
443
443
|
};
|
|
444
444
|
},
|
|
445
|
+
preflight: (args) => {
|
|
446
|
+
assertSelectionChoicesAllowed(args);
|
|
447
|
+
assertFieldFamilyAllowed(args);
|
|
448
|
+
},
|
|
445
449
|
rollback: fieldRollbackSpec("field.create"),
|
|
446
450
|
});
|
|
447
451
|
writeTool({
|
package/dist/lanes/model.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
|
-
import { assertFieldFamilyAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, } from "../field-families.js";
|
|
3
|
+
import { assertFieldFamilyAllowed, assertSelectionChoicesAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, } from "../field-families.js";
|
|
4
4
|
import { fieldChangeWireBody, prepareFieldChange, sameFieldChange } from "../field-change.js";
|
|
5
5
|
import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
|
|
6
6
|
import { mapBounded } from "../support.js";
|
|
@@ -1317,6 +1317,8 @@ export function registerModelLane(tools) {
|
|
|
1317
1317
|
];
|
|
1318
1318
|
if (violations.length > 0)
|
|
1319
1319
|
throw new ToolRefusalError({ status: "refused", blocker: "business-code-invalid", message: "code 未过出门前校验;按 validation 修正(长度/风格/保留字)", validation: violations });
|
|
1320
|
+
for (const entry of entries)
|
|
1321
|
+
assertSelectionChoicesAllowed(entry);
|
|
1320
1322
|
for (const entry of entries)
|
|
1321
1323
|
prepareFieldChange(fieldChangeInputOf(args.code, entry));
|
|
1322
1324
|
},
|
|
@@ -1513,8 +1515,10 @@ function fieldEntriesOf(args) {
|
|
|
1513
1515
|
if (Array.isArray(args.fields) && args.fields.length > 0) {
|
|
1514
1516
|
if (args.fieldCode !== undefined || args.fieldName !== undefined || args.propertyType !== undefined)
|
|
1515
1517
|
throw new ToolRefusalError({ status: "refused", blocker: "configure-model-fields-exclusive", message: "fields[] 与单字段参数(fieldCode/fieldName/propertyType)互斥;多字段用 fields[],单字段用旧三元组" });
|
|
1516
|
-
for (const entry of args.fields)
|
|
1518
|
+
for (const entry of args.fields) {
|
|
1519
|
+
assertSelectionChoicesAllowed(entry);
|
|
1517
1520
|
assertFieldFamilyAllowed({ ...entry });
|
|
1521
|
+
}
|
|
1518
1522
|
return args.fields.map((entry) => ({ code: entry.code, name: entry.name, propertyType: entry.propertyType, ...withLegacy(entry) }));
|
|
1519
1523
|
}
|
|
1520
1524
|
if (typeof args.fieldCode !== "string" || typeof args.fieldName !== "string" || typeof args.propertyType !== "string")
|
|
@@ -1524,6 +1528,7 @@ function fieldEntriesOf(args) {
|
|
|
1524
1528
|
message: "configure_model 需要 fields[](多字段)或 fieldCode+fieldName+propertyType(单字段)",
|
|
1525
1529
|
});
|
|
1526
1530
|
const legacy = { code: args.fieldCode, name: args.fieldName, propertyType: args.propertyType, ...withLegacy(args) };
|
|
1531
|
+
assertSelectionChoicesAllowed(legacy);
|
|
1527
1532
|
assertFieldFamilyAllowed(legacy);
|
|
1528
1533
|
return [legacy];
|
|
1529
1534
|
}
|
|
@@ -531,10 +531,17 @@ export function registerOrganizationLane(tools) {
|
|
|
531
531
|
remarks: z.string().optional(),
|
|
532
532
|
corpId: z.string().min(1).optional().describe("组织 corpId,默认当前登录企业;传 \"other\" 表示非组织分组角色"),
|
|
533
533
|
},
|
|
534
|
-
|
|
534
|
+
preflight: (args) => {
|
|
535
535
|
if (!args.roleGroupCode && !args.roleGroupId) {
|
|
536
|
-
throw new
|
|
536
|
+
throw new ToolRefusalError({
|
|
537
|
+
status: "refused",
|
|
538
|
+
blocker: "role-group-required",
|
|
539
|
+
nextCommand: "omc call org.role.list --args '{}'",
|
|
540
|
+
message: "必须给 roleGroupCode(推荐)或 roleGroupId;先 org.role.list 取 groups 码表",
|
|
541
|
+
});
|
|
537
542
|
}
|
|
543
|
+
},
|
|
544
|
+
execute: async (session, args) => {
|
|
538
545
|
const { groups } = await readRoleTree(session);
|
|
539
546
|
if (groups.length === 0) {
|
|
540
547
|
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "org.role.list 未解析出任何角色组;先确认组织树可读,不要盲建角色" });
|
|
@@ -55,6 +55,7 @@ function dataListCompactProjection(result) {
|
|
|
55
55
|
if (value.length > 0)
|
|
56
56
|
compact.listArtifactHint =
|
|
57
57
|
"items 已外置(truncatedKeys 含 items):完整行见 artifact(detailArtifactId / fetch)。同信封三分法:ids=本页全部 id;itemsPreview=ids 前 3 个的引用(仅 {id},不是第二份行);items=[] 表示正文在 artifact。";
|
|
58
|
+
compact.itemsExternalized = true;
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
60
61
|
// 这两个键由本函数按 items 现算,handler 不再重复内联。
|
|
@@ -921,12 +922,12 @@ export function registerRuntimeDataLane(tools) {
|
|
|
921
922
|
columns: z.array(z.string().min(1)).optional().describe("列投影别名(=displayFields)"),
|
|
922
923
|
// r29b F9: data.load/save/update 都叫 fields;list 面原先只有 displayFields/columns。
|
|
923
924
|
fields: z.array(z.string().min(1)).optional().describe("列投影别名(=displayFields;与 data.load/data.save/data.update 的 fields 同语义)"),
|
|
924
|
-
orders: z.array(z.object({ field: z.string().min(1), dir: z.enum(["ASC", "DESC"]) })).optional(),
|
|
925
|
+
orders: z.array(z.object({ field: z.string().min(1), dir: z.enum(["ASC", "DESC"]) })).optional().describe("运行面 query/list 不支持服务端排序;排序请用 listview.configure 的 sorts(视图级)或在本地对 items 排序"),
|
|
925
926
|
mobile: z.boolean().optional(),
|
|
926
927
|
showTotal: z.boolean().default(true),
|
|
927
928
|
page: z.number().int().min(0).default(0),
|
|
928
929
|
size: z.number().int().min(1).max(200).default(10),
|
|
929
|
-
all: z.boolean().optional().describe("true
|
|
930
|
+
all: z.boolean().optional().describe("true 时自动翻页聚合全部结果(size=200 逐页,上限 1000 行,超出 resultTruncated:true);与 page 互斥;默认 size=10,视图 pagingSetting.pageSize 不生效"),
|
|
930
931
|
full: z.boolean().optional().describe("内联完整列表结果;默认大结果只返回计数与 detailFile"),
|
|
931
932
|
// r31 (2026-10-31 real machine): agents pass `query: {field: value}` as a
|
|
932
933
|
// filter — zod would STRIP the unknown key silently and the server
|
|
@@ -943,6 +944,14 @@ export function registerRuntimeDataLane(tools) {
|
|
|
943
944
|
tool: "data.list",
|
|
944
945
|
message: '没有 `query` 参数——过滤用 `filter: {"op":"eq","field":"<字段>","value":"<值>"}`(或 `filters` 数组=AND、`filterGroup` 嵌套)。r31 真机:`query:{...}` 被静默忽略、total 不变,极易误判为"无数据/已过滤"。',
|
|
945
946
|
});
|
|
947
|
+
if (args.orders !== undefined)
|
|
948
|
+
throw new ToolInputError({
|
|
949
|
+
status: "error",
|
|
950
|
+
blocker: "invalid-input",
|
|
951
|
+
tool: "data.list",
|
|
952
|
+
message: "运行面 query/list 不支持服务端排序;排序请用 listview.configure 的 sorts(视图级)或在本地对 items 排序。",
|
|
953
|
+
nextCommand: `omc call listview.configure --env <env> --args '${JSON.stringify({ schemaCode: args.schemaCode, sorts: args.orders })}'`,
|
|
954
|
+
});
|
|
946
955
|
let condition;
|
|
947
956
|
let fromDsl = false;
|
|
948
957
|
try {
|
|
@@ -1094,7 +1103,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1094
1103
|
const firstPage = extractBizObjectPage(first.data);
|
|
1095
1104
|
const total = Number(firstPage.total ?? 0);
|
|
1096
1105
|
if (total === 0 || firstPage.content.length === 0)
|
|
1097
|
-
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
1106
|
+
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [], itemsCount: 0, itemsReturned: 0, resultTruncated: false, itemsExternalized: false, ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
1098
1107
|
const rows = [...firstPage.content];
|
|
1099
1108
|
const target = Math.min(total, CAP);
|
|
1100
1109
|
let pageIndex = 1;
|
|
@@ -1154,6 +1163,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1154
1163
|
// 形状常量:脚本按 result.items 取数(与内联态一致),itemsCount 与
|
|
1155
1164
|
// total 的差就是被截断的条数——不再只靠 size 反推。
|
|
1156
1165
|
itemsCount: collected.length,
|
|
1166
|
+
itemsReturned: collected.length,
|
|
1167
|
+
resultTruncated: truncatedByCap,
|
|
1168
|
+
itemsExternalized: false,
|
|
1157
1169
|
// r29b F8 的 ids/itemsPreview 由 compact(外置时)现算,不在这里内联——
|
|
1158
1170
|
// 内联会让小结果越过 2048B 外置阈值,反而把 items 挤成 []。
|
|
1159
1171
|
...(truncatedByCap
|
|
@@ -1190,9 +1202,13 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1190
1202
|
if (probeError instanceof WireError && probeError.kind === "business" && probeError.errcode === 300004) {
|
|
1191
1203
|
return {
|
|
1192
1204
|
status: "refused",
|
|
1193
|
-
blocker: "
|
|
1205
|
+
blocker: "schema-not-found",
|
|
1194
1206
|
errcode: 300004,
|
|
1195
|
-
|
|
1207
|
+
schemaCode: args.schemaCode,
|
|
1208
|
+
exists: false,
|
|
1209
|
+
total: 0,
|
|
1210
|
+
message: `数据模型不存在(${args.schemaCode})——运行面读取前模型必须已创建并发布字段。`,
|
|
1211
|
+
nextCommand: "omc call model.list --args '{}'",
|
|
1196
1212
|
};
|
|
1197
1213
|
}
|
|
1198
1214
|
}
|
|
@@ -1280,6 +1296,10 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1280
1296
|
})(),
|
|
1281
1297
|
...(choiceFilterFields.length > 0 && Number(total ?? 0) === 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}),
|
|
1282
1298
|
totalPages,
|
|
1299
|
+
itemsCount: items.length,
|
|
1300
|
+
itemsReturned: items.length,
|
|
1301
|
+
resultTruncated: false,
|
|
1302
|
+
itemsExternalized: false,
|
|
1283
1303
|
page: page.page ?? requestedPage,
|
|
1284
1304
|
size: page.size ?? args.size ?? 20,
|
|
1285
1305
|
...(pageOutOfRange ? { pageOutOfRange: true, emptyPage: true } : {}),
|
|
@@ -78,6 +78,7 @@ export const WORKFLOW_WIRE = Object.freeze({
|
|
|
78
78
|
workItemFilter: "WorkItemSearchVO.filterWorkflowInstanceId is a NOT-equal exclusion (BizWorkitemServiceImpl.toSearchWorkItemsCriterion:400-402), so exact instance filtering is client-side",
|
|
79
79
|
bizObjectResolution: "GET /api/api/runtime/workflow/getBizObjectByWfInstanceId resolves bizObjectId/schemaCode; a failed lookup leaves the identity null and never drops the todo",
|
|
80
80
|
mutuallyExclusiveBinding: "同一处理器绑定多条规则时它们**全部执行、无互斥**:审批通过/驳回这类互斥结果不要用两条各自 FIXED 赋值都绑 end(bench22 病灶)——通过绑 end(endEventHandler)或规则 Available,驳回绑 cancel(cancelEventHandler)或规则 Cancel,或用一条多分支图(生产 case_info_update 只在 endEventHandler 绑一条 updateData 多分支图);workflow.bindRule 回读在 bizActions.length>1 时给 advisory",
|
|
81
|
+
participantRoleValidation: "写前参与人校验:PARTICIPANT 的 FindUserByRole(<org>,\"<roleCode>\",\"<order>\") 会与 org.role.list 的真实角色 code 比对;缺失即拒绝 workflow-participant-role-not-found,并给 activityCode/activityName/roleCode、最多 5 个 candidates 与 nextCommand。org.role.list 读取失败时 fail-open,不阻断写入。{Originator} 是发起人关键字,不按角色校验;所有 PARTICIPANT 都是 {Originator} 时给 approval-by-originator warning。",
|
|
81
82
|
userSearch: "POST /api/api/organization/user/search,@RequestBody {wd (关键字,必填), page, size, filterType?} → PageVO<UserVO>{totalElements,totalPages,content[{id,name,username,userId,departmentId,corpId,status}]}(UserController.search:337-341;缺 wd 报 10005 关键字不能为空)。用于把姓名解析成真实用户,构造参与者对象 type 3=用户(type 2=角色)",
|
|
82
83
|
});
|
|
83
84
|
/** Workflow template object (workflow/update|publish raw body). */
|
|
@@ -117,6 +118,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
117
118
|
bizObjectResolution: WORKFLOW_WIRE.bizObjectResolution,
|
|
118
119
|
mutuallyExclusiveBinding: WORKFLOW_WIRE.mutuallyExclusiveBinding,
|
|
119
120
|
userSearch: WORKFLOW_WIRE.userSearch,
|
|
121
|
+
participantRoleValidation: WORKFLOW_WIRE.participantRoleValidation,
|
|
120
122
|
}),
|
|
121
123
|
wireBindings: Object.freeze({
|
|
122
124
|
templateShape: Object.freeze([behaviorBinding("test/ticket-112-workflow-template-merge.test.ts#workflow.update merges onto the draft", assertWorkflowContract)]),
|
|
@@ -135,6 +137,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
135
137
|
bizObjectResolution: Object.freeze([emitterBinding("workflow-support.resolveBizObjectIdByInstance", assertWorkflowContract)]),
|
|
136
138
|
mutuallyExclusiveBinding: Object.freeze([behaviorBinding("test/issue-14-workflow-bind-rule.test.ts#workflow.bindRule merges endEventHandler", assertWorkflowContract)]),
|
|
137
139
|
userSearch: Object.freeze([behaviorBinding("test/round4-org-user-search.test.ts#org.user.search resolves a user by name into participant fields", assertWorkflowContract)]),
|
|
140
|
+
participantRoleValidation: Object.freeze([behaviorBinding("test/workflow-participant-role.test.ts#workflow.update refuses an unknown FindUserByRole role before the wire", assertWorkflowContract)]),
|
|
138
141
|
}),
|
|
139
142
|
evidence: Object.freeze([
|
|
140
143
|
".scratch/omc-approval-workflow/acceptance/s29-probe-20260910012330184.md",
|
|
@@ -160,7 +163,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
160
163
|
}),
|
|
161
164
|
"workflow.update": Object.freeze({
|
|
162
165
|
scope: WORKFLOW_SCOPE,
|
|
163
|
-
description: "写入工作流模板草稿(`/api/api/workflow/update`,raw template JSON body)。cli 以当前草稿为基线做 merge,未列出的既有节点保留:**模板缺 START 或 END 时自动按增量 merge(部分模板),给全 START..END 才整模板替换**;也可显式 `merge:true/false`。**连线三种写法选一**:`routes`(recipe 写法)/ `edges`(同义)/ `rules`(平台 DTO 字段名,`get_draft` 读回原样)——同传两种会被拒(`workflow-routes-ambiguous`);merge 是并集(草稿边 + 你的边),**绝不删边**,若结果会比草稿少边则拒绝(`workflow-merge-would-drop-routes`,回 routesBefore/After + 缺失连线),dry-run 也会给出 `routesBefore/routesAfter/routesAdded/routesDropped`。**节点形状**:新增节点必须给 `type`/`activityType`(可选 START/PARTICIPANT/END…),`activityCode` 命中草稿既有节点时可省(类型/名称继承草稿);**字段权限只发改动行**——`propertyPermissions` 是载荷体积的主要来源(整模板单次常到 1.0–1.4MB,超过 `--args-file` 上限 `CLI_ARGS_MAX_BYTES`=1MB
|
|
166
|
+
description: "写入工作流模板草稿(`/api/api/workflow/update`,raw template JSON body)。cli 以当前草稿为基线做 merge,未列出的既有节点保留:**模板缺 START 或 END 时自动按增量 merge(部分模板),给全 START..END 才整模板替换**;也可显式 `merge:true/false`。**连线三种写法选一**:`routes`(recipe 写法)/ `edges`(同义)/ `rules`(平台 DTO 字段名,`get_draft` 读回原样)——同传两种会被拒(`workflow-routes-ambiguous`);merge 是并集(草稿边 + 你的边),**绝不删边**,若结果会比草稿少边则拒绝(`workflow-merge-would-drop-routes`,回 routesBefore/After + 缺失连线),dry-run 也会给出 `routesBefore/routesAfter/routesAdded/routesDropped`。**节点形状**:新增节点必须给 `type`/`activityType`(可选 START/PARTICIPANT/END…),`activityCode` 命中草稿既有节点时可省(类型/名称继承草稿);**字段权限只发改动行**——`propertyPermissions` 是载荷体积的主要来源(整模板单次常到 1.0–1.4MB,超过 `--args-file` 上限 `CLI_ARGS_MAX_BYTES`=1MB 会被拒)。**节点字段权限也走这里**:不必读 `get_draft --full`——直接发 `activities:[{activityCode, propertyPermissions:[{propertyCode, visible, editable, required, summary, subPropertyPermissions}]}]`(**type 可省**:activityCode 命中草稿节点时类型/名称继承草稿,不会铸 `_2` 新码;**改名=同 code 原地生效**——工具把新名写入 `name_i18n`(zh/zh_TW/en),activityName 平台侧保持首建值,读回以 `renamedInPlace.displayName` 为准;只有**类型变更**才铸新码并弃旧码),工具按 propertyCode 合并到草稿对应节点(未列的字段权限保留;`merge:true` 也可同用)——没有单独的权限工具,别把「找不到入口」当成不支持(dogfood 实测该误判会假阻塞)。`summary` **无需配置**:工具合并时自动补 `summary:true`(平台怪癖——缺省进不了运行时 `activitySummaries`、`form/load` 读不到你的 `editable:false`;显式传 `false` 才保留);子表字段用 `subPropertyPermissions`(同样自动补)。**参与人写前校验**:`FindUserByRole(…,\"<roleCode>\",…)` 与 org.role.list 比对,未知角色拒 `workflow-participant-role-not-found`(带候选);全 `{Originator}` 给 `approval-by-originator` warning。**子表属性不可设 `required:true`**(接口形状接受但设计器节点字段配置不支持,存入后无法渲染——工具直接拒 `workflow-child-table-not-required`);子表整行只配 visible/editable,字段级必填配到子表内部字段所在节点。**新增节点用语义 code**(如 `approval_finance`),不要模仿草稿的 `ActivityN` 再加 `_1/_2` 后缀。**设计器默认自动补齐**(调用方显式传值优先):填写节点({Originator})自动生成子表操作按钮 `buttonPermission.childTableBtns`(每个子表 ADD/DELETE/IMPORT/EXPORT 启用、BATCH_UPDATE 禁用——API 建的流程缺这份模板,设计器里子表按钮会全部不勾);`permittedAction` 归一为设计器默认(填写节点 showReject:false 不能驳回自己,审批节点 rejectToStart:true 驳回回发起节点)。",
|
|
164
167
|
}),
|
|
165
168
|
"workflow.publish": Object.freeze({
|
|
166
169
|
scope: WORKFLOW_SCOPE,
|
|
@@ -241,6 +244,7 @@ export function assertWorkflowContract() {
|
|
|
241
244
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.role.list", ["organization/role/list", "type", "Originator"]);
|
|
242
245
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.department.list", ["organization/department/tree", "parentId"]);
|
|
243
246
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.user.search", ["organization/user/search", "wd", "type 3"]);
|
|
247
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.update", ["FindUserByRole", "workflow-participant-role-not-found", "approval-by-originator"]);
|
|
244
248
|
for (const [key, field] of Object.entries(WORKFLOW_EVENT_HANDLER_FIELDS)) {
|
|
245
249
|
if (!WORKFLOW_WIRE.bindRuleChain.includes(key))
|
|
246
250
|
throw new Error(`workflow contract bind-rule chain must mention the ${key} handler key`);
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -767,6 +767,18 @@ function participantReferenceWarnings(template) {
|
|
|
767
767
|
}
|
|
768
768
|
return warnings;
|
|
769
769
|
}
|
|
770
|
+
export function approvalByOriginatorWarnings(template) {
|
|
771
|
+
const activities = Array.isArray(template.activities) ? template.activities : [];
|
|
772
|
+
const participants = activities.filter((activity) => String(activity.activityType ?? "") === "PARTICIPANT");
|
|
773
|
+
if (participants.length === 0 || !participants.every((activity) => String(activity.participant ?? "").trim() === "{Originator}"))
|
|
774
|
+
return [];
|
|
775
|
+
const activityCodes = participants.map((activity) => String(activity.activityCode ?? "")).filter((code) => code.length > 0);
|
|
776
|
+
return [{
|
|
777
|
+
code: "approval-by-originator",
|
|
778
|
+
activityCodes,
|
|
779
|
+
message: `所有参与人节点(${activityCodes.join("、")})都指向 {Originator}:该流程会由发起人自审自批;这是允许的单人流程,但请确认该行为符合业务意图。`,
|
|
780
|
+
}];
|
|
781
|
+
}
|
|
770
782
|
/**
|
|
771
783
|
* Role participants are filtered by `ouScope ∩ originator's department chain`.
|
|
772
784
|
* A role whose members all have an empty scope therefore resolves to NOBODY —
|
|
@@ -782,12 +794,15 @@ async function assertRoleParticipantScopes(session, body) {
|
|
|
782
794
|
? Object.values(body.activities)
|
|
783
795
|
: [];
|
|
784
796
|
const roles = [];
|
|
797
|
+
const functionRoles = [];
|
|
785
798
|
for (const activity of rows) {
|
|
786
799
|
if (String(activity.activityType ?? "") !== "PARTICIPANT")
|
|
787
800
|
continue;
|
|
788
801
|
const raw = activity.participant;
|
|
789
802
|
if (typeof raw !== "string" || !raw.trim().startsWith("["))
|
|
790
803
|
continue;
|
|
804
|
+
const activityCode = String(activity.activityCode ?? "");
|
|
805
|
+
const activityName = String(activity.activityName ?? activity.name ?? activityCode);
|
|
791
806
|
for (const segment of raw.split("+")) {
|
|
792
807
|
let parsed;
|
|
793
808
|
try {
|
|
@@ -806,7 +821,86 @@ async function assertRoleParticipantScopes(session, body) {
|
|
|
806
821
|
continue;
|
|
807
822
|
const roleId = String(record.id ?? "");
|
|
808
823
|
if (roleId)
|
|
809
|
-
roles.push({ activityCode
|
|
824
|
+
roles.push({ activityCode, activityName, roleId, roleName: String(record.name ?? roleId) });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
for (const activity of rows) {
|
|
829
|
+
if (String(activity.activityType ?? "") !== "PARTICIPANT")
|
|
830
|
+
continue;
|
|
831
|
+
const participant = typeof activity.participant === "string" ? activity.participant.trim() : "";
|
|
832
|
+
const match = /^FindUserByRole\s*\(\s*[^,]+?\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)\s*$/.exec(participant);
|
|
833
|
+
if (!match)
|
|
834
|
+
continue;
|
|
835
|
+
const quoted = match[1];
|
|
836
|
+
let roleCode;
|
|
837
|
+
try {
|
|
838
|
+
roleCode = quoted.startsWith('"') ? JSON.parse(quoted) : quoted.slice(1, -1).replace(/\\(['\\])/g, "$1");
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (roleCode.trim())
|
|
844
|
+
functionRoles.push({ activityCode: String(activity.activityCode ?? ""), activityName: String(activity.activityName ?? activity.name ?? activity.activityCode ?? ""), roleCode: roleCode.trim() });
|
|
845
|
+
}
|
|
846
|
+
if (functionRoles.length > 0) {
|
|
847
|
+
let listedRoles = [];
|
|
848
|
+
let roleListAvailable = false;
|
|
849
|
+
try {
|
|
850
|
+
const data = (await wireCall(session, { path: ENDPOINTS.organizationRoleList, query: { expandAll: "true" } })).data;
|
|
851
|
+
const flattened = flattenRoleTree(wireRows(data));
|
|
852
|
+
listedRoles = Array.isArray(flattened.roles) ? flattened.roles : [];
|
|
853
|
+
roleListAvailable = true;
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// Role existence is advisory when the organization read face is unavailable.
|
|
857
|
+
// Do not turn a transient read failure into a blanket workflow write outage.
|
|
858
|
+
roleListAvailable = false;
|
|
859
|
+
}
|
|
860
|
+
if (roleListAvailable) {
|
|
861
|
+
const byCode = new Map(listedRoles
|
|
862
|
+
.map((role) => [String(role.code ?? ""), role])
|
|
863
|
+
.filter(([code]) => code.length > 0));
|
|
864
|
+
const missing = functionRoles.filter((role) => !byCode.has(role.roleCode));
|
|
865
|
+
if (missing.length > 0) {
|
|
866
|
+
const candidates = (needle) => {
|
|
867
|
+
const lower = needle.toLowerCase();
|
|
868
|
+
const distance = (left, right) => {
|
|
869
|
+
const row = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
870
|
+
for (let i = 1; i <= left.length; i++) {
|
|
871
|
+
let diagonal = row[0];
|
|
872
|
+
row[0] = i;
|
|
873
|
+
for (let j = 1; j <= right.length; j++) {
|
|
874
|
+
const prior = row[j];
|
|
875
|
+
row[j] = left[i - 1] === right[j - 1] ? diagonal : Math.min(row[j] + 1, row[j - 1] + 1, diagonal + 1);
|
|
876
|
+
diagonal = prior;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return row[right.length];
|
|
880
|
+
};
|
|
881
|
+
return listedRoles
|
|
882
|
+
.map((role) => {
|
|
883
|
+
const code = String(role.code ?? "");
|
|
884
|
+
const name = String(role.name ?? "");
|
|
885
|
+
const haystack = `${code} ${name}`.toLowerCase();
|
|
886
|
+
const substringRank = haystack.includes(lower) ? 0 : 1;
|
|
887
|
+
const editRank = Math.min(distance(lower, code.toLowerCase()), distance(lower, name.toLowerCase()));
|
|
888
|
+
return { code, name, substringRank, editRank };
|
|
889
|
+
})
|
|
890
|
+
.filter((role) => role.code.length > 0)
|
|
891
|
+
.sort((left, right) => left.substringRank - right.substringRank || left.editRank - right.editRank || left.code.localeCompare(right.code))
|
|
892
|
+
.slice(0, 5)
|
|
893
|
+
.map(({ code, name }) => ({ code, name }));
|
|
894
|
+
};
|
|
895
|
+
const first = missing[0];
|
|
896
|
+
throw new ToolRefusalError({
|
|
897
|
+
status: "refused",
|
|
898
|
+
blocker: "workflow-participant-role-not-found",
|
|
899
|
+
roles: missing,
|
|
900
|
+
candidates: candidates(first.roleCode),
|
|
901
|
+
nextCommand: "omc call org.role.list --args '{}'",
|
|
902
|
+
message: `写前拒绝:节点 ${first.activityCode}(${first.activityName})引用了不存在的角色 ${first.roleCode}。放行会导致待办静默回退给发起人(r9/r10/r15 真机)。请先用 org.role.list 确认真实 role code。`,
|
|
903
|
+
});
|
|
810
904
|
}
|
|
811
905
|
}
|
|
812
906
|
}
|
|
@@ -2359,12 +2453,14 @@ export function registerWorkflowLane(tools) {
|
|
|
2359
2453
|
return { schemaCode: args.schemaCode, exists: false, note: "该模型没有流程可校验" };
|
|
2360
2454
|
const template = (args.template ?? {});
|
|
2361
2455
|
const body = await mergedTemplateBody(session, args.schemaCode ?? "", workflowCode, template, activitiesLackBoundaries(template));
|
|
2456
|
+
await assertRoleParticipantScopes(session, body);
|
|
2362
2457
|
const result = await wireCall(session, { path: ENDPOINTS.workflowValidate, method: "POST", body });
|
|
2363
2458
|
const messages = Array.isArray(result.data) ? result.data : [];
|
|
2364
2459
|
const errors = messages.filter((message) => message.errorLevel === "ERROR");
|
|
2365
2460
|
const warnings = messages.filter((message) => message.errorLevel !== "ERROR");
|
|
2366
2461
|
// r31c F23: local reachability / else-coverage — warning only, never flips valid.
|
|
2367
2462
|
const reachability = workflowReachabilityWarnings(body);
|
|
2463
|
+
const originatorWarnings = approvalByOriginatorWarnings(body);
|
|
2368
2464
|
const allWarnings = [
|
|
2369
2465
|
...warnings,
|
|
2370
2466
|
...reachability.map((entry) => ({
|
|
@@ -2375,6 +2471,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2375
2471
|
name: entry.name,
|
|
2376
2472
|
problem: entry.problem,
|
|
2377
2473
|
})),
|
|
2474
|
+
...originatorWarnings.map((entry) => ({ errorLevel: "WARNING", errCode: entry.code, errContent: entry.message, activityCodes: entry.activityCodes })),
|
|
2378
2475
|
];
|
|
2379
2476
|
return {
|
|
2380
2477
|
workflowCode,
|
|
@@ -2384,6 +2481,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2384
2481
|
errors,
|
|
2385
2482
|
warnings: allWarnings,
|
|
2386
2483
|
...(reachability.length > 0 ? { reachabilityWarnings: reachability } : {}),
|
|
2484
|
+
...(originatorWarnings.length > 0 ? { approvalWarnings: originatorWarnings } : {}),
|
|
2387
2485
|
};
|
|
2388
2486
|
}, { target: `workflow.validate:${args.workflowCode ?? args.schemaCode}` }),
|
|
2389
2487
|
}));
|
|
@@ -2486,6 +2584,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2486
2584
|
// Evaluate on `body` (post-backfill): the readback may drop the handler
|
|
2487
2585
|
// keys, and the backfilled permittedAction is exactly what will run.
|
|
2488
2586
|
const rejectWarnings = rejectToStartCancelWarnings(body);
|
|
2587
|
+
const originatorWarnings = approvalByOriginatorWarnings(body);
|
|
2489
2588
|
const renamedActivities = renamedActivityPairs(template, data);
|
|
2490
2589
|
const renamedInPlace = inPlaceRenames(data);
|
|
2491
2590
|
// Caller sent no routes at all: the draft's edges (branches included) are
|
|
@@ -2503,7 +2602,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2503
2602
|
const activityNames = Object.fromEntries(rawActivityRows
|
|
2504
2603
|
.map((row) => [String(row.activityCode ?? ""), effectiveActivityName(row)])
|
|
2505
2604
|
.filter(([code]) => code !== ""));
|
|
2506
|
-
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : rawActivityRows.length, templateType: data.templateType ?? null, activityNames, rollbackHint: `本次改动的 before 快照已记录:omc rollback plan --target workflow:${args.schemaCode}/${args.code} 查看可回退项,omc rollback apply --target … --step <n> 执行(草稿回退后仍需 workflow.publish 才生效)`, ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length ? { workflowWarnings: rejectWarnings } : {}), ...(branchEdges.length ? { routesInherited: { edges: branchEdges, hint: "本次未传 routes/edges:以上分支边沿用草稿(含 else 兜底)。要改连线请显式回传完整 routes" } } : {}), ...(renamedInPlace.length ? { renamedInPlace, renamedInPlaceHint: "改名已同 code 原地生效(载体 name_i18n,displayName 才是当前名;activityName 是首建值,平台不再变更)" } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "类型变更铸了新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
2605
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : rawActivityRows.length, templateType: data.templateType ?? null, activityNames, rollbackHint: `本次改动的 before 快照已记录:omc rollback plan --target workflow:${args.schemaCode}/${args.code} 查看可回退项,omc rollback apply --target … --step <n> 执行(草稿回退后仍需 workflow.publish 才生效)`, ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length || originatorWarnings.length ? { workflowWarnings: [...rejectWarnings, ...originatorWarnings] } : {}), ...(branchEdges.length ? { routesInherited: { edges: branchEdges, hint: "本次未传 routes/edges:以上分支边沿用草稿(含 else 兜底)。要改连线请显式回传完整 routes" } } : {}), ...(renamedInPlace.length ? { renamedInPlace, renamedInPlaceHint: "改名已同 code 原地生效(载体 name_i18n,displayName 才是当前名;activityName 是首建值,平台不再变更)" } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "类型变更铸了新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
2507
2606
|
},
|
|
2508
2607
|
preflight: (args) => {
|
|
2509
2608
|
// Zero-wire refusal (ticket 77): fires in semanticGate BEFORE the
|
|
@@ -2592,6 +2691,7 @@ export function registerWorkflowLane(tools) {
|
|
|
2592
2691
|
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, activitiesLackBoundaries(template));
|
|
2593
2692
|
await backfillDesignerDefaults(session, args.schemaCode, body, template);
|
|
2594
2693
|
assertFirstActivityIsOriginator(template, "workflow.publish", body);
|
|
2694
|
+
await assertRoleParticipantScopes(session, body);
|
|
2595
2695
|
const unregistered = unregisteredParticipantFunctions(body);
|
|
2596
2696
|
if (unregistered.length > 0)
|
|
2597
2697
|
throw new ToolRefusalError({
|
|
@@ -2629,7 +2729,8 @@ export function registerWorkflowLane(tools) {
|
|
|
2629
2729
|
validateWorkflowRender(data, "workflow.publish readback");
|
|
2630
2730
|
const participantWarnings = participantReferenceWarnings(data);
|
|
2631
2731
|
const rejectWarnings = rejectToStartCancelWarnings(data);
|
|
2632
|
-
|
|
2732
|
+
const originatorWarnings = approvalByOriginatorWarnings(data);
|
|
2733
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, published: isPublished, workflowVersion: data.workflowVersion ?? null, ...(warnings.length ? { warnings } : {}), ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length || originatorWarnings.length ? { workflowWarnings: [...rejectWarnings, ...originatorWarnings] } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
2633
2734
|
},
|
|
2634
2735
|
preflight: (args) => {
|
|
2635
2736
|
assertTemplateNotMinimal(args.template ?? {}, "workflow.publish");
|
package/dist/mcp-graph.js
CHANGED
|
@@ -59,7 +59,7 @@ const EXPLORE_TOOL = {
|
|
|
59
59
|
description: [
|
|
60
60
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
61
61
|
"输入 query=字段/模型/服务的 code(或规则检索词);kind 可选 field|model|service,不给则三种都查。",
|
|
62
|
-
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{rules}}。",
|
|
62
|
+
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
63
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
64
64
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
65
65
|
].join(" "),
|
|
@@ -116,7 +116,15 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
116
116
|
for (const one of kinds) {
|
|
117
117
|
const ref = queryGraphReferences(one, query);
|
|
118
118
|
const capped = cap(ref.matches);
|
|
119
|
-
|
|
119
|
+
// r35a-F4c(真机 2026-11-06):**MCP 面是第三个消费者**——CLI 的 references
|
|
120
|
+
// 与 impact 都带 coverage 了,这里若不显式透传,MCP 用户看到的仍是
|
|
121
|
+
// 「matches:[] + 一句旧 note」,照样会把「表单没扫」误读成「没人引用」。
|
|
122
|
+
references[one] = {
|
|
123
|
+
count: ref.matches.length,
|
|
124
|
+
...capped,
|
|
125
|
+
...(ref.coverage ? { coverage: ref.coverage } : {}),
|
|
126
|
+
...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}),
|
|
127
|
+
};
|
|
120
128
|
if (ref.matches.length > 0 || kind !== undefined) {
|
|
121
129
|
const imp = impactGraph(one, query);
|
|
122
130
|
impact[one] = {
|
|
@@ -124,6 +132,7 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
124
132
|
rules: cap(imp.rules),
|
|
125
133
|
datarules: cap(imp.datarules),
|
|
126
134
|
listviews: cap(imp.listviews),
|
|
135
|
+
...(imp.coverage ? { coverage: imp.coverage } : {}),
|
|
127
136
|
};
|
|
128
137
|
}
|
|
129
138
|
}
|
|
@@ -135,9 +144,11 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
135
144
|
index,
|
|
136
145
|
references,
|
|
137
146
|
impact,
|
|
138
|
-
search: { count: search.rules.length, rules: search.rules },
|
|
147
|
+
search: { count: search.rules.length, rules: search.rules, matches: search.matches, coverage: search.coverage },
|
|
139
148
|
...(search.note ? { searchNote: search.note } : {}),
|
|
140
|
-
|
|
149
|
+
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
|
150
|
+
// 探针据此差点误删被表单引用的字段)。改为指向结构化 coverage,不再靠散文。
|
|
151
|
+
note: "零命中只代表 references[].coverage.scanned 面没有命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响",
|
|
141
152
|
});
|
|
142
153
|
}
|
|
143
154
|
return toolError(`未知工具:${name}`);
|
package/dist/rule-when.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* 编译目标只用**设计器验证过**的形状(前台生效约束,2026-10-16 用户裁决:后端
|
|
12
12
|
* 接口大量不拦截,save/publish 通过≠门户生效):
|
|
13
13
|
* - 预置触发行 Create/Update(设计器原生行,非自定义)
|
|
14
|
-
* - 单 UPDATE 节点:
|
|
14
|
+
* - 单 UPDATE 节点:dataCondition(输入数据满足条件时执行)+
|
|
15
|
+
* filterCondition(默认按当前输入行定位)+ dataActions(赋值)
|
|
15
16
|
* - FIXED 字面量 / DYNAMIC 字段引用 / EXPRESSION 公式(含 DATEDIF 语法,均真机验证)
|
|
16
17
|
* - 布局/路由交给 prepareRuleGraph 注入(points 画布兼容,Ticket 109)
|
|
17
18
|
*
|
|
@@ -23,6 +24,16 @@ import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contr
|
|
|
23
24
|
import { writeEnvelope } from "./cli-output.js";
|
|
24
25
|
import { argvValue } from "./args.js";
|
|
25
26
|
const WHEN_OPS = { "=": "EQ", "==": "EQ", "!=": "NEQ", ">": "GT", "<": "LT", ">=": "GTEQ", "<=": "LTEQ", "~=": "CT", "!~": "NCT" };
|
|
27
|
+
function parseWhen(raw) {
|
|
28
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|~=|!~|==|!=|=|>|<)\s*([\s\S]+)$/.exec(raw.trim());
|
|
29
|
+
if (!match)
|
|
30
|
+
throw new Error(`--when "${raw}" 解析失败:形如 字段=值 / 字段!=值 / 字段>=数字 / 字段~=文本(包含)`);
|
|
31
|
+
const [, field, op, rawValue] = match;
|
|
32
|
+
if (rawValue.startsWith("="))
|
|
33
|
+
throw new Error(`--when "${raw}" 疑似操作符手误(值以 = 开头):检查是否多打了等号`);
|
|
34
|
+
const value = /^-?\d+(\.\d+)?$/.test(rawValue.trim()) ? Number(rawValue.trim()) : rawValue.trim();
|
|
35
|
+
return { field, op, value };
|
|
36
|
+
}
|
|
26
37
|
/** 编译 --when/--then DSL 为设计器验证形状的规则图(导出供测试断言)。 */
|
|
27
38
|
export function composeRuleWhenGraph(options) {
|
|
28
39
|
if (options.when.length === 0)
|
|
@@ -33,17 +44,14 @@ export function composeRuleWhenGraph(options) {
|
|
|
33
44
|
for (const raw of options.when) {
|
|
34
45
|
// 最长操作符优先:>= <= ~= !~ == != > <(操作符两侧允许空格——r34 探针
|
|
35
46
|
// 实测 `dr_price > 1000` 因空格被拒,属于自然书写)
|
|
36
|
-
const
|
|
37
|
-
if (!match)
|
|
38
|
-
throw new Error(`--when "${raw}" 解析失败:形如 字段=值 / 字段!=值 / 字段>=数字 / 字段~=文本(包含)`);
|
|
39
|
-
const [, field, op, rawValue] = match;
|
|
40
|
-
if (rawValue.startsWith("="))
|
|
41
|
-
throw new Error(`--when "${raw}" 疑似操作符手误(值以 = 开头):检查是否多打了等号`);
|
|
47
|
+
const { field, op, value } = parseWhen(raw);
|
|
42
48
|
const opCode = WHEN_OPS[op];
|
|
43
49
|
if (!opCode)
|
|
44
50
|
throw new Error(`--when 操作符 "${op}" 不受支持`);
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
// DataCondition uses DataFilterCondition, not TableCondition:
|
|
52
|
+
// currentSchemaDataItem is the input field and the operator wire key is
|
|
53
|
+
// ruleDataConditionType (ruleConditionType is @JsonIgnore here).
|
|
54
|
+
leaves.push({ currentSchemaDataItem: field, ruleDataConditionType: opCode, ruleValueType: "FIXED", value });
|
|
47
55
|
}
|
|
48
56
|
const actions = [];
|
|
49
57
|
for (const raw of options.then) {
|
|
@@ -70,7 +78,11 @@ export function composeRuleWhenGraph(options) {
|
|
|
70
78
|
nodes: [
|
|
71
79
|
{ nodeCode: "Start", nodeType: "START" },
|
|
72
80
|
{ nodeCode: "UpdateData1", nodeType: "UPDATE", targetObjectCode: options.schema, inputParam: "_input",
|
|
73
|
-
filterCondition
|
|
81
|
+
// UPDATE.filterCondition selects rows; it is not an execution gate.
|
|
82
|
+
// Keep the trigger row as the default target and put --when on the
|
|
83
|
+
// platform's real execution-gate field (dataCondition).
|
|
84
|
+
dataCondition: { conditionGroup: [leaves] },
|
|
85
|
+
filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] },
|
|
74
86
|
dataActions: actions },
|
|
75
87
|
{ nodeCode: "End", nodeType: "END" },
|
|
76
88
|
],
|
|
@@ -130,9 +142,27 @@ export async function cmdRuleWhen(args) {
|
|
|
130
142
|
catch (error) {
|
|
131
143
|
return invalid(error instanceof Error ? error.message : String(error));
|
|
132
144
|
}
|
|
133
|
-
const
|
|
145
|
+
const rowScope = {
|
|
146
|
+
mode: "current-row",
|
|
147
|
+
matchedRows: null,
|
|
148
|
+
sampleIds: [],
|
|
149
|
+
note: "当前行由运行时 _input.id 确定;保存面没有具体行 id,无法预先 count,按 fail-open 继续保存",
|
|
150
|
+
};
|
|
151
|
+
// print: () => undefined —— stdout 必须恰好一个 JSON 信封(repo 契约)。默认 print 是
|
|
152
|
+
// console.log,会把工具进度行打到信封前面:任何按行解析 stdout 的管道(agent、
|
|
153
|
+
// 重放脚本)第一行就 json.loads 失败(0.6.17 重放 R8 真机撞上)。approval-flow
|
|
154
|
+
// 等 composer 都覆写了,这里漏了;compute.ts 同病同修。
|
|
155
|
+
const { result, exitCode } = await runToolCall("rule.save", { args: createArgs, requestId: request, environment, print: () => undefined });
|
|
156
|
+
// `rule.save` owns the wire write; enrich this high-level command's receipt
|
|
157
|
+
// with the impact face it can prove from the compiled graph. The generated
|
|
158
|
+
// id=id locator is current-row scope, so no count query is needed and the
|
|
159
|
+
// answer is fail-open rather than inventing a table-wide count.
|
|
160
|
+
const scopedResult = {
|
|
161
|
+
...result,
|
|
162
|
+
rowScope,
|
|
163
|
+
};
|
|
134
164
|
const classified = classifyResult(result);
|
|
135
|
-
writeEnvelope(envelope(classified.status,
|
|
165
|
+
writeEnvelope(envelope(classified.status, scopedResult, request, classified.error));
|
|
136
166
|
if (apply && classified.status === "ok")
|
|
137
167
|
process.stderr.write("[omc] 前台生效验证清单(后端通过≠门户生效):1) omc call rule.enable 启用;2) 造一条命中 --when 的测试数据提交;3) 门户打开该记录核对 --then 字段已变更;4) 再造一条不命中的数据确认不误改;5) 设计器打开本规则确认节点/条件显示正常。\n");
|
|
138
168
|
return exitCode;
|
package/dist/skills.js
CHANGED
|
@@ -109,7 +109,7 @@ const SYNTHETIC_SKILLS = Object.freeze({
|
|
|
109
109
|
"# 批量写",
|
|
110
110
|
"",
|
|
111
111
|
"通用批量入口是 `omc call <tool> --args-list '<json数组>'`,逐条执行且默认不中断。",
|
|
112
|
-
"先做零写入形状校验:`omc call data.save --args-list '<
|
|
112
|
+
"先做零写入形状校验:`omc call data.save --args-list '<json数组>' --preflight`;回执 `shapeOnly:true`,只校验 JSON 形状与 schema 必填键,不做业务语义校验;业务校验要真写一次才知道。大数组请用 shell 变量传入(`--args-list` 暂不读取文件或 stdin)。",
|
|
113
113
|
"支持 `data.save`、`data.delete`、`field.create` 等同一工具逐条批量调用;没有专用 `data.import`,导入数据就是逐条 `data.save`。",
|
|
114
114
|
"失败项回 `row`(1 基)、`index`(0 基)和尽力推断的字段/原因;成功项回输入中的 `code` 等业务标识。",
|
|
115
115
|
"",
|
package/dist/workflow-support.js
CHANGED
|
@@ -173,13 +173,14 @@ export function diagnoseInstanceActivity(input) {
|
|
|
173
173
|
const originator = input.originator ?? null;
|
|
174
174
|
const fallbackSuspect = originator !== null &&
|
|
175
175
|
active.some((activity) => activity.participantCount > 0 && activity.participants.every((person) => person.id === originator));
|
|
176
|
+
const unresolved = active.filter((activity) => activity.activityType === "PARTICIPANT" && activity.participantCount === 0).map((activity) => activity.activityCode ?? "?");
|
|
176
177
|
return {
|
|
177
178
|
code: "has-active-activity",
|
|
178
179
|
message: `当前激活节点:${active.map((activity) => `${activity.activityCode}(${activity.activityName ?? ""})`).join("、")}`,
|
|
179
180
|
checks: fallbackSuspect
|
|
180
181
|
? ["若这些节点本应流转给角色或他人,现在却落在发起人身上:参与人解析为空时平台会退回发起人(1.1.25 实测),流程看似推进实为错派。核对参与人写法(参与者对象数组 / FindUserByRole(org, roleCode[, order]))与角色成员。"]
|
|
181
182
|
: [],
|
|
182
|
-
warnings: fallbackSuspect ? ["participant-fallback-to-originator"] : [],
|
|
183
|
+
warnings: [...(fallbackSuspect ? ["participant-fallback-to-originator"] : []), ...unresolved.map((activityCode) => `participant-unresolved:${activityCode}`)],
|
|
183
184
|
};
|
|
184
185
|
}
|
|
185
186
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"commander": "14.0.3",
|
|
58
|
-
"omc-core": "0.6.
|
|
58
|
+
"omc-core": "0.6.17",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
60
|
"write-file-atomic": "6.0.0",
|
|
61
61
|
"zod": "^3.23.8",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"directory": "packages/omc-cli"
|
|
74
74
|
},
|
|
75
75
|
"optionalDependencies": {
|
|
76
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
76
|
+
"@stalfh233/omc-fake-platform": "0.6.17"
|
|
77
77
|
}
|
|
78
78
|
}
|