@stalfh233/omc-cli 0.4.75 → 0.4.76
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/artifacts.js +2 -2
- package/dist/cli.js +91 -40
- package/dist/command-transport.js +1 -1
- package/dist/compute.js +2 -1
- package/dist/discovery.js +18 -0
- package/dist/form-layout.js +25 -2
- package/dist/form-support.js +52 -7
- package/dist/lanes/datarule.js +33 -9
- package/dist/lanes/form-contract.js +2 -2
- package/dist/lanes/form.js +1 -0
- package/dist/path-projection.js +129 -0
- package/dist/write.js +9 -3
- package/package.json +3 -3
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 {
|
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 {
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
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/form-layout.js
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/dist/form-support.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
};
|
package/dist/lanes/datarule.js
CHANGED
|
@@ -453,14 +453,37 @@ function childFormulaBareReferences(formula, childFieldCodes) {
|
|
|
453
453
|
* field (→ platform 50000 on submit). Field-aware via the child schema's
|
|
454
454
|
* published fields; a caller-provided set avoids a duplicate fieldList read.
|
|
455
455
|
*/
|
|
456
|
+
/**
|
|
457
|
+
* 1.4 of issue 2026-10-23: `datarule.list` resolves a CHILD-table schema
|
|
458
|
+
* (bizdatarule/list answers rows) while `bizproperty/list?schemaCode=<child>`
|
|
459
|
+
* answers 601003 业务模型不存在 — the same schema code reads on one face and
|
|
460
|
+
* fails on the other. Do not let the field-metadata probe veto a write that the
|
|
461
|
+
* rule face itself accepts: return null (metadata unavailable) and let callers
|
|
462
|
+
* skip the field-level guards with a warning.
|
|
463
|
+
*/
|
|
464
|
+
async function readPublishedFieldCodesOrNull(session, schemaCode) {
|
|
465
|
+
try {
|
|
466
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
467
|
+
return new Set(rows.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (error instanceof WireError && error.kind === "business" && (error.errcode === 601003 || error.errcode === 10011))
|
|
471
|
+
return null;
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
456
475
|
async function assertChildFormulaRefs(session, args, knownChildFields) {
|
|
457
476
|
if (args.dataRuleType !== 2 || args.childRule !== true)
|
|
458
477
|
return;
|
|
459
478
|
const formula = type2FormulaText(args.options);
|
|
460
479
|
if (formula === null)
|
|
461
480
|
return;
|
|
462
|
-
const codes = knownChildFields
|
|
463
|
-
|
|
481
|
+
const codes = knownChildFields === undefined ? await readPublishedFieldCodesOrNull(session, args.schemaCode) : knownChildFields;
|
|
482
|
+
// Metadata unavailable (child-table schema on a face that only answers the
|
|
483
|
+
// rule side): the bare-reference heuristic needs the field list, so skip it
|
|
484
|
+
// rather than refusing a write the platform accepts.
|
|
485
|
+
if (codes === null)
|
|
486
|
+
return;
|
|
464
487
|
const bare = childFormulaBareReferences(formula, codes);
|
|
465
488
|
if (bare.length === 0)
|
|
466
489
|
return;
|
|
@@ -658,8 +681,8 @@ export async function syncFormConditionDataRules(session, schemaCode, fieldCode,
|
|
|
658
681
|
const existing = await listDataRules(session, schemaCode);
|
|
659
682
|
// The designer's rule rows live on the PUBLISHED face (like datarule.create's
|
|
660
683
|
// own guard); a draft-only field cannot carry a designer-visible row yet.
|
|
661
|
-
const
|
|
662
|
-
|
|
684
|
+
const publishedCodes = await readPublishedFieldCodesOrNull(session, schemaCode);
|
|
685
|
+
const published = publishedCodes === null ? true : publishedCodes.has(fieldCode);
|
|
663
686
|
for (const key of changedKeys) {
|
|
664
687
|
const dataRuleType = FORM_CONDITION_RULE_TYPES[key];
|
|
665
688
|
const next = text(nextOptions, key);
|
|
@@ -726,12 +749,13 @@ export function registerDataRuleLane(tools) {
|
|
|
726
749
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
727
750
|
assertDataRuleTarget(args);
|
|
728
751
|
const codes = dataRuleTargetCodes(args);
|
|
729
|
-
const
|
|
730
|
-
const published = new Set(fields.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
752
|
+
const published = await readPublishedFieldCodesOrNull(session, args.schemaCode);
|
|
731
753
|
await assertChildFormulaRefs(session, args, published);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
754
|
+
if (published !== null) {
|
|
755
|
+
const missing = codes.filter((code) => !published.has(code));
|
|
756
|
+
if (missing.length > 0)
|
|
757
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST` });
|
|
758
|
+
}
|
|
735
759
|
const existingRules = await listDataRules(session, args.schemaCode);
|
|
736
760
|
// dataRuleType 1 (CHECK_RULE) fans out into 5 checkTypes that COEXIST on one
|
|
737
761
|
// field (the platform auto-creates a checkType 4 maxLength row per text
|
|
@@ -19,7 +19,7 @@ export const FORM_WIRE = Object.freeze({
|
|
|
19
19
|
actionsColumns: "ticket 100: draftActionsJson/publishedActionsJson are REAL bizsheet columns (BizForm.java:95-100 @SerializableField; BizFormModel.java:59-62 serialized by bizsheet/get) — the designer writes draftActionsJson on save and both faces on publish (frontend-dist/admin/js/4280.cfa75764.js offsets 193538/185347); form.get full:true surfaces them; publish promotes draftActionsJson → publishedActionsJson",
|
|
20
20
|
});
|
|
21
21
|
const FORM_GET_DESCRIPTION = '读取默认表单(bizsheet/get)的发布状态与控件清单(紧凑默认)。**发布态以返回的 `published` / `existDraft` 布尔为准**(`published:true`=已发布;`published:false,existDraft:true`=只有草稿,**未发布**)——不要用 `publishedControlCodes` 是否非空来推断(草稿字段会让它非空,误判为已发布)。**每个模型交付前其默认表单必须 `published:true`**:用 `configure_form` 或 `form.draft(rebuild:true)`+`form.publish` 发布,并逐模型 `form.get` 核对。`full:true` 还返回 `options` 与解析后的 `customButtonJson`(表单工具栏按钮)。传 `controls:["code",…]` 只取指定控件的 options(如关联控件的 mappings);传 `full:true` 才返回 draft/published 的完整 view/attributes/html/actions JSON(actions 列为设计器保存/发布的表单级按钮动作)。体积大,仅在需要整体布局时用。';
|
|
22
|
-
const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list).
|
|
22
|
+
const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list). **读回原样回写**:从 `form.get` 取回的 view/attributes 直接回传时加 `fromReadback:true`——工具会与已存草稿/发布面逐字比对,一致才放行(平台自身布局可能含子表列行/系统行默认宽度),被改动过的面照常按 validation 拒绝。**表单工具栏按钮**:`buttons`(数组)或 `customButtonJson`(原样字符串)写进 `biz_sheet.options.customButtonJson`;**要到 form.publish 才生成 h_biz_button(triggerType=FORM)**,且表单页是否显示还取决于流程节点 `buttonPermission.customBtns[].enabled`(设计器「表单操作」勾选)。`useLocation` 用 FORM/ROW/LIST,表单工具栏按钮用 FORM。 **表单控件属性(含关联填充 mappings)从字段行派生**:改关联映射的事实源在字段 options.mappings(field.update → field.publish),直接改这里的控件映射会被平台按字段重算覆盖(真机 2026-10-14 实证);字段发布后如需刷新已发布表单的控件映射,用本工具 rebuild:false 重存(保留现有布局与在线JS)再 form.publish。";
|
|
23
23
|
const FORM_PUBLISH_DESCRIPTION = "Publish the default form (bizsheet/publish, proven shape: draft*Json promoted to published*Json verbatim) with published-face readback. Degrade guard: refuses to promote a draft carrying FEWER controls than the published face (form-publish-degrade) — the designer's Save clears drafts non-losslessly; recover via the form version history (/api/api/app/bizsheet/history/list) and rewrite the draft first, or pass allowDegrade:true to force the reduction. rebuild:true is the one-call refresh: discard the existing draft, re-scaffold from the published fields (non-structural control options preserved), then force-promote — use it after a field shape change. **表单工具栏按钮**:传 `buttons`(或 `customButtonJson`)→ 写进 `options.customButtonJson` 并在本次 publish 生成 `h_biz_button(triggerType=FORM, triggerCode=<formCode>)`(平台 `publishBizForm:1022`);`options.customButtonJson` 必须是**字符串化数组**(对象会 50000 syntax error);表单页是否显示再由流程节点 `buttonPermission.customBtns[].enabled` 过滤。";
|
|
24
24
|
const FORM_RUN_BUTTON_DESCRIPTION = "执行表单工具栏按钮(平台同款面:`POST /api/api/runtime/form/executeCustomButton`,body `{bizButtonModel, bizObjectList:[<完整业务对象>], reacquireDataFromDb:true}`)。**与列表行按钮不同**:列表按钮发 `launcher/domain_service_runtime/<schema>/<rule>` + `ids[]`(用 `rule.run`),表单按钮带**整条记录**,不能互换。按钮来源 `GET /api/api/system/getCustomButtons`(只列 triggerType=FORM 的 h_biz_button)。前提:按钮已由 `form.publish` 的 `buttons`/`customButtonJson` 生成。返回是执行结果信封,规则副作用用 `data.load` 另读;被规则 UPDATE 的目标行须是 COMPLETED(引擎查不到 DRAFT)。`302045` = 数据被占用。";
|
|
25
25
|
const CONFIGURE_FORM_DESCRIPTION = "High-level: bind fields into the default form and publish it (form.draft → form.publish), subject to the write permission gate. Delegates to the proven form.draft scaffold + form.publish paths so the real empty-form layout (and any caller-supplied draftHtmlJson) is persisted and promoted — never bypasses with a shortcut body. `sheetCode` is optional: omitted means the model's default sheet (sheetCode = schemaCode).";
|
|
@@ -79,7 +79,7 @@ export function assertFormContract() {
|
|
|
79
79
|
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.get", ["bizsheet/get", "draft/published", "mappings", "`full:true`", "actions"]);
|
|
80
80
|
if (!FORM_WIRE.actionsColumns.includes("BizForm.java:95-100"))
|
|
81
81
|
throw new Error("form contract must pin the actions-columns evidence (ticket 100)");
|
|
82
|
-
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["bizsheet/update", "draftViewJson", "draftAttributesJson", "scaffold", "draftHtmlJson", "form-empty-layout"]);
|
|
82
|
+
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["bizsheet/update", "draftViewJson", "draftAttributesJson", "scaffold", "draftHtmlJson", "form-empty-layout", "fromReadback"]);
|
|
83
83
|
if (!FORM_WIRE.redundantFieldVisibility.includes("propertyPermissions"))
|
|
84
84
|
throw new Error("form contract must carry the redundant-field visibility (propertyPermissions) fact");
|
|
85
85
|
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.publish", ["bizsheet/publish", "draft*Json", "published*Json", "verbatim", "form-publish-degrade", "history/list", "rebuild:true", "force-promote"]);
|
package/dist/lanes/form.js
CHANGED
|
@@ -148,6 +148,7 @@ export function registerFormLane(tools) {
|
|
|
148
148
|
draftHtmlJson: z.string().optional(),
|
|
149
149
|
fieldCodes: z.array(z.string().min(1)).optional().describe("脚手架绑定的已发布字段(按此顺序);由 configure_form/intent 显式传入"),
|
|
150
150
|
rebuild: z.boolean().optional().describe("true 时丢弃现有草稿布局,按已发布字段重新生成(新增字段/子表要用它;默认保留现有草稿)"),
|
|
151
|
+
fromReadback: z.boolean().optional().describe("true = 本次 draftViewJson/draftAttributesJson 来自 form.get 的原样读回(平台自身布局可能含子表列行/系统行超宽);工具会与已存面逐字比对,只有确实一致才放行,被改动过的面照常拒绝"),
|
|
151
152
|
allowEmptyLayout: z.boolean().optional().describe("显式放行空布局(\"[]\"/\"{}\")写入;默认拒绝(form-empty-layout),防设计器 Save 清空后照写丢控件"),
|
|
152
153
|
buttons: z
|
|
153
154
|
.array(z.record(z.unknown()))
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readArtifact } from "./artifacts.js";
|
|
3
|
+
import { artifactRoot } from "./command-transport.js";
|
|
4
|
+
export function projectJsonPath(value, path) {
|
|
5
|
+
// 1.5 of issue 2026-10-23: an array-of-objects selection WITHOUT jq pipes.
|
|
6
|
+
// `.rules[code=Create]` (or `[code="Create"]`) picks the FIRST element whose
|
|
7
|
+
// key matches — exactly the shape callers reached for with
|
|
8
|
+
// `jq '.[] | select(...)'`, which then died on shell quoting.
|
|
9
|
+
const selector = /\[\s*([A-Za-z_][\w.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\]]*?))\s*\]/.exec(path);
|
|
10
|
+
if (selector !== null) {
|
|
11
|
+
const [, key, dq, sq, bare] = selector;
|
|
12
|
+
const expected = String(dq ?? sq ?? bare ?? "").trim();
|
|
13
|
+
const before = path.slice(0, selector.index);
|
|
14
|
+
const after = path.slice(selector.index + selector[0].length);
|
|
15
|
+
const base = projectJsonPath(value, before);
|
|
16
|
+
if (base.missed)
|
|
17
|
+
return base;
|
|
18
|
+
const array = base.value;
|
|
19
|
+
if (!Array.isArray(array))
|
|
20
|
+
return { missed: true, at: before || "$", available: `该层不是数组(是 ${array === null ? "null" : typeof array});选择器 [${key}=${expected}] 需要数组` };
|
|
21
|
+
const readKey = (entry) => (entry !== null && typeof entry === "object" ? String(entry[key] ?? "") : "");
|
|
22
|
+
const index = array.findIndex((entry) => readKey(entry) === expected);
|
|
23
|
+
if (index < 0)
|
|
24
|
+
return { missed: true, at: `${before}[${key}=${expected}]`, available: `数组长度 ${array.length};${key} 的候选值:${[...new Set(array.map(readKey))].slice(0, 12).join("、") || "(无)"}` };
|
|
25
|
+
return after === "" ? { missed: false, value: array[index] } : projectJsonPath(array[index], after);
|
|
26
|
+
}
|
|
27
|
+
const normalized = path
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/^[.\/]+/, "")
|
|
30
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
31
|
+
.replace(/\[(?:"([^"]+)"|'([^']+)')\]/g, (_all, dq, sq) => `.${dq || sq}`)
|
|
32
|
+
.replace(/\.{2,}/g, ".");
|
|
33
|
+
const segments = normalized.split(/[./]/).filter((segment) => segment.length > 0);
|
|
34
|
+
let current = value;
|
|
35
|
+
for (const segment of segments) {
|
|
36
|
+
if (Array.isArray(current)) {
|
|
37
|
+
const index = Number(segment);
|
|
38
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length)
|
|
39
|
+
return { missed: true, at: segment, available: `数组长度 ${current.length}(下标 0-${Math.max(current.length - 1, 0)})` };
|
|
40
|
+
current = current[index];
|
|
41
|
+
}
|
|
42
|
+
else if (current && typeof current === "object") {
|
|
43
|
+
const record = current;
|
|
44
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
45
|
+
const keys = Object.keys(record);
|
|
46
|
+
return { missed: true, at: segment, available: keys.length > 0 ? keys.slice(0, 12) : "(该层是空对象)" };
|
|
47
|
+
}
|
|
48
|
+
current = record[segment];
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return { missed: true, at: segment, available: `路径在第 ${segments.indexOf(segment) + 1} 段之前已到标量/undefined(前段类型 ${current === null ? "null" : typeof current})` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { missed: false, value: current };
|
|
55
|
+
}
|
|
56
|
+
/** Collect every value of a repeatable flag (`--extract a --extract b`). */
|
|
57
|
+
function argvValues(args, name) {
|
|
58
|
+
const values = [];
|
|
59
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
60
|
+
if (args[index] !== name)
|
|
61
|
+
continue;
|
|
62
|
+
const value = args[index + 1];
|
|
63
|
+
if (value !== undefined && !value.startsWith("--"))
|
|
64
|
+
values.push(value);
|
|
65
|
+
}
|
|
66
|
+
return values;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `call --extract/--extract-keys`: resolve paths inside the result's own
|
|
70
|
+
* artifact (or inside the result object when the tool returned no artifact).
|
|
71
|
+
* Returns undefined when there is nothing to project so the caller's result is
|
|
72
|
+
* emitted unchanged.
|
|
73
|
+
*/
|
|
74
|
+
export function projectResultExtract(result, paths, keysOnly) {
|
|
75
|
+
const artifactId = typeof result.detailArtifactId === "string" ? result.detailArtifactId : typeof result.artifactId === "string" ? result.artifactId : undefined;
|
|
76
|
+
// Tools hand an in-process artifact as `detailFile` (a local path);
|
|
77
|
+
// command-transport later swaps it for detailArtifactId. Extraction runs
|
|
78
|
+
// BEFORE that swap, so accept both.
|
|
79
|
+
const detailFile = typeof result.detailFile === "string" ? result.detailFile : undefined;
|
|
80
|
+
let source = result;
|
|
81
|
+
let artifact;
|
|
82
|
+
let readFromArtifact = false;
|
|
83
|
+
try {
|
|
84
|
+
const content = artifactId !== undefined
|
|
85
|
+
? (artifact = readArtifact(artifactRoot(), artifactId)).content
|
|
86
|
+
: detailFile !== undefined
|
|
87
|
+
? readFileSync(detailFile)
|
|
88
|
+
: undefined;
|
|
89
|
+
if (content !== undefined) {
|
|
90
|
+
readFromArtifact = true;
|
|
91
|
+
const text = content.toString("utf8");
|
|
92
|
+
try {
|
|
93
|
+
source = JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
source = text;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// A pruned/expired artifact must not mask the real tool answer.
|
|
102
|
+
return { ...result, extract: { artifactId: artifactId ?? null, error: "artifact not found", hint: "产物已过期或被清理;去掉 --extract 重新调用该工具即可重新生成" } };
|
|
103
|
+
}
|
|
104
|
+
const out = {
|
|
105
|
+
status: result.status ?? "ok",
|
|
106
|
+
...(artifactId !== undefined ? { artifactId, sha256: artifact?.sha256 } : {}),
|
|
107
|
+
from: readFromArtifact ? "artifact" : "result",
|
|
108
|
+
};
|
|
109
|
+
if (keysOnly) {
|
|
110
|
+
const record = source && typeof source === "object" && !Array.isArray(source) ? source : undefined;
|
|
111
|
+
out.keys = record
|
|
112
|
+
? Object.entries(record)
|
|
113
|
+
.map(([key, value]) => ({ key, bytes: Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8"), kind: Array.isArray(value) ? `array[${value.length}]` : value === null ? "null" : typeof value }))
|
|
114
|
+
.sort((left, right) => right.bytes - left.bytes)
|
|
115
|
+
: [];
|
|
116
|
+
}
|
|
117
|
+
if (paths.length > 0) {
|
|
118
|
+
out.extract = paths.map((path) => {
|
|
119
|
+
const projected = projectJsonPath(source, path);
|
|
120
|
+
return projected.missed
|
|
121
|
+
? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
|
|
122
|
+
: { path, content: projected.value ?? null };
|
|
123
|
+
});
|
|
124
|
+
if (paths.length === 1)
|
|
125
|
+
out.content = out.extract[0]?.content ?? null;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=path-projection.js.map
|
package/dist/write.js
CHANGED
|
@@ -22,7 +22,7 @@ import { plannableArgsRaw } from "./plan.js";
|
|
|
22
22
|
import { defineTool, ToolRefusalError } from "./tool-types.js";
|
|
23
23
|
import { assertFieldFamilyAllowed } from "./field-families.js";
|
|
24
24
|
import { validateBusinessCode } from "./code-rules.js";
|
|
25
|
-
import { validateFormLayout } from "./form-layout.js";
|
|
25
|
+
import { hardLayoutViolations, validateFormLayout } from "./form-layout.js";
|
|
26
26
|
import { prepareRuleGraph, validateRuleNodeGraph } from "./lanes/rule-graph.js";
|
|
27
27
|
import { WireError } from "./wire.js";
|
|
28
28
|
/**
|
|
@@ -219,8 +219,14 @@ export function writeTool(tools, name, lane, scope, description, inputSchema, ex
|
|
|
219
219
|
}
|
|
220
220
|
// CFG-form-layout: a supplied draft layout must satisfy the platform's
|
|
221
221
|
// span/row/coverage invariants before it reaches the environment.
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
// `fromReadback:true` means "these faces came from form.get" — the strict
|
|
223
|
+
// shape gate cannot tell a platform layout from a hand-written one without
|
|
224
|
+
// the session, so defer to form.draft's session-aware comparison (which
|
|
225
|
+
// still refuses a modified face). See issue 2026-10-23 §1.3.
|
|
226
|
+
if (name === "form.draft" && args.fromReadback !== true && typeof args.draftViewJson === "string" && typeof args.draftAttributesJson === "string") {
|
|
227
|
+
// Only HARD violations refuse here: the platform's own scaffold/layouts
|
|
228
|
+
// carry overflow/full-row/child-column shapes (issue 2026-10-23 §1.3).
|
|
229
|
+
const violations = hardLayoutViolations(validateFormLayout(parseJsonOrUndefined(args.draftViewJson), parseJsonOrUndefined(args.draftAttributesJson)));
|
|
224
230
|
if (violations.length > 0)
|
|
225
231
|
throw new ToolRefusalError({ status: "refused", blocker: "form-layout-invalid", message: "表单布局未过出门前校验;按 validation 修正(span 白名单/行宽≤24/整行独占/控件覆盖率)", validation: violations });
|
|
226
232
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.76",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.76",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.76",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|