@stalfh233/omc-cli 0.5.7 → 0.5.9
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 +12 -2
- package/dist/field-add.js +149 -7
- package/dist/form-support.js +9 -0
- package/dist/goal-routes.js +3 -3
- package/dist/human-summary.js +1 -1
- 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 +25 -1
- 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-capabilities.md +1 -1
- package/skills/omc-start-here.md +1 -1
- package/skills/omc-write-hazards.md +13 -5
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,6 +1747,10 @@ 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
1756
|
.addHelpText("after", `\n存量模型加字段一条命令:建字段 → 发布字段 → 重建表单 → 列表追加列(增量合并,保留既有列/条件/排序)。\n默认 dry-run 只列计划;--apply 执行。幂等:字段已存在记 skipped 继续。\n子表不属于本命令(存量挂子表走 field.create CHILD_TABLE + 子列,见 model-scaffold)。\n\nr30/r31 探针实证:同一条链手工要走 4 次写调用 + 各自回读。\n`)
|
|
@@ -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
|
@@ -17,6 +17,70 @@ import { argvValue } from "./args.js";
|
|
|
17
17
|
import { runToolCall } from "./call.js";
|
|
18
18
|
import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from "./cli-contract.js";
|
|
19
19
|
import { writeEnvelope } from "./cli-output.js";
|
|
20
|
+
/**
|
|
21
|
+
* 字典简写展开(r30/r36 实证:字典字段要先 names 查 id、records 取记录、
|
|
22
|
+
* 再拼 optionsSet.dictionary 三段 4–5 调;`dictCode` 把它折进 field-add 一步):
|
|
23
|
+
* `{"code":"x","propertyType":"DROPDOWN_BOX","dictCode":"purchase_coop_status"}`
|
|
24
|
+
* → 内部解析 checkedDictionary(id)+checkedDictionaryCode+useDictionariesData(records)
|
|
25
|
+
* 按 dictionary-backed-single-select recipe 形状组装 options.optionsSet。
|
|
26
|
+
*/
|
|
27
|
+
async function expandDictShorthand(fields, environment, contextForTest, request) {
|
|
28
|
+
const problems = [];
|
|
29
|
+
const expanded = [];
|
|
30
|
+
const call = async (tool, args) => {
|
|
31
|
+
const outcome = await runToolCall(tool, {
|
|
32
|
+
args,
|
|
33
|
+
requestId: request,
|
|
34
|
+
...(environment !== undefined ? { environment } : {}),
|
|
35
|
+
...(contextForTest !== undefined ? { context: contextForTest } : {}),
|
|
36
|
+
print: () => undefined,
|
|
37
|
+
});
|
|
38
|
+
return outcome.exitCode === 0 ? outcome.result : null;
|
|
39
|
+
};
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const field of fields) {
|
|
42
|
+
const dictCode = field.dictCode;
|
|
43
|
+
if (typeof dictCode !== "string" || dictCode.length === 0) {
|
|
44
|
+
out.push({ ...field });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const rest = { ...field };
|
|
48
|
+
delete rest.dictCode;
|
|
49
|
+
// dictionary.list 是真假两平台都稳的清单面(names 的 getDictionaryIdAndNames
|
|
50
|
+
// 在 fake 上未实现);list 的回执可能是 {dictionaries:{content:[...]}} 分页
|
|
51
|
+
// 或裸数组,两种都吃。
|
|
52
|
+
const listResult = await call("dictionary.list", { limit: 200 });
|
|
53
|
+
const listPayload = listResult === null ? null : (listResult.dictionaries ?? listResult.names ?? listResult.content ?? listResult);
|
|
54
|
+
const names = Array.isArray(listPayload)
|
|
55
|
+
? listPayload
|
|
56
|
+
: (Array.isArray(listPayload?.content) ? listPayload.content : []);
|
|
57
|
+
const hit = names.find((row) => String(row.code ?? row.dictionaryCode ?? "") === dictCode);
|
|
58
|
+
if (hit === undefined) {
|
|
59
|
+
problems.push(`${String(field.code)}: 字典 ${dictCode} 不存在(dictionary.names 未命中)——先 dictionary.save 或修正 dictCode`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const dictId = String(hit.id ?? hit.dictionaryId ?? "");
|
|
63
|
+
const recordsResult = await call("dictionary.records", { code: dictCode, enabledOnly: true });
|
|
64
|
+
const recordsRaw = recordsResult === null ? [] : (recordsResult.records ?? {});
|
|
65
|
+
const recordRows = (Array.isArray(recordsRaw) ? recordsRaw : (recordsRaw.content ?? []));
|
|
66
|
+
if (recordRows.length === 0) {
|
|
67
|
+
problems.push(`${String(field.code)}: 字典 ${dictCode} 无启用记录——先补 dictionary.records`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const useDictionariesData = recordRows.map((row) => ({ id: String(row.id ?? ""), code: String(row.code ?? ""), name: String(row.name ?? row.code ?? "") }));
|
|
71
|
+
out.push({
|
|
72
|
+
...rest,
|
|
73
|
+
options: {
|
|
74
|
+
optionsSet: {
|
|
75
|
+
optionsType: "dictionary",
|
|
76
|
+
dictionary: { checkedDictionary: dictId, checkedDictionaryCode: dictCode, useDictionariesData },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
expanded.push(String(field.code));
|
|
81
|
+
}
|
|
82
|
+
return { fields: out, expanded, problems };
|
|
83
|
+
}
|
|
20
84
|
export function validateFields(schemaCode, fields) {
|
|
21
85
|
const problems = [];
|
|
22
86
|
const seen = new Set();
|
|
@@ -50,12 +114,17 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
50
114
|
const publishForm = !args.includes("--no-form");
|
|
51
115
|
const addToList = !args.includes("--no-list");
|
|
52
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");
|
|
53
119
|
const usage = [
|
|
54
120
|
"用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--apply]",
|
|
55
|
-
" --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options
|
|
121
|
+
" --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options 原样透传);",
|
|
122
|
+
" 字典简写:{\"dictCode\":\"<字典code>\"} 代替手拼 options.optionsSet(内部解析字典 id+启用记录,recipe 同形状)。",
|
|
56
123
|
" 默认 dry-run;--apply 依次执行 建字段 → 发布字段 → 重建表单 → 列表追加列。",
|
|
57
124
|
" 幂等:字段已存在记 skipped 继续;有真失败默认不发布/不重建(--force-publish 强发)。",
|
|
58
125
|
" --no-form 跳过表单重建;--no-list 跳过列表加列(默认两者都做)。",
|
|
126
|
+
" --conditions '<JSON 数组>' 列表固定过滤条件(listview.configure 契约;**整体替换**既有固定条件——要保留旧的须一并带上;不传则不动);需 --list(默认开)。",
|
|
127
|
+
" --demo-rows '<JSON 数组>' 演示数据行(每项一份 formData,逐行 data.submit);非幂等——重复执行会重复插行。",
|
|
59
128
|
].join("\n");
|
|
60
129
|
const invalid = (message) => {
|
|
61
130
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
@@ -78,6 +147,45 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
78
147
|
const problems = validateFields(schemaCode, fields);
|
|
79
148
|
if (problems.length > 0)
|
|
80
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
|
+
}
|
|
179
|
+
// 字典简写展开(dictCode → optionsSet.dictionary,含 id/记录解析)——展开失败
|
|
180
|
+
// 在任何写之前拒绝(名字对不上/无启用记录)。
|
|
181
|
+
let dictExpanded = 0;
|
|
182
|
+
if (fields.some((field) => typeof field.dictCode === "string")) {
|
|
183
|
+
const expandedResult = await expandDictShorthand(fields, environment, contextForTest, request);
|
|
184
|
+
if (expandedResult.problems.length > 0)
|
|
185
|
+
return invalid(`dictCode 展开失败(未写入):\n - ${expandedResult.problems.join("\n - ")}`);
|
|
186
|
+
fields = expandedResult.fields;
|
|
187
|
+
dictExpanded = expandedResult.expanded.length;
|
|
188
|
+
}
|
|
81
189
|
const options = {
|
|
82
190
|
schemaCode, fields, apply, publishForm, addToList, forcePublish,
|
|
83
191
|
...(environment !== undefined ? { environment } : {}),
|
|
@@ -115,11 +223,14 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
115
223
|
status: "dry-run",
|
|
116
224
|
schema: schemaCode,
|
|
117
225
|
fields: fields.map((field) => String(field.code)),
|
|
226
|
+
...(dictExpanded > 0 ? { dictExpanded } : {}),
|
|
118
227
|
steps: [
|
|
119
228
|
`field.create ×${fields.length}`,
|
|
120
229
|
"field.publish(发布失败/有字段失败则停)",
|
|
121
230
|
...(publishForm ? [`form.publish ${schemaCode} --rebuild(重建让新字段进默认表单)`] : []),
|
|
122
|
-
...(addToList ? [`listview.configure ${schemaCode}
|
|
231
|
+
...(addToList ? [`listview.configure ${schemaCode} 增量追加列(列保留;固定条件整体替换)`] : []),
|
|
232
|
+
...(conditions !== undefined ? [`listview.configure 附带固定过滤条件 ×${conditions.length}`] : []),
|
|
233
|
+
...(demoRows !== undefined ? [`data.submit 演示行 ×${demoRows.length}(非幂等)`] : []),
|
|
123
234
|
],
|
|
124
235
|
preflight: { checked: fields.length, passed: true },
|
|
125
236
|
note: "dry-run 只列计划;已对每个字段跑过本地形状预检(零 wire)。--apply 依次执行全部步骤。",
|
|
@@ -199,21 +310,38 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
199
310
|
}
|
|
200
311
|
// 4) 列表增量加列:默认合并语义保留既有列/条件/排序(r31 实证);只有字段全部
|
|
201
312
|
// 失败才跳过(部分成功时把成功的列加上去仍有意义)。
|
|
202
|
-
|
|
203
|
-
|
|
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);
|
|
204
320
|
steps.push({
|
|
205
321
|
step: "listview.configure",
|
|
206
322
|
status: configured.ok ? "ok" : "failed",
|
|
207
323
|
detail: configured.ok
|
|
208
|
-
? { schemaCode, appended: createdCodes, semantics: "
|
|
209
|
-
: { schemaCode,
|
|
324
|
+
? { schemaCode, ...(createdCodes.length > 0 ? { appended: createdCodes } : {}), ...(conditions !== undefined ? { conditions: conditions.length, conditionsSemantics: "整体替换(既有固定条件要保留须一并带上)" } : {}), semantics: "列增量合并保留既有列/排序;固定条件整体替换" }
|
|
325
|
+
: { schemaCode, ...listviewArgs, message: String(configured.result.message ?? "") },
|
|
210
326
|
});
|
|
211
327
|
}
|
|
212
328
|
else if (addToList) {
|
|
213
329
|
steps.push({
|
|
214
330
|
step: "listview.configure",
|
|
215
331
|
status: "skipped",
|
|
216
|
-
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 ?? "") },
|
|
217
345
|
});
|
|
218
346
|
}
|
|
219
347
|
const failedSteps = steps.filter((step) => step.status === "failed");
|
|
@@ -230,6 +358,20 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
230
358
|
...(lastSummaries.length > 0 ? { summaries: lastSummaries } : {}),
|
|
231
359
|
steps,
|
|
232
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
|
+
: {}),
|
|
233
375
|
};
|
|
234
376
|
const classified = classifyResult(status === "partial" ? { ...result, requested: steps.length, succeeded: steps.length - failedSteps.length } : result);
|
|
235
377
|
writeEnvelope(envelope(classified.status, result, request, classified.error));
|
package/dist/form-support.js
CHANGED
|
@@ -1147,6 +1147,15 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
: {}),
|
|
1149
1149
|
...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}),
|
|
1150
|
+
// r36(2026-11-01):rebuild 后草稿与重建前逐字节一致时明示——rebuild 的
|
|
1151
|
+
// 语义就是"按已发布字段重搭",一致通常意味着预期的新控件没进来(字段未
|
|
1152
|
+
// 发布/写错槽位),静默 no-op 会让"新增字段后刷新表单"看起来成功实则没变。
|
|
1153
|
+
...(args.rebuild === true && previousAttributes !== undefined && typeof attributes === "string" && attributes.trim() === JSON.stringify(previousAttributes).trim()
|
|
1154
|
+
? {
|
|
1155
|
+
rebuildUnchanged: true,
|
|
1156
|
+
rebuildNote: "rebuild 后草稿与重建前完全一致——若你预期新增控件(新字段上表单),检查:① 字段是否已 field.publish;② 写入槽位 sheetCode 是否正确(省略=模型 code 槽位);r36 实测同序列下重建静默 no-op。",
|
|
1157
|
+
}
|
|
1158
|
+
: {}),
|
|
1150
1159
|
},
|
|
1151
1160
|
};
|
|
1152
1161
|
}
|
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/human-summary.js
CHANGED
|
@@ -126,7 +126,7 @@ function phraseOf(tool, args, outcome) {
|
|
|
126
126
|
case "field.publish":
|
|
127
127
|
return `发布模型 ${schemaCode ?? "?"} 的全部草稿字段(模型级一次发布)`;
|
|
128
128
|
case "form.draft":
|
|
129
|
-
return `保存表单 ${stringArg(args, "sheetCode") ?? "?"}(模型 ${schemaCode ?? "?"}
|
|
129
|
+
return `保存表单 ${stringArg(args, "sheetCode") ?? (schemaCode !== undefined ? String(schemaCode) : "?")}(模型 ${schemaCode ?? "?"})的草稿布局——sheetCode 省略时实际写入 = 模型 code 槽位`;
|
|
130
130
|
case "form.publish":
|
|
131
131
|
// r26 finding: sheetCode is optional (resolved from schemaCode for the
|
|
132
132
|
// default form) — the summary showed "?" when the agent omitted it.
|
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,30 @@ 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 = "";
|
|
11
|
+
// 2026-11-01(r36 维护者实测):jq 切片 `a[0:6]`/`a[:n]`/`a[n:]` 此前
|
|
12
|
+
// pathMiss 且 hint 的 available 还把数组列成"可用键",误导下一步。数组
|
|
13
|
+
// 切片与 jq 语义一致(含头不含尾)。
|
|
14
|
+
const sliceSel = /\[(\d*):(\d*)\]/.exec(path);
|
|
15
|
+
if (sliceSel !== null) {
|
|
16
|
+
const [, rawFrom, rawTo] = sliceSel;
|
|
17
|
+
const before = path.slice(0, sliceSel.index);
|
|
18
|
+
const after = path.slice(sliceSel.index + sliceSel[0].length);
|
|
19
|
+
const base = projectJsonPath(value, before);
|
|
20
|
+
if (base.missed)
|
|
21
|
+
return base;
|
|
22
|
+
if (!Array.isArray(base.value))
|
|
23
|
+
return { missed: true, at: before || "$", available: `该层不是数组(是 ${base.value === null ? "null" : typeof base.value});切片 [from:to] 需要数组` };
|
|
24
|
+
const from = rawFrom === "" ? undefined : Number(rawFrom);
|
|
25
|
+
const to = rawTo === "" ? undefined : Number(rawTo);
|
|
26
|
+
const sliced = base.value.slice(from, to);
|
|
27
|
+
return after === "" ? { missed: false, value: sliced } : projectJsonPath(sliced, after);
|
|
28
|
+
}
|
|
5
29
|
// 1.5 of issue 2026-10-23: an array-of-objects selection WITHOUT jq pipes.
|
|
6
30
|
// `.rules[code=Create]` (or `[code="Create"]`) picks the FIRST element whose
|
|
7
31
|
// key matches — exactly the shape callers reached for with
|
|
@@ -97,7 +121,7 @@ function argvValues(args, name) {
|
|
|
97
121
|
return values;
|
|
98
122
|
}
|
|
99
123
|
/** 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 是该层真实存在的键。";
|
|
124
|
+
const PATH_MISS_HINT = "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、切片 a[0:6]、数组映射 items[].fields.x、按键选择 rules[code=Create](返回第一个匹配项)。available 是该层真实存在的键。";
|
|
101
125
|
/**
|
|
102
126
|
* `call --extract/--extract-keys`: resolve paths inside the result's own
|
|
103
127
|
* artifact (or inside the result object when the tool returned no artifact).
|
|
@@ -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.9",
|
|
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.9",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.5.
|
|
57
|
+
"omc-core": "0.5.9",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|
|
@@ -68,7 +68,7 @@ only the section for the capability you need —
|
|
|
68
68
|
- **能做哪些事**:字典与字典记录全生命周期(键值型/树型、启停);作为选择字段的选项来源;字典分类。
|
|
69
69
|
- **工具**:`dictionary.list`、`dictionary.type`、`dictionary.names`、`dictionary.get`、`dictionary.records`、`dictionary.save`、`dictionary.status`、`dictionary.usage`、`dictionary.parent`、`dictionary.classes`、`dictionary.class.save`、`dictionary.class.delete`
|
|
70
70
|
- **recipe**:`omc describe dictionary.save --example dictionary-key-value`
|
|
71
|
-
- **边界 / 陷阱**:选择字段有三种选项来源——`custom` / `dictionary` / `businessModel`;关联单选/多选支持 `searchFormula` 与 `conditions` JSON
|
|
71
|
+
- **边界 / 陷阱**:选择字段有三种选项来源——`custom` / `dictionary` / `businessModel`;关联单选/多选支持 `searchFormula` 与 `conditions` JSON 筛选。**字典没有删除**——平台只有停用/启用(`dictionary.status`,target= dictionary|record);不要找 dictionary.delete(分类有 `class.delete`,字典本体没有),清理测试残留用停用即可。
|
|
72
72
|
|
|
73
73
|
## 业务模型 `model`
|
|
74
74
|
|
package/skills/omc-start-here.md
CHANGED
|
@@ -12,7 +12,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
12
12
|
|
|
13
13
|
## 最小路径(先读这段就能开始)
|
|
14
14
|
|
|
15
|
-
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**对输出做内容检索前先加 `--utf8`(单次明文)或先解 JSON,"grep 搜不到中文"≠"没有配置"
|
|
15
|
+
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**对输出做内容检索前先加 `--utf8`(单次明文)或先解 JSON,"grep 搜不到中文"≠"没有配置"**(静默假阴性坑)。stdout 恒为纯 JSON、人读进度在 stderr——脚本解析勿 `2>&1` 合流。
|
|
16
16
|
|
|
17
17
|
0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true`——命中这些形态先看 `--help`,比 recipe 手拼省 10+ 轮。
|
|
18
18
|
0c. **清理一个模型用 `omc model-purge --code <x> [--purge-instances] --apply`**——按依赖倒序(实例 → 模型级联带走流程/视图/字段/表单),**不要手工逐层删**:删部门/字段留下缓存或孤儿,会让后续调用出现「假 40009 / 假孤儿 / 全局待办 50000」这类幽灵故障(r12–r14 三次实测)。若返回 `unremovableInstances`(业务对象已删的停滞实例,平台 `delete_instance` NPE),按返回的 `maintainerAction` 让维护者清 `biz_workflow_instance`;**不要改用 model.delete 硬来**。
|
|
@@ -189,16 +189,24 @@ 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` 必须紧跟 `
|
|
193
|
-
- **`transferDepartment` 迁人不迁作用域**(r33
|
|
194
|
-
- **`user.delete` 是软删**(r33 真机):回 `deleted:true` 但 `user.info` 仍 `exists:true
|
|
192
|
+
- **`org.role.addUsers` 必须紧跟 `setUserScope`**:不布作用域仅警告但成员无法被流程解析为参与人。一对调用别做半。
|
|
193
|
+
- **`transferDepartment` 迁人不迁作用域**(r33 真机):换部门后 `role.userScope` 仍指旧部门——迁移后复核成员作用域。
|
|
194
|
+
- **`user.delete` 是软删**(r33 真机):回 `deleted:true` 但 `user.info` 仍 `exists:true`(DISABLE)——验证口径用列表/状态,别称物理清除。
|
|
195
195
|
- **「名称重复 200001」≠ 失败**(r33 真机):多为首次已建成、重发才撞重复——先读回确认首建结果再决定。
|
|
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。
|
|
196
205
|
|
|
197
206
|
## workflow.update 的行级替换语义(r32 真机 2026-10-31)
|
|
198
207
|
|
|
199
208
|
- **`btnConfigList` 局部行=整行替换**:只传 `{code, remark}` 会把该按钮的 `enabled` 静默置 `false`、`commentPermission` 置 `null`("驳回意见必填"丢失),同提交未提及的其他属性也被重置。改前先 `workflow.get_published`/`get_draft` 取整行,改目标属性后**整条回写**;`propertyPermissions` 同粒度。
|
|
200
|
-
-
|
|
201
|
-
- **`SINGLE_PARTICIPANT` 配多角色只取第一个解析出的成员**(`MULTI_PARTICIPANT` 则全员会签)——演示/测试时"配置没生效"常是它;回读确认实际解析人。
|
|
209
|
+
- **`SINGLE_PARTICIPANT` 多角色只取第一个解析成员**(MULTI 则全员会签)——"配置没生效"常是它,回读确认解析人。
|
|
202
210
|
|
|
203
211
|
## 跨表回写的真实语义(1.1.25 源码 + 真机 2026-10-20)
|
|
204
212
|
|