@stalfh233/omc-cli 0.6.20 → 0.6.22
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 +72 -11
- package/dist/command-transport.js +9 -0
- package/dist/graph.js +104 -29
- 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 +37 -7
- package/dist/lanes/runtime-data-contract.js +1 -1
- package/dist/lanes/runtime-data.js +34 -17
- package/dist/lanes/workflow.js +10 -0
- package/dist/mcp-graph.js +46 -12
- 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-capabilities.md +1 -0
- package/skills/omc-config-draft-review.md +2 -3
- package/skills/omc-glossary.md +2 -0
- package/skills/omc-playbook.md +5 -3
- package/skills/omc-start-here.md +3 -6
- package/skills/omc-write-hazards.md +9 -2
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import { formatDoctorHumanSummary, unsupportedCapabilities } from "./version-fam
|
|
|
23
23
|
import { beginWriteFace, configureOutput, endWriteFace, humanSummary, maybeWarnMergedStreams, stripOutputFlags, writeEnvelope } from "./cli-output.js";
|
|
24
24
|
import { beginUsage, completeUsage, currentUsageArgsDigest, ensureProjectIgnore, priorIdenticalSuccess, recordUsageOutput, usageReport } from "./usage-log.js";
|
|
25
25
|
import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitThrown, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
|
|
26
|
-
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
|
|
26
|
+
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, createArtifact, pruneArtifacts } from "./artifacts.js";
|
|
27
27
|
import { artifactPathMissHint, nullProjectionHint, partialPathMissHint, projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
|
|
28
28
|
import { basename, join, resolve } from "node:path";
|
|
29
29
|
import { buildToolRegistry } from "./tools.js";
|
|
@@ -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";
|
|
@@ -802,6 +802,24 @@ async function cmdCall(store, args) {
|
|
|
802
802
|
// Do not also copy the single-path value to top-level content.
|
|
803
803
|
if (extractPaths.length > 0 && !extractKeys && typeof result === "object" && result !== null)
|
|
804
804
|
delete result.content;
|
|
805
|
+
// r40a: unsupported jq language is an input error, never an `ok:true`
|
|
806
|
+
// projection with a null value. Keep the path engine's detailed hint but
|
|
807
|
+
// make the CLI contract actionable and machine-classifiable.
|
|
808
|
+
if (typeof result === "object" && result !== null && Array.isArray(result.extract)) {
|
|
809
|
+
for (const entry of result.extract)
|
|
810
|
+
delete entry.autoStripped;
|
|
811
|
+
const unsupported = result.extract.filter((entry) => entry.unsupportedSyntax === true);
|
|
812
|
+
if (unsupported.length > 0) {
|
|
813
|
+
const first = unsupported[0];
|
|
814
|
+
const message = `--jq 只接受路径选择器,不支持对象构造或管道语法(${String(first?.path ?? "")})。可运行示例:--jq 'fields[].code,fields[].typeName'`;
|
|
815
|
+
const invalid = { status: "error", blocker: "tool-input-invalid", unsupportedSyntax: true, paths: unsupported.map((entry) => entry.path), message, nextCommand: "omc call <tool> --args '<json>' --jq 'fields[].code,fields[].typeName'" };
|
|
816
|
+
return emit("error", invalid, CLI_EXIT_CODES.invalidInput);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// `extract[]` is the canonical multi-path result. Do not emit the second
|
|
820
|
+
// row-alignment copy: callers can align the explicitly named path values.
|
|
821
|
+
if (typeof result === "object" && result !== null && extractPaths.length > 1)
|
|
822
|
+
delete result.rows;
|
|
805
823
|
}
|
|
806
824
|
// 读面未知键的非阻断回显:放在投影**之后**,否则会被 --jq 的窄投影吞掉。
|
|
807
825
|
if (ignoredReadKeys.length > 0 && typeof result === "object" && result !== null) {
|
|
@@ -1586,14 +1604,20 @@ async function cmdIntentReadback(file, environment, transport) {
|
|
|
1586
1604
|
* registry or `commands --json`. Results are candidate pointers only — plan /
|
|
1587
1605
|
* apply / readback keep reading the live baseline.
|
|
1588
1606
|
*/
|
|
1589
|
-
async function cmdGraphPull(environment, concurrency, transport) {
|
|
1607
|
+
async function cmdGraphPull(environment, concurrency, transport, path, applicationCodes, allApplications = false) {
|
|
1590
1608
|
const workers = concurrency ?? DEFAULT_PULL_CONCURRENCY;
|
|
1591
1609
|
if (!Number.isSafeInteger(workers) || workers < 1 || workers > MAX_PULL_CONCURRENCY)
|
|
1592
1610
|
return emitInvalid(transport, { message: `--concurrency 必须是 1–${MAX_PULL_CONCURRENCY} 的整数(平台负载有界;默认 ${DEFAULT_PULL_CONCURRENCY})` });
|
|
1593
1611
|
const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
1594
1612
|
try {
|
|
1595
1613
|
const session = await context.sessions.session(environment);
|
|
1596
|
-
|
|
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 } : {}) });
|
|
1597
1621
|
// issue#9 C4:扫到 0 模型仍回 ok,让调用者误以为索引就绪(随后 search 报
|
|
1598
1622
|
// graph-index-absent 才发现)。0 模型 fail-fast,点名 env/applicationCode
|
|
1599
1623
|
// 与解析来源,把「索引没建成」和「建成了但为空」分开。
|
|
@@ -1626,13 +1650,15 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1626
1650
|
identityFingerprint: data.meta.identityFingerprint,
|
|
1627
1651
|
pulledAtIso: data.meta.pulledAtIso,
|
|
1628
1652
|
scope: data.meta.scope,
|
|
1629
|
-
coverage: { applicationCodes:
|
|
1653
|
+
coverage: { applicationCodes: data.meta.coverage.applicationCodes, ...(data.meta.coverage.tenantApplications ? { tenantApplications: data.meta.coverage.tenantApplications } : {}) },
|
|
1630
1654
|
freshness: data.gaps.length === 0 ? "complete" : "partial",
|
|
1631
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 } : {}) },
|
|
1632
1656
|
edges: edgesByKind,
|
|
1633
1657
|
...(data.gaps.length > 0 ? { gaps: { count: data.gaps.length, byKind: gapsByKind } } : {}),
|
|
1634
1658
|
dbBytes: data.dbBytes,
|
|
1635
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>`,
|
|
1636
1662
|
nextCommand: "omc graph references <kind> <code>",
|
|
1637
1663
|
}, transport);
|
|
1638
1664
|
}
|
|
@@ -1670,6 +1696,32 @@ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
|
1670
1696
|
return emitThrown(error, transport);
|
|
1671
1697
|
}
|
|
1672
1698
|
}
|
|
1699
|
+
function cmdGraphReferencesBatch(kind, codeList, transport) {
|
|
1700
|
+
if (!["service", "model", "field"].includes(kind))
|
|
1701
|
+
return emitInvalid(transport, { message: "kind 必须是 service / model / field" });
|
|
1702
|
+
try {
|
|
1703
|
+
const result = queryGraphReferencesBatch(kind, codeList.split(",").map((code) => code.trim()));
|
|
1704
|
+
return emitResult({ status: "ok", ...result }, transport);
|
|
1705
|
+
}
|
|
1706
|
+
catch (error) {
|
|
1707
|
+
return emitThrown(error, transport);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function cmdGraphEdges(fromSchema, toSchema, requestedEnvironment, transport) {
|
|
1711
|
+
try {
|
|
1712
|
+
const result = queryGraphEdges(fromSchema, toSchema);
|
|
1713
|
+
if (requestedEnvironment !== undefined && requestedEnvironment !== result.index.environmentName)
|
|
1714
|
+
return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${result.index.environmentName},不是 ${requestedEnvironment};请先 omc graph pull --env ${requestedEnvironment}`, index: result.index }, transport);
|
|
1715
|
+
if (result.total > 200) {
|
|
1716
|
+
const artifact = createArtifact(artifactRoot(), JSON.stringify(result.edges, null, 2));
|
|
1717
|
+
return emitResult({ status: "ok", total: result.total, index: result.index, artifactId: artifact.artifactId, sha256: artifact.sha256, edges: { count: result.edges.length, truncated: true } }, transport);
|
|
1718
|
+
}
|
|
1719
|
+
return emitResult({ status: "ok", ...result }, transport);
|
|
1720
|
+
}
|
|
1721
|
+
catch (error) {
|
|
1722
|
+
return emitThrown(error, transport);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1673
1725
|
function cmdGraphStatus(transport) {
|
|
1674
1726
|
try {
|
|
1675
1727
|
const status = readGraphStatus();
|
|
@@ -2470,7 +2522,7 @@ async function main() {
|
|
|
2470
2522
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2471
2523
|
});
|
|
2472
2524
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
2473
|
-
command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true
|
|
2525
|
+
command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true 即零残留。清理入口:omc model-purge --code <schemaCode>(按依赖倒序,默认 dry-run)。只读零写入;report 面暂停维护未扫描").option("--env <environment>", "环境名(默认按 cwd 的 .omc-env 解析)").option("--apps <codes>", "all(默认,全租户)或逗号分隔应用码").option("--no-deep", "只扫 model/folder/custompage,不深查命中模型").option("--max-output-bytes <bytes>").action(async (prefix, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdResidue(prefix, { env: options.env, apps: options.apps, ...(options.deep === false ? { noDeep: true } : {}) }, transport)); });
|
|
2474
2526
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2475
2527
|
command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) })); });
|
|
2476
2528
|
command.command("describe <tool>").option("--full").option("--cache-ok", "允许 --full 在同会话重复读时短路为 artifact 指针(默认每次回全文)").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (tool, options) => {
|
|
@@ -2535,13 +2587,22 @@ async function main() {
|
|
|
2535
2587
|
});
|
|
2536
2588
|
// ADR-0008: local derived rule-graph index — CLI management face, not a call
|
|
2537
2589
|
// tool (so it stays out of `commands --json`/coverage by construction).
|
|
2538
|
-
const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008
|
|
2539
|
-
graphCommand.command("pull").option("--env <environment>").option("--environment <environment>").option("--
|
|
2590
|
+
const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008)");
|
|
2591
|
+
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) => {
|
|
2540
2592
|
const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
|
|
2541
|
-
|
|
2593
|
+
if (options.path)
|
|
2594
|
+
process.env.OMC_GRAPH_PATH = options.path;
|
|
2595
|
+
const codes = options.applicationCodes ? String(options.applicationCodes).split(",").map((code) => code.trim()).filter(Boolean) : undefined;
|
|
2596
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport, options.path, codes, options.allApplications === true));
|
|
2597
|
+
});
|
|
2598
|
+
graphCommand.command("references [kind] [code]").option("--batch <codes>").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references 第一版边界;支持 --batch code1,code2 共享同一 index 块。\n").action(async (kind, code, options) => {
|
|
2599
|
+
commandExitCode = await withTransport(options, request, (transport) => options.batch ? cmdGraphReferencesBatch(kind ?? "model", options.batch, transport) : cmdGraphReferences(kind ?? "", code ?? "", transport, options.environment ?? options.env));
|
|
2600
|
+
});
|
|
2601
|
+
graphCommand.command("references-batch <kind> <code>").option("--max-output-bytes <bytes>").action(async (kind, code, options) => {
|
|
2602
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferencesBatch(kind, code, transport));
|
|
2542
2603
|
});
|
|
2543
|
-
graphCommand.command("
|
|
2544
|
-
commandExitCode = await withTransport(options, request, (transport) =>
|
|
2604
|
+
graphCommand.command("edges").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--from-schema <code>").option("--to-schema <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线全图边清单(零 wire);--env 仅校验索引绑定环境,与 references 一致。\n").action(async (options) => {
|
|
2605
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphEdges(options.fromSchema, options.toSchema, options.environment ?? options.env, transport));
|
|
2545
2606
|
});
|
|
2546
2607
|
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) => {
|
|
2547
2608
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
|
|
@@ -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 = "6";
|
|
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,11 +192,11 @@ 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 = [];
|
|
@@ -206,7 +206,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
206
206
|
for (const row of rows) {
|
|
207
207
|
const code = String(row.code ?? row.propertyCode ?? "");
|
|
208
208
|
if (code)
|
|
209
|
-
fields.push({ schemaCode, code, name: String(row.name ?? row.propertyName ?? code) });
|
|
209
|
+
fields.push({ schemaCode, code, name: String(row.name ?? row.propertyName ?? code), applicationCode });
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
catch (error) {
|
|
@@ -302,7 +302,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
302
302
|
dataruleRows = wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query: { schemaCode } })).data);
|
|
303
303
|
dataruleCount += dataruleRows.length;
|
|
304
304
|
for (const row of dataruleRows)
|
|
305
|
-
edges.push(...extractDataRuleEdges(schemaCode, row));
|
|
305
|
+
edges.push(...extractDataRuleEdges(schemaCode, row).map((edge) => ({ ...edge, applicationCode })));
|
|
306
306
|
}
|
|
307
307
|
catch (error) {
|
|
308
308
|
gaps.push({ kind: "datarule-list", ref: schemaCode, note: error instanceof Error ? error.message : String(error) });
|
|
@@ -361,7 +361,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
361
361
|
rowId: ruleId,
|
|
362
362
|
graphSha256: sha256({ nodes, routes: decodeJsonColumn(record.route, JSON_COLUMN_NULL_COALESCE) }),
|
|
363
363
|
});
|
|
364
|
-
edges.push(...extractReferenceEdges(schemaCode, ruleCode, nodes));
|
|
364
|
+
edges.push(...extractReferenceEdges(schemaCode, ruleCode, nodes).map((edge) => ({ ...edge, applicationCode })));
|
|
365
365
|
}
|
|
366
366
|
catch (error) {
|
|
367
367
|
gaps.push({ kind: "rule-get", ref: `${schemaCode}/${ruleCode}`, note: error instanceof Error ? error.message : String(error) });
|
|
@@ -378,7 +378,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
viewCount += 1;
|
|
381
|
-
edges.push(...extractListViewEdges(schemaCode, viewCode, got));
|
|
381
|
+
edges.push(...extractListViewEdges(schemaCode, viewCode, got).map((edge) => ({ ...edge, applicationCode })));
|
|
382
382
|
}
|
|
383
383
|
catch (error) {
|
|
384
384
|
gaps.push({ kind: "listview-get", ref: `${schemaCode}/${viewCode}`, note: error instanceof Error ? error.message : String(error) });
|
|
@@ -496,10 +496,24 @@ async function withGraphPullLock(run) {
|
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
/** Wholesalerebuild + atomic publish (staging file + rename), 0600. */
|
|
499
|
-
export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCURRENCY) {
|
|
499
|
+
export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCURRENCY, options = {}) {
|
|
500
500
|
try {
|
|
501
501
|
return await withGraphPullLock(async () => {
|
|
502
|
-
const
|
|
502
|
+
const requested = options.applicationCodes?.length ? options.applicationCodes : [undefined];
|
|
503
|
+
const parts = await Promise.all(requested.map((application) => pullRuleGraph(session, concurrency, application)));
|
|
504
|
+
const first = parts[0];
|
|
505
|
+
const data = parts.length === 1 ? first : {
|
|
506
|
+
...first,
|
|
507
|
+
meta: { ...first.meta, applicationCode: "*", coverage: { applicationCodes: parts.flatMap((part) => part.meta.coverage.applicationCodes), tenantApplications: parts.flatMap((part) => part.meta.coverage.applicationCodes) } },
|
|
508
|
+
rules: parts.flatMap((part) => part.rules),
|
|
509
|
+
edges: parts.flatMap((part) => part.edges),
|
|
510
|
+
models: parts.flatMap((part) => part.models),
|
|
511
|
+
fields: parts.flatMap((part) => part.fields),
|
|
512
|
+
services: parts.flatMap((part) => part.services),
|
|
513
|
+
gaps: parts.flatMap((part) => part.gaps),
|
|
514
|
+
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 }),
|
|
515
|
+
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) },
|
|
516
|
+
};
|
|
503
517
|
const staging = `${graphDbPath()}.staging-${process.pid}`;
|
|
504
518
|
rmSync(staging, { force: true });
|
|
505
519
|
const DatabaseSyncCtor = nodeSqlite().DatabaseSync;
|
|
@@ -531,13 +545,14 @@ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCU
|
|
|
531
545
|
rule_code TEXT NOT NULL,
|
|
532
546
|
node_code TEXT,
|
|
533
547
|
node_type TEXT,
|
|
534
|
-
|
|
548
|
+
via TEXT NOT NULL,
|
|
549
|
+
application_code TEXT
|
|
535
550
|
);
|
|
536
551
|
CREATE INDEX edges_lookup ON edges(kind, code);
|
|
537
552
|
CREATE TABLE gaps (kind TEXT NOT NULL, ref TEXT NOT NULL, note TEXT);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
553
|
+
CREATE TABLE models (schema_code TEXT PRIMARY KEY, name TEXT NOT NULL, application_code TEXT);
|
|
554
|
+
CREATE TABLE fields (schema_code TEXT NOT NULL, code TEXT NOT NULL, name TEXT NOT NULL, application_code TEXT);
|
|
555
|
+
CREATE TABLE services (code TEXT PRIMARY KEY, name TEXT NOT NULL);
|
|
541
556
|
`);
|
|
542
557
|
const insertMeta = db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)");
|
|
543
558
|
// `applicationResolution` is an object; String(value) would persist
|
|
@@ -548,19 +563,19 @@ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCU
|
|
|
548
563
|
const insertRule = db.prepare("INSERT INTO rules (schema_code, rule_code, name, enabled, status, default_rule, row_id, graph_sha256) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
549
564
|
for (const rule of data.rules)
|
|
550
565
|
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 (?, ?, ?, ?, ?, ?, ?)");
|
|
566
|
+
const insertEdge = db.prepare("INSERT INTO edges (kind, code, schema_code, rule_code, node_code, node_type, via, application_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
552
567
|
for (const edge of data.edges)
|
|
553
|
-
insertEdge.run(edge.kind, edge.code, edge.schemaCode, edge.ruleCode, edge.nodeCode, edge.nodeType, edge.via);
|
|
568
|
+
insertEdge.run(edge.kind, edge.code, edge.schemaCode, edge.ruleCode, edge.nodeCode, edge.nodeType, edge.via, edge.applicationCode ?? null);
|
|
554
569
|
const insertGap = db.prepare("INSERT INTO gaps (kind, ref, note) VALUES (?, ?, ?)");
|
|
555
570
|
for (const gap of data.gaps)
|
|
556
571
|
insertGap.run(gap.kind, gap.ref, gap.note ?? null);
|
|
557
|
-
const insertModel = db.prepare("INSERT INTO models (schema_code, name) VALUES (?, ?)");
|
|
572
|
+
const insertModel = db.prepare("INSERT INTO models (schema_code, name, application_code) VALUES (?, ?, ?)");
|
|
558
573
|
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 (?, ?, ?)");
|
|
574
|
+
insertModel.run(row.schemaCode, row.name, row.applicationCode ?? null);
|
|
575
|
+
const insertField = db.prepare("INSERT INTO fields (schema_code, code, name, application_code) VALUES (?, ?, ?, ?)");
|
|
561
576
|
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 (?, ?)");
|
|
577
|
+
insertField.run(row.schemaCode, row.code, row.name, row.applicationCode ?? null);
|
|
578
|
+
const insertService = db.prepare("INSERT OR IGNORE INTO services (code, name) VALUES (?, ?)");
|
|
564
579
|
for (const row of data.services)
|
|
565
580
|
insertService.run(row.code, row.name);
|
|
566
581
|
// FTS5 mirrors rules (code+name) for `graph search`; one rebuild after
|
|
@@ -598,7 +613,7 @@ function openGraphIndex() {
|
|
|
598
613
|
for (const row of db.prepare("SELECT key, value FROM meta").all())
|
|
599
614
|
meta[row.key] = row.value;
|
|
600
615
|
if (meta.schemaVersion !== GRAPH_SCHEMA_VERSION)
|
|
601
|
-
throw new GraphIndexAbsentError(`索引 schema 版本不匹配(${meta.schemaVersion} ≠ ${GRAPH_SCHEMA_VERSION}
|
|
616
|
+
throw new GraphIndexAbsentError(`索引 schema 版本不匹配(${meta.schemaVersion ?? "unknown"} ≠ ${GRAPH_SCHEMA_VERSION});旧版索引已作废,请重跑 omc graph pull(schema 升版需重拉)`);
|
|
602
617
|
const ruleCount = db.prepare("SELECT COUNT(*) AS n FROM rules").get().n;
|
|
603
618
|
const modelCount = db.prepare("SELECT COUNT(DISTINCT schema_code) AS n FROM rules").get().n;
|
|
604
619
|
const entityCounts = {
|
|
@@ -620,7 +635,21 @@ function openGraphIndex() {
|
|
|
620
635
|
const pulledAtIso = meta.pulledAtIso ?? "";
|
|
621
636
|
const ageDays = indexAgeDays(pulledAtIso);
|
|
622
637
|
const ageMinutes = indexAgeMinutes(pulledAtIso);
|
|
623
|
-
|
|
638
|
+
let applicationCodes = meta.applicationCode ? [meta.applicationCode] : [];
|
|
639
|
+
try {
|
|
640
|
+
const parsedCoverage = JSON.parse(meta.coverage ?? "null");
|
|
641
|
+
if (Array.isArray(parsedCoverage.applicationCodes))
|
|
642
|
+
applicationCodes = parsedCoverage.applicationCodes.filter((code) => typeof code === "string");
|
|
643
|
+
}
|
|
644
|
+
catch { /* legacy index */ }
|
|
645
|
+
let tenantApplications;
|
|
646
|
+
try {
|
|
647
|
+
const parsedCoverage = JSON.parse(meta.coverage ?? "null");
|
|
648
|
+
if (Array.isArray(parsedCoverage.tenantApplications))
|
|
649
|
+
tenantApplications = parsedCoverage.tenantApplications.filter((code) => typeof code === "string");
|
|
650
|
+
}
|
|
651
|
+
catch { /* legacy index */ }
|
|
652
|
+
const coverage = { applicationCodes, ...(tenantApplications ? { tenantApplications } : {}) };
|
|
624
653
|
return {
|
|
625
654
|
db,
|
|
626
655
|
summary: {
|
|
@@ -663,6 +692,14 @@ function openGraphIndex() {
|
|
|
663
692
|
export const GRAPH_COVERAGE = Object.freeze({
|
|
664
693
|
scanned: Object.freeze(["businessrule", "datarule", "listview"]),
|
|
665
694
|
notScanned: Object.freeze(["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"]),
|
|
695
|
+
answerable: Object.freeze(["businessrule", "datarule", "listview"]),
|
|
696
|
+
mustCheckManually: Object.freeze([
|
|
697
|
+
{ surface: "form", why: "表单控件关联未扫描,可能仍引用目标字段", how: "omc call form.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
698
|
+
{ surface: "workflow.permissions", why: "流程权限未扫描,无法判断谁能发起/操作", how: "omc call workflow.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
699
|
+
{ surface: "workflow.bindings", why: "流程规则绑定未扫描,规则删除可能留下绑定", how: "omc call workflow.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
700
|
+
{ surface: "onlinejs", why: "在线 JS 可能通过字符串引用字段", how: "omc call onlinejs.get --args '{\"schemaCode\":\"<x>\"}'" },
|
|
701
|
+
{ surface: "rule.conditions", why: "规则条件叶子未扫描,命中可能不完整", how: "omc call rule.list --args '{\"schemaCode\":\"<x>\"}' 后人工检查条件公式" },
|
|
702
|
+
]),
|
|
666
703
|
note: "零命中/全零 totals 只代表 scanned 面没有命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响",
|
|
667
704
|
});
|
|
668
705
|
/**
|
|
@@ -692,6 +729,8 @@ export function queryGraphReferences(kind, code) {
|
|
|
692
729
|
nodeCode: row.node_code ?? "",
|
|
693
730
|
nodeType: row.node_type ?? "",
|
|
694
731
|
via: row.via,
|
|
732
|
+
referrer: { schemaCode: row.schema_code, ruleCode: row.rule_code },
|
|
733
|
+
isSelf: row.schema_code === code,
|
|
695
734
|
}));
|
|
696
735
|
const gapped = summary.gaps.count > 0;
|
|
697
736
|
// r35a-F4(真机 2026-11-06):探针拿 matches:[] 差点把一个**被表单引用**的
|
|
@@ -722,6 +761,39 @@ export function queryGraphReferences(kind, code) {
|
|
|
722
761
|
db.close();
|
|
723
762
|
}
|
|
724
763
|
}
|
|
764
|
+
export function queryGraphReferencesBatch(kind, codes) {
|
|
765
|
+
const { db, summary } = openGraphIndex();
|
|
766
|
+
try {
|
|
767
|
+
const wanted = [...new Set(codes.filter(Boolean))];
|
|
768
|
+
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);
|
|
769
|
+
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 };
|
|
770
|
+
}
|
|
771
|
+
finally {
|
|
772
|
+
db.close();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
export function queryGraphEdges(fromSchema, toSchema) {
|
|
776
|
+
const { db, summary } = openGraphIndex();
|
|
777
|
+
try {
|
|
778
|
+
const conditions = [];
|
|
779
|
+
const params = [];
|
|
780
|
+
if (fromSchema) {
|
|
781
|
+
conditions.push("schema_code = ?");
|
|
782
|
+
params.push(fromSchema);
|
|
783
|
+
}
|
|
784
|
+
if (toSchema) {
|
|
785
|
+
conditions.push("code = ?");
|
|
786
|
+
params.push(toSchema);
|
|
787
|
+
}
|
|
788
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
789
|
+
const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM edges${where}`).get(...params).n);
|
|
790
|
+
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`).all(...params);
|
|
791
|
+
return { 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 } : {}) })), index: summary, total };
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
db.close();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
725
797
|
export function readGraphStatus() {
|
|
726
798
|
const { db, summary } = openGraphIndex();
|
|
727
799
|
try {
|
|
@@ -732,10 +804,13 @@ export function readGraphStatus() {
|
|
|
732
804
|
dbBytes: statSync(dbPath).size,
|
|
733
805
|
dbPath,
|
|
734
806
|
dbId: sha256(dbPath).slice(0, 12),
|
|
807
|
+
mcpHint: `MCP 工具按注册时的 --path 读库;要让 MCP 看到本次拉取,请执行 omc graph pull --path <MCP 的配置根> --env <env>。`,
|
|
735
808
|
ageMinutes: summary.ageMinutes,
|
|
736
809
|
staleAfterMinutes: summary.staleAfterMinutes,
|
|
737
810
|
...(summary.staleWarning ? { warning: summary.staleWarning } : {}),
|
|
738
811
|
coverage: summary.coverage,
|
|
812
|
+
coveredApplications: summary.coverage.applicationCodes,
|
|
813
|
+
tenantApplications: summary.coverage.tenantApplications ?? summary.coverage.applicationCodes,
|
|
739
814
|
gaps,
|
|
740
815
|
};
|
|
741
816
|
}
|
|
@@ -794,8 +869,8 @@ export function searchGraphRules(term, limit = 20, offset = 0) {
|
|
|
794
869
|
...(offset + rows.length < total ? { truncated: true, nextOffset: offset + rows.length } : {}),
|
|
795
870
|
};
|
|
796
871
|
};
|
|
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 }));
|
|
872
|
+
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 }));
|
|
873
|
+
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
874
|
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
875
|
}
|
|
801
876
|
const gapped = summary.gaps.count > 0;
|
|
@@ -804,7 +879,7 @@ export function searchGraphRules(term, limit = 20, offset = 0) {
|
|
|
804
879
|
rules: matches,
|
|
805
880
|
matches: entityMatches,
|
|
806
881
|
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;规则体、表单与流程权限未扫描" },
|
|
882
|
+
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
883
|
...(term.trim() !== "" && !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(term.trim()) ? { note: "疑似业务词;本工具同时检索实体 name/code 与规则 code/name,引用明细请使用匹配到的 code 再查 references/impact" } : {}),
|
|
809
884
|
...(matches.length === 0 && entityMatches.model.total === 0 && entityMatches.field.total === 0 && entityMatches.service.total === 0
|
|
810
885
|
? {
|
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
|
});
|