@stalfh233/omc-cli 0.5.8 → 0.5.10
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.js +13 -3
- package/dist/field-add.js +72 -6
- package/dist/goal-routes.js +3 -3
- package/dist/journal.js +4 -0
- package/dist/lanes/b2-registration.js +4 -0
- package/dist/lanes/businessrule.js +14 -0
- package/dist/lanes/environment.js +25 -1
- package/dist/lanes/form.js +35 -1
- package/dist/path-projection.js +6 -0
- package/dist/project-card.js +55 -0
- package/dist/read.js +21 -1
- package/dist/skills.js +18 -1
- package/package.json +3 -3
- package/skills/omc-write-hazards.md +10 -2
package/dist/cli.js
CHANGED
|
@@ -1405,8 +1405,10 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
|
|
|
1405
1405
|
sectionCount: outline.sections.length,
|
|
1406
1406
|
// Each section carries its price inline. `{"heading":"入口","bytes":282}`
|
|
1407
1407
|
// is cheaper than parallel arrays and reads better than a heading→bytes map;
|
|
1408
|
-
// the heading names are what `--section` accepts.
|
|
1409
|
-
|
|
1408
|
+
// the heading names are what `--section` accepts. The blurb carries the
|
|
1409
|
+
// section's first line so a bare index read still transfers key pointers
|
|
1410
|
+
// (r36: four bare reads, zero section opens → body-only knowledge invisible).
|
|
1411
|
+
sections: outline.sections.map((entry) => ({ heading: entry.heading, bytes: entry.bytes, ...(entry.blurb !== undefined ? { blurb: entry.blurb } : {}) })),
|
|
1410
1412
|
hint: "默认只回节索引:先按各节 bytes 挑要读的节,再用 --section 取正文(可重复或逗号一次多节,也可用节序号或唯一前缀)。确需整篇才 --all。",
|
|
1411
1413
|
sectionCommand: `omc help ${outline.document.name} --section "<heading>"`,
|
|
1412
1414
|
fullCommand: `omc help ${outline.document.name} --all`,
|
|
@@ -1745,9 +1747,13 @@ async function main() {
|
|
|
1745
1747
|
.option("--environment <environment>")
|
|
1746
1748
|
.option("--no-form", "跳过表单重建")
|
|
1747
1749
|
.option("--no-list", "跳过列表加列")
|
|
1750
|
+
.option("--conditions <json>", "列表固定过滤条件 JSON 数组(listview.configure 契约;整体替换既有固定条件,保留旧的须一并带上)")
|
|
1751
|
+
.option("--conditions-json <json>")
|
|
1752
|
+
.option("--demo-rows <json>", "演示数据行 JSON 数组(每项一份 formData,逐行 data.submit;非幂等)")
|
|
1753
|
+
.option("--demo-rows-json <json>")
|
|
1748
1754
|
.option("--force-publish", "有字段失败也发布(默认停在发布前)")
|
|
1749
1755
|
.option("--apply")
|
|
1750
|
-
.addHelpText("after", `\n存量模型加字段一条命令:建字段 → 发布字段 → 重建表单 →
|
|
1756
|
+
.addHelpText("after", `\n存量模型加字段一条命令:建字段 → 发布字段 → 重建表单 → 列表追加列(列增量合并;固定条件整体替换)。\n默认 dry-run 只列计划;--apply 执行。幂等:字段已存在记 skipped 继续。\n子表不属于本命令(存量挂子表走 field.create CHILD_TABLE + 子列,见 model-scaffold)。\n\nr30/r31 探针实证:同一条链手工要走 4 次写调用 + 各自回读。\n`)
|
|
1751
1757
|
.action(async (options) => {
|
|
1752
1758
|
const rest = ["field-add",
|
|
1753
1759
|
...(options.schema ? ["--schema", options.schema] : []),
|
|
@@ -1759,6 +1765,10 @@ async function main() {
|
|
|
1759
1765
|
...(options.environment ? ["--environment", options.environment] : []),
|
|
1760
1766
|
...(options.form === false ? ["--no-form"] : []),
|
|
1761
1767
|
...(options.list === false ? ["--no-list"] : []),
|
|
1768
|
+
...(options.conditions !== undefined ? ["--conditions", options.conditions] : []),
|
|
1769
|
+
...(options.conditionsJson !== undefined ? ["--conditions-json", options.conditionsJson] : []),
|
|
1770
|
+
...(options.demoRows !== undefined ? ["--demo-rows", options.demoRows] : []),
|
|
1771
|
+
...(options.demoRowsJson !== undefined ? ["--demo-rows-json", options.demoRowsJson] : []),
|
|
1762
1772
|
...(options.forcePublish ? ["--force-publish"] : []),
|
|
1763
1773
|
...(options.apply ? ["--apply"] : [])];
|
|
1764
1774
|
commandExitCode = await cmdFieldAdd(rest);
|
package/dist/field-add.js
CHANGED
|
@@ -114,6 +114,8 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
114
114
|
const publishForm = !args.includes("--no-form");
|
|
115
115
|
const addToList = !args.includes("--no-list");
|
|
116
116
|
const forcePublish = args.includes("--force-publish");
|
|
117
|
+
const conditionsJson = flag("--conditions") ?? flag("--conditions-json");
|
|
118
|
+
const demoRowsJson = flag("--demo-rows") ?? flag("--demo-rows-json");
|
|
117
119
|
const usage = [
|
|
118
120
|
"用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--apply]",
|
|
119
121
|
" --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options 原样透传);",
|
|
@@ -121,6 +123,8 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
121
123
|
" 默认 dry-run;--apply 依次执行 建字段 → 发布字段 → 重建表单 → 列表追加列。",
|
|
122
124
|
" 幂等:字段已存在记 skipped 继续;有真失败默认不发布/不重建(--force-publish 强发)。",
|
|
123
125
|
" --no-form 跳过表单重建;--no-list 跳过列表加列(默认两者都做)。",
|
|
126
|
+
" --conditions '<JSON 数组>' 列表固定过滤条件(listview.configure 契约;**整体替换**既有固定条件——要保留旧的须一并带上;不传则不动);需 --list(默认开)。",
|
|
127
|
+
" --demo-rows '<JSON 数组>' 演示数据行(每项一份 formData,逐行 data.submit);非幂等——重复执行会重复插行。",
|
|
124
128
|
].join("\n");
|
|
125
129
|
const invalid = (message) => {
|
|
126
130
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
@@ -143,6 +147,35 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
143
147
|
const problems = validateFields(schemaCode, fields);
|
|
144
148
|
if (problems.length > 0)
|
|
145
149
|
return invalid(`--fields 有 ${problems.length} 处问题:\n - ${problems.join("\n - ")}`);
|
|
150
|
+
// r38 后执行层折叠(调查批 v2):--conditions/--demo-rows 把「加字段+过滤+演示行」
|
|
151
|
+
// 整链折进一条命令(此前 6-8 调)。conditions 走 listview.configure 契约;
|
|
152
|
+
// demo-rows 逐行 data.submit(非幂等,文档声明)。
|
|
153
|
+
let conditions;
|
|
154
|
+
if (conditionsJson !== undefined) {
|
|
155
|
+
if (!addToList)
|
|
156
|
+
return invalid("--conditions 需要列表配置面(不要与 --no-list 同用)");
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(conditionsJson);
|
|
159
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
160
|
+
throw new Error("必须是非空 JSON 数组");
|
|
161
|
+
conditions = parsed;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return invalid(`--conditions 解析失败:${error instanceof Error ? error.message : String(error)}(形状见 omc describe listview.configure --param conditions)`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
let demoRows;
|
|
168
|
+
if (demoRowsJson !== undefined) {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(demoRowsJson);
|
|
171
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
172
|
+
throw new Error("必须是非空 JSON 数组");
|
|
173
|
+
demoRows = parsed;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return invalid(`--demo-rows 解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
146
179
|
// 字典简写展开(dictCode → optionsSet.dictionary,含 id/记录解析)——展开失败
|
|
147
180
|
// 在任何写之前拒绝(名字对不上/无启用记录)。
|
|
148
181
|
let dictExpanded = 0;
|
|
@@ -195,7 +228,9 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
195
228
|
`field.create ×${fields.length}`,
|
|
196
229
|
"field.publish(发布失败/有字段失败则停)",
|
|
197
230
|
...(publishForm ? [`form.publish ${schemaCode} --rebuild(重建让新字段进默认表单)`] : []),
|
|
198
|
-
...(addToList ? [`listview.configure ${schemaCode}
|
|
231
|
+
...(addToList ? [`listview.configure ${schemaCode} 增量追加列(列保留;固定条件整体替换)`] : []),
|
|
232
|
+
...(conditions !== undefined ? [`listview.configure 附带固定过滤条件 ×${conditions.length}`] : []),
|
|
233
|
+
...(demoRows !== undefined ? [`data.submit 演示行 ×${demoRows.length}(非幂等)`] : []),
|
|
199
234
|
],
|
|
200
235
|
preflight: { checked: fields.length, passed: true },
|
|
201
236
|
note: "dry-run 只列计划;已对每个字段跑过本地形状预检(零 wire)。--apply 依次执行全部步骤。",
|
|
@@ -275,21 +310,38 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
275
310
|
}
|
|
276
311
|
// 4) 列表增量加列:默认合并语义保留既有列/条件/排序(r31 实证);只有字段全部
|
|
277
312
|
// 失败才跳过(部分成功时把成功的列加上去仍有意义)。
|
|
278
|
-
|
|
279
|
-
|
|
313
|
+
const listviewArgs = { schemaCode };
|
|
314
|
+
if (createdCodes.length > 0)
|
|
315
|
+
listviewArgs.fieldCodes = createdCodes;
|
|
316
|
+
if (conditions !== undefined)
|
|
317
|
+
listviewArgs.conditions = conditions;
|
|
318
|
+
if (addToList && (createdCodes.length > 0 || conditions !== undefined) && !publishingBlocked) {
|
|
319
|
+
const configured = await call("listview.configure", listviewArgs);
|
|
280
320
|
steps.push({
|
|
281
321
|
step: "listview.configure",
|
|
282
322
|
status: configured.ok ? "ok" : "failed",
|
|
283
323
|
detail: configured.ok
|
|
284
|
-
? { schemaCode, appended: createdCodes, semantics: "
|
|
285
|
-
: { schemaCode,
|
|
324
|
+
? { schemaCode, ...(createdCodes.length > 0 ? { appended: createdCodes } : {}), ...(conditions !== undefined ? { conditions: conditions.length, conditionsSemantics: "整体替换(既有固定条件要保留须一并带上)" } : {}), semantics: "列增量合并保留既有列/排序;固定条件整体替换" }
|
|
325
|
+
: { schemaCode, ...listviewArgs, message: String(configured.result.message ?? "") },
|
|
286
326
|
});
|
|
287
327
|
}
|
|
288
328
|
else if (addToList) {
|
|
289
329
|
steps.push({
|
|
290
330
|
step: "listview.configure",
|
|
291
331
|
status: "skipped",
|
|
292
|
-
detail: { reason: createdCodes.length === 0 ? "没有字段落到发布面" : "有字段创建失败,默认不动列表(--force-publish 可强发)" },
|
|
332
|
+
detail: { reason: createdCodes.length === 0 && conditions === undefined ? "没有字段落到发布面" : "有字段创建失败,默认不动列表(--force-publish 可强发)" },
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// 演示数据(--demo-rows):逐行 data.submit;失败记 failed 不阻断(行级独立)。
|
|
336
|
+
for (const [index, row] of (demoRows ?? []).entries()) {
|
|
337
|
+
const submitted = await call("data.submit", { schemaCode, formData: row });
|
|
338
|
+
const readback = submitted.ok ? (submitted.result.readback ?? {}) : {};
|
|
339
|
+
steps.push({
|
|
340
|
+
step: "data.submit",
|
|
341
|
+
status: submitted.ok ? "ok" : "failed",
|
|
342
|
+
detail: submitted.ok
|
|
343
|
+
? { row: index + 1, bizObjectId: readback.objectId ?? null, sequenceStatus: readback.sequenceStatus ?? null }
|
|
344
|
+
: { row: index + 1, message: String(submitted.result.message ?? "") },
|
|
293
345
|
});
|
|
294
346
|
}
|
|
295
347
|
const failedSteps = steps.filter((step) => step.status === "failed");
|
|
@@ -306,6 +358,20 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
306
358
|
...(lastSummaries.length > 0 ? { summaries: lastSummaries } : {}),
|
|
307
359
|
steps,
|
|
308
360
|
folded: "field.create → field.publish → form.publish(rebuild) → listview.configure(增量) 4 写调合成 1 条命令",
|
|
361
|
+
...(status === "ok"
|
|
362
|
+
? {
|
|
363
|
+
hint: [
|
|
364
|
+
"本命令覆盖单模型加字段;变更还涉及多模型/规则/流程/视图重排时,改走声明式 intent 链(omc intent schema 看清单 → validate → plan → apply,dry-run→读回校验)",
|
|
365
|
+
// r39 实证:agent 做完字段后把过滤/演示行拆成 listview.configure×N + data.submit×N
|
|
366
|
+
// 串行做。回执直接给续段一条命令——conditions 增量幂等可单独重跑,demo-rows 非幂等。
|
|
367
|
+
...(conditions === undefined || demoRows === undefined
|
|
368
|
+
? [
|
|
369
|
+
`同模型还要列表固定过滤/演示数据?直接续一条(不必另跑 listview.configure/data.submit):omc field-add --schema ${schemaCode} ${conditions === undefined ? "--conditions '<固定过滤>' " : ""}${demoRows === undefined ? "--demo-rows '<演示行>' " : ""}--apply(conditions 增量幂等;demo-rows 非幂等重复插行)`,
|
|
370
|
+
]
|
|
371
|
+
: []),
|
|
372
|
+
].join(";"),
|
|
373
|
+
}
|
|
374
|
+
: {}),
|
|
309
375
|
};
|
|
310
376
|
const classified = classifyResult(status === "partial" ? { ...result, requested: steps.length, succeeded: steps.length - failedSteps.length } : result);
|
|
311
377
|
writeEnvelope(envelope(classified.status, result, request, classified.error));
|
package/dist/goal-routes.js
CHANGED
|
@@ -383,9 +383,9 @@ const COMPOSER_ROUTES = Object.freeze([
|
|
|
383
383
|
},
|
|
384
384
|
{
|
|
385
385
|
id: "field-add",
|
|
386
|
-
command: "omc field-add --schema <存量模型code> --fields '<JSON数组>' --apply",
|
|
386
|
+
command: "omc field-add --schema <存量模型code> --fields '<JSON数组>' [--conditions '<固定过滤>'] [--demo-rows '<演示行>'] --apply",
|
|
387
387
|
usage: "omc field-add --help",
|
|
388
|
-
summary: "存量模型加字段一条命令:建字段 → 发布 → 重建表单 →
|
|
388
|
+
summary: "存量模型加字段一条命令:建字段 → 发布 → 重建表单 → 列表追加列;--conditions 固定过滤 + --demo-rows 演示行同链折入——字段+过滤+演示数据的多段需求整句一次问,4-8 次调用合成 1 条命令",
|
|
389
389
|
// (add verb) × (field noun)——「给既有/存量模型加字段」是变更类需求的最高频动作(r30/r31 探针实证手工链 4 调)
|
|
390
390
|
groups: [["加", "新增", "添加", "补", "增加"], ["字段"]],
|
|
391
391
|
// 只收「加字段本体」;规则/校验/计算/编号/流程语义各有归宿(round10:'给金额字段加自动校验规则' 应归 datarule,不能被本 composer 抢走)
|
|
@@ -452,7 +452,7 @@ export const COMPOSER_ROUTES_FOR_TEST = COMPOSER_ROUTES;
|
|
|
452
452
|
const COMPOSER_QUESTIONS = Object.freeze({
|
|
453
453
|
"model-purge": "要清理哪个模型?给我 schemaCode(不确定就先跑下面的只读判别动作拿清单)",
|
|
454
454
|
"model-scaffold": "给我模型 code、显示名和字段清单(每字段 code/名称/类型;子表放 subFields),我一条命令建完",
|
|
455
|
-
"field-add": "目标存量模型 schemaCode 和字段清单(每字段 code/名称/类型;choices
|
|
455
|
+
"field-add": "目标存量模型 schemaCode 和字段清单(每字段 code/名称/类型;choices 给字符串数组、字典 dictCode 简写)是什么?需求还带列表固定过滤或演示行的话**一并整句给我——全折进本命令(--conditions/--demo-rows),不要拆成字段/过滤/数据三段串行做**;建完自动发布、重建表单、列表加列",
|
|
456
456
|
"approval-flow": "目标模型 schemaCode 和每级审批节点(节点名@角色code,多级用重复 --steps)是什么?",
|
|
457
457
|
compute: "目标模型 schemaCode、结果字段 code、公式是什么?(子表条件聚合再加 --where \"子列 = 值\")",
|
|
458
458
|
"rule-when": "目标模型 schemaCode、触发时机(Create/Update…)、条件(--when)和动作(--then)各是什么?",
|
package/dist/journal.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, appendFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { invalidateProjectCard } from "./project-card.js";
|
|
3
4
|
/**
|
|
4
5
|
* Per-environment append-only journal of delivered governed config writes.
|
|
5
6
|
*
|
|
@@ -26,6 +27,9 @@ export function appendJournalEntry(artifactsDir, entry) {
|
|
|
26
27
|
const seq = countLines(file) + 1;
|
|
27
28
|
const full = { ...entry, seq, ts: new Date().toISOString() };
|
|
28
29
|
appendFileSync(file, `${JSON.stringify(full)}\n`, "utf8");
|
|
30
|
+
// 2026-11-01 项目卡失效:模型面写操作改变清单——下次会话不得读旧卡。
|
|
31
|
+
if (/^model\.(create|delete|move|update)/.test(entry.tool))
|
|
32
|
+
invalidateProjectCard(artifactsDir);
|
|
29
33
|
return full;
|
|
30
34
|
}
|
|
31
35
|
export function readJournal(environment, artifactsDir) {
|
|
@@ -722,6 +722,10 @@ export function registerB2Lane(tools) {
|
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
724
|
return { adapters, rawPresent: typeof result.data === "string" };
|
|
725
|
+
}, {
|
|
726
|
+
// 2026-11-01 读门 2048:适配器面本身就是这个工具的存在意义(注册预检的
|
|
727
|
+
// 同源依据)——compact 保留 adapters 本体,不折码表。
|
|
728
|
+
compact: (result) => result,
|
|
725
729
|
}),
|
|
726
730
|
}));
|
|
727
731
|
tools.push(defineTool({
|
|
@@ -88,8 +88,22 @@ export function registerBusinessRuleLane(tools) {
|
|
|
88
88
|
// key (schemaCode+ruleCode), so the row id is rarely needed.
|
|
89
89
|
const fullRows = args.full === true;
|
|
90
90
|
const visible = args.onlyCustom === true ? rows.filter((row) => row.defaultRule !== true) : rows;
|
|
91
|
+
// r40 维护者自踩:schemaCode 不存在时平台静默回空数组——空规则被读成
|
|
92
|
+
// 『模型在、无规则』走错分支(mcc_contract 从不存在却答 0 规则)。
|
|
93
|
+
// 空结果时用 field.list 探一次真身(300004=不存在),把 modelPresent 标进结果。
|
|
94
|
+
let modelPresent = true;
|
|
95
|
+
if (visible.length === 0) {
|
|
96
|
+
try {
|
|
97
|
+
await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (String(error.message ?? error).includes("errcode 300004"))
|
|
101
|
+
modelPresent = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
91
104
|
return {
|
|
92
105
|
schemaCode: args.schemaCode,
|
|
106
|
+
...(modelPresent ? {} : { modelPresent: false, hint: "模型不存在(field.list 300004)——空规则是假象;先 model.create / configure_model,判定模型面用 model.inspect" }),
|
|
93
107
|
...(args.onlyCustom === true ? { filtered: { onlyCustom: true, total: rows.length, returned: visible.length } } : {}),
|
|
94
108
|
rules: visible.map((row) => ({
|
|
95
109
|
code: row.code,
|
|
@@ -7,6 +7,7 @@ import { flattenTreeRows } from "../support.js";
|
|
|
7
7
|
import { defineTool } from "../tool-types.js";
|
|
8
8
|
import { wireCall, wireRows } from "../wire.js";
|
|
9
9
|
import { ENVIRONMENT_LANE_CONTRACT } from "./environment-contract.js";
|
|
10
|
+
import { readProjectCard, writeProjectCard } from "../project-card.js";
|
|
10
11
|
const ENVIRONMENT_LANE = ENVIRONMENT_LANE_CONTRACT.lane;
|
|
11
12
|
const ENVIRONMENT_TOOLS = ENVIRONMENT_LANE_CONTRACT.tools;
|
|
12
13
|
export function registerEnvironmentLane(tools) {
|
|
@@ -22,9 +23,17 @@ export function registerEnvironmentLane(tools) {
|
|
|
22
23
|
applicationCode: z.string().optional(),
|
|
23
24
|
modelCode: z.string().optional(),
|
|
24
25
|
full: z.boolean().optional().describe("内联完整发现结果;默认返回计数与 detailFile"),
|
|
26
|
+
refresh: z.boolean().optional().describe("跳过项目卡缓存强制重查(模型清单有变更但未被自动失效时用)"),
|
|
25
27
|
capability: capabilityArg,
|
|
26
28
|
},
|
|
27
29
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
30
|
+
// 2026-11-01 项目卡(预算压缩批 v1):新鲜缓存命中时一次本地读替代
|
|
31
|
+
// 2–3 次 wire(r31–r37 每会话重新盘点的纯浪费)。refresh:true 强制重查。
|
|
32
|
+
if (args.refresh !== true && args.applicationCode === undefined && args.modelCode === undefined && args.full !== true) {
|
|
33
|
+
const cached = readProjectCard(ctx.artifactsDir, String(args.environment ?? session.environmentName ?? ""));
|
|
34
|
+
if (cached !== undefined)
|
|
35
|
+
return { ...cached, status: "ok", fromCache: true };
|
|
36
|
+
}
|
|
28
37
|
// List every application the tenant exposes — the configured
|
|
29
38
|
// applicationCode is a default, never the only application. Resolve
|
|
30
39
|
// the active code through the same priority chain as writes:
|
|
@@ -120,7 +129,7 @@ export function registerEnvironmentLane(tools) {
|
|
|
120
129
|
.filter((row) => !args.modelCode || row.code === args.modelCode || row.schemaCode === args.modelCode)
|
|
121
130
|
.map((row) => ({ schemaCode: row.code ?? row.schemaCode, name: row.name }));
|
|
122
131
|
}
|
|
123
|
-
|
|
132
|
+
const discovered = {
|
|
124
133
|
applications: apps,
|
|
125
134
|
groups,
|
|
126
135
|
applicationCode: selected ?? null,
|
|
@@ -130,6 +139,21 @@ export function registerEnvironmentLane(tools) {
|
|
|
130
139
|
foldersAndModels,
|
|
131
140
|
models,
|
|
132
141
|
};
|
|
142
|
+
// 项目卡写入(软失败):默认形态(无收窄参数)时缓存模型清单,下次会话一次本地读。
|
|
143
|
+
if (args.applicationCode === undefined && args.modelCode === undefined && args.full !== true) {
|
|
144
|
+
const envName = String(args.environment ?? session.environmentName ?? "");
|
|
145
|
+
if (envName !== "" && Array.isArray(discovered.models) && discovered.models.length > 0 && typeof selected === "string" && selected !== "") {
|
|
146
|
+
writeProjectCard(ctx.artifactsDir, {
|
|
147
|
+
environment: envName,
|
|
148
|
+
applicationCode: selected,
|
|
149
|
+
...(applicationResolution !== undefined ? { applicationResolution: { code: String(selected ?? applicationResolution.requested ?? ""), source: String(applicationResolution.source ?? "") } } : {}),
|
|
150
|
+
models: discovered.models.slice(0, 300).map((row) => ({ code: String(row.schemaCode), name: String(row.name ?? "") })),
|
|
151
|
+
modelCount: discovered.models.length,
|
|
152
|
+
generatedAt: new Date().toISOString(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return discovered;
|
|
133
157
|
}, {
|
|
134
158
|
full: args.full === true,
|
|
135
159
|
target: `discover_environment:${args.applicationCode ?? ""}:${args.modelCode ?? ""}`,
|
package/dist/lanes/form.js
CHANGED
|
@@ -143,6 +143,24 @@ export function registerFormLane(tools) {
|
|
|
143
143
|
publishedControlCodes: publishedControls,
|
|
144
144
|
...(wanted ? { draftControls: pick(sheet.draftAttributesJson), publishedControls: pick(sheet.publishedAttributesJson) } : {}),
|
|
145
145
|
};
|
|
146
|
+
}, {
|
|
147
|
+
// 2026-11-01 读门 2048:controls=true 是**显式**取控件定义的请求——compact
|
|
148
|
+
// 保留定义映射与按钮/options(--full 的语义面),其余大数组归码表/计数。
|
|
149
|
+
compact: (result) => {
|
|
150
|
+
const KEEP = new Set(["draftControls", "publishedControls", "customButtonJson", "options", "formButtons", "formButtonCodes"]);
|
|
151
|
+
const out = {};
|
|
152
|
+
for (const [key, value] of Object.entries(result)) {
|
|
153
|
+
if (KEEP.has(key))
|
|
154
|
+
out[key] = value;
|
|
155
|
+
else if (Array.isArray(value))
|
|
156
|
+
out[`${key}Codes`] = value.map((entry) => (entry && typeof entry === "object" ? String(entry.code ?? entry.propertyCode ?? "") : String(entry))).filter((entry) => entry.length > 0), out[`${key}Count`] = value.length;
|
|
157
|
+
else if (value !== null && typeof value === "object")
|
|
158
|
+
out[`${key}Count`] = Object.keys(value).length;
|
|
159
|
+
else
|
|
160
|
+
out[key] = value;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
},
|
|
146
164
|
}),
|
|
147
165
|
}));
|
|
148
166
|
writeTool(tools, "form.draft", FORM_LANE, FORM_TOOLS["form.draft"].scope, FORM_TOOLS["form.draft"].description, {
|
|
@@ -242,7 +260,23 @@ export function registerFormLane(tools) {
|
|
|
242
260
|
});
|
|
243
261
|
results.push({ step: "form.publish", errcode: publishOutcome.performed.errcode });
|
|
244
262
|
const published = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode } });
|
|
245
|
-
|
|
263
|
+
const pub = (published.data ?? {});
|
|
264
|
+
const publishedControlCodes = parseControlCodes(pub.publishedAttributesJson);
|
|
265
|
+
// r40 F4:published=true ≠ 请求的字段进了布局——模型已有手工布局时,非 rebuild
|
|
266
|
+
// 草稿保留原控件,新字段不会出现,而 formPublished 仍是 true(假成功)。
|
|
267
|
+
// readback 必须核「请求面是否真的落进 published 控件」,缺了就点名+给重建路。
|
|
268
|
+
const missing = (args.fieldCodes ?? []).filter((code) => !publishedControlCodes.includes(code));
|
|
269
|
+
return {
|
|
270
|
+
steps: results,
|
|
271
|
+
readback: {
|
|
272
|
+
formPublished: pub.published === true,
|
|
273
|
+
publishedHtmlJsonPresent: typeof pub.publishedHtmlJson === "string",
|
|
274
|
+
publishedControlCodes,
|
|
275
|
+
...(missing.length > 0
|
|
276
|
+
? { missingFieldCodes: missing, warning: `表单已发布,但请求的 ${missing.join("、")} 不在 published 控件里(既有手工布局未带上新字段)——重跑 configure_form 带 rebuild:true(或 form.draft {rebuild:true} → form.publish)后重验本字段` }
|
|
277
|
+
: {}),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
246
280
|
},
|
|
247
281
|
summarize: (args, _outcome, phase) => writeSummary("configure_form", args, undefined, phase),
|
|
248
282
|
});
|
package/dist/path-projection.js
CHANGED
|
@@ -2,6 +2,12 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { readArtifact } from "./artifacts.js";
|
|
3
3
|
import { artifactRoot } from "./command-transport.js";
|
|
4
4
|
export function projectJsonPath(value, path) {
|
|
5
|
+
// r40 F9:`$.x`(jq 标准根写法)此前 pathMiss——agent/人都习惯带根前缀。
|
|
6
|
+
// 归一剥掉前导 `$.`/`$`,语义等价(本 DSL 根就是整份结果)。
|
|
7
|
+
if (path.startsWith("$."))
|
|
8
|
+
path = path.slice(2);
|
|
9
|
+
else if (path === "$")
|
|
10
|
+
path = "";
|
|
5
11
|
// 2026-11-01(r36 维护者实测):jq 切片 `a[0:6]`/`a[:n]`/`a[n:]` 此前
|
|
6
12
|
// pathMiss 且 hint 的 available 还把数组列成"可用键",误导下一步。数组
|
|
7
13
|
// 切片与 jq 语义一致(含头不含尾)。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project card — one ~1KB local read that replaces the per-session 3–5 live
|
|
3
|
+
* wire calls of environment discovery (discover_environment → 2–3 wire reads;
|
|
4
|
+
* model.list → 1 wire read; r31–r37 every probe re-inventoried the platform).
|
|
5
|
+
*
|
|
6
|
+
* Design copied from describe-cache (`.omc/describe-full-cache.json`):
|
|
7
|
+
* an index file inside the artifacts dir points at an artifact body; a hit is
|
|
8
|
+
* soft (failure never breaks the main path), freshness = TTL + model-write
|
|
9
|
+
* invalidation; content changes that don't touch models (fields/rules/views)
|
|
10
|
+
* do NOT change the model inventory and don't invalidate.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { createArtifact, readArtifact } from "./artifacts.js";
|
|
15
|
+
const CARD_INDEX = "project-card.json";
|
|
16
|
+
const CARD_TTL_MS = 6 * 60 * 60 * 1000;
|
|
17
|
+
function indexPath(root) {
|
|
18
|
+
return path.join(root, CARD_INDEX);
|
|
19
|
+
}
|
|
20
|
+
/** Fresh card for this environment, or undefined (missing/stale/other env). */
|
|
21
|
+
export function readProjectCard(root, environment) {
|
|
22
|
+
try {
|
|
23
|
+
const index = JSON.parse(fs.readFileSync(indexPath(root), "utf8"));
|
|
24
|
+
if (index.environment !== environment)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (Date.now() - index.at > CARD_TTL_MS)
|
|
27
|
+
return undefined;
|
|
28
|
+
const card = JSON.parse(readArtifact(root, index.artifactId).content.toString("utf8"));
|
|
29
|
+
return { ...card, fromCacheHint: `项目卡 ${new Date(index.at).toISOString()} 生成(TTL 6h,模型写操作失效);要强制重查传 refresh:true` };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Persist the card (artifact body + index). Soft-fail: never throws. */
|
|
36
|
+
export function writeProjectCard(root, card) {
|
|
37
|
+
try {
|
|
38
|
+
const artifact = createArtifact(root, JSON.stringify({ ...card, fromCacheHint: undefined }));
|
|
39
|
+
const index = { artifactId: artifact.artifactId, environment: card.environment, at: Date.now(), modelCount: card.modelCount };
|
|
40
|
+
fs.writeFileSync(indexPath(root), JSON.stringify(index), "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// The card is a pure optimization layer; failure to cache is not an error.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Invalidate on model-shape writes (create/delete) so a fresh session never reads a stale inventory. */
|
|
47
|
+
export function invalidateProjectCard(root) {
|
|
48
|
+
try {
|
|
49
|
+
fs.rmSync(indexPath(root), { force: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// best-effort
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=project-card.js.map
|
package/dist/read.js
CHANGED
|
@@ -56,7 +56,10 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
56
56
|
if (ctx.readbackCacheBypass === 0 && cached && cached.environmentFingerprint === environmentFingerprint && cached.targetFingerprint === targetFingerprint && options.full !== true)
|
|
57
57
|
return { status: "ok", unchanged: true, detailFile: cached.detailFile, detailSha256: cached.detailSha256, environmentFingerprint };
|
|
58
58
|
const detail = { status: "ok", ...result, environmentFingerprint };
|
|
59
|
-
|
|
59
|
+
// 2026-11-01 预算批 v2(r38 usage 实证):1-4KB 中结果 59 次/135KB≈37% 全内联,
|
|
60
|
+
// model.inspect 17 次全部卡在旧门下。降到 2048;compact 对配置清单保形为码表
|
|
61
|
+
// (CODE_LIST_KEYS),完整正文仍在 detailArtifactId。
|
|
62
|
+
const large = jsonBytes(result) > 2048;
|
|
60
63
|
const fileName = `${tool.name.replace(/[^a-zA-Z0-9._-]/g, "_")}-${sha256(target).slice(0, 16)}.json`;
|
|
61
64
|
const artifactRef = writeArtifact(ctx, fileName, detail);
|
|
62
65
|
ctx.readbackCache.set(target, { environmentFingerprint, targetFingerprint, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 });
|
|
@@ -100,12 +103,29 @@ function compactReadback(result) {
|
|
|
100
103
|
if (BULKY_READ_KEYS.has(key)) {
|
|
101
104
|
compact[`${key}Count`] = Array.isArray(value) ? value.length : value && typeof value === "object" ? Object.keys(value).length : 0;
|
|
102
105
|
}
|
|
106
|
+
else if (CODE_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
107
|
+
// 2026-11-01 预算批 v2:读门 4096→2048 后,中结果(r38 实测 model.inspect
|
|
108
|
+
// 17 次 51KB 全部卡旧门下全内联)走 compact——配置清单类数组保形为码表
|
|
109
|
+
// (不是只给 count),否则 agent 被迫二次 artifact get 反加回合。
|
|
110
|
+
compact[key] = value.map(codeOfEntry).filter((entry) => entry !== undefined);
|
|
111
|
+
compact[`${key}Count`] = value.length;
|
|
112
|
+
}
|
|
103
113
|
else {
|
|
104
114
|
compact[key] = summarizeValue(value);
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
return compact;
|
|
108
118
|
}
|
|
119
|
+
/** Config-inventory keys that compact to a code list (not a bare count). */
|
|
120
|
+
const CODE_LIST_KEYS = new Set(["fields", "columns", "conditions", "sorts", "datarules", "buttons", "views"]);
|
|
121
|
+
function codeOfEntry(entry) {
|
|
122
|
+
for (const key of ["propertyCode", "code", "schemaCode", "ruleCode", "fieldCode"]) {
|
|
123
|
+
const value = entry[key];
|
|
124
|
+
if (typeof value === "string" && value.length > 0)
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
109
129
|
/**
|
|
110
130
|
* Keys whose values are the bulk a compact projection replaces with a count.
|
|
111
131
|
* Everything else is a small facet identity/decision fields (workflowCode,
|
package/dist/skills.js
CHANGED
|
@@ -157,6 +157,23 @@ export function readSkillSection(name, requested) {
|
|
|
157
157
|
* headings. An index costs ~600 B and names the price of each part, so the
|
|
158
158
|
* caller chooses instead of paying for the whole document by default.
|
|
159
159
|
*/
|
|
160
|
+
/** First meaningful body line of a section: skip headings/blank/fence/list markers. */
|
|
161
|
+
function sectionBlurb(body) {
|
|
162
|
+
const limit = 70;
|
|
163
|
+
// sliceSectionText includes the section's own heading line — skip it.
|
|
164
|
+
for (const raw of body.split(/\r?\n/).slice(1)) {
|
|
165
|
+
const line = raw.trim();
|
|
166
|
+
if (line.length === 0)
|
|
167
|
+
continue;
|
|
168
|
+
if (line.startsWith("#") || line.startsWith("```") || line.startsWith("<!--") || line.startsWith("|"))
|
|
169
|
+
continue;
|
|
170
|
+
const text = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s*/, "").replace(/\*\*/g, "");
|
|
171
|
+
if (text.length === 0)
|
|
172
|
+
continue;
|
|
173
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
160
177
|
export function readSkillOutline(name) {
|
|
161
178
|
const document = readSkill(name);
|
|
162
179
|
if (!document)
|
|
@@ -165,7 +182,7 @@ export function readSkillOutline(name) {
|
|
|
165
182
|
const headings = scanHeadings(lines);
|
|
166
183
|
const sections = headings.map((entry) => {
|
|
167
184
|
const body = sliceSectionText(lines, entry.index, entry.level);
|
|
168
|
-
return { heading: entry.heading, level: entry.level, bytes: Buffer.byteLength(body, "utf8") };
|
|
185
|
+
return { heading: entry.heading, level: entry.level, bytes: Buffer.byteLength(body, "utf8"), blurb: sectionBlurb(body) };
|
|
169
186
|
});
|
|
170
187
|
const bytes = Buffer.byteLength(document.content, "utf8");
|
|
171
188
|
// A topic whose whole body sits under a single H1 (the title) would produce a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
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.10",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.5.
|
|
57
|
+
"omc-core": "0.5.10",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|
|
@@ -189,11 +189,19 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
189
189
|
## 审批身份与角色作用域(r30 真机 2026-10-31)
|
|
190
190
|
|
|
191
191
|
- `workitem.list` 只回**当前登录人**的待办:审批人配别人 → 看不到待办,`data.submit` 被 `submit-workitem-not-mine` 拒(40005)。演示闭环要么节点参与者用 `{Originator}`,要么把登录人加进审批角色。
|
|
192
|
-
- **`org.role.addUsers` 必须紧跟 `
|
|
192
|
+
- **`org.role.addUsers` 必须紧跟 `setUserScope`**:不布作用域仅警告但成员无法被流程解析为参与人。一对调用别做半。
|
|
193
193
|
- **`transferDepartment` 迁人不迁作用域**(r33 真机):换部门后 `role.userScope` 仍指旧部门——迁移后复核成员作用域。
|
|
194
194
|
- **`user.delete` 是软删**(r33 真机):回 `deleted:true` 但 `user.info` 仍 `exists:true`(DISABLE)——验证口径用列表/状态,别称物理清除。
|
|
195
195
|
- **「名称重复 200001」≠ 失败**(r33 真机):多为首次已建成、重发才撞重复——先读回确认首建结果再决定。
|
|
196
|
-
-
|
|
196
|
+
- **删了的东西≠删干净(孤儿三连,1101 三例)**:①删模型留收藏+h_biz_* 行→日志刷 `appFunction not exist`;②规则死服务 A1→submit 500015;③流程模板删后绑定残留(h_workflow_relative_object/header 指死 code)→getWorkflow 50000。都是删上游留下游引用、报错在别处——先查孤儿引用,维护者按表清。
|
|
197
|
+
- **两套条件 DSL(r38/r39)**:listview.configure `conditions`={propertyCode, filterType, **defaultValue**}(值键叫 defaultValue);data.list `filter`={op, field, value}。互抄必错——形状各查 `--param conditions`/`--param filter`。
|
|
198
|
+
- **表单读回以控件清单为准,不看布尔位(r40)**:form.get 的 `published` 布尔会 false→true 抖动;draftViewJson 可含已删字段悬空引用(rebuild 自清)。判定布局依据=`publishedControlCodes` 内容;configure_form 回执已点名 `missingFieldCodes`。
|
|
199
|
+
- **submit 被拒仍可能已落草稿(r40)**:`data.save --submit:true` 遇许可证/权限拒绝时平台可能在拒答前已写草稿——拒后 data.list 对账 total,残留 data.delete 单清。
|
|
200
|
+
- **空聚合≠模型存在(r40)**:rule.list/datarule.list 对不存在模型静默回空——「0 条规则」≠模型在(mcc_contract 从不存在却一路答空)。rule.list 已修(空时探真身标 `modelPresent:false`);datarule.list 同陷阱——判存在性一律 model.inspect。
|
|
201
|
+
- **form.publish(rebuild) 后立即 submit 可能丢字段(r41 疑似竞态)**:链内演示行 `order_no` 落 null,同形状延迟复现正常。落演示行后 `data.load` 抽验非选项字段,缺值 data.save 单补,不重跑整链。
|
|
202
|
+
- **data.list 选项列投影可为 null(r40/r41)**:`all:true`/queryCode 下 RADIO 读 null,`data.load`/`filter` 才有值——验证选项字段以 data.load 为权威。
|
|
203
|
+
- **读回形状≠可写形状**(r38):`listview.get` 读回条件带 `null` 字段,原样回写被拒——读回是输出形态,可写面有自己的契约;null 归一(false/省略)或只回写要改的键。
|
|
204
|
+
- **data.list 的 columns 收窄可能静默空**(r38):非视图列字段投影 null;甚至 columns+分页 total>0 而 items 0——判"无数据"先去 columns 复查或用 data.load。
|
|
197
205
|
|
|
198
206
|
## workflow.update 的行级替换语义(r32 真机 2026-10-31)
|
|
199
207
|
|