@stalfh233/omc-cli 0.4.88 → 0.4.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-contract.js +97 -7
- package/dist/cli.js +82 -14
- package/dist/goal-routes.js +57 -11
- package/dist/lanes/field.js +74 -2
- package/dist/lanes/list-view.js +21 -1
- package/dist/lanes/model.js +4 -1
- package/dist/lanes/org-tree.js +52 -0
- package/dist/lanes/organization.js +110 -6
- package/dist/lanes/rule-lifecycle.js +26 -5
- package/dist/lanes/runtime-data.js +59 -3
- package/dist/lanes/workflow.js +95 -70
- package/dist/model-purge.js +123 -45
- package/dist/path-projection.js +40 -0
- package/dist/rule-support.js +8 -0
- package/dist/runtime-support.js +1 -0
- package/dist/write.js +1 -1
- package/package.json +3 -3
- package/skills/omc-start-here.md +2 -2
package/dist/cli-contract.js
CHANGED
|
@@ -75,8 +75,83 @@ export function serializeEnvelope(value) {
|
|
|
75
75
|
export function requestId() {
|
|
76
76
|
return randomUUID();
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Derive a runnable next command from a refusal payload. Tools that already
|
|
80
|
+
* carry a structured recovery (an explicit `nextCommand`, an artifact `fetch`
|
|
81
|
+
* hint, `recovery[].command`, command-shaped `checks`, or a prepared
|
|
82
|
+
* `nextSubmit`) do not need their prose parsed; everything else stays null
|
|
83
|
+
* rather than inventing a command that does not apply.
|
|
84
|
+
*/
|
|
85
|
+
export function nextCommandFor(result) {
|
|
86
|
+
const commandFrom = (text) => {
|
|
87
|
+
if (typeof text !== "string")
|
|
88
|
+
return undefined;
|
|
89
|
+
const trimmed = text.trim();
|
|
90
|
+
if (/^omc\s/.test(trimmed))
|
|
91
|
+
return trimmed;
|
|
92
|
+
// Recovery entries are often "① 按流程收窄(最可靠):omc call …" — the
|
|
93
|
+
// command is there, just after a human marker.
|
|
94
|
+
const index = trimmed.indexOf("omc ");
|
|
95
|
+
return index > 0 ? trimmed.slice(index).trim() : undefined;
|
|
96
|
+
};
|
|
97
|
+
const explicit = result.nextCommand;
|
|
98
|
+
if (typeof explicit === "string" && explicit.trim().length > 0)
|
|
99
|
+
return explicit.trim();
|
|
100
|
+
const fromFetch = commandFrom(result.fetch);
|
|
101
|
+
if (fromFetch !== undefined)
|
|
102
|
+
return fromFetch;
|
|
103
|
+
const recovery = result.recovery;
|
|
104
|
+
if (Array.isArray(recovery)) {
|
|
105
|
+
for (const entry of recovery) {
|
|
106
|
+
if (entry === null || typeof entry !== "object")
|
|
107
|
+
continue;
|
|
108
|
+
const command = commandFrom(entry.command);
|
|
109
|
+
if (command !== undefined)
|
|
110
|
+
return command;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const list of [result.checks, result.workaround]) {
|
|
114
|
+
if (!Array.isArray(list))
|
|
115
|
+
continue;
|
|
116
|
+
for (const entry of list) {
|
|
117
|
+
const command = commandFrom(entry);
|
|
118
|
+
if (command !== undefined)
|
|
119
|
+
return command;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// A submit that advanced the flow already prepared the NEXT approval args —
|
|
123
|
+
// expose the command instead of making the caller reassemble JSON by hand.
|
|
124
|
+
const nextSubmit = result.nextSubmit;
|
|
125
|
+
if (nextSubmit !== null && typeof nextSubmit === "object" && !Array.isArray(nextSubmit)) {
|
|
126
|
+
const record = nextSubmit;
|
|
127
|
+
if (typeof record.schemaCode === "string" && typeof record.workItemId === "string") {
|
|
128
|
+
const args = {
|
|
129
|
+
schemaCode: record.schemaCode,
|
|
130
|
+
bizObjectId: record.bizObjectId ?? null,
|
|
131
|
+
workflowInstanceId: record.workflowInstanceId ?? null,
|
|
132
|
+
workItemId: record.workItemId,
|
|
133
|
+
agree: true,
|
|
134
|
+
};
|
|
135
|
+
return `omc call data.submit --args '${JSON.stringify(args)}'`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const nextWorkItems = result.nextWorkItems;
|
|
139
|
+
if (Array.isArray(nextWorkItems) && nextWorkItems.length > 0 && typeof result.schemaCode === "string") {
|
|
140
|
+
const item = nextWorkItems[0];
|
|
141
|
+
if (item !== null && typeof item === "object") {
|
|
142
|
+
const workItemId = item.workItemId;
|
|
143
|
+
if (typeof workItemId === "string")
|
|
144
|
+
return `omc call data.submit --args '${JSON.stringify({ schemaCode: result.schemaCode, workItemId, agree: true })}'`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
78
149
|
export function classifyResult(result) {
|
|
79
150
|
const status = String(result.status ?? "ok");
|
|
151
|
+
const withNext = (error) => {
|
|
152
|
+
const nextCommand = nextCommandFor(result);
|
|
153
|
+
return nextCommand === undefined ? error : { ...error, nextCommand };
|
|
154
|
+
};
|
|
80
155
|
if (status === "ok")
|
|
81
156
|
return { status, error: null, exitCode: CLI_EXIT_CODES.ok };
|
|
82
157
|
if (status === "dry-run")
|
|
@@ -88,30 +163,45 @@ export function classifyResult(result) {
|
|
|
88
163
|
const succeeded = Number(result.succeeded ?? 0);
|
|
89
164
|
const requested = Number(result.requested ?? 0);
|
|
90
165
|
const failed = Number(result.failed ?? 0);
|
|
166
|
+
// A `--preflight` batch shape-checks entries instead of executing them; the
|
|
167
|
+
// message must not claim "N 成功/失败" for calls that never ran.
|
|
168
|
+
// A step-chain command (model-purge) also answers `partial`, and the batch
|
|
169
|
+
// wording read "6/7 成功,0 失败" — meaningless for steps (r15).
|
|
170
|
+
const isBatch = typeof result.tool === "string";
|
|
171
|
+
const message = result.mode === "preflight"
|
|
172
|
+
? `${failed}/${requested} 条未过写前校验(preflight:零 wire、零写入);逐条原因见 failures[]`
|
|
173
|
+
: isBatch
|
|
174
|
+
? `${succeeded}/${requested} 成功,${failed} 失败;逐条原因见 failures[] / failedSteps[](--args-list 的 index 指向第几条)`
|
|
175
|
+
: `部分步骤失败:逐条原因见 steps[] 的 status/detail(已在失败处停下;幂等,修好后重跑同一条命令)`;
|
|
91
176
|
return {
|
|
92
177
|
status,
|
|
93
|
-
error: { code: "partial-batch", message
|
|
178
|
+
error: withNext({ code: "partial-batch", message, retryable: false }),
|
|
94
179
|
exitCode: 1,
|
|
95
180
|
};
|
|
96
181
|
}
|
|
97
182
|
if (status === "unknown-tool") {
|
|
98
183
|
const candidates = Array.isArray(result.candidates) ? result.candidates : [];
|
|
99
184
|
const suffix = candidates.length > 0 ? `;did you mean: ${candidates.join("、")}` : ";用 `omc commands --json --compact --lane <lane>` 或 `omc next` 定位工具";
|
|
100
|
-
return { status: "error", error: { code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }, exitCode: CLI_EXIT_CODES.invalidInput };
|
|
185
|
+
return { status: "error", error: withNext({ code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
|
|
101
186
|
}
|
|
102
187
|
if (result.blocker === "tool-input-invalid")
|
|
103
|
-
return { status: "error", error: { code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }, exitCode: CLI_EXIT_CODES.invalidInput };
|
|
188
|
+
return { status: "error", error: withNext({ code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
|
|
104
189
|
if (status === "uncertain")
|
|
105
|
-
return { status, error: { code: "uncertain", message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }, exitCode: CLI_EXIT_CODES.uncertain };
|
|
190
|
+
return { status, error: withNext({ code: "uncertain", message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }), exitCode: CLI_EXIT_CODES.uncertain };
|
|
106
191
|
if (status === "refused") {
|
|
107
192
|
if (result.blocker === "write-lock-held")
|
|
108
|
-
return { status, error: { code: "policy-refused", message: String(result.message ?? "write lock held"), retryable: true }, exitCode: CLI_EXIT_CODES.policyRefused };
|
|
193
|
+
return { status, error: withNext({ code: "policy-refused", message: String(result.message ?? "write lock held"), retryable: true }), exitCode: CLI_EXIT_CODES.policyRefused };
|
|
109
194
|
const auth = String(result.blocker ?? "").startsWith("auth") || String(result.blocker ?? "").includes("credential");
|
|
110
|
-
|
|
195
|
+
// Every refusal gets a next step: a specific command when the tool knows
|
|
196
|
+
// one, otherwise the refusal router (`omc-refusals` explains which gate
|
|
197
|
+
// produced it — the documented path in omc-start-here).
|
|
198
|
+
const error = { code: auth ? "auth-config" : "policy-refused", message: String(result.message ?? result.blocker ?? "operation refused"), retryable: false };
|
|
199
|
+
const nextCommand = nextCommandFor(result) ?? (auth ? undefined : "omc help omc-refusals");
|
|
200
|
+
return { status, error: nextCommand === undefined ? error : { ...error, nextCommand }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
|
|
111
201
|
}
|
|
112
202
|
const message = String(result.message ?? "internal error");
|
|
113
203
|
const auth = /credential|configuration|environment|login|authentication|auth/i.test(message);
|
|
114
|
-
return { status: "error", error: { code: auth ? "auth-config" : "internal", message, retryable: false }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.internal };
|
|
204
|
+
return { status: "error", error: withNext({ code: auth ? "auth-config" : "internal", message, retryable: false }), exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.internal };
|
|
115
205
|
}
|
|
116
206
|
export function errorEnvelope(error, code, exitCode, request) {
|
|
117
207
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/cli.js
CHANGED
|
@@ -15,12 +15,12 @@ import { BATCH_MAX_ENTRIES, runBatchCall } from "./batch-call.js";
|
|
|
15
15
|
import { cmdModelScaffold } from "./model-scaffold.js";
|
|
16
16
|
import { cmdModelPurge } from "./model-purge.js";
|
|
17
17
|
import { environmentWritable, requiresProductionConfirmation } from "./environment-policy.js";
|
|
18
|
-
import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, requestId } from "./cli-contract.js";
|
|
18
|
+
import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, refusalText, requestId } from "./cli-contract.js";
|
|
19
19
|
import { configureOutput, humanSummary, stripOutputFlags, writeEnvelope } from "./cli-output.js";
|
|
20
20
|
import { beginUsage, completeUsage, ensureProjectIgnore, recordUsageOutput, usageReport } from "./usage-log.js";
|
|
21
21
|
import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
|
|
22
22
|
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
|
|
23
|
-
import { projectJsonPath, projectResultExtract } from "./path-projection.js";
|
|
23
|
+
import { projectJsonPath, projectJsonPathFanout, projectResultExtract } from "./path-projection.js";
|
|
24
24
|
import { basename, join, resolve } from "node:path";
|
|
25
25
|
import { buildToolRegistry } from "./tools.js";
|
|
26
26
|
import { acquireWriteLock } from "./write-lock.js";
|
|
@@ -319,6 +319,52 @@ async function cmdBatchCall(store, toolName, rawList, options) {
|
|
|
319
319
|
}));
|
|
320
320
|
return CLI_EXIT_CODES.invalidInput;
|
|
321
321
|
}
|
|
322
|
+
// Whole-batch shape precheck (issue 2026-10-25 §6): `--args-list --dry-run`
|
|
323
|
+
// runs every entry through the full tool (reads included) to report N
|
|
324
|
+
// previews. When the caller only wants "will this batch be accepted", the
|
|
325
|
+
// per-tool semantic gate is the thing that refuses mis-shapes, and it is
|
|
326
|
+
// pure — no wire, no writes. Run it for every entry and report ALL failures
|
|
327
|
+
// in one envelope instead of N round-trips.
|
|
328
|
+
if (options.preflight === true) {
|
|
329
|
+
const registry = buildToolRegistry();
|
|
330
|
+
const tool = registry.find((definition) => definition.name === toolName);
|
|
331
|
+
if (!tool) {
|
|
332
|
+
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `unknown tool ${toolName};用 omc next 或 omc commands --json --compact --lane <lane> 定位`, retryable: false }));
|
|
333
|
+
return CLI_EXIT_CODES.invalidInput;
|
|
334
|
+
}
|
|
335
|
+
const failures = [];
|
|
336
|
+
entries.forEach((entry, index) => {
|
|
337
|
+
try {
|
|
338
|
+
const parsedEntry = parseToolInput(tool, {
|
|
339
|
+
...entry,
|
|
340
|
+
...(options.environment !== undefined && entry.environment === undefined ? { environment: options.environment } : {}),
|
|
341
|
+
});
|
|
342
|
+
tool.writePreflight?.(parsedEntry);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
// ToolRefusalError carries the real payload (blocker/citation/validation);
|
|
346
|
+
// rebuild it only when the failure is a plain Error.
|
|
347
|
+
const carried = error.payload;
|
|
348
|
+
const payload = error instanceof ToolInputError ? error.payload : carried ?? { status: "refused", blocker: "tool-input-invalid", message: error instanceof Error ? error.message : String(error) };
|
|
349
|
+
failures.push({ index, blocker: payload.blocker ?? null, message: refusalText(payload), ...(Array.isArray(payload.validation) ? { validation: payload.validation } : {}), ...(payload.citation !== undefined ? { citation: payload.citation } : {}) });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const result = {
|
|
353
|
+
status: failures.length > 0 ? "partial" : "ok",
|
|
354
|
+
mode: "preflight",
|
|
355
|
+
tool: toolName,
|
|
356
|
+
requested: entries.length,
|
|
357
|
+
shapeChecked: entries.length,
|
|
358
|
+
failed: failures.length,
|
|
359
|
+
writes: 0,
|
|
360
|
+
// On failure the caller has to change the JSON first, so there is no
|
|
361
|
+
// runnable command yet; on success, hand over the exact execution command.
|
|
362
|
+
...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list ${JSON.stringify(JSON.stringify(entries))}` }),
|
|
363
|
+
};
|
|
364
|
+
const classified = classifyResult(result);
|
|
365
|
+
writeEnvelope(envelope(classified.status, result, request, classified.error));
|
|
366
|
+
return failures.length > 0 ? 1 : 0;
|
|
367
|
+
}
|
|
322
368
|
const batched = await runBatchCall({
|
|
323
369
|
tool: toolName,
|
|
324
370
|
argsList: entries,
|
|
@@ -352,7 +398,7 @@ async function cmdCall(store, args) {
|
|
|
352
398
|
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")));
|
|
353
399
|
const toolName = positional[0];
|
|
354
400
|
if (!toolName || toolName === "help" || args.includes("--help")) {
|
|
355
|
-
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--extract-keys]");
|
|
401
|
+
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
|
|
356
402
|
console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
|
|
357
403
|
return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
|
|
358
404
|
}
|
|
@@ -361,14 +407,19 @@ async function cmdCall(store, args) {
|
|
|
361
407
|
// Batch mode: the SAME tool N times, one process (r12: 36 field.create calls).
|
|
362
408
|
const argsList = flag("--args-list");
|
|
363
409
|
if (argsList !== undefined)
|
|
364
|
-
return cmdBatchCall(store, toolName, argsList, { environment, requestValue, args });
|
|
410
|
+
return cmdBatchCall(store, toolName, argsList, { environment, requestValue, args, preflight: args.includes("--preflight") });
|
|
365
411
|
// 1.2 of issue 2026-10-23: read faces answer {detailArtifactId, sha256} and the
|
|
366
412
|
// caller then needs a SECOND call to see one value (16 two-step chains in one
|
|
367
413
|
// session; 15 more were pure --keys probes). `--extract <path>` (repeatable)
|
|
368
414
|
// resolves the path inside the artifact of THIS result and returns only the
|
|
369
415
|
// value; `--extract-keys` lists the artifact's top-level keys with sizes.
|
|
370
|
-
|
|
371
|
-
|
|
416
|
+
//
|
|
417
|
+
// issue 2026-10-25 §8: `artifact get` had `--jq` while `omc call` did not, so
|
|
418
|
+
// every list read that only wanted one column paid a second artifact round.
|
|
419
|
+
// `--jq` is the same path projection plus `[]` fan-out; a bare `--jq` is the
|
|
420
|
+
// key inventory (the same muscle memory as `artifact get <id> --jq`).
|
|
421
|
+
const extractPaths = [...argvValues(args, "--extract"), ...argvValues(args, "--jq")];
|
|
422
|
+
const extractKeys = args.includes("--extract-keys") || args.some((token, index) => token === "--jq" && (args[index + 1] === undefined || args[index + 1].startsWith("--")));
|
|
372
423
|
const dryRun = args.includes("--dry-run");
|
|
373
424
|
const waitValue = flag("--wait");
|
|
374
425
|
let waitMs = 0;
|
|
@@ -547,6 +598,23 @@ function parseDurationMs(text) {
|
|
|
547
598
|
* template re-read three times is 81 KB of the session.
|
|
548
599
|
*/
|
|
549
600
|
const ARTIFACT_INLINE_CEILING_BYTES = 8192;
|
|
601
|
+
/**
|
|
602
|
+
* `artifact get --jq`: one path walk, with `[]` fan-out (same syntax as
|
|
603
|
+
* `omc call --jq`). Returns the envelope fields so both the `--summary` and the
|
|
604
|
+
* plain branch stay identical.
|
|
605
|
+
*/
|
|
606
|
+
function projectArtifactPath(value, path) {
|
|
607
|
+
if (!path.includes("[]")) {
|
|
608
|
+
const projected = projectJsonPath(value, path);
|
|
609
|
+
return projected.missed
|
|
610
|
+
? { missed: true, ...(projected.at !== undefined ? { at: projected.at } : {}), ...(projected.available !== undefined ? { available: projected.available } : {}) }
|
|
611
|
+
: { missed: false, content: projected.value ?? null };
|
|
612
|
+
}
|
|
613
|
+
const fanout = projectJsonPathFanout(value, path);
|
|
614
|
+
return fanout.missed
|
|
615
|
+
? { missed: true, ...(fanout.at !== undefined ? { at: fanout.at } : {}), ...(fanout.available !== undefined ? { available: fanout.available } : {}) }
|
|
616
|
+
: { missed: false, content: fanout.values, mapped: fanout.values.length, ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries } : {}) };
|
|
617
|
+
}
|
|
550
618
|
function formatBytes(bytes) {
|
|
551
619
|
if (bytes < 1024)
|
|
552
620
|
return `${bytes}B`;
|
|
@@ -579,10 +647,10 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
579
647
|
// this the caller had to spend one round on the shape and another on the
|
|
580
648
|
// path (or pull the whole body); r5 hit that on 67 KB workflow templates.
|
|
581
649
|
if (options.jq !== undefined) {
|
|
582
|
-
const projected =
|
|
650
|
+
const projected = projectArtifactPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
|
|
583
651
|
return emitTransportResult(projected.missed
|
|
584
652
|
? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }, fetch: artifactFetchHint(id) }
|
|
585
|
-
: { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: projected.
|
|
653
|
+
: { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}) } }, transport);
|
|
586
654
|
}
|
|
587
655
|
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, fetch: artifactFetchHint(id) }, transport);
|
|
588
656
|
}
|
|
@@ -611,10 +679,10 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
611
679
|
return emitTransportResult(result, transport);
|
|
612
680
|
}
|
|
613
681
|
if (options.jq !== undefined) {
|
|
614
|
-
const projected =
|
|
682
|
+
const projected = projectArtifactPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
|
|
615
683
|
return emitTransportResult(projected.missed
|
|
616
|
-
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0
|
|
617
|
-
: { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.
|
|
684
|
+
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价;[] 映射数组(items[].fields.x)` }
|
|
685
|
+
: { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}) }, transport);
|
|
618
686
|
}
|
|
619
687
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
620
688
|
// large ones (up to 55 KB) pulled whole workflow templates just to read one
|
|
@@ -1090,7 +1158,7 @@ function onboardingHelp() {
|
|
|
1090
1158
|
"首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
|
|
1091
1159
|
"方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。",
|
|
1092
1160
|
"目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
|
|
1093
|
-
"大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --
|
|
1161
|
+
"大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --jq <path>(可重复;`[]` 映射数组,如 items[].fields.planNo)或 --extract/--extract-keys,省掉再跑一次 omc artifact get;artifact get 正文超 8KB 默认也只回形状(--jq/--keys 定向取,--full 才整包)。批量形状预检用 --args-list … --preflight(零 wire、零写入,一次报全)。",
|
|
1094
1162
|
"工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
|
|
1095
1163
|
"单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
|
|
1096
1164
|
"写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
|
|
@@ -1214,8 +1282,8 @@ async function main() {
|
|
|
1214
1282
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
1215
1283
|
});
|
|
1216
1284
|
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); });
|
|
1217
|
-
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').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) => {
|
|
1218
|
-
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.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(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"] : [])];
|
|
1285
|
+
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").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) => {
|
|
1286
|
+
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.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", 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"] : [])];
|
|
1219
1287
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
1220
1288
|
});
|
|
1221
1289
|
command.command("compute").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--target <propertyCode>").option("--property <propertyCode>").option("--formula <formula>").option("--where <condition>", "条件聚合:\"<子列code> = <值>\",与 --formula \"{<子表code>.<子列code>}\" 配合编译为 COUNTIF/SUMIF").option("--aggregate <count|sum>", "条件聚合用哪种:count(默认,→COUNTIF)/ sum(→SUMIF)").option("--allow-unfiltered", "显式接受「对整个子表列无条件聚合」;不给则拒绝(会把不符合条件的行也算进去)").option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n字段计算规则拼装器:公式 {字段} 引用 → datarule type2 完整形状(editorText/editorMark 自动生成)。默认 dry-run,--apply 写入。条件聚合用 --formula \"{子表.子列}\" --where \"子列 = 值\"。复杂场景(条件分支/跨模型)走 omc next 的 recipe 路径。\n").action(async (options) => {
|
package/dist/goal-routes.js
CHANGED
|
@@ -113,6 +113,29 @@ function capabilityScores(registry, goal) {
|
|
|
113
113
|
}
|
|
114
114
|
return scores;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Default ladder examples (r15). A ladder step with no goal-matching recipe only
|
|
118
|
+
* got `omc commands --lane`, so "创建业务规则" handed the caller nothing runnable
|
|
119
|
+
* even though `rule.save` ships examples. These are the lane's canonical shapes,
|
|
120
|
+
* used ONLY when the goal does not match a specific recipe.
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_LADDER_RECIPES = {
|
|
123
|
+
businessrule: { tool: "rule.save", recipe: "business-rule-branch-conditional" },
|
|
124
|
+
datarule: { tool: "datarule.create", recipe: "field-compute-rule" },
|
|
125
|
+
};
|
|
126
|
+
function defaultRecipeFor(registry, capabilityId, lane) {
|
|
127
|
+
const wanted = DEFAULT_LADDER_RECIPES[capabilityId];
|
|
128
|
+
if (!wanted)
|
|
129
|
+
return undefined;
|
|
130
|
+
for (const tool of registry) {
|
|
131
|
+
if (tool.name !== wanted.tool || tool.lane !== lane)
|
|
132
|
+
continue;
|
|
133
|
+
const recipe = (tool.executionMetadata?.recipes ?? []).find((entry) => entry.id === wanted.recipe);
|
|
134
|
+
if (recipe)
|
|
135
|
+
return { tool: tool.name, recipe: recipe.id, title: recipe.title };
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
116
139
|
function bestRecipe(registry, capabilityId, lane, goal, minScore = 3) {
|
|
117
140
|
let best;
|
|
118
141
|
for (const tool of registry) {
|
|
@@ -238,17 +261,27 @@ function sectionPointersFor(ids) {
|
|
|
238
261
|
return pointers;
|
|
239
262
|
}
|
|
240
263
|
/**
|
|
241
|
-
* Ready-to-run methodology reads
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
264
|
+
* Ready-to-run methodology reads.
|
|
265
|
+
*
|
|
266
|
+
* Round 15 (bench60-62) collapsed ~18 per-section reads into one per topic to
|
|
267
|
+
* save turns. r10 F7 found the other edge: following that hint fetched
|
|
268
|
+
* EVERYTHING at once (~15 KB) when most tasks need only the first section.
|
|
269
|
+
* So the primary command per topic fetches the FIRST required section, and the
|
|
270
|
+
* remaining sections are offered as separate incremental commands (each keeps
|
|
271
|
+
* its byte count in `readSections`) — cheap by default, still one command away.
|
|
246
272
|
*/
|
|
247
|
-
function
|
|
273
|
+
function splitReadCommands(pointers) {
|
|
248
274
|
const byTopic = new Map();
|
|
249
275
|
for (const pointer of pointers)
|
|
250
276
|
byTopic.set(pointer.topic, [...(byTopic.get(pointer.topic) ?? []), pointer.section]);
|
|
251
|
-
|
|
277
|
+
const readCommands = [];
|
|
278
|
+
const readMore = [];
|
|
279
|
+
for (const [topic, sections] of byTopic.entries()) {
|
|
280
|
+
readCommands.push(`omc help ${topic} --section "${sections[0]}"`);
|
|
281
|
+
for (const section of sections.slice(1))
|
|
282
|
+
readMore.push(`omc help ${topic} --section "${section}"`);
|
|
283
|
+
}
|
|
284
|
+
return { readCommands, readMore };
|
|
252
285
|
}
|
|
253
286
|
/**
|
|
254
287
|
* The route's methodology-read surface: pointers (+ per-section bytes) +
|
|
@@ -279,14 +312,15 @@ function routeReads(readSections) {
|
|
|
279
312
|
}
|
|
280
313
|
}
|
|
281
314
|
}
|
|
282
|
-
|
|
315
|
+
const commands = splitReadCommands(readSections);
|
|
316
|
+
return { readSections: budgeted, readCommands: commands.readCommands, ...(commands.readMore.length > 0 ? { readMore: commands.readMore } : {}), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
|
|
283
317
|
}
|
|
284
318
|
/** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
|
|
285
319
|
function sectionBytes(pointer) {
|
|
286
320
|
const resolved = readSkillSection(pointer.topic, pointer.section);
|
|
287
321
|
return resolved ? Buffer.byteLength(resolved.section, "utf8") : 0;
|
|
288
322
|
}
|
|
289
|
-
const READ_HINT = '
|
|
323
|
+
const READ_HINT = '先跑 readCommands(每个 topic 只取第一节,形状 `omc help <topic> --section "<section>"`,字节数见 readSections);不够再按需补 readMore,不要一次全读。`omc help <topic>` 默认只回节索引(每节带字节数,约 1KB),不要再用 `--all` 读整篇(5–14KB,读一次就留在后续每一轮)。';
|
|
290
324
|
function orderedCapabilities(ids, index) {
|
|
291
325
|
const selected = new Set(ids);
|
|
292
326
|
const nodes = [...index.nodeById.values()];
|
|
@@ -322,6 +356,18 @@ function orderedCapabilities(ids, index) {
|
|
|
322
356
|
*/
|
|
323
357
|
const WHOLE_PACKAGE_CAPABILITIES = ["model", "field", "form", "listview", "datarule", "businessrule", "workflow", "onlinejs"];
|
|
324
358
|
const COMPOSER_ROUTES = Object.freeze([
|
|
359
|
+
{
|
|
360
|
+
id: "model-purge",
|
|
361
|
+
command: "omc model-purge --code <模型code> --purge-instances --apply",
|
|
362
|
+
usage: "omc model-purge --help",
|
|
363
|
+
summary: "清理一个模型:先删流程实例,再 model.delete 级联带走流程模板/视图/字段/表单/规则;默认 dry-run,不可删的停滞实例会跳过并在结尾列出",
|
|
364
|
+
// r15: "清理本轮创建的模型和流程实例" matched approval-flow (流程 + 创建)
|
|
365
|
+
// and would have CREATED a workflow instead of removing one. Cleanup intents
|
|
366
|
+
// are their own composer, and this one comes first.
|
|
367
|
+
groups: [["清理", "删除", "移除", "回收", "清除", "purge", "teardown", "clean", "remove"], ["模型", "实例", "数据", "model", "instance", "data"]],
|
|
368
|
+
avoid: ["字段", "角色", "部门", "视图", "按钮", "规则图"],
|
|
369
|
+
topics: ["omc-write-hazards"],
|
|
370
|
+
},
|
|
325
371
|
{
|
|
326
372
|
id: "model-scaffold",
|
|
327
373
|
command: "omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON数组>' --apply",
|
|
@@ -339,7 +385,7 @@ const COMPOSER_ROUTES = Object.freeze([
|
|
|
339
385
|
summary: "线性审批流一条命令:建流程 → 模板+字段只读权限 → 发布(多级用重复 --steps)",
|
|
340
386
|
groups: [["审批", "流程"], ["建", "创建", "新建", "配置", "加", "搭"]],
|
|
341
387
|
// Branching / countersign shapes are NOT this composer's business.
|
|
342
|
-
avoid: ["条件分支", "分支", "会签", "并行", "或签", "网关", "子流程", "动态审批人"],
|
|
388
|
+
avoid: ["条件分支", "分支", "会签", "并行", "或签", "网关", "子流程", "动态审批人", "清理", "删除", "移除", "purge", "teardown"],
|
|
343
389
|
topics: ["omc-five-piece-flow"],
|
|
344
390
|
},
|
|
345
391
|
{
|
|
@@ -531,7 +577,7 @@ export function goalRoute(registry, goal) {
|
|
|
531
577
|
if (readish) {
|
|
532
578
|
return { capability: id, lane: capability.lane, readTools: readToolsInLane(registry, capability.lane), firstCommand: `omc capability ${id}` };
|
|
533
579
|
}
|
|
534
|
-
const recipe = bestRecipe(registry, id, capability.lane, goal);
|
|
580
|
+
const recipe = bestRecipe(registry, id, capability.lane, goal) ?? defaultRecipeFor(registry, id, capability.lane);
|
|
535
581
|
return {
|
|
536
582
|
capability: id,
|
|
537
583
|
lane: capability.lane,
|
package/dist/lanes/field.js
CHANGED
|
@@ -45,6 +45,54 @@ function fieldRollbackSpec(toolName) {
|
|
|
45
45
|
function parseFieldOptionsForReadback(options) {
|
|
46
46
|
return decodeJsonColumn(options, JSON_COLUMN_PASSTHROUGH);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* CHILD_TABLE(8) columns as a compact list (issue 2026-10-25 §7).
|
|
50
|
+
*
|
|
51
|
+
* The stored `subSchema` is the full designer payload — system columns included,
|
|
52
|
+
* ~37.6 KB for a 10-column child table when asked for one field by code. A caller
|
|
53
|
+
* almost always wants the column codes (to build formData / verify the attach);
|
|
54
|
+
* the raw payload stays available behind `full:true`. Accepts both the object and
|
|
55
|
+
* the JSON-string wire form.
|
|
56
|
+
*/
|
|
57
|
+
function childColumnsOf(subSchema) {
|
|
58
|
+
let parsed = subSchema;
|
|
59
|
+
if (typeof subSchema === "string") {
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(subSchema);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (parsed === null || typeof parsed !== "object")
|
|
68
|
+
return [];
|
|
69
|
+
const properties = parsed.properties;
|
|
70
|
+
if (!Array.isArray(properties))
|
|
71
|
+
return [];
|
|
72
|
+
return properties
|
|
73
|
+
.filter((child) => child !== null && typeof child === "object")
|
|
74
|
+
.map((child) => {
|
|
75
|
+
const rawType = child.propertyType ?? child.type;
|
|
76
|
+
const type = rawType === undefined || rawType === null || Number.isNaN(Number(rawType)) ? null : Number(rawType);
|
|
77
|
+
const typeLabel = type === null ? undefined : fieldTypeLabel(type);
|
|
78
|
+
const code = String(child.code ?? child.propertyCode ?? "");
|
|
79
|
+
return {
|
|
80
|
+
code,
|
|
81
|
+
name: String(child.name ?? child.code ?? child.propertyCode ?? ""),
|
|
82
|
+
type,
|
|
83
|
+
...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
|
|
84
|
+
...(typeof child.relativeCode === "string" && child.relativeCode ? { relativeCode: child.relativeCode } : {}),
|
|
85
|
+
// r15 feedback: the projection mixed the platform's system columns with
|
|
86
|
+
// the authored ones, so "which child columns did I declare?" still needed
|
|
87
|
+
// a filter. Keep every column but mark the system ones.
|
|
88
|
+
// `parentId` / `sortKey` are injected by the platform into every child
|
|
89
|
+
// schema (they are the child row's link + order), so they are system
|
|
90
|
+
// columns here even though a MAIN model never carries them.
|
|
91
|
+
...(SYSTEM_FIELD_CODES.has(code) || code === "parentId" || code === "sortKey" ? { system: true } : {}),
|
|
92
|
+
};
|
|
93
|
+
})
|
|
94
|
+
.filter((child) => child.code.length > 0);
|
|
95
|
+
}
|
|
48
96
|
/**
|
|
49
97
|
* `name_i18n` is a JSON locale map. When every locale value equals the display
|
|
50
98
|
* `name` there is no translation to convey, so echoing the map on every field of
|
|
@@ -117,7 +165,21 @@ export function registerFieldLane(tools) {
|
|
|
117
165
|
...(redundantNameI18n(row.name_i18n, row.name) ? {} : typeof row.name_i18n === "string" ? { name_i18n: row.name_i18n } : {}),
|
|
118
166
|
type: row.type ?? row.propertyType,
|
|
119
167
|
...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
|
|
120
|
-
|
|
168
|
+
// A CHILD_TABLE row's `subSchema` is the whole designer payload; the
|
|
169
|
+
// default face carries the column list + count and keeps the raw
|
|
170
|
+
// payload behind `full:true` (issue 2026-10-25 §7: one field read was
|
|
171
|
+
// 37.6 KB for a 5-code answer).
|
|
172
|
+
...(row.subSchema !== undefined && row.subSchema !== null
|
|
173
|
+
? args.full === true
|
|
174
|
+
? { subSchema: row.subSchema }
|
|
175
|
+
: (() => {
|
|
176
|
+
const subColumns = childColumnsOf(row.subSchema);
|
|
177
|
+
const businessColumns = subColumns.filter((column) => column.system !== true).length;
|
|
178
|
+
return subColumns.length > 0
|
|
179
|
+
? { subColumnCount: subColumns.length, businessSubColumnCount: businessColumns, subColumns, subSchemaHint: "子表列已投影为 code/name/type(system:true 是平台注入的系统列);要平台原始 subSchema 加 full:true" }
|
|
180
|
+
: { subSchema: row.subSchema };
|
|
181
|
+
})()
|
|
182
|
+
: {}),
|
|
121
183
|
...(row.relativeCode ? { relativeCode: row.relativeCode } : {}),
|
|
122
184
|
...(typeof row.relativePropertyCode === "string" && row.relativePropertyCode ? { relativePropertyCode: row.relativePropertyCode } : {}),
|
|
123
185
|
...(options !== undefined ? { options } : {}),
|
|
@@ -150,11 +212,20 @@ export function registerFieldLane(tools) {
|
|
|
150
212
|
const value = options[key];
|
|
151
213
|
return key === "readonlyFormula" ? value === true : typeof value === "string" && value.trim() !== "";
|
|
152
214
|
});
|
|
215
|
+
// Inventory reads could not see child columns at all (r13 §7): the
|
|
216
|
+
// only face that carried them was the 37.6 KB subSchema. Codes alone
|
|
217
|
+
// are ~12 B each and answer "did the child columns land?".
|
|
218
|
+
const subColumns = field.subColumns;
|
|
219
|
+
const businessCodes = Array.isArray(subColumns) ? subColumns.filter((column) => column.system !== true).map((column) => column.code) : undefined;
|
|
153
220
|
return {
|
|
154
221
|
code: field.code,
|
|
155
222
|
type: field.type,
|
|
156
223
|
...(field.typeName !== undefined ? { typeName: field.typeName } : {}),
|
|
157
224
|
...(field.name !== undefined ? { name: field.name } : {}),
|
|
225
|
+
// Inventory answers "which columns did I author?" — system
|
|
226
|
+
// columns are reported as a count, not mixed into the list.
|
|
227
|
+
...(businessCodes !== undefined ? { subColumnCodes: businessCodes } : {}),
|
|
228
|
+
...(Array.isArray(subColumns) && subColumns.length > businessCodes.length ? { systemSubColumnCount: subColumns.length - businessCodes.length } : {}),
|
|
158
229
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
159
230
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
160
231
|
};
|
|
@@ -202,6 +273,7 @@ export function registerFieldLane(tools) {
|
|
|
202
273
|
type: field.type,
|
|
203
274
|
...(field.typeName !== undefined ? { typeName: field.typeName } : {}),
|
|
204
275
|
...(field.name !== undefined ? { name: field.name } : {}),
|
|
276
|
+
...(Array.isArray(field.subColumns) ? { subColumns: field.subColumns } : {}),
|
|
205
277
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
206
278
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
207
279
|
};
|
|
@@ -225,7 +297,7 @@ export function registerFieldLane(tools) {
|
|
|
225
297
|
const draft = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data);
|
|
226
298
|
const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
227
299
|
if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code))
|
|
228
|
-
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST` });
|
|
300
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST`, nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true, code: args.code })}'` });
|
|
229
301
|
// One seam owns field-change normalization, every reproduced gate, and the
|
|
230
302
|
// wire body (src/field-change.ts), so configure_model and intent share it.
|
|
231
303
|
const prepared = prepareFieldChange(args);
|
package/dist/lanes/list-view.js
CHANGED
|
@@ -776,7 +776,27 @@ export function registerListViewLane(tools) {
|
|
|
776
776
|
const before = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
|
|
777
777
|
if (before.data == null)
|
|
778
778
|
return { performed: { skipped: "already-absent" }, readback: { absent: true, header: null } };
|
|
779
|
-
|
|
779
|
+
let performed;
|
|
780
|
+
try {
|
|
781
|
+
performed = await wireCall(session, { path: ENDPOINTS.listQueryDelete, method: "POST", query: { schemaCode: args.schemaCode, code: args.code } });
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
// 306103 删除后将无可用视图 — the platform requires at least one visible
|
|
785
|
+
// list view per model. r15's `nextCommand` fell back to the generic
|
|
786
|
+
// refusal router; name the constraint and the only real deletion path.
|
|
787
|
+
if (error instanceof WireError && error.kind === "business" && (error.errcode === 306103 || /无可用视图/.test(error.message)))
|
|
788
|
+
throw new ToolRefusalError({
|
|
789
|
+
status: "refused",
|
|
790
|
+
blocker: "listview-last-visible",
|
|
791
|
+
schemaCode: args.schemaCode,
|
|
792
|
+
code: args.code,
|
|
793
|
+
nextCommand: `omc model-purge --code ${args.schemaCode}`,
|
|
794
|
+
message: `不能删除视图 ${args.code}:平台要求每个模型至少保留一条可见列表视图(306103)。` +
|
|
795
|
+
`单独删最后一条视图没有意义——视图会随模型一起级联删除。要继续用这个模型就**就地改**(listview.configure 省略 code 即改默认视图),` +
|
|
796
|
+
`要整体清理就用 model-purge(默认 dry-run,先看计划)。`,
|
|
797
|
+
});
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
780
800
|
const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
|
|
781
801
|
return { performed: { errcode: performed.errcode }, readback: { absent: readback.data == null, header: readback.data ?? null } };
|
|
782
802
|
}, undefined, undefined, listviewRollbackSpec("listview.delete"));
|
package/dist/lanes/model.js
CHANGED
|
@@ -199,7 +199,10 @@ export function registerModelLane(tools) {
|
|
|
199
199
|
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
200
200
|
const all = tree
|
|
201
201
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code)
|
|
202
|
-
|
|
202
|
+
// `schemaCode` is the parameter name every other tool takes; `code` is
|
|
203
|
+
// the tree row's key. Echo both so `--jq 'models[].schemaCode'` works
|
|
204
|
+
// (r15 P6: the alias was missing and the path missed).
|
|
205
|
+
.map((row) => ({ code: row.code, schemaCode: row.code, name: row.name ?? row.text ?? null }));
|
|
203
206
|
const needle = args.filter?.trim();
|
|
204
207
|
const models = needle ? all.filter((model) => String(model.code).includes(needle) || String(model.name ?? "").includes(needle)) : all;
|
|
205
208
|
// `filterApplied` disambiguates an empty result: it distinguishes "the
|