@stalfh233/omc-cli 0.6.15 → 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 +73 -3
- package/dist/capability-overview.js +3 -3
- package/dist/cli.js +25 -8
- package/dist/compute.js +1 -1
- package/dist/field-families.js +38 -1
- package/dist/goal-routes.js +23 -0
- package/dist/graph.js +119 -15
- package/dist/lanes/businessrule.js +22 -0
- package/dist/lanes/field.js +14 -1
- package/dist/lanes/list-view.js +0 -27
- package/dist/lanes/model.js +27 -4
- package/dist/lanes/organization.js +9 -2
- package/dist/lanes/runtime-data.js +29 -5
- package/dist/lanes/workflow-contract.js +7 -3
- package/dist/lanes/workflow.js +123 -4
- package/dist/mcp-graph.js +15 -4
- package/dist/read-shapes.js +4 -1
- package/dist/read.js +4 -1
- package/dist/recipes/field.js +39 -1
- package/dist/rule-when.js +42 -12
- package/dist/skills.js +13 -0
- package/dist/tool-types.js +21 -3
- package/dist/workflow-support.js +2 -1
- package/package.json +3 -3
package/dist/batch-call.js
CHANGED
|
@@ -19,7 +19,57 @@
|
|
|
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";
|
|
25
|
+
function inputBusinessKey(args) {
|
|
26
|
+
const keys = ["code", "name", "bizObjectId", "id"];
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const key of keys)
|
|
29
|
+
if (args[key] !== undefined)
|
|
30
|
+
out[key] = args[key];
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function failureDetails(entry, args) {
|
|
34
|
+
const message = refusalText(entry.result);
|
|
35
|
+
const formData = args.formData && typeof args.formData === "object" && !Array.isArray(args.formData)
|
|
36
|
+
? args.formData
|
|
37
|
+
: args;
|
|
38
|
+
const numericName = Object.keys(formData).find((key) => /amount|qty|quantity|price|number|count/i.test(key));
|
|
39
|
+
const numericValue = numericName === undefined ? undefined : formData[numericName];
|
|
40
|
+
if (numericName !== undefined && typeof numericValue === "string" && /50000|服务异常|wire-business/i.test(message)) {
|
|
41
|
+
return {
|
|
42
|
+
fieldCode: numericName,
|
|
43
|
+
value: numericValue,
|
|
44
|
+
reason: `NUMERICAL 字段收到字符串 "${numericValue}"`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { hint: "平台回了无明细 50000,常见成因见 help omc-refusals;请用该行输入与已发布字段逐项核对" };
|
|
48
|
+
}
|
|
49
|
+
export function batchWarnings(argsList) {
|
|
50
|
+
const warnings = [];
|
|
51
|
+
const seenCodes = new Map();
|
|
52
|
+
argsList.forEach((args, index) => {
|
|
53
|
+
const row = index + 1;
|
|
54
|
+
const data = args.formData && typeof args.formData === "object" && !Array.isArray(args.formData) ? args.formData : args;
|
|
55
|
+
if (data.code === undefined || data.code === null || data.code === "")
|
|
56
|
+
warnings.push({ row, fieldCode: "code", code: "required", message: "必填字段 code 缺失" });
|
|
57
|
+
else if (typeof data.code === "string") {
|
|
58
|
+
const previous = seenCodes.get(data.code);
|
|
59
|
+
if (previous !== undefined)
|
|
60
|
+
warnings.push({ row, fieldCode: "code", code: "duplicate", message: `code 与第 ${previous} 行重复` });
|
|
61
|
+
else
|
|
62
|
+
seenCodes.set(data.code, row);
|
|
63
|
+
}
|
|
64
|
+
for (const [fieldCode, value] of Object.entries(data)) {
|
|
65
|
+
if (typeof value === "string" && value.length > 255)
|
|
66
|
+
warnings.push({ row, fieldCode, code: "maxLength", message: `${fieldCode} 长度 ${value.length} 超过常见上限 255` });
|
|
67
|
+
if (typeof value === "number" && !Number.isInteger(value) && /qty|quantity|count|level/i.test(fieldCode))
|
|
68
|
+
warnings.push({ row, fieldCode, code: "scale-exceeded", message: `${fieldCode} 含小数,但字段可能是 0 位数值字段` });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return warnings;
|
|
72
|
+
}
|
|
23
73
|
/** Cap mirrors `omc script`: a runaway list should be a refusal, not a 20-min hang. */
|
|
24
74
|
export const BATCH_MAX_ENTRIES = 200;
|
|
25
75
|
/**
|
|
@@ -69,9 +119,12 @@ export async function runBatchCall(options) {
|
|
|
69
119
|
.filter((entry) => entry.status !== "ok")
|
|
70
120
|
.map((entry) => ({
|
|
71
121
|
index: entry.index,
|
|
122
|
+
row: entry.index + 1,
|
|
123
|
+
indexBase: 0,
|
|
72
124
|
status: String(entry.result.status ?? "failed"),
|
|
73
125
|
blocker: entry.result.blocker ?? null,
|
|
74
126
|
message: refusalText(entry.result),
|
|
127
|
+
...failureDetails(entry, options.argsList[entry.index] ?? {}),
|
|
75
128
|
...(Array.isArray(entry.result.issues) ? { issues: entry.result.issues } : {}),
|
|
76
129
|
}));
|
|
77
130
|
const executed = entries.length;
|
|
@@ -92,13 +145,22 @@ export async function runBatchCall(options) {
|
|
|
92
145
|
const skippedMarker = performed?.skipped;
|
|
93
146
|
return {
|
|
94
147
|
index: entry.index,
|
|
148
|
+
row: entry.index + 1,
|
|
149
|
+
indexBase: 0,
|
|
95
150
|
ok: entry.status === "ok",
|
|
151
|
+
...inputBusinessKey(options.argsList[entry.index] ?? {}),
|
|
96
152
|
...(bizObjectId !== undefined ? { bizObjectId } : {}),
|
|
97
153
|
...(entry.status === "ok" ? {} : { status: String(entry.result.status ?? "failed"), blocker: entry.result.blocker ?? null }),
|
|
98
154
|
...(skippedMarker !== undefined ? { skipped: String(skippedMarker) } : {}),
|
|
99
155
|
};
|
|
100
156
|
});
|
|
101
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;
|
|
102
164
|
return {
|
|
103
165
|
exitCode: failed > 0 ? 1 : 0,
|
|
104
166
|
result: {
|
|
@@ -112,9 +174,17 @@ export async function runBatchCall(options) {
|
|
|
112
174
|
// 每条一行摘要(index/ok/bizObjectId)——不是完整信封,但足以回答
|
|
113
175
|
// 「第 3 条建成了吗、它的 id 是什么」。失败项另有 failures 带完整原因。
|
|
114
176
|
entries: entriesInline,
|
|
115
|
-
...(
|
|
116
|
-
|
|
117
|
-
: {}),
|
|
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
|
+
} : {}),
|
|
118
188
|
// Only failures carry full per-entry detail: echoing 8 full success
|
|
119
189
|
// envelopes would rebuild exactly the wall of text this command exists
|
|
120
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"],
|
|
@@ -252,12 +252,12 @@ export const CAPABILITY_CUES = Object.freeze({
|
|
|
252
252
|
programming: ["在线开发", "在线编码", "后端源码", "java源码", "编码模式", "handler", "programming", "源码编译"],
|
|
253
253
|
bizservice: ["集成", "接口", "服务", "回调", "对接", "外部系统", "datasql", "restful"],
|
|
254
254
|
report: ["报表", "图表", "看板", "bi", "dashboard"],
|
|
255
|
+
data: ["运行数据", "提交数据", "保存记录", "查记录", "造数据", "测试数据", "测试单据", "造单据", "造单", "录数据", "填数据", "模拟数据", "业务数据", "造几条", "造两张", "造几张", "验数据", "批量导入", "批量写入", "批量删除", "导入数据"],
|
|
255
256
|
// r18 (2026-10-26): "造数据 / 造测试单据 / 录数据" is the RUN-DATA capability, but
|
|
256
257
|
// the goal usually also says 子表/明细 (field cues) — without these compound
|
|
257
258
|
// cues the router sent the probe to `field` in read mode (write:false), so it
|
|
258
259
|
// never reached data.submit. Compound (not bare 数据) to avoid stealing
|
|
259
260
|
// ordinary model/field asks that merely mention data.
|
|
260
|
-
data: ["运行数据", "提交数据", "保存记录", "查记录", "造数据", "测试数据", "测试单据", "造单据", "造单", "录数据", "填数据", "模拟数据", "业务数据", "造几条", "造两张", "造几张", "验数据"],
|
|
261
261
|
});
|
|
262
262
|
/**
|
|
263
263
|
* Boundary cues — authored alongside CAPABILITY_CUES (same runtime authority).
|
package/dist/cli.js
CHANGED
|
@@ -463,9 +463,10 @@ async function cmdBatchCall(store, toolName, rawList, options) {
|
|
|
463
463
|
failures.push({ index, blocker: payload.blocker ?? null, message: refusalText(payload), ...(Array.isArray(payload.validation) ? { validation: payload.validation } : {}), ...(payload.citation !== undefined ? { citation: payload.citation } : {}) });
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
|
-
|
|
466
|
+
let result = {
|
|
467
467
|
status: failures.length > 0 ? "partial" : "ok",
|
|
468
468
|
mode: "preflight",
|
|
469
|
+
shapeOnly: true,
|
|
469
470
|
tool: toolName,
|
|
470
471
|
requested: entries.length,
|
|
471
472
|
shapeChecked: entries.length,
|
|
@@ -473,8 +474,16 @@ async function cmdBatchCall(store, toolName, rawList, options) {
|
|
|
473
474
|
writes: 0,
|
|
474
475
|
// On failure the caller has to change the JSON first, so there is no
|
|
475
476
|
// runnable command yet; on success, hand over the exact execution command.
|
|
476
|
-
...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list
|
|
477
|
+
...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list '<file|stdin>'` }),
|
|
477
478
|
};
|
|
479
|
+
const preflightPaths = [...argvValues(options.args, "--extract"), ...argvValues(options.args, "--jq")]
|
|
480
|
+
.flatMap((path) => path.split(",")).map((path) => path.trim()).filter(Boolean);
|
|
481
|
+
const preflightKeys = options.args.includes("--extract-keys") || options.args.includes("--keys") || options.args.some((token, index) => token === "--jq" && (options.args[index + 1] === undefined || options.args[index + 1].startsWith("--")));
|
|
482
|
+
if (preflightPaths.length > 0 || preflightKeys) {
|
|
483
|
+
const projected = projectResultExtract(result, preflightPaths, preflightKeys);
|
|
484
|
+
if (projected !== undefined)
|
|
485
|
+
result = projected;
|
|
486
|
+
}
|
|
478
487
|
const classified = classifyResult(result);
|
|
479
488
|
writeEnvelope(envelope(classified.status, result, request, classified.error));
|
|
480
489
|
return failures.length > 0 ? 1 : 0;
|
|
@@ -525,7 +534,7 @@ async function cmdCall(store, args) {
|
|
|
525
534
|
return emitTransportResult({ status: "ok", ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, { request, maxOutputBytes });
|
|
526
535
|
}
|
|
527
536
|
}
|
|
528
|
-
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file
|
|
537
|
+
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径(必须位于配置根内;跨目录临时文件用 --args -(stdin))>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
|
|
529
538
|
console.error("输出预算:--max-output-bytes 默认 65536(只约束最终 stdout 信封;读面 diet ~2048 独立外置 detailArtifactId);artifact get 内联上限 8192");
|
|
530
539
|
console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
|
|
531
540
|
return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
|
|
@@ -1632,17 +1641,22 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1632
1641
|
return emitThrown(error, transport);
|
|
1633
1642
|
}
|
|
1634
1643
|
}
|
|
1635
|
-
function cmdGraphReferences(kind, code, transport) {
|
|
1644
|
+
function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
1636
1645
|
if (!["service", "model", "field"].includes(kind))
|
|
1637
1646
|
return emitInvalid(transport, { message: "kind 必须是 service / model / field(同 rule.references 第一版边界)" });
|
|
1638
1647
|
try {
|
|
1639
1648
|
const result = queryGraphReferences(kind, code);
|
|
1649
|
+
if (requestedEnvironment !== undefined && requestedEnvironment !== result.index.environmentName)
|
|
1650
|
+
return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${result.index.environmentName},不是 ${requestedEnvironment};请先 omc graph pull --env ${requestedEnvironment}`, index: result.index }, transport);
|
|
1640
1651
|
return emitResult({
|
|
1641
1652
|
status: "ok",
|
|
1642
1653
|
kind: result.kind,
|
|
1643
1654
|
code: result.code,
|
|
1644
1655
|
matches: result.matches,
|
|
1645
1656
|
index: result.index,
|
|
1657
|
+
// r35a-F4:覆盖声明必须透传——CLI 这里是白名单式重建,漏加就等于没修
|
|
1658
|
+
// (matches:[] 曾被当成可安全删除的依据)。
|
|
1659
|
+
...(result.coverage !== undefined ? { coverage: result.coverage } : {}),
|
|
1646
1660
|
...(result.note !== undefined ? { note: result.note } : {}),
|
|
1647
1661
|
...(result.gapWarning !== undefined ? { gapWarning: result.gapWarning } : {}),
|
|
1648
1662
|
}, transport);
|
|
@@ -1656,7 +1670,10 @@ function cmdGraphReferences(kind, code, transport) {
|
|
|
1656
1670
|
function cmdGraphStatus(transport) {
|
|
1657
1671
|
try {
|
|
1658
1672
|
const status = readGraphStatus();
|
|
1659
|
-
|
|
1673
|
+
// r31a-F16:这里是**第四个**白名单式重建(前三次:compact() 投影、references
|
|
1674
|
+
// 覆盖声明、mcp-graph 的 explore)。白名单每漏一个键就等于没修,且不会被任何
|
|
1675
|
+
// 单测发现——所以改成展开,新增字段自动透传。
|
|
1676
|
+
return emitResult({ status: "ok", ...status }, transport);
|
|
1660
1677
|
}
|
|
1661
1678
|
catch (error) {
|
|
1662
1679
|
if (error instanceof GraphIndexAbsentError)
|
|
@@ -2145,7 +2162,7 @@ async function main() {
|
|
|
2145
2162
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2146
2163
|
});
|
|
2147
2164
|
command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
|
|
2148
|
-
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
2165
|
+
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
2149
2166
|
const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
|
|
2150
2167
|
...(options.keys ? ["--keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
|
|
2151
2168
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
@@ -2513,8 +2530,8 @@ async function main() {
|
|
|
2513
2530
|
const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
|
|
2514
2531
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport));
|
|
2515
2532
|
});
|
|
2516
|
-
graphCommand.command("references <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references
|
|
2517
|
-
commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport));
|
|
2533
|
+
graphCommand.command("references <kind> <code>").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references 第一版边界;结果带索引新鲜度与扫描缺口(有缺口时零命中不构成「无引用」证明)。覆盖:规则图/数据规则/视图;不含表单、workflow.permissions、流程绑定、在线 JS。在线版 rule.references 不含数据规则;两者都不含上述未扫描面。\n").action(async (kind, code, options) => {
|
|
2534
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport, options.environment ?? options.env));
|
|
2518
2535
|
});
|
|
2519
2536
|
graphCommand.command("impact <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n影响面(离线):跳1=引用该编码的规则(同 references),跳2=这些规则还读写哪些 service/model/field——改前看爆炸半径。截断上限 50 规则×每类 20 码。\n").action(async (kind, code, options) => {
|
|
2520
2537
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
|
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/goal-routes.js
CHANGED
|
@@ -672,6 +672,29 @@ export function goalRoute(registry, goal) {
|
|
|
672
672
|
firstAnswer: { state: "blocked", why: boundary.note },
|
|
673
673
|
};
|
|
674
674
|
}
|
|
675
|
+
if (/批量导入|批量写入|批量删除|导入数据/.test(goal)) {
|
|
676
|
+
const action = /批量删除/.test(goal) ? "data.delete" : "data.save";
|
|
677
|
+
return {
|
|
678
|
+
status: "ok", goal, confident: true, mode: "modify", write: true,
|
|
679
|
+
match: { kind: "capability", ref: "data", title: "运行数据批量写入(--args-list)" },
|
|
680
|
+
route: { capability: "data", lane: "runtime-data", note: "运行数据批量操作逐条执行,失败继续;先用 --preflight 做零写入形状校验", readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: [`omc call ${action} --args-list '<file|stdin>' --preflight`, `omc call ${action} --args-list '<file|stdin>'`], write: true },
|
|
681
|
+
firstAnswer: { state: "needs-input", question: "目标模型 schemaCode 和输入文件/JSON 数组是什么?示例:omc call data.save --args-list '<file|stdin>' --preflight", action: LIST_MODELS_ACTION, pendingCommand: `omc call ${action} --args-list '<file|stdin>'`, why: "批量导入/写入/删除使用 --args-list;先预检形状,再逐条执行" },
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
// Cross-application model moves/copies are not an intent migration. There
|
|
685
|
+
// is no model.move/model.copy path across app boundaries: model.move only
|
|
686
|
+
// reparents a model within its current application. Keep the useful
|
|
687
|
+
// declarative route for rebuilding the target model, but put this fact in
|
|
688
|
+
// the route itself so callers cannot mistake intent for an in-place move.
|
|
689
|
+
if (/模型/.test(goal) && /(跨应用|一个应用.*另一个应用|从一个应用.*到另一个应用)/.test(goal) && /(移动|复制|迁移|搬)/.test(goal)) {
|
|
690
|
+
const routed = wholePackageRoute(registry, goal, index, ["app", "model"]);
|
|
691
|
+
const clarification = "事实澄清:没有跨应用迁移/复制模型的专用工具;model.move 仅支持同一应用内换目录。跨应用移植需要在新应用重建模型,并手工搬运数据。";
|
|
692
|
+
return {
|
|
693
|
+
...routed,
|
|
694
|
+
route: { ...routed.route, note: clarification },
|
|
695
|
+
firstAnswer: { ...routed.firstAnswer, why: `${clarification} ${routed.firstAnswer.why}` },
|
|
696
|
+
};
|
|
697
|
+
}
|
|
675
698
|
// Composer before the capability solver: a recurring write SHAPE has a single
|
|
676
699
|
// command that does the job with preflight + idempotency, which beats a ladder
|
|
677
700
|
// of atomic tools even when the ladder also matches (r13: 26 field.create).
|
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
|
|
@@ -607,16 +681,22 @@ export function queryGraphReferences(kind, code) {
|
|
|
607
681
|
via: row.via,
|
|
608
682
|
}));
|
|
609
683
|
const gapped = summary.gaps.count > 0;
|
|
684
|
+
// r35a-F4(真机 2026-11-06):探针拿 matches:[] 差点把一个**被表单引用**的
|
|
685
|
+
// 字段判成「可安全删除」——note 只说了「不含规则条件叶子/流程绑定」,从没
|
|
686
|
+
// 明说表单与流程权限表不在扫描范围。散文字段不承担安全契约:这里给结构化
|
|
687
|
+
// coverage,让「没人引用」与「这一面没扫」在机器可读层面可分。
|
|
688
|
+
const coverage = GRAPH_COVERAGE;
|
|
610
689
|
return {
|
|
611
690
|
kind,
|
|
612
691
|
code,
|
|
613
692
|
matches,
|
|
693
|
+
coverage,
|
|
614
694
|
index: summary,
|
|
615
695
|
...(matches.length === 0
|
|
616
696
|
? {
|
|
617
697
|
note: (gapped
|
|
618
698
|
? "零命中,但索引存在扫描缺口——不可作为「无引用」证明;先 omc graph pull 重建再查"
|
|
619
|
-
: "
|
|
699
|
+
: "零命中=仅 scanned 面(规则/数据规则/视图)没有引用它;form(表单控件)与 workflow.permissions 未扫描,见 coverage.notScanned——不可据此判定可安全删除") +
|
|
620
700
|
(zeroHitKindHint(db, kind, code, "references") ?? ""),
|
|
621
701
|
}
|
|
622
702
|
: {}),
|
|
@@ -633,7 +713,14 @@ export function readGraphStatus() {
|
|
|
633
713
|
const { db, summary } = openGraphIndex();
|
|
634
714
|
try {
|
|
635
715
|
const gaps = db.prepare("SELECT kind, ref, note FROM gaps ORDER BY kind, ref LIMIT 50").all();
|
|
636
|
-
|
|
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
|
+
};
|
|
637
724
|
}
|
|
638
725
|
finally {
|
|
639
726
|
db.close();
|
|
@@ -651,6 +738,11 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
651
738
|
try {
|
|
652
739
|
const tokens = term.trim().split(/\s+/).filter((token) => token !== "");
|
|
653
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
|
+
};
|
|
654
746
|
if (tokens.length > 0) {
|
|
655
747
|
const quoted = tokens.map((token, index) => `"${token.replaceAll('"', '""')}"${index === tokens.length - 1 ? "*" : ""}`).join(" ");
|
|
656
748
|
const rows = db
|
|
@@ -673,13 +765,24 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
673
765
|
refs,
|
|
674
766
|
};
|
|
675
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 }));
|
|
676
776
|
}
|
|
677
777
|
const gapped = summary.gaps.count > 0;
|
|
678
778
|
return {
|
|
679
779
|
term,
|
|
680
780
|
rules: matches,
|
|
781
|
+
matches: entityMatches,
|
|
681
782
|
index: summary,
|
|
682
|
-
|
|
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
|
|
683
786
|
? {
|
|
684
787
|
note: tokens.length === 0
|
|
685
788
|
? "检索词为空"
|
|
@@ -757,11 +860,12 @@ export function impactGraph(kind, code) {
|
|
|
757
860
|
listviews: listViewHits.slice(0, 20).map((hit) => ({ schemaCode: hit.schema_code, viewCode: hit.view_code ?? "", via: hit.via })),
|
|
758
861
|
totals: { rules: hop1.length, datarules: dataruleHits.length, listviews: listViewHits.length, touchedServices: services.size, touchedModels: models.size, touchedFields: fields.size },
|
|
759
862
|
index: summary,
|
|
863
|
+
coverage: GRAPH_COVERAGE,
|
|
760
864
|
...(hop1.length === 0 && dataruleHits.length === 0 && listViewHits.length === 0
|
|
761
865
|
? {
|
|
762
866
|
note: (gapped
|
|
763
867
|
? "零命中,但索引存在扫描缺口——不可作为「无影响」证明;先 omc graph pull 重建再查"
|
|
764
|
-
: "
|
|
868
|
+
: "零命中=仅 scanned 面(规则/数据规则/视图)没有影响;form(表单控件)与 workflow.permissions 未扫描,见 coverage.notScanned——不可据此判定「删除无影响」") +
|
|
765
869
|
(zeroHitKindHint(db, kind, code, "impact") ?? ""),
|
|
766
870
|
}
|
|
767
871
|
: {}),
|