@stalfh233/omc-cli 0.6.18 → 0.6.19

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.
@@ -34,6 +34,7 @@ export const CLI_STDOUT_OWNERS = {
34
34
  intent: "envelope",
35
35
  help: "envelope",
36
36
  graph: "envelope",
37
+ residue: "envelope",
37
38
  programming: "envelope",
38
39
  rollback: "envelope",
39
40
  setup: "envelope",
package/dist/cli.js CHANGED
@@ -57,6 +57,7 @@ import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills
57
57
  import { parseToolInput, ToolInputError } from "./tool-types.js";
58
58
  import { WireError, wireCall } from "./wire.js";
59
59
  import { DEFAULT_PULL_CONCURRENCY, GraphIndexAbsentError, GraphPullLockHeldError, MAX_PULL_CONCURRENCY, impactGraph, publishGraphPull, queryGraphReferences, readGraphStatus, searchGraphRules, } from "./graph.js";
60
+ import { cmdResidue } from "./residue.js";
60
61
  import { ENDPOINTS } from "./endpoints.js";
61
62
  import { makeToolContext } from "./context.js";
62
63
  import { cmdRollbackList, cmdRollbackPlan, cmdRollbackApply } from "./rollback.js";
@@ -986,7 +987,6 @@ async function cmdArtifactGet(id, transport, options = {}) {
986
987
  ...artifactProjectionMetadata(content),
987
988
  keys,
988
989
  path: options.jq,
989
- content: projected.content ?? null,
990
990
  projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }],
991
991
  ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}),
992
992
  ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}),
@@ -1038,7 +1038,7 @@ async function cmdArtifactGet(id, transport, options = {}) {
1038
1038
  const projected = projectArtifactPath(artifactContent, options.jq);
1039
1039
  return emitTransportResult(projected.missed
1040
1040
  ? { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
1041
- : { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: projected.content ?? null, projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }], ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
1041
+ : { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }], ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
1042
1042
  }
1043
1043
  // r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
1044
1044
  // large ones (up to 55 KB) pulled whole workflow templates just to read one
@@ -2470,6 +2470,7 @@ async function main() {
2470
2470
  pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
2471
2471
  });
2472
2472
  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 即零残留。只读零写入;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)); });
2473
2474
  const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
2474
2475
  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 } : {}) })); });
