@stalfh233/omc-cli 0.4.75 → 0.4.77

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.
@@ -815,6 +815,7 @@
815
815
  "tools": [
816
816
  "model.create",
817
817
  "model.update",
818
+ "model.move",
818
819
  "model.delete",
819
820
  "model.deleteMany",
820
821
  "model.setSummary",
@@ -823,7 +824,8 @@
823
824
  "model.inspect"
824
825
  ],
825
826
  "scopes": [
826
- "create publish physical delete absence read list inspect"
827
+ "create publish physical delete absence read list inspect",
828
+ "move a model's app-tree node to another folder (bizmodels/update parentId)"
827
829
  ]
828
830
  },
829
831
  {
@@ -1328,6 +1330,38 @@
1328
1330
  "scopes": [
1329
1331
  "report tree read page metadata BI config chart data create update delete configure"
1330
1332
  ]
1333
+ },
1334
+ {
1335
+ "capability": "App-tree folders (目录/分组): read flattened tree, create, rename/move, non-recursive delete",
1336
+ "status": "executable",
1337
+ "scope": "App application-tree folders: flattened tree read with path/depth, folder create (omitted parentId = root), rename/reparent, non-recursive delete, and model reparenting.",
1338
+ "version_family": "8.6",
1339
+ "anchor_version": "8.6.26",
1340
+ "evidence": [
1341
+ {
1342
+ "path": "cp-sources/backend/decompiled-full/launcher-8.6.26/cloudpivot-engine-8.6.26/com/authine/cloudpivot/web/api/controller/app/AppFunctionController.java"
1343
+ },
1344
+ {
1345
+ "path": "cp-sources/backend/decompiled-full/launcher-8.6.26/cloudpivot-metadata-8.6.26/com/authine/cloudpivot/metadata/history/apppackage/service/ApplicationFacadeImpl.java"
1346
+ },
1347
+ {
1348
+ "path": "packages/omc-cli/src/lanes/folder.ts"
1349
+ }
1350
+ ],
1351
+ "lane": "app-tree-folders",
1352
+ "notes": "Live 2026-10-23 customer HAR: POST /api/app/folders/create answered errcode 0 成功创建目录; trees are nested (children[]) and must be flattened.",
1353
+ "tools": [
1354
+ "folder.tree",
1355
+ "folder.create",
1356
+ "folder.update",
1357
+ "folder.delete"
1358
+ ],
1359
+ "scopes": [
1360
+ "app-tree folder/model tree (flattened with path/depth)",
1361
+ "app-tree folder create (folders/create)",
1362
+ "app-tree folder rename/move (folders/update)",
1363
+ "app-tree folder delete (folders/delete, non-recursive)"
1364
+ ]
1331
1365
  }
1332
1366
  ]
1333
1367
  }
