@stalfh233/omc-cli 0.5.3 → 0.5.4
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-output.js +6 -0
- package/dist/cli.js +12 -6
- package/dist/goal-routes.js +1 -1
- package/dist/graph.js +5 -3
- package/dist/intent/plan-apply.js +11 -1
- package/dist/lanes/businessrule.js +10 -3
- package/dist/lanes/dictionary-contract.js +1 -1
- package/dist/lanes/dictionary.js +30 -1
- package/dist/lanes/model.js +16 -3
- package/dist/path-projection.js +10 -3
- package/dist/support.js +44 -8
- package/package.json +3 -3
- package/skills/omc-refusals.md +4 -2
- package/skills/omc-start-here.md +1 -1
- package/skills/omc-write-hazards.md +1 -1
package/dist/cli-output.js
CHANGED
|
@@ -44,6 +44,12 @@ export function stripOutputFlags(argv) {
|
|
|
44
44
|
const token = normalized[index];
|
|
45
45
|
if (token === "--quiet" || token === "--verbose")
|
|
46
46
|
continue;
|
|
47
|
+
// `--utf8` is consumed at module load (cli.ts sets OMC_ASCII_STDOUT=0 from
|
|
48
|
+
// argv BEFORE any output is produced), so it must be stripped here too or
|
|
49
|
+
// commander rejects it as `unknown option` (r-finding 2026-10-30: the
|
|
50
|
+
// banner and README both taught `--utf8` while every invocation failed).
|
|
51
|
+
if (token === "--utf8")
|
|
52
|
+
continue;
|
|
47
53
|
if (token === "--out") {
|
|
48
54
|
index += 1;
|
|
49
55
|
continue;
|
package/dist/cli.js
CHANGED
|
@@ -661,7 +661,7 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
661
661
|
if (options.jq !== undefined) {
|
|
662
662
|
const projected = projectArtifactPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
|
|
663
663
|
return emitTransportResult(projected.missed
|
|
664
|
-
? { 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) }
|
|
664
|
+
? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, unanswered: true, projection: { path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、数组映射 items[].fields.x、按键选择 rules[code=Create]。available 是该层真实存在的键。" }, fetch: artifactFetchHint(id) }
|
|
665
665
|
: { 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);
|
|
666
666
|
}
|
|
667
667
|
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, fetch: artifactFetchHint(id) }, transport);
|
|
@@ -685,15 +685,15 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
685
685
|
else
|
|
686
686
|
missing.push(raw);
|
|
687
687
|
}
|
|
688
|
-
const result = { status: "ok", artifactId: id, sha256: artifact.sha256, keys: wanted, ...(missing.length > 0 ? { missingKeys: missing, availableKeys: record ? Object.keys(record) : [] } : {}), content: projected };
|
|
688
|
+
const result = { status: "ok", artifactId: id, sha256: artifact.sha256, keys: wanted, ...(missing.length > 0 ? { missingKeys: missing, availableKeys: record ? Object.keys(record) : [], unanswered: true, hint: `这些键不在产物顶层:${missing.join("、")}。availableKeys 是真实顶层键;嵌套值用 --jq a.b 或 --extract a.b` } : {}), content: projected };
|
|
689
689
|
if (Buffer.byteLength(JSON.stringify(result), "utf8") > transport.maxOutputBytes)
|
|
690
|
-
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, projection: options.keys, ...(missing.length > 0 ? { missingKeys: missing, availableKeys: record ? Object.keys(record) : [] } : {}), fetch: artifactFetchHint(id) }, transport);
|
|
690
|
+
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, projection: options.keys, ...(missing.length > 0 ? { missingKeys: missing, availableKeys: record ? Object.keys(record) : [], unanswered: true } : {}), fetch: artifactFetchHint(id) }, transport);
|
|
691
691
|
return emitTransportResult(result, transport);
|
|
692
692
|
}
|
|
693
693
|
if (options.jq !== undefined) {
|
|
694
694
|
const projected = projectArtifactPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
|
|
695
695
|
return emitTransportResult(projected.missed
|
|
696
|
-
? { 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
|
|
696
|
+
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价;[] 映射数组(items[].fields.x);按键选择 rules[code=Create]` }
|
|
697
697
|
: { 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);
|
|
698
698
|
}
|
|
699
699
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
@@ -1831,8 +1831,14 @@ async function main() {
|
|
|
1831
1831
|
intentCommand.command("plan <file>").option("--env <environment>").option("--plan-out <file>").option("--max-output-bytes <bytes>").action(async (file, options) => {
|
|
1832
1832
|
commandExitCode = await withTransport(options, request, (transport) => cmdIntentPlan(file, options.env, options.planOut, transport));
|
|
1833
1833
|
});
|
|
1834
|
-
intentCommand.command("apply <file>").option("--env <environment>").option("--dry-run").option("--no-verify").option("--confirm-production").option("--max-output-bytes <bytes>").action(async (file, options) => {
|
|
1835
|
-
|
|
1834
|
+
intentCommand.command("apply <file>").option("--env <environment>").option("--dry-run").option("--no-verify").option("--confirm-production").option("--wait <milliseconds>", "写锁冲突时的有界排队毫秒数(同 `omc call --wait`,默认 0=立刻拒绝)").option("--max-output-bytes <bytes>").action(async (file, options) => {
|
|
1835
|
+
const waitRaw = options.wait === undefined ? 0 : Number(options.wait);
|
|
1836
|
+
if (!Number.isSafeInteger(waitRaw) || waitRaw < 0 || waitRaw > 600_000) {
|
|
1837
|
+
console.error("--wait must be an integer number of milliseconds from 0 to 600000");
|
|
1838
|
+
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdIntentApply(file, { env: options.env, dryRun: options.dryRun === true, verify: options.verify !== false, confirmProduction: options.confirmProduction === true, waitMs: waitRaw }, transport));
|
|
1836
1842
|
});
|
|
1837
1843
|
intentCommand.command("readback <file>").option("--env <environment>").option("--max-output-bytes <bytes>").action(async (file, options) => {
|
|
1838
1844
|
commandExitCode = await withTransport(options, request, (transport) => cmdIntentReadback(file, options.env, transport));
|
package/dist/goal-routes.js
CHANGED
|
@@ -585,7 +585,7 @@ export function goalRoute(registry, goal) {
|
|
|
585
585
|
readiness: DISCOVERY_AUTHORITY.readiness,
|
|
586
586
|
firstCommands: [
|
|
587
587
|
"omc describe field.update --example form-conditional-required",
|
|
588
|
-
"omc call field.list --args '{\"schemaCode\":\"<schemaCode
|
|
588
|
+
"omc call field.list --args '{\"schemaCode\":\"<schemaCode>\",\"full\":true}'",
|
|
589
589
|
],
|
|
590
590
|
write: true,
|
|
591
591
|
},
|
package/dist/graph.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { joinConfigRoot } from "./command-transport.js";
|
|
5
5
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
|
|
6
6
|
import { ENDPOINTS } from "./endpoints.js";
|
|
7
|
-
import { flattenTreeRows,
|
|
7
|
+
import { applicationResolutionOf, flattenTreeRows, resolveApplicationScope, sha256 } from "./support.js";
|
|
8
8
|
import { wireCall, wireRows } from "./wire.js";
|
|
9
9
|
/**
|
|
10
10
|
* `node:sqlite` must stay OUT of the CLI's startup module graph: a static
|
|
@@ -177,7 +177,8 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
177
177
|
// returns EMPTY data on a real platform (works only against the fake face)
|
|
178
178
|
// — using it here made the first live pull enumerate zero models and (pre-
|
|
179
179
|
// zero-guard) wedge the scheduler.
|
|
180
|
-
const
|
|
180
|
+
const scope = await resolveApplicationScope(session, undefined).catch(() => undefined);
|
|
181
|
+
const applicationCode = scope?.code ?? "";
|
|
181
182
|
const schemaCodes = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data))
|
|
182
183
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code !== "")
|
|
183
184
|
.map((row) => String(row.code))
|
|
@@ -368,7 +369,8 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
368
369
|
scope: "businessrule",
|
|
369
370
|
identityFingerprint: identity,
|
|
370
371
|
environmentName: session.environmentName,
|
|
371
|
-
applicationCode
|
|
372
|
+
applicationCode,
|
|
373
|
+
...(scope !== undefined ? { applicationResolution: applicationResolutionOf(scope) } : {}),
|
|
372
374
|
pulledAtIso,
|
|
373
375
|
},
|
|
374
376
|
rules,
|
|
@@ -453,7 +453,17 @@ export async function applyIntent(planPath, options, ports) {
|
|
|
453
453
|
}
|
|
454
454
|
// The whole apply holds the same cross-process write lock `omc call`
|
|
455
455
|
// acquires per write — one writer per environment across every face.
|
|
456
|
-
|
|
456
|
+
// `--wait` mirrors `omc call --wait`: bound the retry inside the CLI instead
|
|
457
|
+
// of forcing the caller to spend a round trip per conflict. Default stays 0
|
|
458
|
+
// (fail fast, structured refusal) — the opt-in is what makes it safe for
|
|
459
|
+
// non-replayable batches.
|
|
460
|
+
const waitMs = options.waitMs ?? 0;
|
|
461
|
+
const deadline = Date.now() + waitMs;
|
|
462
|
+
let lock = await acquireWriteLock(plan.environment);
|
|
463
|
+
while (!lock.held && Date.now() < deadline) {
|
|
464
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(200, Math.max(0, deadline - Date.now()))));
|
|
465
|
+
lock = await acquireWriteLock(plan.environment);
|
|
466
|
+
}
|
|
457
467
|
if (!lock.held) {
|
|
458
468
|
const conflict = {
|
|
459
469
|
status: "refused",
|
|
@@ -4,7 +4,7 @@ import { capabilityArg, environmentArg } from "../args.js";
|
|
|
4
4
|
import { governedRead } from "../read.js";
|
|
5
5
|
import { writeTool } from "../write.js";
|
|
6
6
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "../json-column.js";
|
|
7
|
-
import { flattenTreeRows,
|
|
7
|
+
import { applicationResolutionOf, applicationScopeHint, flattenTreeRows, resolveApplicationScope } from "../support.js";
|
|
8
8
|
import { executeRuleSaveWrite, ruleNodeGraphArg } from "../rule-support.js";
|
|
9
9
|
import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
|
|
10
10
|
import { resolveRuleRowId } from "../identity.js";
|
|
@@ -139,8 +139,11 @@ export function registerBusinessRuleLane(tools) {
|
|
|
139
139
|
// Model enumeration mirrors `model.list`: the bare modelList endpoint
|
|
140
140
|
// returns EMPTY rows on a real platform (live 2026-10-29: a full scan
|
|
141
141
|
// "found" 0 models → false zero-hit), so walk the application tree.
|
|
142
|
+
// The implicit scope is echoed: a zero-hit scan over 1-of-N applications
|
|
143
|
+
// must not read as "nothing references this" tenant-wide.
|
|
144
|
+
const scope = args.schemaCodes === undefined ? await resolveApplicationScope(session, undefined) : undefined;
|
|
142
145
|
const scoped = args.schemaCodes === undefined
|
|
143
|
-
? flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode:
|
|
146
|
+
? flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: scope.code } })).data))
|
|
144
147
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code !== "")
|
|
145
148
|
.map((row) => String(row.code))
|
|
146
149
|
: (Array.isArray(args.schemaCodes) ? args.schemaCodes : [args.schemaCodes]);
|
|
@@ -209,8 +212,12 @@ export function registerBusinessRuleLane(tools) {
|
|
|
209
212
|
code: wanted,
|
|
210
213
|
scanned: { models: scoped.length, rules: scannedRules, ...(parseFailures > 0 ? { parseFailures } : {}) },
|
|
211
214
|
matches,
|
|
215
|
+
...(scope !== undefined ? { applicationResolution: applicationResolutionOf(scope) } : {}),
|
|
212
216
|
...(matches.length === 0
|
|
213
|
-
? {
|
|
217
|
+
? {
|
|
218
|
+
note: "零命中=全平台(或指定 schemaCodes 范围内)没有规则引用它。范围:service→methodMapping.serviceCode;model→targetObjectCode/methodMapping.schemaCode;field→dataActions.targetDataItem/映射 bizCode(不含条件叶子与 datarule,第一版边界)",
|
|
219
|
+
...(scope !== undefined ? { hint: applicationScopeHint(scope, "引用") } : {}),
|
|
220
|
+
}
|
|
214
221
|
: {}),
|
|
215
222
|
};
|
|
216
223
|
}, { target: `rule.references:${args.kind}:${args.code}` }),
|
|
@@ -52,7 +52,7 @@ export const DICTIONARY_LANE_CONTRACT = Object.freeze({
|
|
|
52
52
|
"dictionary.list": Object.freeze({ scope: "dictionary list", description: "分页读取数据字典(data_dictionary/searchDictionary),返回字典元数据。" }),
|
|
53
53
|
"dictionary.get": Object.freeze({ scope: "dictionary read", description: "按 id 或 code 读取数据字典及其元数据。" }),
|
|
54
54
|
"dictionary.records": Object.freeze({ scope: "dictionary records read", description: "读取字典记录;支持按 id/code、启用状态和 parentId 读取,保留树形 children。" }),
|
|
55
|
-
"dictionary.save": Object.freeze({ scope: "dictionary save", description: "创建或更新数据字典及记录(data_dictionary/saveDictionary);KEY_VALUE=0,TREE=1,写入前需审批。" }),
|
|
55
|
+
"dictionary.save": Object.freeze({ scope: "dictionary save", description: "创建或更新数据字典及记录(data_dictionary/saveDictionary);KEY_VALUE=0,TREE=1,写入前需审批。records 是增量 upsert:传 [] 不会清空已有记录,平台也没有字典/记录删除端点——停用请用 dictionary.status,清理需维护者数据库操作。" }),
|
|
56
56
|
"dictionary.status": Object.freeze({ scope: "dictionary status", description: "启用或停用字典或字典记录;停用字典时运行时不会返回选项。" }),
|
|
57
57
|
"dictionary.class": Object.freeze({ scope: "dictionary classification", description: "读取、新建、删除字典分类并维护分类关联字典。" }),
|
|
58
58
|
"dictionary.usage": Object.freeze({ scope: "dictionary record usage", description: "批量设置字典记录 initialUsed 使用状态;记录新增/更新仍随 dictionary.save.records。" }),
|
package/dist/lanes/dictionary.js
CHANGED
|
@@ -278,10 +278,39 @@ export function registerDictionaryLane(tools) {
|
|
|
278
278
|
writeTool(tools, "dictionary.class.delete", LANE, TOOLS["dictionary.class.delete"].scope, TOOLS["dictionary.class.delete"].description, {
|
|
279
279
|
ids: z.array(z.string().min(1)).min(1),
|
|
280
280
|
}, async (session, args) => {
|
|
281
|
+
// The platform's deleteDictionaryClass(Batch) answers errcode 0 for ANY id —
|
|
282
|
+
// a dictionary id, a typo, anything — while deleting nothing. Writing
|
|
283
|
+
// `absent: true` unconditionally turned that into a silent false success
|
|
284
|
+
// (2026-10-30 probe: passing a dictionary id reported success and left the
|
|
285
|
+
// object in place). Verify the inputs are class ids BEFORE the write and
|
|
286
|
+
// derive absence from a post-delete read.
|
|
287
|
+
const classIds = async () => {
|
|
288
|
+
const listed = (await wireCall(session, { path: ENDPOINTS.dictionaryClasses, query: {} })).data;
|
|
289
|
+
return new Set((Array.isArray(listed) ? listed : [])
|
|
290
|
+
.map((entry) => (typeof entry.id === "string" ? entry.id : ""))
|
|
291
|
+
.filter((id) => id !== ""));
|
|
292
|
+
};
|
|
293
|
+
const before = await classIds();
|
|
294
|
+
const unmatched = args.ids.filter((id) => !before.has(id));
|
|
295
|
+
if (unmatched.length > 0) {
|
|
296
|
+
throw new ToolRefusalError({
|
|
297
|
+
status: "refused",
|
|
298
|
+
blocker: "dictionary-class-absent",
|
|
299
|
+
ids: args.ids,
|
|
300
|
+
unmatched,
|
|
301
|
+
candidates: [...before].slice(0, 20),
|
|
302
|
+
message: `${unmatched.join("、")} 不是字典分类 id(删前分类列表里没有它)。平台 deleteDictionaryClass 对任意 id 都回 errcode 0 却不删任何行——传字典 id 会假成功。现有分类候选:${[...before].slice(0, 20).join("、") || "(当前没有分类)"}。另:字典本体与字典项平台无删除端点,只能 dictionary.status 停用。`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
281
305
|
const path = args.ids.length === 1 ? ENDPOINTS.dictionaryClassDelete : ENDPOINTS.dictionaryClassDeleteBatch;
|
|
282
306
|
const body = args.ids.length === 1 ? { id: args.ids[0] } : { ids: args.ids };
|
|
283
307
|
const performed = await wireCall(session, { path, method: "POST", body });
|
|
284
|
-
|
|
308
|
+
const after = await classIds();
|
|
309
|
+
const stillPresent = args.ids.filter((id) => after.has(id));
|
|
310
|
+
return {
|
|
311
|
+
performed: { errcode: performed.errcode, ids: args.ids },
|
|
312
|
+
readback: { absent: stillPresent.length === 0, ids: args.ids, ...(stillPresent.length > 0 ? { stillPresent } : {}) },
|
|
313
|
+
};
|
|
285
314
|
}, undefined, undefined, dictionaryRollbackSpec("dictionary.class.delete"));
|
|
286
315
|
}
|
|
287
316
|
//# sourceMappingURL=dictionary.js.map
|
package/dist/lanes/model.js
CHANGED
|
@@ -9,7 +9,7 @@ import { writeSummary } from "../human-summary.js";
|
|
|
9
9
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
10
10
|
import { governedRead, withoutReadbackCache } from "../read.js";
|
|
11
11
|
import { parseToolInput } from "../tool-types.js";
|
|
12
|
-
import { flattenTreeRows, resolveApplicationCode } from "../support.js";
|
|
12
|
+
import { applicationResolutionOf, applicationScopeHint, flattenTreeRows, resolveApplicationCode, resolveApplicationScope } from "../support.js";
|
|
13
13
|
import { writeTool } from "../write.js";
|
|
14
14
|
import { registerPackageTool } from "../package-tool.js";
|
|
15
15
|
import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
|
|
@@ -196,7 +196,8 @@ export function registerModelLane(tools) {
|
|
|
196
196
|
capability: capabilityArg,
|
|
197
197
|
},
|
|
198
198
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
199
|
-
const
|
|
199
|
+
const scope = await resolveApplicationScope(session, args.applicationCode);
|
|
200
|
+
const applicationCode = scope.code;
|
|
200
201
|
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
201
202
|
const all = tree
|
|
202
203
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code)
|
|
@@ -209,7 +210,19 @@ export function registerModelLane(tools) {
|
|
|
209
210
|
// `filterApplied` disambiguates an empty result: it distinguishes "the
|
|
210
211
|
// filter ran and matched nothing" from "the filter was not applied"
|
|
211
212
|
// (dogfood R1: count:0 alone gave no signal).
|
|
212
|
-
|
|
213
|
+
// `applicationResolution` says WHY this application was scanned: the
|
|
214
|
+
// implicit fallback to the tenant's first application narrows the scan to
|
|
215
|
+
// 1-of-N, so `count: 0` must never read as tenant-wide absence.
|
|
216
|
+
return {
|
|
217
|
+
applicationCode,
|
|
218
|
+
applicationResolution: applicationResolutionOf(scope),
|
|
219
|
+
count: models.length,
|
|
220
|
+
totalBeforeFilter: all.length,
|
|
221
|
+
filterApplied: needle !== undefined && needle !== "",
|
|
222
|
+
...(needle ? { filter: needle } : {}),
|
|
223
|
+
models,
|
|
224
|
+
...(models.length === 0 ? { hint: applicationScopeHint(scope, "模型") } : {}),
|
|
225
|
+
};
|
|
213
226
|
}, { target: `model.list:${args.applicationCode ?? ""}:${args.filter ?? ""}` }),
|
|
214
227
|
}));
|
|
215
228
|
const siblings = tools;
|
package/dist/path-projection.js
CHANGED
|
@@ -96,6 +96,8 @@ function argvValues(args, name) {
|
|
|
96
96
|
}
|
|
97
97
|
return values;
|
|
98
98
|
}
|
|
99
|
+
/** Shared wording for every projection miss, so all three faces teach the same syntax. */
|
|
100
|
+
const PATH_MISS_HINT = "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、数组映射 items[].fields.x、按键选择 rules[code=Create](返回第一个匹配项)。available 是该层真实存在的键。";
|
|
99
101
|
/**
|
|
100
102
|
* `call --extract/--extract-keys`: resolve paths inside the result's own
|
|
101
103
|
* artifact (or inside the result object when the tool returned no artifact).
|
|
@@ -153,16 +155,21 @@ export function projectResultExtract(result, paths, keysOnly) {
|
|
|
153
155
|
if (path.includes("[]")) {
|
|
154
156
|
const fanout = projectJsonPathFanout(source, path);
|
|
155
157
|
return fanout.missed
|
|
156
|
-
? { path, content: null, pathMiss: true, missedAt: fanout.at, available: fanout.available, missedEntries: fanout.missedEntries }
|
|
157
|
-
: { path, content: fanout.values, mapped: fanout.values.length, ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries } : {}) };
|
|
158
|
+
? { path, content: null, pathMiss: true, missedAt: fanout.at, available: fanout.available, missedEntries: fanout.missedEntries, hint: PATH_MISS_HINT }
|
|
159
|
+
: { path, content: fanout.values, mapped: fanout.values.length, ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries, hint: `有 ${fanout.missedEntries} 条记录在该路径下缺失,values 只含命中项` } : {}) };
|
|
158
160
|
}
|
|
159
161
|
const projected = projectJsonPath(source, path);
|
|
160
162
|
return projected.missed
|
|
161
|
-
? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
|
|
163
|
+
? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: PATH_MISS_HINT }
|
|
162
164
|
: { path, content: projected.value ?? null };
|
|
163
165
|
});
|
|
164
166
|
if (paths.length === 1)
|
|
165
167
|
out.content = out.extract[0]?.content ?? null;
|
|
168
|
+
// A projection that missed is not an answer. `status` stays `ok` (adding a
|
|
169
|
+
// new status value would fall through classifyResult to error/exit 15 and
|
|
170
|
+
// change the exit contract), but the envelope now says so out loud.
|
|
171
|
+
if (out.extract.some((entry) => entry.pathMiss === true))
|
|
172
|
+
out.unanswered = true;
|
|
166
173
|
}
|
|
167
174
|
return out;
|
|
168
175
|
}
|
package/dist/support.js
CHANGED
|
@@ -71,16 +71,52 @@ export function refusalFromCoverage(error) {
|
|
|
71
71
|
* callers may operate across applications.
|
|
72
72
|
*/
|
|
73
73
|
export async function resolveApplicationCode(session, requested) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
return (await resolveApplicationScope(session, requested)).code;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `resolveApplicationCode` with provenance. The fallback to the tenant's first
|
|
78
|
+
* application is legitimate (it is the documented chain) but it silently
|
|
79
|
+
* narrows a scan to 1-of-N applications — a `count: 0` from such a call reads
|
|
80
|
+
* as "does not exist" when it really means "not in this application"
|
|
81
|
+
* (2026-10-30 live: a 97-application tenant answered "no purchase model").
|
|
82
|
+
* Callers that enumerate must echo the source so a zero cannot be misread.
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveApplicationScope(session, requested) {
|
|
85
|
+
if (typeof requested === "string" && requested.trim()) {
|
|
86
|
+
const code = requested.trim();
|
|
87
|
+
return { code, source: "explicit", requested: code };
|
|
88
|
+
}
|
|
89
|
+
if (session.profile.applicationCode && session.profile.applicationCode.trim()) {
|
|
90
|
+
const code = session.profile.applicationCode.trim();
|
|
91
|
+
return { code, source: "configured", requested: code };
|
|
92
|
+
}
|
|
78
93
|
const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
const codes = apps
|
|
95
|
+
.map((row) => row.appCode ?? row.code)
|
|
96
|
+
.filter((code) => typeof code === "string" && code !== "");
|
|
97
|
+
if (codes.length === 0)
|
|
82
98
|
throw new WireError("business", "no application found for this environment", 302001);
|
|
83
|
-
return code;
|
|
99
|
+
return { code: codes[0], source: "first-tenant-application", requested: null, applicationCount: codes.length };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The echo payload for a scan-style read: `source` says why this application,
|
|
103
|
+
* `applicationCount` says how many the tenant has (fallback case only), so a
|
|
104
|
+
* zero result can never be mistaken for tenant-wide absence.
|
|
105
|
+
*/
|
|
106
|
+
export function applicationResolutionOf(scope) {
|
|
107
|
+
return {
|
|
108
|
+
code: scope.code,
|
|
109
|
+
source: scope.source,
|
|
110
|
+
...(scope.requested !== null ? { requested: scope.requested } : {}),
|
|
111
|
+
...(scope.applicationCount !== undefined ? { applicationCount: scope.applicationCount } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** One-line nudge appended to a zero-result scan read; names the real scope of the answer. */
|
|
115
|
+
export function applicationScopeHint(scope, noun) {
|
|
116
|
+
if (scope.source === "first-tenant-application" && (scope.applicationCount ?? 1) > 1) {
|
|
117
|
+
return `本次只扫描了租户首个应用 ${scope.code}(租户共 ${scope.applicationCount} 个应用)——零命中不代表其他应用没有${noun};先 app.list 取 applicationCode 再按应用重查。`;
|
|
118
|
+
}
|
|
119
|
+
return `本次只扫描了应用 ${scope.code}(source=${scope.source})——零命中只代表该应用没有${noun};跨应用请先 app.list 取 applicationCode。`;
|
|
84
120
|
}
|
|
85
121
|
export function flattenTreeRows(rows) {
|
|
86
122
|
const flat = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@stalfh233/omc-fake-platform": "0.5.
|
|
55
|
+
"@stalfh233/omc-fake-platform": "0.5.4",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.5.
|
|
57
|
+
"omc-core": "0.5.4",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|
package/skills/omc-refusals.md
CHANGED
|
@@ -108,8 +108,10 @@ approved draft, and decide with the human. A second write may duplicate.
|
|
|
108
108
|
3. Never blind-retry — that risks a second write.
|
|
109
109
|
|
|
110
110
|
`write-lock-held` — another write holds the environment lock; `owner` and
|
|
111
|
-
`recovery` are included.
|
|
112
|
-
|
|
111
|
+
`recovery` are included. Retry with `--wait <ms>` (`omc call <tool> --wait 5000`,
|
|
112
|
+
`omc intent apply ... --wait 5000`): the CLI does the bounded queueing in-process,
|
|
113
|
+
so it costs no extra round trip. It also auto-recovers when the holding process
|
|
114
|
+
exits. This guards concurrency, not approval.
|
|
113
115
|
|
|
114
116
|
## 8. Runtime / gateway errors (not tool refusals, same discipline)
|
|
115
117
|
|
package/skills/omc-start-here.md
CHANGED
|
@@ -31,7 +31,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
31
31
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|
|
32
32
|
4. 只有工具不在 `next` 命中能力内、或要看 purpose/枚举时,才补一次 `commands --json --compact --lane <lane>`。非显然形状(规则图、流程模板、表单布局、RESTFUL)才单独 `describe --example`。
|
|
33
33
|
5. 键名按对象不同:模型 `schemaCode`、默认表单 `sheetCode`(可省略,默认 == 模型 code)、流程 `workflowCode`/`code`——报错会提示同义键;字段族名认不出会给「最接近的族」。
|
|
34
|
-
6. **写操作串行**:同一环境不要并发写(含多个 `omc call` 并行进程——环境写锁互斥,r31 实证并行 field.create 2/3 被拒);多个写合并为一条 `omc script` 逐行跑(进程内自动串行);`write-lock-held`
|
|
34
|
+
6. **写操作串行**:同一环境不要并发写(含多个 `omc call` 并行进程——环境写锁互斥,r31 实证并行 field.create 2/3 被拒);多个写合并为一条 `omc script` 逐行跑(进程内自动串行);`write-lock-held` 是瞬态:**优先在发起时就排队**——`omc call <tool> --wait 5000`(或 `omc intent apply ... --wait 5000`)让 CLI 内部有界重试,不必自己再花一轮。
|
|
35
35
|
7. **单个对象优先高阶编排工具**:模型+字段用 `configure_model`(一次含全部字段),表单用 `configure_form`,列表用 `listview.configure`(**默认视图已存在,省略 code 即配置它,不要 `listview.create`**)。原子工具留给单点修改。
|
|
36
36
|
8. 拿到 `detailArtifactId` 先 `artifact get <id> --summary` 看结构,再 `--keys/--jq` 取所需;**读面要某一列时直接 `omc call <tool> … --jq 'items[].fields.xxx'`(`[]` 映射数组,一次调用拿整列;裸 `--jq` 列键),不要再落 artifact 二次取**;`field.list`/`workflow.*` 默认面已给清单与节点摘要;`workflow.get_draft` 默认面已含 `edges`,改流程只需传 `activities`+`routes`。**注意**:`form.get --full`、`workflow.get_draft` 的 `full:true` 大结果返回的是 `{length, sha256}` 摘要桩——真身走 `artifact get <id>`,勿把桩误判为「子表/控件没配」(用户实测陷阱)。
|
|
37
37
|
9. **Windows/PowerShell 乱码**:CLI 输出是 UTF-8,PowerShell 管道默认按 GBK(936) 解码会全屏乱码——根因在终端不在 CLI。消费端先 `chcp 65001` + `[Console]::OutputEncoding=[Text.Encoding]::UTF8` 再读 CLI 输出。
|
|
@@ -110,7 +110,7 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
110
110
|
|
|
111
111
|
## 写调用纪律
|
|
112
112
|
|
|
113
|
-
- **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish`
|
|
113
|
+
- **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish` 各失败一次)。读可并行,写不要。确实要并发进程时用 `--wait <ms>`(`omc call` / `omc intent apply` 都支持)让 CLI 内部排队,别把重试循环写在 shell 里。
|
|
114
114
|
|
|
115
115
|
## 审批提交的真实形状(r18b 2026-10-26 真机,1.1.25)
|
|
116
116
|
|