2475
2476
  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) => {
@@ -15,7 +15,7 @@ export const propertyTypeArg = z
15
15
  export const relativeCodeArg = z
16
16
  .string()
17
17
  .optional()
18
- .describe("relation target model code;WORK_SHEET/MULT_WORK_SHEET 必填,且与 options.schemaCode 相同");
18
+ .describe("关联目标 code;租户唯一、按 code 全局解析,可跨应用");
19
19
  /**
20
20
  * Ticket 59 (P0): the wire `name_i18n` column must be a *parseable JSON object
21
21
  * string*, never a bare display string. The admin designer runs an unguarded
@@ -167,7 +167,7 @@ export const subFieldArg = z
167
167
  */
168
168
  export const fieldOptionsArg = z
169
169
  .object({
170
- schemaCode: z.string().optional().describe("relation target model code(WORK_SHEET/MULT_WORK_SHEET;与 relativeCode 都填且必须相同)"),
170
+ schemaCode: z.string().optional().describe("关联目标 code;租户唯一、按 code 全局解析,可跨应用"),
171
171
  queryCode: z.string().optional().describe("relation query code (usually the target model code)"),
172
172
  displayField: z.string().optional().describe("relation display field (usually 'name')"),
173
173
  mappings: z
@@ -301,9 +301,9 @@ export function assertRelationTargetExists(args, candidates) {
301
301
  : typeof raw === "string" && raw in FIELD_PROPERTY_TYPES ? FIELD_PROPERTY_TYPES[raw] : undefined;
302
302
  if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
303
303
  return;
304
- const optionsCode = typeof args.options?.schemaCode === "string" ? args.options.schemaCode.trim() : "";
305
304
  const relativeCode = typeof args.relativeCode === "string" ? args.relativeCode.trim() : "";
306
- const target = optionsCode || relativeCode;
305
+ const optionsCode = typeof args.options?.schemaCode === "string" ? args.options.schemaCode.trim() : "";
306
+ const target = relativeCode || optionsCode;
307
307
  // Some offline/fake transports do not expose a model-list face. An empty
308
308
  // candidate set is therefore "unknown", not proof of absence; live model
309
309
  // lists are non-empty and get the strict pre-write gate.
@@ -336,7 +336,8 @@ export function relationPublishFailureMessage(fields, candidates) {
336
336
  const type = typeof raw === "number" ? raw : typeof raw === "string" && raw in FIELD_PROPERTY_TYPES ? FIELD_PROPERTY_TYPES[raw] : undefined;
337
337
  if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
338
338
  continue;
339
- const target = typeof options?.schemaCode === "string" ? options.schemaCode.trim() : typeof field.relativeCode === "string" ? field.relativeCode.trim() : "";
339
+ const relativeCode = typeof field.relativeCode === "string" ? field.relativeCode.trim() : "";
340
+ const target = relativeCode || (typeof options?.schemaCode === "string" ? options.schemaCode.trim() : "");
340
341
  if (typeof options?.schemaCode === "string" && options.schemaCode.trim() && (!field.relativeCode || String(field.relativeCode).trim() === "")) {
341
342
  return `字段 ${String(field.code ?? "(未知)")} 的关联目标 ${options.schemaCode} 仅写在 options.schemaCode;发布面不解析该位置(平台原始 errcode 300010「数据模型编码不能为空!」)。请重建/归一化为顶层 relativeCode=${options.schemaCode}`;
342
343
  }
@@ -12,16 +12,21 @@ import { defineTool, ToolRefusalError } from "../tool-types.js";
12
12
  import { wireCall, wireRows, WireError } from "../wire.js";
13
13
  import { executeFormDraftWrite, executeFormPublishWrite, readFormOrNull } from "../form-support.js";
14
14
  import { FIELD_LANE_CONTRACT } from "./field-contract.js";
15
- import { flattenTreeRows, mapBounded, resolveApplicationScope } from "../support.js";
15
+ import { flattenTreeRows, mapBounded } from "../support.js";
16
16
  import { extractBizObjectPage } from "./runtime-data.js";
17
17
  const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
18
18
  const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
19
19
  /** Read actual model schema codes; bare modelList may expose internal node ids. */
20
20
  export async function modelSchemaCodes(session) {
21
21
  try {
22
- const scope = await resolveApplicationScope(session);
23
- const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: scope.code } })).data));
24
- const codes = tree
22
+ // Relation codes are tenant-global on the platform, not application-scoped.
23
+ // Enumerate every application for this call only; deliberately do not cache
24
+ // this merged candidate set across calls because model writes can change it.
25
+ const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
26
+ .map((row) => row.appCode ?? row.code)
27
+ .filter((code) => typeof code === "string" && code.length > 0);
28
+ const trees = await mapBounded(apps, 4, async (appCode) => flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data)));
29
+ const codes = trees.flat()
25
30
  .filter((row) => row.type === "BizModel")
26
31
  .map((row) => String(row.code ?? row.schemaCode ?? row.bizSchemaCode ?? ""))
27
32
  .filter((code) => code.length > 0);
@@ -29,7 +34,8 @@ export async function modelSchemaCodes(session) {
29
34
  return codes;
30
35
  }
31
36
  catch {
32
- // Fall through to the compatibility endpoint below.
37
+ // Fall through to the compatibility endpoint below when the tenant-wide
38
+ // discovery face is unavailable (older/fake transports).
33
39
  }
34
40
  return wireRows((await wireCall(session, { path: ENDPOINTS.modelList, query: {} })).data)
35
41
  .map((row) => String(row.schemaCode ?? row.bizSchemaCode ?? row.code ?? ""))
@@ -5,7 +5,7 @@ import { writeTool } from "../write.js";
5
5
  import { wireCall, wireRows } from "../wire.js";
6
6
  import { ENDPOINTS } from "../endpoints.js";
7
7
  import { capabilityArg, environmentArg } from "../args.js";
8
- import { resolveApplicationCode } from "../support.js";
8
+ import { jsonBytes, resolveApplicationCode } from "../support.js";
9
9
  import { LANES } from "./lane-ids.js";
10
10
  import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
11
11
  /**
@@ -38,6 +38,33 @@ import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
38
38
  */