package/dist/artifacts.js CHANGED
@@ -83,7 +83,7 @@ export function isWithinRoot(root, candidate) {
83
83
  export function assertSafeOutputPath(root, candidate, label) {
84
84
  const normalized = resolve(root, candidate);
85
85
  if (!isWithinRoot(root, normalized))
86
- throw new Error(`${label} must be inside the configuration root`);
86
+ throw new Error(`${label} must be inside the configuration root(配置根 ${resolve(root)};本次解析为 ${normalized})`);
87
87
  const parent = resolve(normalized, "..");
88
88
  if (!existsSync(parent))
89
89
  throw new Error(`${label} parent directory does not exist`);
@@ -101,7 +101,7 @@ export function assertSafeInputPath(root, candidate, label) {
101
101
  const resolvedRoot = resolve(root);
102
102
  const normalized = resolve(root, candidate);
103
103
  if (!isWithinRoot(root, normalized))
104
- throw new Error(`${label} must be inside the configuration root`);
104
+ throw new Error(`${label} must be inside the configuration root(配置根 ${resolvedRoot};本次解析为 ${normalized})`);
105
105
  let current = normalized;
106
106
  while (current !== resolvedRoot) {
107
107
  try {
@@ -36,6 +36,16 @@ const SURFACES = [
36
36
  intent: { level: "none", note: "未覆盖;用 omc call app.*。" },
37
37
  notes: "环境已带默认 applicationCode,建模不必先建应用。",
38
38
  },
39
+ {
40
+ id: "folder",
41
+ title: "应用目录(分组)",
42
+ lane: LANES.folder,
43
+ description: "应用树目录:读(递归展平带 path/depth)、建、改名/挂载、非递归删除。",
44
+ operations: ["tree", "create", "update", "delete"],
45
+ requires: ["app"],
46
+ intent: { level: "none", note: "未覆盖;用 omc call folder.*。移动模型用 model.move。" },
47
+ notes: "省略 parentId 建顶层目录(平台默认挂应用根);平台不校验重名(工具默认同父同名拒绝);删除非递归(有子节点回 DIRECTORY_NOT_EMPTY);应用树是嵌套 children,folder.tree 已展平。",
48
+ },
39
49
  {
40
50
  id: "dictionary",
41
51
  title: "数据字典",
@@ -51,7 +61,7 @@ const SURFACES = [
51
61
  title: "业务模型",
52
62
  lane: LANES.model,
53
63
  description: "模型生命周期:创建、改名、物理删除、存在性。",
54
- operations: ["create", "update", "delete", "deleteMany", "setSummary"],
64
+ operations: ["create", "update", "move", "delete", "deleteMany", "setSummary"],
55
65
  intent: { level: "partial", note: "create/update 与 delete(absent:true)已声明式;deleteMany/setSummary 未覆盖。" },
56
66
  },
57
67
  {
package/dist/cli.js CHANGED
@@ -17,7 +17,8 @@ import { configureOutput, humanSummary, stripOutputFlags, writeEnvelope } from "
17
17
  import { beginUsage, completeUsage, recordUsageOutput, usageReport } from "./usage-log.js";
18
18
  import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
19
19
  import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
20
- import { resolve } from "node:path";
20
+ import { projectJsonPath, projectResultExtract } from "./path-projection.js";
21
+ import { basename, join, resolve } from "node:path";
21
22
  import { buildToolRegistry } from "./tools.js";
22
23
  import { acquireWriteLock } from "./write-lock.js";
23
24
  import { cmdCompute } from "./compute.js";
@@ -274,12 +275,19 @@ async function cmdCall(store, args) {
274
275
  const positional = args.filter((token, index) => !token.startsWith("--") && (index === 0 || !args[index - 1]?.startsWith("--env") && !args[index - 1]?.startsWith("--environment") && !args[index - 1]?.startsWith("--args")));
275
276
  const toolName = positional[0];
276
277
  if (!toolName || toolName === "help" || args.includes("--help")) {
277
- console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <path>]");
278
+ console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--extract-keys]");
278
279
  console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
279
280
  return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
280
281
  }
281
282
  const environment = flag("--env") ?? flag("--environment");
282
283
  const requestValue = flag("--request-id");
284
+ // 1.2 of issue 2026-10-23: read faces answer {detailArtifactId, sha256} and the
285
+ // caller then needs a SECOND call to see one value (16 two-step chains in one
286
+ // session; 15 more were pure --keys probes). `--extract <path>` (repeatable)
287
+ // resolves the path inside the artifact of THIS result and returns only the
288
+ // value; `--extract-keys` lists the artifact's top-level keys with sizes.
289
+ const extractPaths = argvValues(args, "--extract");
290
+ const extractKeys = args.includes("--extract-keys");
283
291
  const dryRun = args.includes("--dry-run");
284
292
  const waitValue = flag("--wait");
285
293
  let waitMs = 0;
@@ -331,11 +339,22 @@ async function cmdCall(store, args) {
331
339
  // r10 F9: an out-of-root `--args-file` was reported under "--args 必须是
332
340
  // 合法 JSON", which points the reader at the wrong flag. Name the real
333
341
  // cause (and the configuration root) when the failure came from the file.
334
- const message = /must be inside the configuration root/.test(detail)
335
- ? `--args-file 越界:${detail}(只接受配置根 ${resolve(process.env.OMC_CONFIG_HOME ?? process.cwd())} 内的路径;把参数文件放进项目目录后重试)`
336
- : `--args 必须是合法 JSON:${detail}(Windows PowerShell 下内联 JSON 引号转义不可靠,请改用 --args-file <path> 写入临时文件后引用)`;
342
+ const configRoot = resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
343
+ // 1.1 of issue 2026-10-23: the old hint told the caller to write a TEMP file
344
+ // while the validator only accepts paths under the config root — the two
345
+ // instructions contradicted each other and neither named the root. Offer a
346
+ // single executable recipe: an absolute path inside the root.
347
+ const outOfRoot = /must be inside the configuration root/.test(detail);
348
+ const message = outOfRoot
349
+ ? `--args-file 越界:${detail}。可执行做法:把参数文件放到 ${configRoot} 之下(如 ${join(configRoot, "args.json")}),再用 --args-file 引用;不要把参数文件放在系统临时目录(配置根之外一律拒绝)`
350
+ : `--args 必须是合法 JSON:${detail}。若问题在引号转义(Windows PowerShell),改用 --args-file <配置根内的路径>`;
337
351
  console.error(message);
338
- writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: detail, retryable: false }));
352
+ // The MACHINE envelope must carry the same actionable text: agents read
353
+ // stdout, and the old envelope only repeated the English detail.
354
+ // Keep the REJECTED path on stderr only: the machine envelope must not leak
355
+ // host layout (ticket 103), so it carries the root plus the bare file name.
356
+ const rejectedName = typeof argsFile === "string" ? basename(resolve(configRoot, argsFile)) : "";
357
+ writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: outOfRoot ? `--args-file 必须位于配置根内(配置根 ${configRoot}${rejectedName ? `;本次文件 ${rejectedName}` : ""})` : detail, retryable: false, ...(outOfRoot ? { configurationRoot: configRoot } : {}) }));
339
358
  return CLI_EXIT_CODES.invalidInput;
340
359
  }
