@stalfh233/omc-cli 0.6.21 → 0.6.23
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/cli.js +77 -15
- package/dist/command-transport.js +9 -0
- package/dist/graph.js +126 -33
- package/dist/human-summary.js +1 -1
- package/dist/lanes/field.js +12 -3
- package/dist/lanes/list-view-contract.js +1 -1
- package/dist/lanes/list-view.js +9 -1
- package/dist/lanes/online-js.js +11 -2
- package/dist/lanes/workflow.js +2 -0
- package/dist/mcp-graph.js +51 -18
- package/dist/model-purge.js +1 -0
- package/dist/residue.js +21 -4
- package/dist/rule-when.js +72 -4
- package/package.json +3 -3
- package/skills/omc-start-here.md +1 -4
package/dist/cli.js
CHANGED
|
@@ -56,7 +56,7 @@ import { planIntent, readbackIntent, applyIntent, intentWarnings } from "./inten
|
|
|
56
56
|
import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills, suggestHelpTopics } from "./skills.js";
|
|
57
57
|
import { parseToolInput, ToolInputError } from "./tool-types.js";
|
|
58
58
|
import { WireError, wireCall } from "./wire.js";
|
|
59
|
-
import { DEFAULT_PULL_CONCURRENCY, GraphIndexAbsentError, GraphPullLockHeldError, MAX_PULL_CONCURRENCY, impactGraph, publishGraphPull, queryGraphReferences, readGraphStatus, searchGraphRules, } from "./graph.js";
|
|
59
|
+
import { DEFAULT_PULL_CONCURRENCY, GraphIndexAbsentError, GraphPullLockHeldError, MAX_PULL_CONCURRENCY, impactGraph, publishGraphPull, queryGraphReferences, queryGraphReferencesBatch, queryGraphEdges, readGraphStatus, searchGraphRules, } from "./graph.js";
|
|
60
60
|
import { cmdResidue } from "./residue.js";
|
|
61
61
|
import { ENDPOINTS } from "./endpoints.js";
|
|
62
62
|
import { makeToolContext } from "./context.js";
|
|
@@ -1604,14 +1604,20 @@ async function cmdIntentReadback(file, environment, transport) {
|
|
|
1604
1604
|
* registry or `commands --json`. Results are candidate pointers only — plan /
|
|
1605
1605
|
* apply / readback keep reading the live baseline.
|
|
1606
1606
|
*/
|
|
1607
|
-
async function cmdGraphPull(environment, concurrency, transport) {
|
|
1607
|
+
async function cmdGraphPull(environment, concurrency, transport, path, applicationCodes, allApplications = false) {
|
|
1608
1608
|
const workers = concurrency ?? DEFAULT_PULL_CONCURRENCY;
|
|
1609
1609
|
if (!Number.isSafeInteger(workers) || workers < 1 || workers > MAX_PULL_CONCURRENCY)
|
|
1610
1610
|
return emitInvalid(transport, { message: `--concurrency 必须是 1–${MAX_PULL_CONCURRENCY} 的整数(平台负载有界;默认 ${DEFAULT_PULL_CONCURRENCY})` });
|
|
1611
1611
|
const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
1612
1612
|
try {
|
|
1613
1613
|
const session = await context.sessions.session(environment);
|
|
1614
|
-
|
|
1614
|
+
let selected = applicationCodes;
|
|
1615
|
+
if (allApplications) {
|
|
1616
|
+
const apps = (await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data;
|
|
1617
|
+
const rows = Array.isArray(apps) ? apps : (apps && typeof apps === "object" && Array.isArray(apps.data) ? apps.data : []);
|
|
1618
|
+
selected = rows.map((row) => String(row.appCode ?? row.code ?? "")).filter(Boolean);
|
|
1619
|
+
}
|
|
1620
|
+
const data = await publishGraphPull(session, workers, { ...(selected ? { applicationCodes: selected } : {}) });
|
|
1615
1621
|
// issue#9 C4:扫到 0 模型仍回 ok,让调用者误以为索引就绪(随后 search 报
|
|
1616
1622
|
// graph-index-absent 才发现)。0 模型 fail-fast,点名 env/applicationCode
|
|
1617
1623
|
// 与解析来源,把「索引没建成」和「建成了但为空」分开。
|
|
@@ -1644,13 +1650,15 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1644
1650
|
identityFingerprint: data.meta.identityFingerprint,
|
|
1645
1651
|
pulledAtIso: data.meta.pulledAtIso,
|
|
1646
1652
|
scope: data.meta.scope,
|
|
1647
|
-
coverage: { applicationCodes:
|
|
1653
|
+
coverage: { applicationCodes: data.meta.coverage.applicationCodes, ...(data.meta.coverage.tenantApplications ? { tenantApplications: data.meta.coverage.tenantApplications } : {}) },
|
|
1648
1654
|
freshness: data.gaps.length === 0 ? "complete" : "partial",
|
|
1649
1655
|
scanned: { models: data.scanned.models, rules: data.scanned.rules, datarules: data.scanned.datarules, views: data.scanned.views, ...(data.scanned.parseFailures > 0 ? { parseFailures: data.scanned.parseFailures } : {}) },
|
|
1650
1656
|
edges: edgesByKind,
|
|
1651
1657
|
...(data.gaps.length > 0 ? { gaps: { count: data.gaps.length, byKind: gapsByKind } } : {}),
|
|
1652
1658
|
dbBytes: data.dbBytes,
|
|
1653
1659
|
timings: data.timings,
|
|
1660
|
+
dbPath: path ? `${path}/.omc/graph/graph.db` : undefined,
|
|
1661
|
+
mcpHint: `MCP 工具按注册时的 --path 读库;要让 MCP 看到本次拉取:omc graph pull --path <MCP 的配置根> --env <env>`,
|
|
1654
1662
|
nextCommand: "omc graph references <kind> <code>",
|
|
1655
1663
|
}, transport);
|
|
1656
1664
|
}
|
|
@@ -1663,12 +1671,16 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1663
1671
|
}
|
|
1664
1672
|
}
|
|
1665
1673
|
function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
1666
|
-
if (!["service", "model", "field"].includes(kind))
|
|
1667
|
-
return emitInvalid(transport, { message: "kind 必须是 service / model / field
|
|
1674
|
+
if (kind !== undefined && !["service", "model", "field"].includes(kind))
|
|
1675
|
+
return emitInvalid(transport, { message: "kind 必须是 service / model / field" });
|
|
1668
1676
|
try {
|
|
1669
|
-
const
|
|
1677
|
+
const kinds = kind === undefined ? ["service", "model", "field"] : [kind];
|
|
1678
|
+
const results = kinds.map((one) => queryGraphReferences(one, code));
|
|
1679
|
+
const result = results[0];
|
|
1670
1680
|
if (requestedEnvironment !== undefined && requestedEnvironment !== result.index.environmentName)
|
|
1671
1681
|
return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${result.index.environmentName},不是 ${requestedEnvironment};请先 omc graph pull --env ${requestedEnvironment}`, index: result.index }, transport);
|
|
1682
|
+
if (kind === undefined)
|
|
1683
|
+
return emitResult({ status: "ok", code, index: result.index, references: Object.fromEntries(results.map((item) => [item.kind, { matches: item.matches, ...(item.coverage !== undefined ? { coverage: item.coverage } : {}), ...(item.note !== undefined ? { note: item.note } : {}) }])) }, transport);
|
|
1672
1684
|
return emitResult({
|
|
1673
1685
|
status: "ok",
|
|
1674
1686
|
kind: result.kind,
|
|
@@ -1688,9 +1700,39 @@ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
|
1688
1700
|
return emitThrown(error, transport);
|
|
1689
1701
|
}
|
|
1690
1702
|
}
|
|
1691
|
-
function
|
|
1703
|
+
function cmdGraphReferencesBatch(kind, codeList, transport) {
|
|
1704
|
+
if (!["service", "model", "field"].includes(kind))
|
|
1705
|
+
return emitInvalid(transport, { message: "kind 必须是 service / model / field" });
|
|
1706
|
+
try {
|
|
1707
|
+
const result = queryGraphReferencesBatch(kind, codeList.split(",").map((code) => code.trim()));
|
|
1708
|
+
return emitResult({ status: "ok", ...result }, transport);
|
|
1709
|
+
}
|
|
1710
|
+
catch (error) {
|
|
1711
|
+
return emitThrown(error, transport);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
function cmdGraphEdges(fromSchema, toSchema, kind, offset, limit, requestedEnvironment, transport) {
|
|
1715
|
+
try {
|
|
1716
|
+
if (kind !== undefined && !["service", "model", "field"].includes(kind))
|
|
1717
|
+
return emitInvalid(transport, { message: "kind 必须是 service / model / field" });
|
|
1718
|
+
const pageOffset = offset ?? 0;
|
|
1719
|
+
const pageLimit = limit ?? 50;
|
|
1720
|
+
if (!Number.isSafeInteger(pageOffset) || pageOffset < 0 || !Number.isSafeInteger(pageLimit) || pageLimit < 1)
|
|
1721
|
+
return emitInvalid(transport, { message: "offset 必须是非负整数,limit 必须是正整数" });
|
|
1722
|
+
const result = queryGraphEdges(fromSchema, toSchema, kind, pageOffset, pageLimit);
|
|
1723
|
+
if (requestedEnvironment !== undefined && requestedEnvironment !== result.index.environmentName)
|
|
1724
|
+
return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${result.index.environmentName},不是 ${requestedEnvironment};请先 omc graph pull --env ${requestedEnvironment}`, index: result.index }, transport);
|
|
1725
|
+
return emitResult({ status: "ok", ...result }, transport);
|
|
1726
|
+
}
|
|
1727
|
+
catch (error) {
|
|
1728
|
+
return emitThrown(error, transport);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function cmdGraphStatus(requestedEnvironment, transport) {
|
|
1692
1732
|
try {
|
|
1693
1733
|
const status = readGraphStatus();
|
|
1734
|
+
if (requestedEnvironment !== undefined && requestedEnvironment !== status.index.environmentName)
|
|
1735
|
+
return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${status.index.environmentName},不是 ${requestedEnvironment}`, index: status.index }, transport);
|
|
1694
1736
|
// r31a-F16:这里是**第四个**白名单式重建(前三次:compact() 投影、references
|
|
1695
1737
|
// 覆盖声明、mcp-graph 的 explore)。白名单每漏一个键就等于没修,且不会被任何
|
|
1696
1738
|
// 单测发现——所以改成展开,新增字段自动透传。
|
|
@@ -2553,13 +2595,33 @@ async function main() {
|
|
|
2553
2595
|
});
|
|
2554
2596
|
// ADR-0008: local derived rule-graph index — CLI management face, not a call
|
|
2555
2597
|
// tool (so it stays out of `commands --json`/coverage by construction).
|
|
2556
|
-
const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008
|
|
2557
|
-
graphCommand.command("pull").option("--env <environment>").option("--environment <environment>").option("--
|
|
2598
|
+
const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008)");
|
|
2599
|
+
graphCommand.command("pull").option("--env <environment>").option("--environment <environment>").option("--path <dir>").option("--application-codes <codes>").option("--all-applications").option("--concurrency <n>").option("--max-output-bytes <bytes>").action(async (options) => {
|
|
2558
2600
|
const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
|
|
2559
|
-
|
|
2601
|
+
if (options.path)
|
|
2602
|
+
process.env.OMC_GRAPH_PATH = options.path;
|
|
2603
|
+
const codes = options.applicationCodes ? String(options.applicationCodes).split(",").map((code) => code.trim()).filter(Boolean) : undefined;
|
|
2604
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport, options.path, codes, options.allApplications === true));
|
|
2605
|
+
});
|
|
2606
|
+
graphCommand.command("references [kind] [code]").option("--batch <codes>").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):省略 kind 时=service|model|field 三类全查;支持 --batch code1,code2(例如 --batch svc_a,svc_b)。\n").action(async (kind, code, options) => {
|
|
2607
|
+
// r42b 接线(2026-11-08,r42a-F8):commander 把首个位置参数绑给 [kind],
|
|
2608
|
+
// 「references <code>」这种最自然写法会把 code 当 kind 拒掉,而
|
|
2609
|
+
// cmdGraphReferences 的三类全查分支只认 kind===undefined——help 承诺的
|
|
2610
|
+
// 「省略 kind」在 CLI 面不可达。这里做智能重绑:首参不是合法 kind 且
|
|
2611
|
+
// 第二参缺席时,视首参为 code。
|
|
2612
|
+
let boundKind = kind;
|
|
2613
|
+
let boundCode = code;
|
|
2614
|
+
if (kind !== undefined && !["service", "model", "field"].includes(kind) && code === undefined) {
|
|
2615
|
+
boundKind = undefined;
|
|
2616
|
+
boundCode = kind;
|
|
2617
|
+
}
|
|
2618
|
+
commandExitCode = await withTransport(options, request, (transport) => options.batch ? cmdGraphReferencesBatch(boundKind ?? "model", options.batch, transport) : cmdGraphReferences(boundKind, boundCode ?? "", transport, options.environment ?? options.env));
|
|
2619
|
+
});
|
|
2620
|
+
graphCommand.command("references-batch <kind> <code>").option("--max-output-bytes <bytes>").action(async (kind, code, options) => {
|
|
2621
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferencesBatch(kind, code, transport));
|
|
2560
2622
|
});
|
|
2561
|
-
graphCommand.command("
|
|
2562
|
-
commandExitCode = await withTransport(options, request, (transport) =>
|
|
2623
|
+
graphCommand.command("edges").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--from-schema <code>").option("--to-schema <code>").option("--kind <kind>").option("--offset <n>").option("--limit <n>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线全图边清单(零 wire);支持 --kind model|field|service 与 --offset/--limit 分页。\n").action(async (options) => {
|
|
2624
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphEdges(options.fromSchema, options.toSchema, options.kind, options.offset === undefined ? undefined : Number(options.offset), options.limit === undefined ? undefined : Number(options.limit), options.environment ?? options.env, transport));
|
|
2563
2625
|
});
|
|
2564
2626
|
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) => {
|
|
2565
2627
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
|
|
@@ -2567,8 +2629,8 @@ async function main() {
|
|
|
2567
2629
|
graphCommand.command("search <term...>").option("--max-output-bytes <bytes>").addHelpText("after", "\n检索规则的 code+name(FTS5,末词前缀匹配;**不含规则体/配置内容**)。找到\"关于 X 的规则\"拿 code,再走 references/impact。中文必须是全词或词首通配(如 报名* / 增加预算*)——子串不命中:FTS5 把「增加预算」当一个 token,搜「预算」为 0。\n").action(async (term, options) => {
|
|
2568
2630
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphSearch(term.join(" "), transport));
|
|
2569
2631
|
});
|
|
2570
|
-
graphCommand.command("status").option("--max-output-bytes <bytes>").action(async (options) => {
|
|
2571
|
-
commandExitCode = await withTransport(options, request, (transport) => cmdGraphStatus(transport));
|
|
2632
|
+
graphCommand.command("status").option("--env <environment>").option("--environment <environment>").option("--max-output-bytes <bytes>").action(async (options) => {
|
|
2633
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphStatus(options.environment ?? options.env, transport));
|
|
2572
2634
|
});
|
|
2573
2635
|
// MCP stdio server (host registration: cpdocs installer's omc-graph entry;
|
|
2574
2636
|
// top-level verb like `codegraph serve --mcp`, not a graph subcommand).
|
|
@@ -120,6 +120,15 @@ function sanitizePaths(value) {
|
|
|
120
120
|
}
|
|
121
121
|
continue;
|
|
122
122
|
}
|
|
123
|
+
// r41a 接线(2026-11-08,E2E F34 三撞收口):graph 回执的 dbPath 是「索引身份」
|
|
124
|
+
// 字段——MCP 面(omc mcp-graph --path)本就明文回显自己的库路径,CLI 侧把它
|
|
125
|
+
// 脱成 [local-path-redacted] 后两面无法比对(探针正是靠比对 dbPath 才发现
|
|
126
|
+
// 「MCP 读的是 ext-mcc 目录的旧库」);graph-index-absent 的拒绝消息也在叫用户
|
|
127
|
+
// 「比对 dbId/dbPath」。仅豁免这一个键,其余路径照脱敏不放松。
|
|
128
|
+
if (key === "dbPath" && typeof entry === "string") {
|
|
129
|
+
output[key] = entry;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
123
132
|
output[key] = sanitizePaths(entry);
|
|
124
133
|
}
|
|
125
134
|
return output;
|
package/dist/graph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { joinConfigRoot } from "./command-transport.js";
|
|
5
5
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
|
|
6
6
|
import { ENDPOINTS } from "./endpoints.js";
|
|
@@ -46,7 +46,7 @@ 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
|
+
export const GRAPH_SCHEMA_VERSION = "7";
|
|
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;
|
|
@@ -77,8 +77,8 @@ export class GraphPullLockHeldError extends Error {
|
|
|
77
77
|
this.name = "GraphPullLockHeldError";
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
export function graphDbPath() {
|
|
81
|
-
return join(joinConfigRoot(), ".omc", "graph", "graph.db");
|
|
80
|
+
export function graphDbPath(root) {
|
|
81
|
+
return join(resolve(root ?? process.env.OMC_GRAPH_PATH ?? joinConfigRoot()), ".omc", "graph", "graph.db");
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Extract api-fact reference edges from one rule's stored node graph. The
|
|
@@ -181,7 +181,7 @@ export function extractListViewEdges(schemaCode, viewCode, model) {
|
|
|
181
181
|
* `datarule-list`) and vice versa (`rule-list`). Output is sorted so the
|
|
182
182
|
* published DB is byte-stable regardless of completion order.
|
|
183
183
|
*/
|
|
184
|
-
export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRENCY) {
|
|
184
|
+
export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRENCY, requestedApplication) {
|
|
185
185
|
const identity = session.environmentFingerprint();
|
|
186
186
|
const pulledAtIso = new Date().toISOString();
|
|
187
187
|
const totalStart = Date.now();
|
|
@@ -192,21 +192,27 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
192
192
|
// returns EMPTY data on a real platform (works only against the fake face)
|
|
193
193
|
// — using it here made the first live pull enumerate zero models and (pre-
|
|
194
194
|
// zero-guard) wedge the scheduler.
|
|
195
|
-
const scope = await resolveApplicationScope(session,
|
|
195
|
+
const scope = await resolveApplicationScope(session, requestedApplication).catch(() => undefined);
|
|
196
196
|
const applicationCode = scope?.code ?? "";
|
|
197
197
|
const gaps = [];
|
|
198
198
|
const treeRows = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
199
|
-
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));
|
|
199
|
+
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), applicationCode })).sort((a, b) => a.schemaCode.localeCompare(b.schemaCode));
|
|
200
200
|
const schemaCodes = modelEntities.map((row) => row.schemaCode);
|
|
201
201
|
const fields = [];
|
|
202
202
|
const services = [];
|
|
203
203
|
await Promise.all(schemaCodes.map(async (schemaCode) => {
|
|
204
204
|
try {
|
|
205
|
-
|
|
205
|
+
let rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
206
|
+
// Several applications expose fields only on the draft face. An empty
|
|
207
|
+
// published response is not evidence that the model has no fields.
|
|
208
|
+
if (rows.length === 0)
|
|
209
|
+
rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "false" } })).data);
|
|
210
|
+
if (rows.length === 0)
|
|
211
|
+
gaps.push({ kind: "field-list", ref: schemaCode, note: "published and draft faces are empty" });
|
|
206
212
|
for (const row of rows) {
|
|
207
213
|
const code = String(row.code ?? row.propertyCode ?? "");
|
|
208
214
|
if (code)
|
|
209
|
-
fields.push({ schemaCode, code, name: String(row.name ?? row.propertyName ?? code) });
|
|
215
|
+
fields.push({ schemaCode, code, name: String(row.name ?? row.propertyName ?? code), applicationCode });
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
218
|
catch (error) {
|
|
@@ -219,7 +225,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
219
225
|
for (const row of rows) {
|
|
220
226
|
const code = String(row.code ?? "");
|
|
221
227
|
if (code)
|
|
222
|
-
services.push({ code, name: String(row.name ?? code) });
|
|
228
|
+
services.push({ code, name: String(row.name ?? code), applicationCode });
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
catch (error) {
|
|
@@ -302,7 +308,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
302
308
|
dataruleRows = wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query: { schemaCode } })).data);
|
|
303
309
|
dataruleCount += dataruleRows.length;
|
|
304
310
|
for (const row of dataruleRows)
|
|
305
|
-
edges.push(...extractDataRuleEdges(schemaCode, row));
|
|
311
|
+
edges.push(...extractDataRuleEdges(schemaCode, row).map((edge) => ({ ...edge, applicationCode })));
|
|
306
312
|
}
|
|
307
313
|
catch (error) {
|
|
308
314
|
gaps.push({ kind: "datarule-list", ref: schemaCode, note: error instanceof Error ? error.message : String(error) });
|
|
@@ -361,7 +367,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
361
367
|
rowId: ruleId,
|
|
362
368
|
graphSha256: sha256({ nodes, routes: decodeJsonColumn(record.route, JSON_COLUMN_NULL_COALESCE) }),
|
|
363
369
|
});
|
|
364
|
-
edges.push(...extractReferenceEdges(schemaCode, ruleCode, nodes));
|
|
370
|
+
edges.push(...extractReferenceEdges(schemaCode, ruleCode, nodes).map((edge) => ({ ...edge, applicationCode })));
|
|
365
371
|
}
|
|
366
372
|
catch (error) {
|
|
367
373
|
gaps.push({ kind: "rule-get", ref: `${schemaCode}/${ruleCode}`, note: error instanceof Error ? error.message : String(error) });
|
|
@@ -378,7 +384,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
378
384
|
return;
|
|
379
385
|
}
|
|
380
386
|
viewCount += 1;
|
|
381
|
-
edges.push(...extractListViewEdges(schemaCode, viewCode, got));
|
|
387
|
+
edges.push(...extractListViewEdges(schemaCode, viewCode, got).map((edge) => ({ ...edge, applicationCode })));
|
|
382
388
|
}
|
|
383
389
|
catch (error) {
|
|
384
390
|
gaps.push({ kind: "listview-get", ref: `${schemaCode}/${viewCode}`, note: error instanceof Error ? error.message : String(error) });
|
|
@@ -496,10 +502,24 @@ async function withGraphPullLock(run) {
|
|
|
496
502
|
}
|
|
497
503
|
}
|
|
498
504
|
/** Wholesalerebuild + atomic publish (staging file + rename), 0600. */
|
|
499
|
-
export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCURRENCY) {
|
|
505
|
+
export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCURRENCY, options = {}) {
|
|
500
506
|
try {
|
|
501
507
|
return await withGraphPullLock(async () => {
|
|
502
|
-
const
|
|
508
|
+
const requested = options.applicationCodes?.length ? options.applicationCodes : [undefined];
|
|
509
|
+
const parts = await Promise.all(requested.map((application) => pullRuleGraph(session, concurrency, application)));
|
|
510
|
+
const first = parts[0];
|
|
511
|
+
const data = parts.length === 1 ? first : {
|
|
512
|
+
...first,
|
|
513
|
+
meta: { ...first.meta, applicationCode: "*", applicationResolution: { code: "*", source: "wildcard" }, coverage: { applicationCodes: parts.flatMap((part) => part.meta.coverage.applicationCodes), tenantApplications: parts.flatMap((part) => part.meta.coverage.applicationCodes) } },
|
|
514
|
+
rules: parts.flatMap((part) => part.rules),
|
|
515
|
+
edges: parts.flatMap((part) => part.edges),
|
|
516
|
+
models: parts.flatMap((part) => part.models),
|
|
517
|
+
fields: parts.flatMap((part) => part.fields),
|
|
518
|
+
services: parts.flatMap((part) => part.services),
|
|
519
|
+
gaps: parts.flatMap((part) => part.gaps),
|
|
520
|
+
scanned: parts.reduce((sum, part) => ({ models: sum.models + part.scanned.models, rules: sum.rules + part.scanned.rules, datarules: sum.datarules + part.scanned.datarules, views: sum.views + part.scanned.views, parseFailures: sum.parseFailures + part.scanned.parseFailures }), { models: 0, rules: 0, datarules: 0, views: 0, parseFailures: 0 }),
|
|
521
|
+
timings: { listMs: parts.reduce((n, part) => n + part.timings.listMs, 0), readMs: parts.reduce((n, part) => n + part.timings.readMs, 0), totalMs: parts.reduce((n, part) => n + part.timings.totalMs, 0) },
|
|
522
|
+
};
|
|
503
523
|
const staging = `${graphDbPath()}.staging-${process.pid}`;
|
|
504
524
|
rmSync(staging, { force: true });
|
|
505
525
|
const DatabaseSyncCtor = nodeSqlite().DatabaseSync;
|
|
@@ -531,13 +551,14 @@ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCU
|
|
|
531
551
|
rule_code TEXT NOT NULL,
|
|
532
552
|
node_code TEXT,
|
|
533
553
|
node_type TEXT,
|
|
534
|
-
|
|
554
|
+
via TEXT NOT NULL,
|
|
555
|
+
application_code TEXT
|
|
535
556
|
);
|
|
536
557
|
CREATE INDEX edges_lookup ON edges(kind, code);
|
|
537
558
|
CREATE TABLE gaps (kind TEXT NOT NULL, ref TEXT NOT NULL, note TEXT);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
559
|
+
CREATE TABLE models (schema_code TEXT PRIMARY KEY, name TEXT NOT NULL, application_code TEXT);
|
|
560
|
+
CREATE TABLE fields (schema_code TEXT NOT NULL, code TEXT NOT NULL, name TEXT NOT NULL, application_code TEXT);
|
|
561
|
+
CREATE TABLE services (code TEXT PRIMARY KEY, name TEXT NOT NULL, application_code TEXT);
|
|
541
562
|
`);
|
|
542
563
|
const insertMeta = db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)");
|
|
543
564
|
// `applicationResolution` is an object; String(value) would persist
|
|
@@ -548,21 +569,21 @@ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCU
|
|
|
548
569
|
const insertRule = db.prepare("INSERT INTO rules (schema_code, rule_code, name, enabled, status, default_rule, row_id, graph_sha256) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
549
570
|
for (const rule of data.rules)
|
|
550
571
|
insertRule.run(rule.schemaCode, rule.ruleCode, rule.name, rule.enabled === null ? null : rule.enabled ? 1 : 0, rule.status, rule.defaultRule === null ? null : rule.defaultRule ? 1 : 0, rule.rowId, rule.graphSha256);
|
|
551
|
-
const insertEdge = db.prepare("INSERT INTO edges (kind, code, schema_code, rule_code, node_code, node_type, via) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
572
|
+
const insertEdge = db.prepare("INSERT INTO edges (kind, code, schema_code, rule_code, node_code, node_type, via, application_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
552
573
|
for (const edge of data.edges)
|
|
553
|
-
insertEdge.run(edge.kind, edge.code, edge.schemaCode, edge.ruleCode, edge.nodeCode, edge.nodeType, edge.via);
|
|
574
|
+
insertEdge.run(edge.kind, edge.code, edge.schemaCode, edge.ruleCode, edge.nodeCode, edge.nodeType, edge.via, edge.applicationCode ?? null);
|
|
554
575
|
const insertGap = db.prepare("INSERT INTO gaps (kind, ref, note) VALUES (?, ?, ?)");
|
|
555
576
|
for (const gap of data.gaps)
|
|
556
577
|
insertGap.run(gap.kind, gap.ref, gap.note ?? null);
|
|
557
|
-
const insertModel = db.prepare("INSERT INTO models (schema_code, name) VALUES (?, ?)");
|
|
578
|
+
const insertModel = db.prepare("INSERT INTO models (schema_code, name, application_code) VALUES (?, ?, ?)");
|
|
558
579
|
for (const row of data.models)
|
|
559
|
-
insertModel.run(row.schemaCode, row.name);
|
|
560
|
-
const insertField = db.prepare("INSERT INTO fields (schema_code, code, name) VALUES (?, ?, ?)");
|
|
580
|
+
insertModel.run(row.schemaCode, row.name, row.applicationCode ?? null);
|
|
581
|
+
const insertField = db.prepare("INSERT INTO fields (schema_code, code, name, application_code) VALUES (?, ?, ?, ?)");
|
|
561
582
|
for (const row of data.fields)
|
|
562
|
-
insertField.run(row.schemaCode, row.code, row.name);
|
|
563
|
-
const insertService = db.prepare("INSERT INTO services (code, name) VALUES (?, ?)");
|
|
583
|
+
insertField.run(row.schemaCode, row.code, row.name, row.applicationCode ?? null);
|
|
584
|
+
const insertService = db.prepare("INSERT OR REPLACE INTO services (code, name, application_code) VALUES (?, ?, ?)");
|
|
564
585
|
for (const row of data.services)
|
|
565
|
-
insertService.run(row.code, row.name);
|
|
586
|
+
insertService.run(row.code, row.name, row.applicationCode ?? null);
|
|
566
587
|
// FTS5 mirrors rules (code+name) for `graph search`; one rebuild after
|
|
567
588
|
// the bulk insert is cheaper than per-row triggers on a wholesale rebuild.
|
|
568
589
|
db.exec(`
|
|
@@ -598,7 +619,7 @@ function openGraphIndex() {
|
|
|
598
619
|
for (const row of db.prepare("SELECT key, value FROM meta").all())
|
|
599
620
|
meta[row.key] = row.value;
|
|
600
621
|
if (meta.schemaVersion !== GRAPH_SCHEMA_VERSION)
|
|
601
|
-
throw new GraphIndexAbsentError(`索引 schema 版本不匹配(${meta.schemaVersion} ≠ ${GRAPH_SCHEMA_VERSION}
|
|
622
|
+
throw new GraphIndexAbsentError(`索引 schema 版本不匹配(${meta.schemaVersion ?? "unknown"} ≠ ${GRAPH_SCHEMA_VERSION});当前版本需要 schema ${GRAPH_SCHEMA_VERSION},旧版索引已作废,请重跑 omc graph pull(schema 升版需重拉)`);
|
|
602
623
|
const ruleCount = db.prepare("SELECT COUNT(*) AS n FROM rules").get().n;
|
|
603
624
|
const modelCount = db.prepare("SELECT COUNT(DISTINCT schema_code) AS n FROM rules").get().n;
|
|
604
625
|
const entityCounts = {
|
|
@@ -620,7 +641,27 @@ function openGraphIndex() {
|
|
|
620
641
|
const pulledAtIso = meta.pulledAtIso ?? "";
|
|
621
642
|
const ageDays = indexAgeDays(pulledAtIso);
|
|
622
643
|
const ageMinutes = indexAgeMinutes(pulledAtIso);
|
|
623
|
-
|
|
644
|
+
let applicationCodes = meta.applicationCode ? [meta.applicationCode] : [];
|
|
645
|
+
try {
|
|
646
|
+
const parsedCoverage = JSON.parse(meta.coverage ?? "null");
|
|
647
|
+
if (Array.isArray(parsedCoverage.applicationCodes))
|
|
648
|
+
applicationCodes = parsedCoverage.applicationCodes.filter((code) => typeof code === "string");
|
|
649
|
+
}
|
|
650
|
+
catch { /* legacy index */ }
|
|
651
|
+
let tenantApplications;
|
|
652
|
+
try {
|
|
653
|
+
const parsedCoverage = JSON.parse(meta.coverage ?? "null");
|
|
654
|
+
if (Array.isArray(parsedCoverage.tenantApplications))
|
|
655
|
+
tenantApplications = parsedCoverage.tenantApplications.filter((code) => typeof code === "string");
|
|
656
|
+
}
|
|
657
|
+
catch { /* legacy index */ }
|
|
658
|
+
const fieldRows = db.prepare("SELECT application_code AS applicationCode, COUNT(*) AS n FROM fields GROUP BY application_code").all();
|
|
659
|
+
const countsByApplication = {};
|
|
660
|
+
for (const row of fieldRows)
|
|
661
|
+
if (row.applicationCode)
|
|
662
|
+
countsByApplication[row.applicationCode] = Number(row.n);
|
|
663
|
+
const fieldApplications = Object.keys(countsByApplication);
|
|
664
|
+
const coverage = { applicationCodes, ...(tenantApplications ? { tenantApplications } : {}), fields: { coveredApplications: fieldApplications, notCovered: applicationCodes.filter((code) => !fieldApplications.includes(code)), countsByApplication } };
|
|
624
665
|
return {
|
|
625
666
|
db,
|
|
626
667
|
summary: {
|
|
@@ -663,6 +704,14 @@ function openGraphIndex() {
|
|
|
663
704
|
export const GRAPH_COVERAGE = Object.freeze({
|
|
664
705
|
scanned: Object.freeze(["businessrule", "datarule", "listview"]),
|
|
665
706
|
notScanned: Object.freeze(["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"]),
|
|
707
|
+
answerable: Object.freeze(["businessrule", "datarule", "listview"]),
|
|
708
|
+
mustCheckManually: Object.freeze([
|
|
709
|
+
{ surface: "form", why: "表单控件关联未扫描,可能仍引用目标字段", how: "omc call form.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
710
|
+
{ surface: "workflow.permissions", why: "流程权限未扫描,无法判断谁能发起/操作", how: "omc call workflow.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
711
|
+
{ surface: "workflow.bindings", why: "流程规则绑定未扫描,规则删除可能留下绑定", how: "omc call workflow.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
712
|
+
{ surface: "onlinejs", why: "在线 JS 可能通过字符串引用字段", how: "omc call onlinejs.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
713
|
+
{ surface: "rule.conditions", why: "规则条件叶子未扫描,命中可能不完整", how: "omc call rule.list --args '{\"schemaCode\":\"<x>\"}' 后人工检查条件公式" },
|
|
714
|
+
]),
|
|
666
715
|
note: "零命中/全零 totals 只代表 scanned 面没有命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响",
|
|
667
716
|
});
|
|
668
717
|
/**
|
|
@@ -684,7 +733,7 @@ export function queryGraphReferences(kind, code) {
|
|
|
684
733
|
const { db, summary } = openGraphIndex();
|
|
685
734
|
try {
|
|
686
735
|
const rows = db
|
|
687
|
-
.prepare("SELECT schema_code, rule_code, node_code, node_type, via FROM edges WHERE kind = ? AND code = ? ORDER BY schema_code, rule_code, node_code, via")
|
|
736
|
+
.prepare("SELECT schema_code, rule_code, node_code, node_type, via, application_code FROM edges WHERE kind = ? AND code = ? ORDER BY schema_code, rule_code, node_code, via")
|
|
688
737
|
.all(kind, code);
|
|
689
738
|
const matches = rows.map((row) => ({
|
|
690
739
|
schemaCode: row.schema_code,
|
|
@@ -692,6 +741,9 @@ export function queryGraphReferences(kind, code) {
|
|
|
692
741
|
nodeCode: row.node_code ?? "",
|
|
693
742
|
nodeType: row.node_type ?? "",
|
|
694
743
|
via: row.via,
|
|
744
|
+
referrer: { schemaCode: row.schema_code, ruleCode: row.rule_code },
|
|
745
|
+
isSelf: row.schema_code === code,
|
|
746
|
+
...(row.application_code ? { applicationCode: row.application_code } : {}),
|
|
695
747
|
}));
|
|
696
748
|
const gapped = summary.gaps.count > 0;
|
|
697
749
|
// r35a-F4(真机 2026-11-06):探针拿 matches:[] 差点把一个**被表单引用**的
|
|
@@ -722,6 +774,44 @@ export function queryGraphReferences(kind, code) {
|
|
|
722
774
|
db.close();
|
|
723
775
|
}
|
|
724
776
|
}
|
|
777
|
+
export function queryGraphReferencesBatch(kind, codes) {
|
|
778
|
+
const { db, summary } = openGraphIndex();
|
|
779
|
+
try {
|
|
780
|
+
const wanted = [...new Set(codes.filter(Boolean))];
|
|
781
|
+
const rows = wanted.length === 0 ? [] : db.prepare(`SELECT code, schema_code, rule_code, node_code, node_type, via FROM edges WHERE kind = ? AND code IN (${wanted.map(() => "?").join(",")}) ORDER BY code, schema_code, rule_code, node_code, via`).all(kind, ...wanted);
|
|
782
|
+
return { kind, codes: wanted, matches: rows.map((row) => ({ targetCode: row.code, schemaCode: row.schema_code, ruleCode: row.rule_code, nodeCode: row.node_code ?? "", nodeType: row.node_type ?? "", via: row.via, referrer: { schemaCode: row.schema_code, ruleCode: row.rule_code }, isSelf: row.schema_code === row.code })), index: summary, coverage: GRAPH_COVERAGE };
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
db.close();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
export function queryGraphEdges(fromSchema, toSchema, kind, offset = 0, limit = 50) {
|
|
789
|
+
const { db, summary } = openGraphIndex();
|
|
790
|
+
try {
|
|
791
|
+
const conditions = [];
|
|
792
|
+
const params = [];
|
|
793
|
+
if (fromSchema) {
|
|
794
|
+
conditions.push("schema_code = ?");
|
|
795
|
+
params.push(fromSchema);
|
|
796
|
+
}
|
|
797
|
+
if (toSchema) {
|
|
798
|
+
conditions.push("code = ?");
|
|
799
|
+
params.push(toSchema);
|
|
800
|
+
}
|
|
801
|
+
if (kind) {
|
|
802
|
+
conditions.push("kind = ?");
|
|
803
|
+
params.push(kind);
|
|
804
|
+
}
|
|
805
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
806
|
+
const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM edges${where}`).get(...params).n);
|
|
807
|
+
const rows = db.prepare(`SELECT kind, code, schema_code, rule_code, node_code, node_type, via, application_code FROM edges${where} ORDER BY schema_code, code, rule_code, node_code, via LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
808
|
+
const edges = rows.map((row) => ({ kind: row.kind, code: row.code, schemaCode: row.schema_code, ruleCode: row.rule_code, nodeCode: row.node_code ?? "", nodeType: row.node_type ?? "", via: row.via, ...(row.application_code ? { applicationCode: row.application_code } : {}) }));
|
|
809
|
+
return { edges, index: summary, total, truncated: offset + edges.length < total, ...(offset + edges.length < total ? { nextOffset: offset + edges.length } : {}) };
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
db.close();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
725
815
|
export function readGraphStatus() {
|
|
726
816
|
const { db, summary } = openGraphIndex();
|
|
727
817
|
try {
|
|
@@ -732,10 +822,13 @@ export function readGraphStatus() {
|
|
|
732
822
|
dbBytes: statSync(dbPath).size,
|
|
733
823
|
dbPath,
|
|
734
824
|
dbId: sha256(dbPath).slice(0, 12),
|
|
825
|
+
mcpHint: `MCP 工具按注册时的 --path 读库(当前索引根:${dirname(dirname(dbPath))});要让 MCP 看到本次拉取,请执行 omc graph pull --path ${dirname(dirname(dbPath))} --env <env>。`,
|
|
735
826
|
ageMinutes: summary.ageMinutes,
|
|
736
827
|
staleAfterMinutes: summary.staleAfterMinutes,
|
|
737
828
|
...(summary.staleWarning ? { warning: summary.staleWarning } : {}),
|
|
738
829
|
coverage: summary.coverage,
|
|
830
|
+
coveredApplications: summary.coverage.applicationCodes,
|
|
831
|
+
tenantApplications: summary.coverage.tenantApplications ?? summary.coverage.applicationCodes,
|
|
739
832
|
gaps,
|
|
740
833
|
};
|
|
741
834
|
}
|
|
@@ -794,8 +887,8 @@ export function searchGraphRules(term, limit = 20, offset = 0) {
|
|
|
794
887
|
...(offset + rows.length < total ? { truncated: true, nextOffset: offset + rows.length } : {}),
|
|
795
888
|
};
|
|
796
889
|
};
|
|
797
|
-
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 }));
|
|
798
|
-
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 }));
|
|
890
|
+
entityMatches.model = searchEntity("SELECT COUNT(*) AS n FROM models WHERE instr(name, ?) > 0 OR instr(schema_code, ?) > 0", "SELECT schema_code, name, application_code 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, applicationCode: row.application_code ?? undefined }));
|
|
891
|
+
entityMatches.field = searchEntity("SELECT COUNT(*) AS n FROM fields WHERE instr(name, ?) > 0 OR instr(code, ?) > 0", "SELECT schema_code, code, name, application_code 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, applicationCode: row.application_code ?? undefined }));
|
|
799
892
|
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 }));
|
|
800
893
|
}
|
|
801
894
|
const gapped = summary.gaps.count > 0;
|
|
@@ -804,7 +897,7 @@ export function searchGraphRules(term, limit = 20, offset = 0) {
|
|
|
804
897
|
rules: matches,
|
|
805
898
|
matches: entityMatches,
|
|
806
899
|
index: summary,
|
|
807
|
-
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;规则体、表单与流程权限未扫描" },
|
|
900
|
+
coverage: { scanned: ["rule.code", "rule.name", "model.code", "model.name", "field.code", "field.name", "service.code", "service.name"], notScanned: ["rule.body", "form", "workflow.permissions"], answerable: ["rule.code", "rule.name", "model.code", "model.name", "field.code", "field.name", "service.code", "service.name"], mustCheckManually: [{ surface: "rule.body", why: "规则体未检索", how: "omc call rule.list --args '{\"schemaCode\":\"<x>\"}'" }, { surface: "form", why: "表单控件未扫描", how: "omc call form.get --args '{\"schemaCode\":\"<x>\"}'" }, { surface: "workflow.permissions", why: "流程权限未扫描", how: "omc call workflow.get --args '{\"schemaCode\":\"<x>\"}'" }], note: "检索覆盖规则与实体的 code/name;规则体、表单与流程权限未扫描" },
|
|
808
901
|
...(term.trim() !== "" && !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(term.trim()) ? { note: "疑似业务词;本工具同时检索实体 name/code 与规则 code/name,引用明细请使用匹配到的 code 再查 references/impact" } : {}),
|
|
809
902
|
...(matches.length === 0 && entityMatches.model.total === 0 && entityMatches.field.total === 0 && entityMatches.service.total === 0
|
|
810
903
|
? {
|
package/dist/human-summary.js
CHANGED
|
@@ -191,7 +191,7 @@ function phraseOf(tool, args, outcome) {
|
|
|
191
191
|
case "workflow.delete":
|
|
192
192
|
return `删除流程 ${stringArg(args, "code") ?? "?"}`;
|
|
193
193
|
case "onlinejs.compose":
|
|
194
|
-
return `编排并发布表单脚本 customScript(模型 ${schemaCode ?? "?"} / 表单 ${stringArg(args, "sheetCode") ?? "?"})`;
|
|
194
|
+
return `编排并发布表单脚本 customScript(模型 ${schemaCode ?? "?"} / 表单 ${stringArg(args, "sheetCode") ?? `sheetCodeFromSchema:${String(outcome?.readback?.sheetCodeFromSchema ?? schemaCode ?? "?")}`})`;
|
|
195
195
|
case "service.register":
|
|
196
196
|
return `注册后端服务 ${stringArg(args, "serviceCode") ?? "?"}`;
|
|
197
197
|
case "service.delete":
|
package/dist/lanes/field.js
CHANGED
|
@@ -134,6 +134,10 @@ function childSchemaCodes(rows) {
|
|
|
134
134
|
const codes = new Set(rows.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
135
135
|
return codes.has("parentId") && codes.has("sortKey");
|
|
136
136
|
}
|
|
137
|
+
function isSystemFieldCode(code) {
|
|
138
|
+
const value = String(code ?? "");
|
|
139
|
+
return SYSTEM_FIELD_CODES.has(value) || value === "parentId" || value === "sortKey";
|
|
140
|
+
}
|
|
137
141
|
/**
|
|
138
142
|
* r17 (2026-10-25, live + decompiled A/B): a FORMULA field CANNOT aggregate its
|
|
139
143
|
* own model's child table. `BizPropertyManagerImpl.publishFormula` rewrites
|
|
@@ -232,8 +236,10 @@ export function registerFieldLane(tools) {
|
|
|
232
236
|
// Round 4 (ADR 0005): read-side annotation for stored scale-less
|
|
233
237
|
// NUMERICAL landmines (the platform accepted them for years).
|
|
234
238
|
const scaleWarning = numericalScaleWarning(type, options);
|
|
239
|
+
const fieldCode = row.code ?? row.propertyCode;
|
|
240
|
+
const system = isSystemFieldCode(fieldCode);
|
|
235
241
|
return {
|
|
236
|
-
code:
|
|
242
|
+
code: fieldCode,
|
|
237
243
|
name: row.name,
|
|
238
244
|
...(redundantNameI18n(row.name_i18n, row.name) ? {} : typeof row.name_i18n === "string" ? { name_i18n: row.name_i18n } : {}),
|
|
239
245
|
type: row.type ?? row.propertyType,
|
|
@@ -261,7 +267,8 @@ export function registerFieldLane(tools) {
|
|
|
261
267
|
// readback.committed。
|
|
262
268
|
...(Object.prototype.hasOwnProperty.call(row, "propertyEmpty") ? { propertyEmpty: row.propertyEmpty } : {}),
|
|
263
269
|
...(options !== undefined ? { options } : {}),
|
|
264
|
-
...(scaleWarning !== undefined ? { shapeWarning: scaleWarning } : {}),
|
|
270
|
+
...(scaleWarning !== undefined ? { shapeWarning: scaleWarning, ...(system ? { origin: "system" } : {}) } : {}),
|
|
271
|
+
...(system && scaleWarning === undefined ? { origin: "system" } : {}),
|
|
265
272
|
};
|
|
266
273
|
});
|
|
267
274
|
// Projection: ask for one field (`--code`) or a named set (`--fields`) so a
|
|
@@ -281,7 +288,7 @@ export function registerFieldLane(tools) {
|
|
|
281
288
|
// does this model have" paid for noise it then had to filter. `all:true`
|
|
282
289
|
// restores the previous full list (uses the SAME SYSTEM_FIELD_CODES set the
|
|
283
290
|
// list-view/form scaffolds already filter with).
|
|
284
|
-
const visible = args.all === true ? selected : selected.filter((field) => !
|
|
291
|
+
const visible = args.all === true ? selected : selected.filter((field) => !isSystemFieldCode(field.code));
|
|
285
292
|
const projected = detailed
|
|
286
293
|
? visible
|
|
287
294
|
: visible.map((field) => {
|
|
@@ -306,6 +313,7 @@ export function registerFieldLane(tools) {
|
|
|
306
313
|
...(Array.isArray(subColumns) && subColumns.length > businessCodes.length ? { systemSubColumnCount: subColumns.length - businessCodes.length } : {}),
|
|
307
314
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
308
315
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
316
|
+
...(field.origin !== undefined ? { origin: field.origin } : {}),
|
|
309
317
|
};
|
|
310
318
|
});
|
|
311
319
|
return {
|
|
@@ -363,6 +371,7 @@ export function registerFieldLane(tools) {
|
|
|
363
371
|
...(Array.isArray(field.subColumns) ? { subColumns: field.subColumns } : {}),
|
|
364
372
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
365
373
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
374
|
+
...(field.origin !== undefined ? { origin: field.origin } : {}),
|
|
366
375
|
};
|
|
367
376
|
}),
|
|
368
377
|
hint: "字段详情(options/subSchema)用 omc call field.list --args '{\"schemaCode\":\"...\",\"published\":true,\"code\":\"<field>\"}'",
|
|
@@ -12,7 +12,7 @@ export const LIST_VIEW_WIRE = Object.freeze({
|
|
|
12
12
|
configureDefault: "code omitted means the default view (code=schemaCode)",
|
|
13
13
|
configureColumns: "query/get then query/update: for each fieldCodes entry, append a missing column as visible, or repair an existing column's name/propertyType/displayFormat/visible in place (never skip an already-present column), then query/get readback; propertyType comes from published field metadata; NUMERICAL displayFormat is derived from the field's 0-based `options.scale` (0=integer,1=1dp,2=2dp,3=3dp…8=8dp) through the platform parseNumber table ({0:1,1:2,2:3,3:11,4:12,23:…, percent 0..:4/5/6/13/14/27..30, currency ¥ 2dp..:7/15/16/31..34} — verified live: scale 2 → displayFormat 3 = 2 decimals) and DATE displayFormat from options.format (1=yyyy-MM-dd,2=datetime,3=minute,4=yyyy-MM); the readback columns array carries {propertyCode,propertyType,displayFormat,visible,sumType} and added/updated name the fieldCodes touched",
|
|
14
14
|
configureSorts: "sorts -> querySorts [{propertyCode,propertyType,direction,sortKey,clientType}]; direction SortDirectionType numeric ASC(1)/DESC(2); omitted leaves the existing sorts untouched, provided replaces them",
|
|
15
|
-
configureConditions: "conditions -> queryConditions [{propertyCode,propertyType,displayType,filterType,defaultValue,options,startValue,endValue,shortcutFilter,choiceType,dateType,accurateSearch,showNull,includeSubData,buildTreeBy,visible}]; filterType is the comparison OPERATOR (QueryFilterType STRING enum: Eq/NotEq/Gt/Lt/Gte/Lte/Like/NotLike/IsNull/IsNotNull/In/NotIn/Between/NotBetween/Of/NotOf/…; omitted → the platform default for the propertyType; aliases operator/op and legacy options.op); displayType INPUT(0)/RADIO(1)/CHECKBOX(2)/SELECT(3)/RANGE(4); choiceType SELECT(1)/POPUP(2)/TREE_LEFT(3); options is a JSON string (decoded on readback); conditionReadback is equivalent only for field-by-field equality, otherwise coerced with from/to details; viewConditionsAppliedRest:false records that REST data.list --queryCode does not apply fixed conditions",
|
|
15
|
+
configureConditions: "conditions -> queryConditions [{propertyCode,propertyType,displayType,filterType,defaultValue,options,startValue,endValue,shortcutFilter,choiceType,dateType,accurateSearch,showNull,includeSubData,buildTreeBy,visible}]; minimum --param shape: {\"conditions\":[{\"propertyCode\":\"<code>\",\"filterType\":\"Eq\",\"defaultValue\":<value>}]}; filterType is the comparison OPERATOR (QueryFilterType STRING enum: Eq/NotEq/Gt/Lt/Gte/Lte/Like/NotLike/IsNull/IsNotNull/In/NotIn/Between/NotBetween/Of/NotOf/…; omitted → the platform default for the propertyType; aliases operator/op and legacy options.op); displayType INPUT(0)/RADIO(1)/CHECKBOX(2)/SELECT(3)/RANGE(4); choiceType SELECT(1)/POPUP(2)/TREE_LEFT(3); options is a JSON string (decoded on readback); conditionReadback is equivalent only for field-by-field equality, otherwise coerced with from/to details; viewConditionsAppliedRest:false records that REST data.list --queryCode does not apply fixed conditions",
|
|
16
16
|
configureConditionMap: "queryConditionMap {<schemaCode>: queryConditions[]} is read-derived (query/get with a formCode child-table filter); BizQueryDetailConfig persists queryConditions/querySorts only, so the map is not a configure input",
|
|
17
17
|
configureActions: "actions -> queryActions [{actionCode,name,icon,queryActionType,systemAction,serviceCode,serviceMethod,customService,associationCode,associationType,sortKey}]; queryActionType QueryActionType numeric QUERY(1)/ADD(2)/.../BATCH_SUBMIT(23); associationType QueryActionRelativeType numeric BIZSHEET(0)/WORKFLOW(1); associationCode/associationType are the JSON names of the DTO fields relativeCode/queryActionRelativeType (@JsonProperty); omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (live 2026-09-17: query/update replaces-then-re-adds the submitted list, so a merge keeps the platform's built-in 新增/删除/导入/导出 and updates a matching actionCode — replaceActions:true restores the authoritative full set, defaultActions:true re-seeds the platform default set); query/get auto-injects default actions (qr_code/batch_print/batch_update/import_attachment/export_attachment/batch_submit) and query/update resets sortKey to i+1",
|
|
18
18
|
configurePresentation: "presentation -> queryPresentation {clientType,htmlJson,actionsJson,columnsJson,queryViewDataSource,schemaRelations,options}; htmlJson/actionsJson/columnsJson JSON strings are decoded once before render-shape validation (html element tagName+attrs, action actionCode/name/icon, column propertyCode/propertyType); invalid input refuses with listview-presentation-render-invalid before wire and is never double-encoded; clientType is forced to PC by query/update; schemaRelations is platform-derived from the queryColumns tree (resolveRelations) and overwrites your input; a non-empty htmlJson updates the environment record, which query/delete cleans up; omitted leaves the existing presentation untouched (the read-face object must be echoed, because saveBizQueryDetail removes every presentation whose clientType matches before re-adding), provided replaces it",
|
package/dist/lanes/list-view.js
CHANGED
|
@@ -920,7 +920,15 @@ export function registerListViewLane(tools) {
|
|
|
920
920
|
: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true })}'`,
|
|
921
921
|
});
|
|
922
922
|
}
|
|
923
|
-
|
|
923
|
+
const normalizedFieldCodes = args.fieldCodes.map((ref) => resolveFieldRef(fieldRows, ref)?.code ?? ref);
|
|
924
|
+
return {
|
|
925
|
+
wouldChange: true,
|
|
926
|
+
summary: `即将配置列表视图 ${args.schemaCode}/${code} 的列`,
|
|
927
|
+
fieldCodes: normalizedFieldCodes.length > 20
|
|
928
|
+
? { count: normalizedFieldCodes.length, sha256: createHash("sha256").update(JSON.stringify(normalizedFieldCodes)).digest("hex"), note: "超过 20 项才摘要;顺序复核请分批配置" }
|
|
929
|
+
: normalizedFieldCodes,
|
|
930
|
+
...(args.conditions !== undefined ? { conditions: args.conditions } : {}),
|
|
931
|
+
};
|
|
924
932
|
},
|
|
925
933
|
rollback: listviewRollbackSpec("listview.configure"),
|
|
926
934
|
});
|
package/dist/lanes/online-js.js
CHANGED
|
@@ -81,11 +81,12 @@ export function registerOnlineJsLane(tools) {
|
|
|
81
81
|
sheetCode,
|
|
82
82
|
present: draft.present || published.present,
|
|
83
83
|
sheetPublished: sheet.published === true,
|
|
84
|
-
draft: { ...draft, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
85
|
-
published: { ...published, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
84
|
+
draft: { ...draft, scriptSha256: draft.sha256 || undefined, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
85
|
+
published: { ...published, scriptSha256: published.sha256 || undefined, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
86
86
|
...(draft.sha256 !== "" && published.sha256 !== ""
|
|
87
87
|
? { draftPublishedInSync: draft.sha256 === published.sha256 }
|
|
88
88
|
: {}),
|
|
89
|
+
scriptSha256: published.sha256 || draft.sha256 || undefined,
|
|
89
90
|
hint: "脚本是 innerHTML 原文(含平台 (function(form){…}) 生命周期包装——那就是平台存储形态);改脚本用 onlinejs.compose(默认 INCREMENTAL 并入,replace:true 整段替换);同文件修订直接 replace:true 传完整脚本",
|
|
90
91
|
};
|
|
91
92
|
}, {
|
|
@@ -125,6 +126,8 @@ export function registerOnlineJsLane(tools) {
|
|
|
125
126
|
throw new Error("remove:true 不能与非空 source 同时传入");
|
|
126
127
|
// r28e F9:和同 lane 其它工具一致——省略 sheetCode 即模型码槽位。
|
|
127
128
|
const targetSheetCode = args.sheetCode ?? args.schemaCode;
|
|
129
|
+
// Keep the human summary truthful when the caller relies on the documented
|
|
130
|
+
// default (sheetCode omitted): it is the model's default form slot.
|
|
128
131
|
// 1. Read the existing draftHtmlJson.
|
|
129
132
|
const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: targetSheetCode } });
|
|
130
133
|
const existingSheet = (current.data ?? null);
|
|
@@ -215,6 +218,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
215
218
|
return {
|
|
216
219
|
performed: { errcode: 0, published },
|
|
217
220
|
readback: {
|
|
221
|
+
...(args.sheetCode === undefined ? { sheetCodeFromSchema: targetSheetCode } : {}),
|
|
218
222
|
publishedSha256,
|
|
219
223
|
draftSha256,
|
|
220
224
|
draftPublishedInSync,
|
|
@@ -223,11 +227,14 @@ export function registerOnlineJsLane(tools) {
|
|
|
223
227
|
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
224
228
|
composeMode: args.replace === true ? "replace" : "incremental",
|
|
225
229
|
...(removing ? { removed: true } : {}),
|
|
230
|
+
scriptSha256: customScriptSha256,
|
|
226
231
|
customScriptSha256,
|
|
232
|
+
scriptSha256DeprecatedAlias: "customScriptSha256",
|
|
227
233
|
customScriptBytes: Buffer.byteLength(composedScript, "utf8"),
|
|
228
234
|
composedDraftBytes: Buffer.byteLength(composedDraftHtml, "utf8"),
|
|
229
235
|
publishedHtmlBytes: Buffer.byteLength(publishedRaw, "utf8"),
|
|
230
236
|
htmlRenderCompat,
|
|
237
|
+
verifyWith: "onlinejs.get → published.scriptSha256",
|
|
231
238
|
hint: "summary 模式不回原文;要脚本原文用 onlinejs.get,要完整布局用 artifact 或 form.get --full",
|
|
232
239
|
},
|
|
233
240
|
};
|
|
@@ -235,6 +242,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
235
242
|
return {
|
|
236
243
|
performed: { errcode: 0, published },
|
|
237
244
|
readback: {
|
|
245
|
+
...(args.sheetCode === undefined ? { sheetCodeFromSchema: targetSheetCode } : {}),
|
|
238
246
|
// Dedupe: after a successful publish the draft and the published
|
|
239
247
|
// face are the same bytes, and the script also rides inside them.
|
|
240
248
|
// Return the composed payload ONCE (composedDraftHtmlJson keeps the
|
|
@@ -255,6 +263,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
255
263
|
composeMode: args.replace === true ? "replace" : "incremental",
|
|
256
264
|
...(removing ? { removed: true } : {}),
|
|
257
265
|
htmlRenderCompat,
|
|
266
|
+
verifyWith: "onlinejs.get → published.scriptSha256",
|
|
258
267
|
portalVerificationPlan: ["在门户打开该表单", "触发已绑定的在线 JS 事件", "检查事件产生的副作用字段或校验提示"],
|
|
259
268
|
},
|
|
260
269
|
};
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -3506,6 +3506,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3506
3506
|
fullName: row.fullName ?? null,
|
|
3507
3507
|
corpId: row.corpId ?? null,
|
|
3508
3508
|
code: row.code ?? null,
|
|
3509
|
+
...(row.origin === "system-preset" || row.defaultPermission === true || row.preset === true ? { origin: "system-preset" } : row.origin === "user" ? { origin: "user" } : {}),
|
|
3509
3510
|
}));
|
|
3510
3511
|
const actor = await resolvePermissionActor(session);
|
|
3511
3512
|
const effectiveFor = {
|
|
@@ -3520,6 +3521,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3520
3521
|
note: actor.isAdmin
|
|
3521
3522
|
? "当前登录人是平台管理员:发起成功不证明白名单生效。要用非管理员账号另建环境验证。"
|
|
3522
3523
|
: "本读面只回配置行;membership 命中需非管理员实发 data.submit 才能证伪。",
|
|
3524
|
+
...(actor.isAdmin ? { verifiable: false, verifyWith: `以普通账号凭据跑同查询:omc call workflow.permissions --env <env> --args '${JSON.stringify({ workflowCode })}'` } : {}),
|
|
3523
3525
|
};
|
|
3524
3526
|
return {
|
|
3525
3527
|
workflowCode,
|
package/dist/mcp-graph.js
CHANGED
|
@@ -47,7 +47,7 @@ const INSTRUCTIONS_INDEXED = [
|
|
|
47
47
|
"- 改名/删除/重构一个 字段(field)/模型(model)/服务(service) 前,先用 omc_graph_explore 查引用与影响面;查\"谁引用了 X\"也走它。",
|
|
48
48
|
"- 索引只覆盖:业务规则(methodMapping/targetObjectCode)、数据规则(propertyCodes 与公式 {字段})、视图(列/排序/条件)。**不含**规则条件叶子与流程绑定。",
|
|
49
49
|
"- search 语义:只匹配规则 code+name 的**词首**(中文子串不命中:搜「预算」匹配不到「增加预算」)。",
|
|
50
|
-
"答案带 freshness/pulledAtIso——索引是快照,平台侧改动后需 CLI 重建:omc graph pull --env <env>(只读远端,约 1 分钟)。",
|
|
50
|
+
"答案带 freshness/pulledAtIso——索引是快照,平台侧改动后需 CLI 重建:omc graph pull --env <env>(只读远端,约 1 分钟)。MCP 按注册时的 --path 读库;CLI pull 必须把 --path 指到本目录才能写入本库。",
|
|
51
51
|
"索引绑定单一环境(environmentName);查别的环境先重新 pull。",
|
|
52
52
|
].join("\n");
|
|
53
53
|
const INSTRUCTIONS_NO_INDEX = [
|
|
@@ -60,7 +60,7 @@ const EXPLORE_TOOL = {
|
|
|
60
60
|
description: [
|
|
61
61
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
62
62
|
"输入 query 可为业务名(实体 name+code 子串)或规则 code+name 词首;也可直接给字段/模型/服务 code。kind 可选 field|model|service,不给则三种都查。",
|
|
63
|
-
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
|
+
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via,referrer,isSelf}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
64
64
|
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset 继续调用。实体分页结果带 nextCall;分页不会改变 resolved。字段同名时请用 schemaCode 消歧。",
|
|
65
65
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
66
66
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
@@ -69,8 +69,11 @@ const EXPLORE_TOOL = {
|
|
|
69
69
|
type: "object",
|
|
70
70
|
properties: {
|
|
71
71
|
query: { type: "string", description: "业务名(实体 name+code 子串)、字段/模型/服务 code,或规则检索词(规则 code+name 词首匹配)" },
|
|
72
|
+
queries: { type: "array", items: { type: "string" }, description: "实体面多条件检索;与 query 并存时合并,match 默认 any" },
|
|
73
|
+
match: { type: "string", enum: ["all", "any"], description: "queries[] 的模型∪字段所属模型求交/并" },
|
|
72
74
|
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
73
75
|
schemaCode: { type: "string", description: "限定字段所属模型;也可用于校验模型。" },
|
|
76
|
+
applicationCode: { type: "string", description: "限定应用;仅返回该应用的实体与引用" },
|
|
74
77
|
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
75
78
|
offset: { type: "integer", minimum: 0, description: "引用/实体结果起始偏移;truncated 时使用 nextOffset" },
|
|
76
79
|
limit: { type: "integer", minimum: 1, maximum: MATCH_CAP, description: "每类结果最多返回多少条(默认 50)" },
|
|
@@ -125,9 +128,9 @@ function entityInventory() {
|
|
|
125
128
|
const db = new DatabaseSync(graphDbPath(), { readOnly: true });
|
|
126
129
|
try {
|
|
127
130
|
return [
|
|
128
|
-
...db.prepare("SELECT schema_code, name FROM models").all().map((r) => ({ kind: "model", schemaCode: r.schema_code, code: r.schema_code, name: r.name ?? "" })),
|
|
129
|
-
...db.prepare("SELECT schema_code, code, name FROM fields").all().map((r) => ({ kind: "field", schemaCode: r.schema_code, code: r.code, name: r.name ?? "" })),
|
|
130
|
-
...db.prepare("SELECT code, name FROM services").all().map((r) => ({ kind: "service", code: r.code, name: r.name ?? "" })),
|
|
131
|
+
...db.prepare("SELECT schema_code, name, application_code FROM models").all().map((r) => ({ kind: "model", schemaCode: r.schema_code, code: r.schema_code, name: r.name ?? "", applicationCode: r.application_code ?? undefined })),
|
|
132
|
+
...db.prepare("SELECT schema_code, code, name, application_code FROM fields").all().map((r) => ({ kind: "field", schemaCode: r.schema_code, code: r.code, name: r.name ?? "", applicationCode: r.application_code ?? undefined })),
|
|
133
|
+
...db.prepare("SELECT code, name, application_code FROM services").all().map((r) => ({ kind: "service", code: r.code, name: r.name ?? "", applicationCode: r.application_code ?? undefined })),
|
|
131
134
|
];
|
|
132
135
|
}
|
|
133
136
|
finally {
|
|
@@ -159,12 +162,16 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
159
162
|
}
|
|
160
163
|
if (name === EXPLORE_TOOL.name) {
|
|
161
164
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
const queryList = Array.isArray(args.queries) ? args.queries.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
|
|
166
|
+
const queries = queryList.length > 0 ? queryList : query ? [query] : [];
|
|
167
|
+
if (queries.length === 0)
|
|
168
|
+
return toolError("query 或 queries 不能为空");
|
|
169
|
+
const primaryQuery = queries[0];
|
|
164
170
|
const kind = args.kind === undefined ? undefined : String(args.kind);
|
|
165
171
|
if (kind !== undefined && !KINDS.includes(kind))
|
|
166
172
|
return toolError(`kind 必须是 ${KINDS.join("|")} 之一`);
|
|
167
173
|
const schemaCode = typeof args.schemaCode === "string" && args.schemaCode.trim() ? args.schemaCode.trim() : undefined;
|
|
174
|
+
const applicationCode = typeof args.applicationCode === "string" && args.applicationCode.trim() ? args.applicationCode.trim() : undefined;
|
|
168
175
|
if (schemaCode && kind !== "field" && kind !== "model")
|
|
169
176
|
return toolError("schemaCode 仅适用于 kind=field|model");
|
|
170
177
|
const status = readGraphStatus();
|
|
@@ -179,11 +186,34 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
179
186
|
const impact = {};
|
|
180
187
|
// Resolve from an unpaged search. The result page is presentation only;
|
|
181
188
|
// otherwise offset=2 can silently change the subject being inspected.
|
|
182
|
-
const search = searchGraphRules(
|
|
183
|
-
const
|
|
184
|
-
|
|
189
|
+
const search = searchGraphRules(primaryQuery, limit, offset);
|
|
190
|
+
const scopedSearch = applicationCode ? {
|
|
191
|
+
...search,
|
|
192
|
+
matches: {
|
|
193
|
+
...search.matches,
|
|
194
|
+
model: { ...search.matches.model, items: search.matches.model.items.filter((row) => row.applicationCode === applicationCode) },
|
|
195
|
+
field: { ...search.matches.field, items: search.matches.field.items.filter((row) => row.applicationCode === applicationCode) },
|
|
196
|
+
},
|
|
197
|
+
} : search;
|
|
198
|
+
const resolvedSearch = searchGraphRules(primaryQuery, MATCH_CAP, 0);
|
|
199
|
+
const loweredQuery = primaryQuery.toLocaleLowerCase();
|
|
185
200
|
const allCandidates = entityInventory().filter((r) => (r.code.toLocaleLowerCase().includes(loweredQuery) || r.name.toLocaleLowerCase().includes(loweredQuery)) &&
|
|
186
|
-
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode))
|
|
201
|
+
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode)) &&
|
|
202
|
+
(!applicationCode || r.applicationCode === applicationCode));
|
|
203
|
+
const candidatesForQuery = (term) => entityInventory().filter((r) => (r.code.toLocaleLowerCase().includes(term.toLocaleLowerCase()) || r.name.toLocaleLowerCase().includes(term.toLocaleLowerCase())) &&
|
|
204
|
+
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode)) &&
|
|
205
|
+
(!applicationCode || r.applicationCode === applicationCode));
|
|
206
|
+
const perQueryModels = queries.map((term) => new Set(candidatesForQuery(term).filter((r) => r.kind === "model").map((r) => r.schemaCode)));
|
|
207
|
+
const perQueryFieldModels = queries.map((term) => new Set(candidatesForQuery(term).filter((r) => r.kind === "field").map((r) => r.schemaCode)));
|
|
208
|
+
const combine = (sets) => {
|
|
209
|
+
if (sets.length === 0)
|
|
210
|
+
return new Set();
|
|
211
|
+
if (args.match === "all")
|
|
212
|
+
return new Set([...sets[0]].filter((value) => sets.every((set) => set.has(value))));
|
|
213
|
+
return new Set(sets.flatMap((set) => [...set]));
|
|
214
|
+
};
|
|
215
|
+
const hitModels = combine(perQueryModels);
|
|
216
|
+
const hitFieldModels = combine(perQueryFieldModels);
|
|
187
217
|
const typedCandidates = kind ? allCandidates.filter((r) => r.kind === kind) : allCandidates;
|
|
188
218
|
const fieldCandidates = kind === "field" ? typedCandidates : allCandidates.filter((r) => r.kind === "field");
|
|
189
219
|
const exactSame = fieldCandidates.filter((r) => r.code === query);
|
|
@@ -196,7 +226,7 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
196
226
|
for (const one of kinds) {
|
|
197
227
|
const subject = subjects.find((s) => s.kind === one);
|
|
198
228
|
const ref = queryGraphReferences(one, subject?.code ?? query);
|
|
199
|
-
const scopedMatches = subject && one === "field" && schemaCode ?
|
|
229
|
+
const scopedMatches = ref.matches.filter((row) => !applicationCode || row.applicationCode === applicationCode).filter((row) => subject && one === "field" && schemaCode ? row.schemaCode === schemaCode : true);
|
|
200
230
|
const capped = cap(scopedMatches, offset, limit);
|
|
201
231
|
// r35a-F4c(真机 2026-11-06):**MCP 面是第三个消费者**——CLI 的 references
|
|
202
232
|
// 与 impact 都带 coverage 了,这里若不显式透传,MCP 用户看到的仍是
|
|
@@ -207,7 +237,6 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
207
237
|
offset: capped.offset,
|
|
208
238
|
limit: capped.limit,
|
|
209
239
|
...(capped.truncated ? { truncated: true, nextOffset: capped.nextOffset, note: `共 ${capped.total} 条,已给 ${capped.rows.length} 条,offset=${capped.offset} 取下一段(调用时传 offset=${capped.nextOffset}、limit=${capped.limit})` } : {}),
|
|
210
|
-
...(ref.coverage ? { coverage: ref.coverage } : {}),
|
|
211
240
|
...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}),
|
|
212
241
|
};
|
|
213
242
|
if (scopedMatches.length > 0 || kind !== undefined || subject !== undefined) {
|
|
@@ -227,12 +256,11 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
227
256
|
return m;
|
|
228
257
|
}, new Map()).values()],
|
|
229
258
|
} : {}),
|
|
230
|
-
...(imp.coverage ? { coverage: imp.coverage } : {}),
|
|
231
259
|
};
|
|
232
260
|
}
|
|
233
261
|
}
|
|
234
262
|
const decorate = (row) => withMatchedOn({ ...row, ...(row.schemaCode && row.code === undefined ? { code: row.schemaCode } : {}) }, query);
|
|
235
|
-
const decoratedMatches = { ...
|
|
263
|
+
const decoratedMatches = { ...scopedSearch.matches, model: { ...scopedSearch.matches.model, items: scopedSearch.matches.model.items.map(decorate) }, field: { ...scopedSearch.matches.field, items: scopedSearch.matches.field.items.map(decorate) }, service: { ...scopedSearch.matches.service, items: scopedSearch.matches.service.items.map(decorate) } };
|
|
236
264
|
const noEntityHit = search.matches.model.total === 0 && search.matches.field.total === 0 && search.matches.service.total === 0;
|
|
237
265
|
const composite = /\s|\+|,|\b(?:AND|OR)\b/i.test(query);
|
|
238
266
|
const searchNote = "实体检索:业务名/实体 name+code 使用子串匹配。\n规则检索:规则 code+name 使用词首匹配(中文子串不命中)。";
|
|
@@ -242,8 +270,12 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
242
270
|
return toolResult({
|
|
243
271
|
status: "ok",
|
|
244
272
|
query,
|
|
273
|
+
...(queries.length > 1 ? { queries, match: args.match === "all" ? "all" : "any" } : {}),
|
|
274
|
+
hitModels: [...hitModels],
|
|
275
|
+
...(() => { const page = cap([...hitFieldModels].flatMap((schema) => entityInventory().filter((r) => r.kind === "field" && r.schemaCode === schema && (!applicationCode || r.applicationCode === applicationCode)).map((r) => ({ schemaCode: schema, code: r.code, name: r.name }))), offset, limit); return { hitFields: page.rows, hitFieldsCount: page.total, ...(page.truncated ? { hitFieldsTruncated: true, hitFieldsNextOffset: page.nextOffset } : {}) }; })(),
|
|
245
276
|
found: resolvedEntity !== undefined,
|
|
246
|
-
...(resolvedEntity ? { resolved: { kind: resolvedEntity.kind, schemaCode: "schemaCode" in resolvedEntity ? resolvedEntity.schemaCode : "", code: resolvedEntity.code, matchedOn: { on: resolvedEntity.name.includes(query) ? "name" : "code", fragment: query } } } : {}),
|
|
277
|
+
...(resolvedEntity ? { resolved: { kind: resolvedEntity.kind, schemaCode: "schemaCode" in resolvedEntity ? resolvedEntity.schemaCode : "", code: resolvedEntity.code, matchedOn: { on: resolvedEntity.name.includes(query) ? "name" : "code", fragment: query }, ...(resolvedEntity.applicationCode ? { applicationCode: resolvedEntity.applicationCode } : {}) } } : {}),
|
|
278
|
+
...(resolvedEntity ? { otherCandidates: allCandidates.filter((r) => r.code !== resolvedEntity.code).slice(0, 5).map((r) => ({ schemaCode: r.schemaCode ?? r.code, name: r.name, ...(r.applicationCode ? { applicationCode: r.applicationCode } : {}) })) } : {}),
|
|
247
279
|
...(ambiguous ? { ambiguous: true, candidates: exactSame.map((r) => ({ schemaCode: r.schemaCode, code: r.code, name: r.name })) } : {}),
|
|
248
280
|
...(kind !== undefined ? { kind } : {}),
|
|
249
281
|
index,
|
|
@@ -253,11 +285,12 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
253
285
|
...(nextCall ? { nextCall } : {}),
|
|
254
286
|
...(noEntityHit && composite ? { unsupportedQuerySyntax: true, hint: "组合检索暂不支持,请拆词单查后求交" } : {}),
|
|
255
287
|
...(noEntityHit ? { suggestions: suggestions(query), hint: "可尝试 suggestions 中的实体 code/name;引用明细请使用匹配 code 查 references/impact" } : {}),
|
|
256
|
-
searchNote: `${searchNote}${paginationNote}`,
|
|
288
|
+
searchNote: `${searchNote}${paginationNote}${queries.length > 1 ? " 规则面(词首匹配)不参与 queries[] 的实体求交。" : ""}`,
|
|
257
289
|
...(search.note ? { searchNote: `${searchNote}\n${search.note}` } : {}),
|
|
258
290
|
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
|
259
291
|
// 探针据此差点误删被表单引用的字段)。改为指向结构化 coverage,不再靠散文。
|
|
260
|
-
|
|
292
|
+
coverage: { ...index.coverage, scanned: ["businessrule", "datarule", "listview"], notScanned: ["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"] },
|
|
293
|
+
note: `零命中只代表 references[].coverage.scanned 面没有命中;字段面覆盖应用:${index.coverage.fields?.coveredApplications.join("、") || "(未知)"};notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响`,
|
|
261
294
|
});
|
|
262
295
|
}
|
|
263
296
|
return toolError(`未知工具:${name}`);
|
package/dist/model-purge.js
CHANGED
|
@@ -324,6 +324,7 @@ export async function cmdModelPurge(args) {
|
|
|
324
324
|
const result = {
|
|
325
325
|
status,
|
|
326
326
|
schemaCode,
|
|
327
|
+
verdict: plan.modelExists && !failed ? "cascading" : "clean",
|
|
327
328
|
...(plan.workflowCode !== undefined ? { workflowCode: plan.workflowCode } : {}),
|
|
328
329
|
deletedInstances: steps.filter((step) => step.step.startsWith("workflow.delete_instance:") && step.status === "ok").length,
|
|
329
330
|
...(unremovableInstances.length > 0 ? { unremovableInstances } : {}),
|
package/dist/residue.js
CHANGED
|
@@ -7,6 +7,15 @@ import { SessionManager } from "./session-manager.js";
|
|
|
7
7
|
import { makeToolContext } from "./context.js";
|
|
8
8
|
import { emitInvalid, emitResult } from "./command-transport.js";
|
|
9
9
|
const MAX_ITEMS = 20;
|
|
10
|
+
const SYSTEM_RULE_NAMES = new Set(["Available", "Cancel", "Create", "Delete", "GetList", "Load", "Update"]);
|
|
11
|
+
function residueOrigin(row, faceName) {
|
|
12
|
+
if (row.defaultRule === true || row.preset === true || row.system === true || row.systemPreset === true)
|
|
13
|
+
return "system-preset";
|
|
14
|
+
if (faceName === "rule" && SYSTEM_RULE_NAMES.has(nameOf(row, codeOf(row))))
|
|
15
|
+
return "system-preset";
|
|
16
|
+
// Do not guess for datarules or otherwise unmarked rows.
|
|
17
|
+
return row.origin === "system-preset" || row.origin === "user" ? row.origin : undefined;
|
|
18
|
+
}
|
|
10
19
|
function face(faceName, rows, extra = {}) {
|
|
11
20
|
return { face: faceName, ...extra, count: rows.length, items: rows.slice(0, MAX_ITEMS) };
|
|
12
21
|
}
|
|
@@ -64,8 +73,8 @@ async function deepFaces(session, model) {
|
|
|
64
73
|
// A missing form is an absent online-js face, not a scan failure.
|
|
65
74
|
}
|
|
66
75
|
return [
|
|
67
|
-
face("rule", rules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
68
|
-
face("datarule", datarules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
76
|
+
face("rule", rules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)), ...(residueOrigin(row, "rule") ? { origin: residueOrigin(row, "rule") } : {}) })), { model: schemaCode }),
|
|
77
|
+
face("datarule", datarules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)), ...(residueOrigin(row, "datarule") ? { origin: residueOrigin(row, "datarule") } : {}) })), { model: schemaCode }),
|
|
69
78
|
face("onlinejs", online, { model: schemaCode }),
|
|
70
79
|
];
|
|
71
80
|
}
|
|
@@ -92,13 +101,21 @@ export async function scanResidues(session, prefix, options = {}) {
|
|
|
92
101
|
for (const model of models)
|
|
93
102
|
faces.push(...await deepFaces(session, model));
|
|
94
103
|
}
|
|
104
|
+
const systemPresetCount = faces.reduce((sum, item) => sum + item.items.filter((row) => row.origin === "system-preset").length, 0);
|
|
105
|
+
const userResidues = faces.reduce((sum, item) => sum + item.items.filter((row) => row.origin !== "system-preset").length, 0);
|
|
95
106
|
const totalResidues = faces.reduce((sum, item) => sum + item.count, 0);
|
|
107
|
+
const modelFace = faces.filter((item) => item.face === "model");
|
|
108
|
+
const hasModelResidue = modelFace.some((item) => item.count > 0);
|
|
109
|
+
const hasOtherResidue = faces.some((item) => item.face !== "model" && item.count > 0);
|
|
110
|
+
const pendingCascade = !hasModelResidue && hasOtherResidue;
|
|
96
111
|
return {
|
|
97
112
|
prefix,
|
|
98
113
|
faces,
|
|
99
114
|
totalResidues,
|
|
100
|
-
clean:
|
|
101
|
-
|
|
115
|
+
clean: userResidues === 0 && !pendingCascade,
|
|
116
|
+
...(systemPresetCount > 0 ? { systemPresetCount } : {}),
|
|
117
|
+
...(pendingCascade ? { pendingCascade: true } : {}),
|
|
118
|
+
note: pendingCascade ? "模型已不存在但仍有级联面命中:级联窗口内,等 40-60s 重跑,幂等" : "report 面暂停维护未扫描;workflow 模板跟模型走(模型删即删)",
|
|
102
119
|
};
|
|
103
120
|
}
|
|
104
121
|
export async function cmdResidue(prefix, options, transport) {
|
package/dist/rule-when.js
CHANGED
|
@@ -34,10 +34,65 @@ function parseWhen(raw) {
|
|
|
34
34
|
const value = /^-?\d+(\.\d+)?$/.test(rawValue.trim()) ? Number(rawValue.trim()) : rawValue.trim();
|
|
35
35
|
return { field, op, value };
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* 人读层只投影图中真正存在的节点;它不是另一套编译结果。
|
|
39
|
+
* `matchBy` 明确区分执行门(dataCondition)与选行(filterCondition),
|
|
40
|
+
* 避免 dry-run 只给 sha256 时把两者误认为同一种条件。
|
|
41
|
+
*/
|
|
42
|
+
export function compiledRuleSummary(nodeGraph) {
|
|
43
|
+
const nodes = Array.isArray(nodeGraph.nodes) ? nodeGraph.nodes : [];
|
|
44
|
+
return nodes
|
|
45
|
+
.filter((node) => !!node && typeof node === "object" && nodeTypeOf(node) !== undefined && nodeTypeOf(node) !== "START" && nodeTypeOf(node) !== "END")
|
|
46
|
+
.map((node) => {
|
|
47
|
+
const dataCondition = node.dataCondition;
|
|
48
|
+
const filterCondition = node.filterCondition;
|
|
49
|
+
const rawActions = Array.isArray(node.dataActions) ? node.dataActions : Array.isArray(node.actions) ? node.actions : [];
|
|
50
|
+
const currentRowFilter = filterCondition !== undefined && conditionText(filterCondition) === "id=id";
|
|
51
|
+
const matchBy = dataCondition !== undefined
|
|
52
|
+
? `dataCondition: ${conditionText(dataCondition)}`
|
|
53
|
+
: filterCondition !== undefined && !currentRowFilter
|
|
54
|
+
? `filterCondition: ${conditionText(filterCondition)}`
|
|
55
|
+
: "always";
|
|
56
|
+
const actions = rawActions
|
|
57
|
+
.filter((action) => !!action && typeof action === "object")
|
|
58
|
+
.map((action) => {
|
|
59
|
+
const field = action.targetDataItem ?? action.field ?? action.targetField;
|
|
60
|
+
const value = action.value ?? action.currentDataItemValue;
|
|
61
|
+
return {
|
|
62
|
+
...(field !== undefined ? { field } : {}),
|
|
63
|
+
...(value !== undefined ? { value } : {}),
|
|
64
|
+
...(action.ruleValueType !== undefined ? { valueType: action.ruleValueType } : {}),
|
|
65
|
+
...(action.ruleActionType !== undefined ? { actionType: action.ruleActionType } : {}),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return { nodeType: nodeTypeOf(node), matchBy, actions };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function nodeTypeOf(node) {
|
|
72
|
+
return typeof node.nodeType === "string" ? node.nodeType : undefined;
|
|
73
|
+
}
|
|
74
|
+
function conditionText(condition) {
|
|
75
|
+
if (!condition || typeof condition !== "object")
|
|
76
|
+
return "always";
|
|
77
|
+
const value = condition;
|
|
78
|
+
const groups = Array.isArray(value.conditionGroup) ? value.conditionGroup : Array.isArray(value.mainConditions) ? value.mainConditions : [];
|
|
79
|
+
const leaves = Array.isArray(groups[0]) ? groups[0] : groups;
|
|
80
|
+
const text = leaves
|
|
81
|
+
.filter((leaf) => !!leaf && typeof leaf === "object")
|
|
82
|
+
.map((leaf) => {
|
|
83
|
+
const field = leaf.currentSchemaDataItem ?? leaf.targetSchemaDataItem ?? "?";
|
|
84
|
+
const op = leaf.ruleDataConditionType ?? leaf.ruleConditionType ?? "?";
|
|
85
|
+
const operators = { EQ: "=", NEQ: "!=", GT: ">", LT: "<", GTEQ: ">=", LTEQ: "<=", CT: "~=", NCT: "!~" };
|
|
86
|
+
const right = leaf.value
|
|
87
|
+
?? (leaf.currentDataItemValue !== undefined ? `{${leaf.currentDataItemValue}}` : undefined)
|
|
88
|
+
?? (leaf.targetSchemaDataItem !== undefined ? leaf.currentSchemaDataItem : "?");
|
|
89
|
+
return `${String(field)}${operators[String(op)] ?? String(op)}${String(right)}`;
|
|
90
|
+
})
|
|
91
|
+
.join(" AND ");
|
|
92
|
+
return text || "always";
|
|
93
|
+
}
|
|
37
94
|
/** 编译 --when/--then DSL 为设计器验证形状的规则图(导出供测试断言)。 */
|
|
38
95
|
export function composeRuleWhenGraph(options) {
|
|
39
|
-
if (options.when.length === 0)
|
|
40
|
-
throw new Error("至少给一个 --when 条件;无条件更新整表是 bench23 病灶(空 mainConditions=无条件更新全部行)");
|
|
41
96
|
if (options.then.length === 0)
|
|
42
97
|
throw new Error("至少给一个 --then 动作");
|
|
43
98
|
const leaves = [];
|
|
@@ -81,7 +136,7 @@ export function composeRuleWhenGraph(options) {
|
|
|
81
136
|
// UPDATE.filterCondition selects rows; it is not an execution gate.
|
|
82
137
|
// Keep the trigger row as the default target and put --when on the
|
|
83
138
|
// platform's real execution-gate field (dataCondition).
|
|
84
|
-
dataCondition: { conditionGroup: [leaves] },
|
|
139
|
+
...(leaves.length > 0 ? { dataCondition: { conditionGroup: [leaves] } } : {}),
|
|
85
140
|
filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] },
|
|
86
141
|
dataActions: actions },
|
|
87
142
|
{ nodeCode: "End", nodeType: "END" },
|
|
@@ -126,7 +181,7 @@ export async function cmdRuleWhen(args) {
|
|
|
126
181
|
const name = flag("--name");
|
|
127
182
|
const environment = flag("--env") ?? flag("--environment");
|
|
128
183
|
const apply = args.includes("--apply");
|
|
129
|
-
const usage = '用法:omc rule-when --schema <模型> --on create|update --when "字段=值"
|
|
184
|
+
const usage = '用法:omc rule-when --schema <模型> --on create|update [--when "字段=值" ...] --then "set:字段=值|{来源字段}|{公式}" [--then ...] [--apply]\n触发时机只有 create|update(**没有 submit**;“提交时”用 create 或 update)。条件操作符:= != > < >= <= ~=(包含) !~(不包含)。省略 --when 表示 always(仍按当前 _input.id 定位,不更新整表)。默认 dry-run;--apply 保存并发布(rule.enable 仍需显式执行)。';
|
|
130
185
|
const invalid = (message) => {
|
|
131
186
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
132
187
|
return CLI_EXIT_CODES.invalidInput;
|
|
@@ -160,8 +215,21 @@ export async function cmdRuleWhen(args) {
|
|
|
160
215
|
const scopedResult = {
|
|
161
216
|
...result,
|
|
162
217
|
rowScope,
|
|
218
|
+
compiled: compiledRuleSummary(result && typeof result.nodeGraph === "object" && result.nodeGraph !== null
|
|
219
|
+
? result.nodeGraph
|
|
220
|
+
: createArgs.nodeGraph),
|
|
221
|
+
detailReadHint: {
|
|
222
|
+
detailFile: "cat <detailFile>",
|
|
223
|
+
detailArtifactId: "omc artifact get <detailArtifactId>",
|
|
224
|
+
},
|
|
163
225
|
};
|
|
164
226
|
const classified = classifyResult(result);
|
|
227
|
+
if (apply && classified.status === "ok") {
|
|
228
|
+
scopedResult.effectiveAfter = {
|
|
229
|
+
note: "规则保存后生效存在延迟;改数据后以读回的 changedFields 为准,不要以第一次读回判未生效",
|
|
230
|
+
verifyHint: "同一条 data.update 后 data.load 复核",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
165
233
|
writeEnvelope(envelope(classified.status, scopedResult, request, classified.error));
|
|
166
234
|
if (apply && classified.status === "ok")
|
|
167
235
|
process.stderr.write("[omc] 前台生效验证清单(后端通过≠门户生效):1) omc call rule.enable 启用;2) 造一条命中 --when 的测试数据提交;3) 门户打开该记录核对 --then 字段已变更;4) 再造一条不命中的数据确认不误改;5) 设计器打开本规则确认节点/条件显示正常。\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
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.23",
|
|
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.23"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/skills/omc-start-here.md
CHANGED
|
@@ -29,10 +29,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
29
29
|
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB,读一次就留在后续每一轮窗口)。**带着具体问题(错误码/跨能力语义/某个词)直接 `omc help --search "<词>"`**——跨全部 topic 全文检索,命中节的正文随结果内联返回,一次调用就是答案;**严禁自己写 `for t in …; do omc help $t --all | grep …` 循环**(r18 实测 12 调用/176KB,检索一次 8KB 搞定)。
|
|
30
30
|
1b. **改/删/重命名既有对象前先看影响面(本地规则图索引)**:`omc graph pull --env <env>` 一次拉全平台建本地索引(增量、秒级、带锁与 gap 报告),之后 `omc graph references <service|model|field> <code>` 离线答「谁引用了它」——一调替代逐模型 `rule.get` 枚举(r30 实测影响评估手工枚举 8×rule.get,graph 一调可达);`impact <kind> <code>` 看爆炸半径、`search <词>` 找规则 code、`status` 看索引新鲜度。索引不含规则条件叶子与流程绑定,命中后仍以 `rule.get` 原文为准。无索引时会拒绝并指路 pull。宿主已接 MCP 时用 `omc mcp-graph --path <项目根>`(工具 `omc_graph_explore`/`omc_graph_status`)。
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
`omc call app.list` 列出应用,再用
|
|
34
|
-
`omc call model.list --args '{"applicationCode":"<code>"}'` 按应用盘点模型。
|
|
35
|
-
graph 索引 coverage 默认只含配置应用;coverage 未命中不等于应用或模型不存在。
|
|
32
|
+
**图谱多应用覆盖时怎么盘点(只读)**:优先用 `omc graph pull --all-applications --env <env>` 建全租户索引;也可用 `--application-codes <a,b,c>` 指定应用,再用 `omc graph status` 看 coveredApplications/tenantApplications。
|
|
36
33
|
1c. **写后端 Java 源码(在线开发)走工作区而不是一条条 call**:`omc programming pull --env <env> --app <appCode>` 把业务规则源码拉进 `.omc-programming/`(git 可管),改文件后 `omc programming status` → `omc programming apply [--publish]`(逐文件走编程面治理);图形化规则转编码用 `omc programming adopt --schema <s> --rule <r>`;验证用 `omc programming test --schema <s> --rule <r>`(fixture 驱动:compile→publish→触发→断言→清理)。**平台运行时只执行已发布版**:只 save 不 publish 等于没改;publish 内部会编译并把规则切 CODING+enable。包路径/类名不得改。
|
|
37
34
|
2. 写前只需读 `omc-config-draft-review`(人审门)与 `omc-five-piece-flow`(发现→草稿→权限→执行→回读);`omc-business-configuration` 按命中的能力分节读(见 `next` 的 `readSections`),不要整篇预取。
|
|
38
35
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|