39
39
  const LANE = LANES.folder;
40
40
  const T = FOLDER_LANE_CONTRACT.tools;
41
+ /**
42
+ * folder.tree is a node inventory, so its useful inline budget is node-count
43
+ * first rather than the generic read compact threshold. Keep the existing
44
+ * byte guard as a second gate: a pathological 50-node tree still belongs in
45
+ * the artifact when the node payload itself is too large.
46
+ */
47
+ export const FOLDER_TREE_INLINE_NODE_LIMIT = 50;
48
+ export const FOLDER_TREE_NODE_INLINE_CEILING_BYTES = 8192;
49
+ export function compactFolderTree(result) {
50
+ const nodes = Array.isArray(result.nodes) ? result.nodes : undefined;
51
+ if (nodes === undefined)
52
+ return { ...result };
53
+ if (nodes.length <= FOLDER_TREE_INLINE_NODE_LIMIT && jsonBytes(nodes) <= FOLDER_TREE_NODE_INLINE_CEILING_BYTES) {
54
+ return { ...result, nodes };
55
+ }
56
+ const countGate = nodes.length > FOLDER_TREE_INLINE_NODE_LIMIT;
57
+ const { nodes: _nodes, ...withoutNodes } = result;
58
+ return {
59
+ ...withoutNodes,
60
+ nodesPreview: nodes.slice(0, 5),
61
+ note: `${String(result.note ?? "")} ${countGate
62
+ ? `nodes 超过 ${FOLDER_TREE_INLINE_NODE_LIMIT} 条,完整节点已外置到 detailArtifactId;nodesPreview 仅展示前 5 条。`
63
+ : `nodes 正文超过 ${FOLDER_TREE_NODE_INLINE_CEILING_BYTES}B 字节闸,完整节点已外置到 detailArtifactId;这是字节闸,不是节点数限制。`}`.trim(),
64
+ // Deliberately omit nodes so governedRead adds the standard empty
65
+ // placeholder/truncatedKeys while the preview remains available.
66
+ };
67
+ }
41
68
  /**
42
69
  * Live 1.1.25 (r28d F2): platform `apppackage/create` seeds one top-level Folder
43
70
  * whose `name_i18n.en` is the stable token `defaultGroup` (zh display is「默认分组」).
@@ -202,7 +229,7 @@ function folderTools(tools) {
202
229
  : " applicationExists:false——应用不在 apppackage/list;空树不等于「空应用」。") +
203
230
  " nodeCount/folderCount/modelCount 是全应用聚合,共享环境含其它会话资源;核对自己的资源请按 code 前缀过滤 nodes[],不要只比计数。",
204
231
  };
205
- }),
232
+ }, { compact: compactFolderTree }),
206
233
  }));
207
234
  writeTool({
208
235
  tools,
@@ -0,0 +1,116 @@
1
+ import { createHash } from "node:crypto";
2
+ import { ENDPOINTS } from "./endpoints.js";
3
+ import { wireCall, wireRows } from "./wire.js";
4
+ import { flattenTreeRows } from "./support.js";
5
+ import { ConfigStore, defaultPaths } from "./config-store.js";
6
+ import { SessionManager } from "./session-manager.js";
7
+ import { makeToolContext } from "./context.js";
8
+ import { emitInvalid, emitResult } from "./command-transport.js";
9
+ const MAX_ITEMS = 20;
10
+ function face(faceName, rows, extra = {}) {
11
+ return { face: faceName, ...extra, count: rows.length, items: rows.slice(0, MAX_ITEMS) };
12
+ }
13
+ function codeOf(row) {
14
+ return String(row.code ?? row.schemaCode ?? row.bizSchemaCode ?? "");
15
+ }
16
+ function nameOf(row, code) {
17
+ return String(row.name ?? row.displayName ?? row.title ?? code);
18
+ }
19
+ function codeStarts(row, prefix) {
20
+ return codeOf(row).startsWith(prefix);
21
+ }
22
+ function folderStarts(row, prefix) {
23
+ return codeStarts(row, prefix) || String(row.name ?? "").startsWith(prefix);
24
+ }
25
+ function scriptPresent(raw) {
26
+ if (typeof raw !== "string")
27
+ return false;
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (!Array.isArray(parsed))
31
+ return false;
32
+ return parsed.some((element) => {
33
+ const row = element;
34
+ return row.id === "customScript" && typeof row.innerHTML === "string" && row.innerHTML.trim() !== "";
35
+ });
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function tree(session, app) {
42
+ return flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: app } })).data));
43
+ }
44
+ async function modelRows(session, app) {
45
+ return (await tree(session, app)).filter((row) => row.type === "BizModel");
46
+ }
47
+ async function customPages(session, appSet) {
48
+ const rows = wireRows((await wireCall(session, { path: ENDPOINTS.customPageList, query: { modeType: "VUE_MODE" } })).data);
49
+ return rows.filter((row) => appSet.has(String(row.appCode ?? "")));
50
+ }
51
+ async function deepFaces(session, model) {
52
+ const schemaCode = codeOf(model);
53
+ const rules = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } })).data);
54
+ const datarules = wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query: { schemaCode } })).data);
55
+ let online = [];
56
+ try {
57
+ const sheet = (await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode: schemaCode } })).data;
58
+ if (sheet && (scriptPresent(sheet.draftHtmlJson) || scriptPresent(sheet.publishedHtmlJson))) {
59
+ const raw = typeof sheet.publishedHtmlJson === "string" && scriptPresent(sheet.publishedHtmlJson) ? sheet.publishedHtmlJson : sheet.draftHtmlJson;
60
+ online = [{ present: true, ...(typeof raw === "string" ? { sha256: createHash("sha256").update(raw).digest("hex") } : {}) }];
61
+ }
62
+ }
63
+ catch {
64
+ // A missing form is an absent online-js face, not a scan failure.
65
+ }
66
+ 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 }),
69
+ face("onlinejs", online, { model: schemaCode }),
70
+ ];
71
+ }
72
+ export async function scanResidues(session, prefix, options = {}) {
73
+ const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
74
+ .map((row) => String(row.appCode ?? row.code ?? ""))
75
+ .filter(Boolean);
76
+ const selected = options.apps && options.apps !== "all" ? options.apps.split(",").map((app) => app.trim()).filter(Boolean) : apps;
77
+ const appSet = new Set(selected);
78
+ const faces = [];
79
+ const models = [];
80
+ for (const app of selected) {
81
+ const rows = await tree(session, app);
82
+ const matchingModels = rows.filter((row) => row.type === "BizModel" && codeStarts(row, prefix));
83
+ models.push(...matchingModels.map((row) => ({ ...row, appCode: app })));
84
+ faces.push(face("model", matchingModels.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
85
+ faces.push(face("folder", rows.filter((row) => row.type === "Folder" && folderStarts(row, prefix)).map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
86
+ }
87
+ const pages = (await customPages(session, appSet)).filter((row) => codeStarts(row, prefix));
88
+ for (const app of selected) {
89
+ faces.push(face("custompage", pages.filter((row) => String(row.appCode ?? "") === app).map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
90
+ }
91
+ if (options.deep !== false) {
92
+ for (const model of models)
93
+ faces.push(...await deepFaces(session, model));
94
+ }
95
+ const totalResidues = faces.reduce((sum, item) => sum + item.count, 0);
96
+ return {
97
+ prefix,
98
+ faces,
99
+ totalResidues,
100
+ clean: totalResidues === 0,
101
+ note: "report 面暂停维护未扫描;workflow 模板跟模型走(模型删即删)",
102
+ };
103
+ }
104
+ export async function cmdResidue(prefix, options, transport) {
105
+ if (!prefix || prefix.trim() === "")
106
+ return emitInvalid(transport, { message: "residue 的 prefix 必填" });
107
+ try {
108
+ const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
109
+ const session = await context.sessions.session(options.env);
110
+ return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true }), transport);
111
+ }
112
+ catch (error) {
113
+ return emitResult({ status: "error", blocker: "residue-scan-failed", message: error instanceof Error ? error.message : String(error) }, transport);
114
+ }
115
+ }
116
+ //# sourceMappingURL=residue.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.6.18",
3
+ "version": "0.6.19",
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.18",
58
+ "omc-core": "0.6.19",
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.18"
76
+ "@stalfh233/omc-fake-platform": "0.6.19"
77
77
  }
78
78
  }