341
360
  const registry = buildToolRegistry();
@@ -404,9 +423,26 @@ async function cmdCall(store, args) {
404
423
  await lock.release();
405
424
  }
406
425
  }
426
+ if (extractPaths.length > 0 || extractKeys) {
427
+ const projected = projectResultExtract(result, extractPaths, extractKeys);
428
+ if (projected !== undefined)
429
+ result = projected;
430
+ }
407
431
  const classified = classifyResult(result);
408
432
  return emit(classified.status, result, classified.exitCode);
409
433
  }
434
+ /** Collect every value of a repeatable flag (`--extract a --extract b`). */
435
+ function argvValues(args, name) {
436
+ const values = [];
437
+ for (let index = 0; index < args.length; index += 1) {
438
+ if (args[index] !== name)
439
+ continue;
440
+ const value = args[index + 1];
441
+ if (value !== undefined && !value.startsWith("--"))
442
+ values.push(value);
443
+ }
444
+ return values;
445
+ }
410
446
  function resolvedDefaultEnvironment(store) {
411
447
  try {
412
448
  return store.resolveEnvironment(undefined).name;
@@ -415,36 +451,6 @@ function resolvedDefaultEnvironment(store) {
415
451
  return "default";
416
452
  }
417
453
  }
418
- function projectJsonPath(value, path) {
419
- const normalized = path
420
- .trim()
421
- .replace(/^[.\/]+/, "")
422
- .replace(/\[(\d+)\]/g, ".$1")
423
- .replace(/\[(?:"([^"]+)"|'([^']+)')\]/g, (_all, dq, sq) => `.${dq || sq}`)
424
- .replace(/\.{2,}/g, ".");
425
- const segments = normalized.split(/[./]/).filter((segment) => segment.length > 0);
426
- let current = value;
427
- for (const segment of segments) {
428
- if (Array.isArray(current)) {
429
- const index = Number(segment);
430
- if (!Number.isInteger(index) || index < 0 || index >= current.length)
431
- return { missed: true, at: segment, available: `数组长度 ${current.length}(下标 0-${Math.max(current.length - 1, 0)})` };
432
- current = current[index];
433
- }
434
- else if (current && typeof current === "object") {
435
- const record = current;
436
- if (!Object.prototype.hasOwnProperty.call(record, segment)) {
437
- const keys = Object.keys(record);
438
- return { missed: true, at: segment, available: keys.length > 0 ? keys.slice(0, 12) : "(该层是空对象)" };
439
- }
440
- current = record[segment];
441
- }
442
- else {
443
- return { missed: true, at: segment, available: `路径在第 ${segments.indexOf(segment) + 1} 段之前已到标量/undefined(前段类型 ${current === null ? "null" : typeof current})` };
444
- }
445
- }
446
- return { missed: false, value: current };
447
- }
448
454
  function parseDurationMs(text) {
449
455
  const match = /^\s*(\d+)\s*(ms|s|m|h|d|w)?\s*$/u.exec(text);
450
456
  if (!match)
@@ -950,11 +956,47 @@ function onboardingHelp() {
950
956
  return [
951
957
  "首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
952
958
  "目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
959
+ "大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --extract <path>(可重复)或 --extract-keys,省掉再跑一次 omc artifact get。",
953
960
  "工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
954
961
  "单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
955
962
  "写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
956
963
  ].join("\n");
957
964
  }
965
+ /** Last command-level failure, so the no-envelope fallback can name the cause. */
966
+ let lastFailureDetail;
967
+ function failureDetail() {
968
+ return process.env.OMC_LAST_FAILURE ?? lastFailureDetail;
969
+ }
970
+ /**
971
+ * 1.5 of issue 2026-10-23: a `--jq` expression containing spaces/pipes arrives as
972
+ * multiple argv entries when the caller's shell splits it. Commander then
973
+ * answers "too many arguments for 'get'" — an arity complaint, not a quoting
974
+ * hint. Re-join the value before parsing.
975
+ */
976
+ function joinSplitFlagValues(raw, flags) {
977
+ const out = [];
978
+ for (let index = 0; index < raw.length; index += 1) {
979
+ const token = raw[index];
980
+ if (!flags.includes(token)) {
981
+ out.push(token);
982
+ continue;
983
+ }
984
+ const value = raw[index + 1];
985
+ if (value === undefined) {
986
+ out.push(token);
987
+ continue;
988
+ }
989
+ const parts = [value];
990
+ let cursor = index + 2;
991
+ while (cursor < raw.length && !raw[cursor].startsWith("--")) {
992
+ parts.push(raw[cursor]);
993
+ cursor += 1;
994
+ }
995
+ out.push(token, parts.join(" "));
996
+ index = cursor - 1;
997
+ }
998
+ return out;
999
+ }
958
1000
  async function main() {
959
1001
  let commandExitCode = 0;
960
1002
  beginUsage(argv);
@@ -1038,8 +1080,8 @@ async function main() {
1038
1080
  commandExitCode = CLI_EXIT_CODES.ok;
1039
1081
  });
1040
1082
  command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
1041
- command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--max-output-bytes <bytes>").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
1042
- const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
1083
+ command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
1084
+ const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
1043
1085
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
1044
1086
  });
1045
1087
  command.command("compute").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--target <propertyCode>").option("--property <propertyCode>").option("--formula <formula>").option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n字段计算规则拼装器:公式 {字段} 引用 → datarule type2 完整形状(editorText/editorMark 自动生成)。默认 dry-run,--apply 写入。复杂场景(条件分支/跨模型)走 omc next 的 recipe 路径。\n").action(async (options) => {
@@ -1161,7 +1203,8 @@ async function main() {
1161
1203
  intentCommand.command("readback <file>").option("--env <environment>").option("--max-output-bytes <bytes>").action(async (file, options) => {
1162
1204
  commandExitCode = await withTransport(options, request, (transport) => cmdIntentReadback(file, options.env, transport));
1163
1205
  });
1164
- await command.parseAsync(stripOutputFlags(argv));
1206
+ // `--jq` expressions the shell split arrive as several tokens; re-join them.
1207
+ await command.parseAsync(joinSplitFlagValues(stripOutputFlags(argv), ["--jq"]));
1165
1208
  return commandExitCode;
1166
1209
  }
1167
1210
  /**
@@ -1194,7 +1237,15 @@ main().then((code) => {
1194
1237
  finish(code);
1195
1238
  return;
1196
1239
  }
1197
- writeEnvelope(envelope(code === 0 ? "ok" : "error", {}, request, code === 0 ? null : { code: "internal", message: "command failed", retryable: false }));
1240
+ // 1.6 of issue 2026-10-23: a bare "command failed" gives no stage and no cause.
1241
+ writeEnvelope(envelope(code === 0 ? "ok" : "error", {}, request, code === 0
1242
+ ? null
1243
+ : {
1244
+ code: "internal",
1245
+ message: `${argv[2] ?? "omc"} 以退出码 ${code} 结束但没有写出结果信封${failureDetail() ? `:${failureDetail()}` : "(没有捕获到具体异常;加 --verbose 复现,或用相同参数再跑一次看 stderr)"}`,
1246
+ retryable: false,
1247
+ ...(failureDetail() ? { stage: "post-command" } : {}),
1248
+ }));
1198
1249
  completeUsage(code);
1199
1250
  finish(code);
1200
1251
  }, (error) => {
@@ -82,7 +82,7 @@ export function artifactFetchHint(artifactId) {
82
82
  // raw. `--summary` shows the key/size map so they fetch only what they need.
83
83
  // Kept terse on purpose: this line rides EVERY write and every spilled read
84
84
  // (2026-09-25 cost pass — the parenthetical prose was 80 B of the 133 B).
85
- return `omc artifact get ${artifactId} --summary(再 --keys/--jq)`;
85
+ return `omc artifact get ${artifactId} --summary(再 --keys/--jq);或原调用加 --extract <path> / --extract-keys 一次拿到`;
86
86
  }
87
87
  /** Replace local paths with a redaction marker (never leak the host layout). */
88
88
  function sanitizePaths(value) {
package/dist/compute.js CHANGED
@@ -74,9 +74,10 @@ export async function cmdCompute(args) {
74
74
  }
75
75
  catch (error) {
76
76
  const message = error instanceof Error ? error.message : String(error);
77
+ process.env.OMC_LAST_FAILURE = `compute → datarule.create: ${message}`;
77
78
  writeEnvelope(envelope("error", {}, request, {
78
79
  code: "compute-write-failed",
79
- message: `compute 调用 datarule.create 失败:${message}\n` +
80
+ message: `compute 调用 datarule.create 失败(stage=datarule.create):${message}\n` +
80
81
  "若 --schema 是**子表 code**(子表列计算),改用可跑样例直接发:omc describe datarule.create --example field-compute-child-column," +
81
82
  "或 omc call datarule.create --args '<该样例 args>'(含 resultType/formulaType/expression 的嵌套 JSON)。",
82
83
  retryable: false,
@@ -5,6 +5,7 @@ import { DATARULE_LANE_CONTRACT } from "./lanes/datarule-contract.js";
5
5
  import { DICTIONARY_LANE_CONTRACT } from "./lanes/dictionary-contract.js";
6
6
  import { ENVIRONMENT_LANE_CONTRACT } from "./lanes/environment-contract.js";
7
7
  import { FIELD_LANE_CONTRACT } from "./lanes/field-contract.js";
8
+ import { FOLDER_LANE_CONTRACT } from "./lanes/folder-contract.js";
8
9
  import { FORM_LANE_CONTRACT } from "./lanes/form-contract.js";
9
10
  import { LIST_VIEW_LANE_CONTRACT } from "./lanes/list-view-contract.js";
10
11
  import { META_LANE_CONTRACT } from "./lanes/meta-contract.js";
@@ -22,6 +23,7 @@ export const LANE_CONTRACTS = Object.freeze([
22
23
  DICTIONARY_LANE_CONTRACT,
23
24
  ENVIRONMENT_LANE_CONTRACT,
24
25
  FIELD_LANE_CONTRACT,
26
+ FOLDER_LANE_CONTRACT,
25
27
  FORM_LANE_CONTRACT,
26
28
  LIST_VIEW_LANE_CONTRACT,
27
29
  META_LANE_CONTRACT,
@@ -7,6 +7,7 @@ import { DATARULE_COVERAGE } from "./lanes/datarule-coverage.js";
7
7
  import { FIELD_COVERAGE } from "./lanes/field-coverage.js";
8
8
  import { FORM_COVERAGE } from "./lanes/form-coverage.js";
9
9
  import { LIST_VIEW_COVERAGE } from "./lanes/list-view-coverage.js";
10
+ import { FOLDER_COVERAGE } from "./lanes/folder-coverage.js";
10
11
  import { MODEL_COVERAGE } from "./lanes/model-coverage.js";
11
12
  import { ONLINE_JS_COVERAGE } from "./lanes/online-js-coverage.js";
12
13
  import { RULE_LIFECYCLE_COVERAGE } from "./lanes/rule-lifecycle-coverage.js";
@@ -23,6 +24,7 @@ const DECLARATIONS = Object.freeze([
23
24
  ...APP_COVERAGE,
24
25
  ...DICTIONARY_COVERAGE,
25
26
  ...MODEL_COVERAGE,
27
+ ...FOLDER_COVERAGE,
26
28
  ...FIELD_COVERAGE,
27
29
  ...DATARULE_COVERAGE,
28
30
  ...FORM_COVERAGE,
package/dist/discovery.js CHANGED
@@ -200,11 +200,29 @@ export function describeTool(registry, name) {
200
200
  // of a 133 KB task even though a matching recipe existed.
201
201
  ...(hasRecipe ? { exampleHint: `可运行参数用 --example <id>(~1–2KB);单参数用 --param <name>;仅当二者都缺形状才 --full` } : {}),
202
202
  ...(!hasRecipe ? { exampleHint: `无静态 recipe:required + params 已够常规调用;单参数说明用 --param <name>;仅当需要全部参数注释/拒绝形状才 --full(5–20KB)` } : {}),
203
+ // 1.7 of issue 2026-10-23: the parameter DOC for `subFields` was only
204
+ // reachable through --param/--full, i.e. discovering a param needed its
205
+ // name first. Name the documented params inline (cheap: names only) so one
206
+ // `describe` answers "does this tool take X, and what is X called".
207
+ ...(documentedParamNames(tool).length > 0
208
+ ? { paramDocs: { available: documentedParamNames(tool), read: `omc describe ${tool.name} --param <name>(单个参数注释,几百字节)` } }
209
+ : {}),
203
210
  // Keep the cost salient at the decision point: agents reached for --full even
204
211
  // when --example/--param existed (sandbox22: 8 --full calls, one 19 KB).
205
212
  full: `omc describe ${tool.name} --full(5–20KB;先试 --example/--param)`,
206
213
  };
207
214
  }
215
+ /** Param names whose schema entry carries a `.describe()` (documented params). */
216
+ function documentedParamNames(tool) {
217
+ const shape = (tool.inputSchema ?? {});
218
+ const names = [];
219
+ for (const [key, value] of Object.entries(shape)) {
220
+ const description = value?.description;
221
+ if (typeof description === "string" && description.trim() !== "")
222
+ names.push(key);
223
+ }
224
+ return names.sort();
225
+ }
208
226
  /** `omc describe <tool> --example <id>`: one recipe in full, by id. */
209
227
  export function describeExample(registry, name, exampleId) {
210
228
  const tool = registry.find((entry) => entry.name === name);
package/dist/endpoints.js CHANGED
@@ -17,6 +17,22 @@ export const ENDPOINTS = {
17
17
  modelGetSummary: "/api/api/app/bizmodels/get_summary",
18
18
  modelUpdateSummary: "/api/api/app/bizmodels/update_summary",
19
19
  modelUpdate: "/api/api/app/bizmodels/update",
20
+ /**
21
+ * App-tree FOLDERS (目录/分组). AppFunctionController (/api/app/folders,
22
+ * 8.6.26 :90-260):
23
+ * POST create body AppFunctionVO {appCode, name, name_i18n?, parentId?, code?, icon?}
24
+ * GET get ?folderId=
25
+ * POST update body AppFunctionVO {id, name, name_i18n?, parentId?}
26
+ * POST delete ?folderId= (refuses a non-empty folder: DIRECTORY_NOT_EMPTY)
27
+ * createFolder defaults parentId to the app package id (root) and forces
28
+ * type=Folder; updateFolder refuses parentId === id. Live 2026-10-23 customer
29
+ * evidence: /api/app/folders/create answers 0 "成功创建目录"; tree readback is
30
+ * NESTED (children[]), so a caller must flatten recursively.
31
+ */
32
+ folderCreate: "/api/api/app/folders/create",
33
+ folderGet: "/api/api/app/folders/get",
34
+ folderUpdate: "/api/api/app/folders/update",
35
+ folderDelete: "/api/api/app/folders/delete",
20
36
  appPackagePhysicallyDelete: "/api/api/app/apppackage/physically_delete",
21
37
  fieldList: "/api/api/app/bizproperty/list",
22
38
  fieldGet: "/api/api/app/bizproperty/get",
@@ -14,6 +14,23 @@
14
14
  */
15
15
  export const ALLOWED_SPANS = new Set([6, 8, 12, 16, 18, 24]);
16
16
  export const FULL_SPAN = 24;
17
+ /**
18
+ * 1.3 of issue 2026-10-23: these three classifications describe shapes the
19
+ * PLATFORM ITSELF stores — its default scaffold puts creater/createdTime/
20
+ * sequenceNo on one row (each defaulting to span 24 → "overflow"), and some
21
+ * published layouts list child columns as rows. Refusing them makes a
22
+ * read→write round-trip impossible, so they are warnings, not blockers.
23
+ * Coverage/span/options/type violations stay hard.
24
+ */
25
+ export const SOFT_LAYOUT_CLASSIFICATIONS = new Set();
26
+ // No classification is unconditionally soft: a caller-authored bad layout must
27
+ // still refuse with ZERO wire. The platform's own readback is tolerated by
28
+ // `form.draft`'s `fromReadback` path instead (issue 2026-10-23 §1.3), where the
29
+ // supplied faces can be compared against the stored sheet. The default-width
30
+ // row rule below still keeps the platform's scaffold rows from tripping.
31
+ export function hardLayoutViolations(violations) {
32
+ return violations.filter((violation) => !SOFT_LAYOUT_CLASSIFICATIONS.has(violation.classification));
33
+ }
17
34
  /** Child-table (Sheet 201) column keys, which must live on `columns`, never in rows. */
18
35
  function childColumnKeys(attrs) {
19
36
  const keys = new Set();
@@ -99,10 +116,16 @@ export function validateFormLayout(rows, attributes) {
99
116
  const codes = row.map((code) => String(code));
100
117
  const widths = codes.map((code) => spanOf(attrs, code, violations));
101
118
  const total = widths.reduce((sum, width) => sum + width, 0);
102
- if (total > FULL_SPAN) {
119
+ // The platform's OWN scaffold writes rows like creater+createdTime+
120
+ // sequenceNo with NO explicit span (each defaults to 24) — rows made only of
121
+ // default-width system controls are the platform's shape, not a caller
122
+ // error (issue 2026-10-23 §1.3). Only a row that sets a span explicitly is
123
+ // judged by the width rules.
124
+ const hasExplicitSpan = codes.some((code) => attrs[code]?.options !== undefined && (attrs[code]?.options).span !== undefined);
125
+ if (hasExplicitSpan && total > FULL_SPAN) {
103
126
  violations.push({ classification: "layout_row_overflow", row: index + 1, message: `第 ${index + 1} 行 span 合计 ${total},超过 ${FULL_SPAN}(${codes.join(" + ")})` });
104
127
  }
105
- if (codes.length !== 1 && widths.some((width) => width === FULL_SPAN)) {
128
+ if (hasExplicitSpan && codes.length !== 1 && widths.some((width) => width === FULL_SPAN)) {
106
129
  violations.push({ classification: "full_row_control_conflict", row: index + 1, message: `第 ${index + 1} 行含整行控件(span=24),不能与其他控件同行(${codes.join(" + ")})` });
107
130
  }
108
131
  });
@@ -4,7 +4,7 @@ import { decodeJsonObjectColumn } from "./json-column.js";
4
4
  import { ToolRefusalError } from "./tool-types.js";
5
5
  import { FIELD_CONTROL_TYPES, SYSTEM_FIELD_CODES, fieldControlOptions, relationDisplayField, relationMappings, } from "./field-families.js";
6
6
  import { deriveControlOptions } from "./capability-shape.js";
7
- import { validateFormLayoutShape } from "./form-layout.js";
7
+ import { hardLayoutViolations, validateFormLayoutShape } from "./form-layout.js";
8
8
  /**
9
9
  * Propagate each relation field's `options.mappings` into its form control so
10
10
  * the form-fill carry is configured where the form renderer reads it. The
@@ -602,9 +602,7 @@ export function isEmptyLayoutJson(value) {
602
602
  * face (one-sided included), covering configure_form/apply_changes paths that
603
603
  * do not pass through the `form.draft` write permission gate.
604
604
  */
605
- export function assertCallerFormLayout(args) {
606
- if (args.draftViewJson === undefined && args.draftAttributesJson === undefined)
607
- return;
605
+ export function formLayoutViolations(args) {
608
606
  const parse = (value) => {
609
607
  if (typeof value !== "string")
610
608
  return undefined;
@@ -615,7 +613,12 @@ export function assertCallerFormLayout(args) {
615
613
  return undefined;
616
614
  }
617
615
  };
618
- const violations = validateFormLayoutShape(parse(args.draftViewJson), parse(args.draftAttributesJson));
616
+ return validateFormLayoutShape(parse(args.draftViewJson), parse(args.draftAttributesJson));
617
+ }
618
+ export function assertCallerFormLayout(args) {
619
+ if (args.draftViewJson === undefined && args.draftAttributesJson === undefined)
620
+ return;
621
+ const violations = hardLayoutViolations(formLayoutViolations(args));
619
622
  if (violations.length > 0) {
620
623
  throw new ToolRefusalError({
621
624
  status: "refused",
@@ -836,8 +839,6 @@ export async function executeFormDraftWrite(session, args) {
836
839
  }
837
840
  // Caller-provided layouts get the distribution-grade shape gate (options
838
841
  // objects, string viewJson cells, no child keys as rows) before any wire.
839
- if (args.draftViewJson !== undefined || args.draftAttributesJson !== undefined)
840
- assertCallerFormLayout(args);
841
842
  // Ticket 74 — refuse CALLER-PROVIDED empty layouts before any wire call.
842
843
  // The platform designer's Save clears draft faces to "[]"/"{}" (non-lossless);
843
844
  // re-writing that shape explicitly is how the 13-control loss happened.
@@ -888,6 +889,42 @@ export async function executeFormDraftWrite(session, args) {
888
889
  if (pre === null)
889
890
  throw new WireError("business", `form ${args.sheetCode} was created but is not readable`, 302024);
890
891
  }
892
+ //
893
+ // 1.3 of issue 2026-10-23: the platform's OWN readback can carry shapes this
894
+ // gate rejects (`child_column_in_rows` when a published layout lists child
895
+ // columns as rows; `layout_row_overflow` on the auto system row). Refusing a
896
+ // byte-identical round-trip makes "read → write back" impossible, so a face
897
+ // that matches the sheet's own published/draft face is allowed through with
898
+ // the violations reported as warnings. Caller-authored layouts stay strict.
899
+ const roundTripFaces = new Set();
900
+ let layoutWarnings = [];
901
+ if (args.draftViewJson !== undefined || args.draftAttributesJson !== undefined) {
902
+ const pre2 = pre;
903
+ const same = (provided, stored) => typeof provided === "string" && typeof stored === "string" && provided.trim() === stored.trim();
904
+ const viewRoundTrip = same(args.draftViewJson, pre2?.publishedViewJson) || same(args.draftViewJson, pre2?.draftViewJson);
905
+ const attributesRoundTrip = same(args.draftAttributesJson, pre2?.publishedAttributesJson) || same(args.draftAttributesJson, pre2?.draftAttributesJson);
906
+ const allProvidedAreRoundTrip = (args.draftViewJson === undefined || viewRoundTrip) && (args.draftAttributesJson === undefined || attributesRoundTrip);
907
+ if (!allProvidedAreRoundTrip && args.fromReadback === true) {
908
+ throw new ToolRefusalError({
909
+ status: "refused",
910
+ blocker: "form-layout-invalid",
911
+ schemaCode: args.schemaCode,
912
+ sheetCode: args.sheetCode,
913
+ message: "fromReadback:true 但本次布局与已存草稿/发布面不一致——读过之后又改过就必须按普通写面校验(去掉 fromReadback,或按 validation 修正)",
914
+ validation: formLayoutViolations(args),
915
+ });
916
+ }
917
+ if (allProvidedAreRoundTrip) {
918
+ layoutWarnings = formLayoutViolations(args);
919
+ if (args.draftViewJson !== undefined)
920
+ roundTripFaces.add("draftViewJson");
921
+ if (args.draftAttributesJson !== undefined)
922
+ roundTripFaces.add("draftAttributesJson");
923
+ }
924
+ else {
925
+ assertCallerFormLayout(args);
926
+ }
927
+ }
891
928
  // Field-level relation mappings are the source of truth; read the published
892
929
  // fields so the scaffold (or an existing draft) can carry them into the
893
930
  // matching form control, where the form renderer reads them at fill time.
@@ -1056,6 +1093,14 @@ export async function executeFormDraftWrite(session, args) {
1056
1093
  hasDraft: sheet.existDraft === true || typeof sheet.draftViewJson === "string",
1057
1094
  ...(formButtons !== null ? { formButtons, formButtonCodes: formButtons.map((button) => String(button.code ?? "")) } : {}),
1058
1095
  ...(mergedOptions !== undefined ? { customButtonJsonWritten: true } : {}),
1096
+ ...(roundTripFaces.size > 0
1097
+ ? {
1098
+ layoutGate: "platform-roundtrip",
1099
+ layoutRoundTripFaces: [...roundTripFaces],
1100
+ layoutNote: "本次布局与平台读回的原样布局一致,形状告警只提示不拒绝(平台自身产出可能含子表列行/系统行超宽)",
1101
+ ...(layoutWarnings.length > 0 ? { layoutWarnings } : {}),
1102
+ }
1103
+ : {}),
1059
1104
  ...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}),
1060
1105
  },
1061
1106
  };