@stalfh233/omc-cli 0.4.34 → 0.4.36

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 CHANGED
@@ -36,6 +36,7 @@ import { listSkills, readSkill, readSkillSection } from "./skills.js";
36
36
  import { parseToolInput, ToolInputError, ToolRefusalError } from "./tool-types.js";
37
37
  import { WireError, wireCall } from "./wire.js";
38
38
  import { ENDPOINTS } from "./endpoints.js";
39
+ import { extractBizObjectPage } from "./lanes/runtime-data.js";
39
40
  import { makeToolContext } from "./context.js";
40
41
  import { SessionManager } from "./session-manager.js";
41
42
  async function ask(question) {
@@ -843,6 +844,23 @@ async function cmdIntentPlan(file, environment, planOut, transport) {
843
844
  const planJson = JSON.stringify(plan, null, 2) + "\n";
844
845
  const artifact = createArtifact(artifactRoot(), planJson);
845
846
  const warnings = intentWarnings(intent);
847
+ // r39: a destructive plan that deletes a model carrying runtime data got
848
+ // ZERO warning — the approval object must surface the blast radius. One
849
+ // cheap total-only query per declared-absent model (never blocks the plan:
850
+ // an unreadable runtime face just means no count to show).
851
+ for (const model of intent.models) {
852
+ if (model.absent !== true)
853
+ continue;
854
+ try {
855
+ const query = await wireCall(session, { path: ENDPOINTS.runtimeQueryList, method: "POST", body: { schemaCode: model.code, queryCode: model.code, filters: [], mobile: false, page: 0, size: 1, queryVersion: 1, showTotal: true } });
856
+ const total = extractBizObjectPage(query.data).total;
857
+ if (total > 0)
858
+ warnings.push({ resource: `model:${model.code}`, code: "destructive-with-data", message: `模型 ${model.code} 含 ${total} 条运行数据,absent:true 删除将一并销毁(不可恢复)——确认后再 apply` });
859
+ }
860
+ catch {
861
+ // 运行面不可读(模型/视图未就绪等)= 无法计数,不阻塞 plan
862
+ }
863
+ }
846
864
  const toolCounts = new Map();
847
865
  for (const change of compiled.changes)
848
866
  toolCounts.set(change.tool, (toolCounts.get(change.tool) ?? 0) + 1);
@@ -869,6 +887,7 @@ async function cmdIntentPlan(file, environment, planOut, transport) {
869
887
  changeSummary,
870
888
  planArtifactId: artifact.artifactId,
871
889
  planArtifactSha256: artifact.sha256,
890
+ ...(warnings.length > 0 ? { warnings } : {}),
872
891
  nextAction: `omc intent apply ${planOut}`,
873
892
  };
874
893
  }
@@ -1176,12 +1195,24 @@ async function cmdIntentApply(planPath, options, transport) {
1176
1195
  if (options.verify === false)
1177
1196
  return autoPlanned ? { ...base, plannedFrom: "intent-yaml" } : base;
1178
1197
  let verification;
1179
- try {
1180
- const report = await verifyIntentLive(session, plan.intent);
1181
- verification = { ...dietIntentLive(report, context), status: report.equivalent === true ? "ok" : "verification-failed" };
1182
- }
1183
- catch (error) {
1184
- verification = { status: "error", equivalent: false, error: error instanceof Error ? error.message : String(error) };
1198
+ // r39: a destructive (absent:true) delete tears the model down in
1199
+ // stages — fields/views drop first, the model shell lags a moment. An
1200
+ // immediate readback then reports a phantom mismatch. Retry the live
1201
+ // verification briefly (bounded) before declaring failure.
1202
+ const declaresAbsence = plan.intent.models.some((model) => model.absent === true);
1203
+ const attempts = declaresAbsence ? 3 : 1;
1204
+ for (let attempt = 1;; attempt++) {
1205
+ try {
1206
+ const report = await verifyIntentLive(session, plan.intent);
1207
+ verification = { ...dietIntentLive(report, context), status: report.equivalent === true ? "ok" : "verification-failed" };
1208
+ }
1209
+ catch (error) {
1210
+ verification = { status: "error", equivalent: false, error: error instanceof Error ? error.message : String(error) };
1211
+ }
1212
+ if (verification.equivalent === true || attempt >= attempts)
1213
+ break;
1214
+ console.error(`intent apply ${planId}:删除后读回暂不等价(第 ${attempt}/${attempts} 次)——疑似删除传播中,${attempt === attempts - 1 ? "最后" : "1s"}后再核`);
1215
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1185
1216
  }
1186
1217
  if (verification.equivalent === true) {
1187
1218
  console.error(`intent apply ${planId}:最终读回等价 ✓(意图 ${plan.intent.models.length} 个模型)`);
@@ -1193,6 +1224,11 @@ async function cmdIntentApply(planPath, options, transport) {
1193
1224
  status: "refused",
1194
1225
  blocker: "intent-readback-mismatch",
1195
1226
  verification,
1227
+ ...(declaresAbsence
1228
+ ? {
1229
+ note: "本变更含 absent:true 删除:平台删除分阶段传播(字段/视图先撤、模型壳后撤),已做 3 次间隔读回仍不等价——先用 model.inspect 的 modelPresent 独立复核再判定;若 modelPresent:false 则删除实际已生效",
1230
+ }
1231
+ : {}),
1196
1232
  message: "变更已执行,但最终权威读回与意图不等价:交付可能不完整或环境已漂移。按 differences 定位后重新 intent plan(不要盲重试)",
1197
1233
  nextAction: "inspect verification.differences, then re-run intent plan against the live baseline",
1198
1234
  };
@@ -311,6 +311,7 @@ const workflowTemplateUpdate = Object.freeze({
311
311
  "**activities 用数组**(也接受 {0:…,1:…} 索引对象);每个活动必须带 activityCode(Activity1..N)与 activityType,缺了会被拒——以 get_draft 的值为基线改,不要凭记忆拼;",
312
312
  "**一个模型只能有一个流程**:workflow.create 对已有流程的模型会拒(一个模型只能有一个流程);要改现有流程直接 workflow.update,不要另建新 code;",
313
313
  "多级审批:为每个审批节点各写一个 PARTICIPANT 活动(activityCode 唯一),END 放最后;数组里按顺序就是审批链顺序;新增的审批节点由工具自动接入链路(补全到 END 的连线),无需自己写 routes;",
314
+ "**{Originator}=发起人自己**:审批节点用它=提交人自审自己——提交人与审批人同账号时平台可能直接自动通过(COMPLETED,无待办;r40 实证,与节点结构有关)。需要独立审批人时绑定具体用户 id 或 `FindUserByRole({Originator.OU},\"<角色code>\",\"random\")`;验收要 PENDING 待办时先确认审批人≠提交人;",
314
315
  "**participant 必须是平台可解析的三种形态之一(真实生产 2026-09-23 观察)**:(a) 关键字 `{Originator}`;(b) 参与者对象数组 JSON `[{\"code\":\"<角色code>\",\"corpId\":\"<corpId|other>\",\"id\":\"<roleId>\",\"name\":\"<角色名>\",\"type\":2}]`(type 2=角色,3=用户;多个用 `+` 连接);(c) 函数 `FindUserByRole({Originator.OU},\"<roleCode>\",\"random\")`。**自由文本角色名(如 `部门负责人`)能 publish 但解析不到人、不生成待办**——先用 `org.role.list` / `org.department.list` 拿 code/id 再构造。工具会在 readback 里以 `participantWarnings` 标出自由文本。",
315
316
  "**审批节点必须带 sheetCode(=模型默认表单槽位,通常等于 schemaCode)——S29 契约:participant sheetCode 为空 publish 会 401026 WORKFLOW_ACTIVITY_NO_SHEET。工具已自动回填,正常无需手填**;",
316
317
  "节点类型 START / PARTICIPANT / END;省略的既有节点由 merge 保留;",
@@ -426,7 +427,10 @@ const plainSubmit = Object.freeze({
426
427
  tool: "data.submit",
427
428
  args: { environment, schemaCode: model, formData: { name: "正式记录" } },
428
429
  preconditions: ["模型无已发布流程(有流程走 workflow-backed-data-submit)", "模型+表单已发布"],
429
- conditionalRequirements: ["**不带 bizObjectId 即新建并提交**(工具内部建稿+直提,一次调用 COMPLETED);已有草稿要直提时才带 data.save 读回的 bizObjectId;不要手造 id"],
430
+ conditionalRequirements: [
431
+ "**不带 bizObjectId 即新建并提交**(工具内部建稿+直提,一次调用 COMPLETED);已有草稿要直提时才带 data.save 读回的 bizObjectId;不要手造 id",
432
+ "**目标模型已发布流程时行为不同(r36 实证)**:无 workItemId 的 data.submit = 一调新建并**启动流程**(PROCESSING,读回返回首审批待办 workItemId)——合法路径非错误;要『先草稿后提交/中途改稿』用两段式 data.save(草稿读回三元身份)→ data.submit(workItemId)",
433
+ ],
430
434
  priorToolReferences: [],
431
435
  dryRun: { supported: true, command: `omc call data.submit --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 验证参数关系,不推进状态" },
432
436
  expectedReadback: { performed: { errcode: 0 }, readback: { sequenceStatus: "COMPLETED" } },
@@ -1039,7 +1043,7 @@ const dataListFilter = Object.freeze({
1039
1043
  args: { environment, schemaCode: model, filter: { op: "eq", field: "status", value: "OPEN" }, size: 20 },
1040
1044
  preconditions: ["schemaCode 的字段已 field.publish(filter 叶子的 propertyType 从发布面读)"],
1041
1045
  conditionalRequirements: [
1042
- "filter 是**一个**子句 `{op, field, value?}`;op ∈ eq/neq/gt/lt/gte/lte/like/notlike/in/notin/between/notbetween/isnull/isnotnull。",
1046
+ "filter 是**一个**子句 `{op, field, value?}`;op ∈ eq/neq/gt/lt/gte/lte/like/notlike/in/notin/between/notbetween/isnull/isnotnull(camelCase 别名 equals/notEquals/contains/isNull/isNotNull 等价——r36 体检发现别名合法但只列规范名会被误判『静默接受坏 op』)。",
1043
1047
  "多条件 `filters:[…]`(AND);分组 `filterGroup:{op:'and'|'or',clauses:[…]}`;系统/外部字段用 rawCondition。",
1044
1048
  ],
1045
1049
  priorToolReferences: [
@@ -1071,6 +1075,33 @@ const dataDeleteSweep = Object.freeze({
1071
1075
  refusalRecovery: [{ code: "wire-business", action: "模型须已创建且有已发布字段;运行面 50000 时先 model.inspect modelPresent 判存在" }],
1072
1076
  requiresEnvironmentDerivedIds: false,
1073
1077
  });
1078
+ const onlinejsHighlightScript = Object.freeze({
1079
+ id: "field-value-driven-style",
1080
+ title: "按字段值驱动另一控件样式(生命周期外壳 + valueChange 订阅)",
1081
+ status: "offline-valid",
1082
+ tool: "onlinejs.compose",
1083
+ args: {
1084
+ environment,
1085
+ schemaCode: model,
1086
+ sheetCode: "<默认表单 code == 模型 code>",
1087
+ mode: "replace",
1088
+ source: "(function(form){ var amt = form.getControl('amountCode'); var party = form.getControl('targetCode'); function refresh(v){ party.options.style = Number(v) > 100000 ? 'color: red;' : ''; } form.on('onRendered', function(){ refresh(amt.getValue()); amt.valueChange.subscribe(function(change){ refresh(change.value); }); }); })",
1089
+ },
1090
+ preconditions: [
1091
+ "控件编码先 model.inspect/field.list 确认(示例 amountCode/targetCode 换成真实字段 code)",
1092
+ "完整 `(function(form){…})` 生命周期外壳必须 mode:'replace'(merge 模式收外壳会被拒并指路 replace——r38 实证)",
1093
+ ],
1094
+ conditionalRequirements: [
1095
+ "事件模型:`form.on('onRendered', …)` 初始化 + `<控件>.valueChange.subscribe(function(change){ change.value })` 监听;改样式用 `control.options.style`",
1096
+ "**脚本含引号/较长时不要塞 --args 内联**——写 JSON 参数文件走 `--args-file <path>`(shell 引号拆参 r38 实证)",
1097
+ "深入教学面:`.claude/skills/form-online-js/references/`(lifecycle-api/control-api)",
1098
+ ],
1099
+ priorToolReferences: [{ tool: "form.get", args: { environment, schemaCode: model }, purpose: "确认表单已发布与控件编码" }],
1100
+ dryRun: { supported: true, command: `omc call onlinejs.compose --env ${environment} --dry-run --args-file compose.json`, notes: "dry-run 校验外壳/合并模式,不写平台" },
1101
+ expectedReadback: { readback: { deliveryState: "DELIVERED", published: true, scriptElementPresent: true, htmlRenderCompat: { draft: { ok: true }, published: { ok: true } } } },
1102
+ refusalRecovery: [{ code: "merge-mode-requires-shell", action: "完整外壳改 mode:'replace'(六元素模板保留,仅整体替换 customScript)" }],
1103
+ requiresEnvironmentDerivedIds: false,
1104
+ });
1074
1105
  const dictionaryKeyValue = Object.freeze({
1075
1106
  id: "dictionary-key-value",
1076
1107
  title: "建一个键值字典并用其 id 作字段下拉来源",
@@ -1122,6 +1153,7 @@ export const EXECUTION_METADATA = Object.freeze({
1122
1153
  "data.submit": Object.freeze({ recipes: [plainSubmit, workflowSubmit], relatedTools: ["data.save", "workitem.list", "data.load"] }),
1123
1154
  "service.register": Object.freeze({ recipes: [restfulQueryService], relatedTools: ["service.category.list", "service.adapter_config", "service.get", "method.get"] }),
1124
1155
  "method.update": Object.freeze({ recipes: [bizserviceSelfCallCorp], relatedTools: ["method.get", "method.create", "service.get"] }),
1156
+ "onlinejs.compose": Object.freeze({ recipes: [onlinejsHighlightScript], relatedTools: ["form.get", "form.draft", "form.publish"] }),
1125
1157
  "listview.configure": Object.freeze({ recipes: [listViewConfigure], relatedTools: ["listview.list", "listview.get", "field.list", "field.publish"] }),
1126
1158
  "data.list": Object.freeze({ recipes: [dataListFilter], relatedTools: ["data.load", "data.delete", "listview.configure", "field.list"] }),
1127
1159
  "data.delete": Object.freeze({ recipes: [dataDeleteSweep], relatedTools: ["data.list", "data.load", "data.submit"] }),
@@ -30,7 +30,7 @@ async function resolvePublishedWorkflowCode(session, schemaCode) {
30
30
  const code = template && typeof template.workflowCode === "string" && template.workflowCode.length > 0 ? template.workflowCode : null;
31
31
  return code;
32
32
  }
33
- function extractBizObjectPage(value) {
33
+ export function extractBizObjectPage(value) {
34
34
  const root = value && typeof value === "object" ? value : {};
35
35
  const candidate = root.bizObjectPage && typeof root.bizObjectPage === "object" ? root.bizObjectPage : root;
36
36
  const content = Array.isArray(candidate.content) ? candidate.content.filter((row) => !!row && typeof row === "object") : [];
@@ -341,7 +341,7 @@ export function registerRuntimeDataLane(tools) {
341
341
  // `Expected object, received array`). An array is the caller's AND-list,
342
342
  // so accept it and treat it as `filters` rather than costing a retry
343
343
  // (dogfood R1 round17).
344
- filter: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).optional().describe('one clause { op, field, value? } (or an array of clauses = AND); op is one of eq, neq, gt, lt, gte, lte, like, notlike, in, notin, between, notbetween, isnull, isnotnull'),
344
+ filter: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).optional().describe('one clause { op, field, value? } (or an array of clauses = AND); op is one of eq, neq, gt, lt, gte, lte, like, notlike, in, notin, between, notbetween, isnull, isnotnull(camelCase 别名 equally valid: equals/notEquals/contains/isNull/isNotNull)'),
345
345
  filters: z.array(z.record(z.string(), z.unknown())).optional().describe("clauses combined with AND; a malformed clause is refused with blocker invalid-filter-DSL"),
346
346
  filterGroup: z.record(z.string(), z.unknown()).optional().describe('nested group { op:"and"|"or", clauses:[clause|group] }'),
347
347
  rawCondition: z.unknown().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "commander": "14.0.3",
54
- "omc-core": "0.4.34",
54
+ "omc-core": "0.4.36",
55
55
  "proper-lockfile": "4.1.2",
56
56
  "write-file-atomic": "6.0.0",
57
57
  "zod": "^3.23.8",
@@ -54,7 +54,7 @@ only the section for the capability you need —
54
54
 
55
55
  - **能做哪些事**:列出应用包、创建、改名/分组移动、删除(删除级联其模型)。
56
56
  - **工具**:`app.list`、`app.create`、`app.update`、`app.delete`
57
- - **边界 / 陷阱**:环境已带默认 `applicationCode`,建模**不必**先建应用;未进 intent,用 `omc call app.*`。
57
+ - **边界 / 陷阱**:环境已带默认 `applicationCode`,建模**不必**先建应用;`app` 未进 intent 声明式面。**要把 intent 落到新应用**:`omc call app.create`(原子面)→ 把该环境的 `applicationCode` 指向新应用(`omc setup` 重配或改 `~/.config/omc/` 环境配置)→ 再 `intent plan/apply`,模型即落在新应用(用户实测摩擦:曾误以为应用能进 intent)。
58
58
 
59
59
  ## 数据字典 `dictionary`
60
60
 
@@ -25,7 +25,8 @@ be treated as a second source. Read this, then follow the topics it names.
25
25
  5. 键名按对象不同:模型 `schemaCode`、默认表单 `sheetCode`(可省略,默认 == 模型 code)、流程 `workflowCode`/`code`——报错会提示同义键;字段族名认不出会给「最接近的族」。
26
26
  6. **写操作串行**:同一环境不要并发写(含多个 `omc call` 并行进程——环境写锁互斥,r31 实证并行 field.create 2/3 被拒);多个写合并为一条 `omc script` 逐行跑(进程内自动串行);`write-lock-held` 是瞬态,等几秒重试一次即可。
27
27
  7. **单个对象优先高阶编排工具**:模型+字段用 `configure_model`(一次含全部字段),表单用 `configure_form`,列表用 `listview.configure`(**默认视图已存在,省略 code 即配置它,不要 `listview.create`**)。原子工具留给单点修改。
28
- 8. 拿到 `detailArtifactId` 先 `artifact get <id> --summary` 看结构,再 `--keys/--jq` 取所需;`field.list`/`workflow.*` 默认面已给清单与节点摘要;`workflow.get_draft` 默认面已含 `edges`,改流程只需传 `activities`+`routes`。
28
+ 8. 拿到 `detailArtifactId` 先 `artifact get <id> --summary` 看结构,再 `--keys/--jq` 取所需;`field.list`/`workflow.*` 默认面已给清单与节点摘要;`workflow.get_draft` 默认面已含 `edges`,改流程只需传 `activities`+`routes`。**注意**:`form.get --full`、`workflow.get_draft` 的 `full:true` 大结果返回的是 `{length, sha256}` 摘要桩——真身走 `artifact get <id>`,勿把桩误判为「子表/控件没配」(用户实测陷阱)。
29
+ 9. **Windows/PowerShell 乱码**:CLI 输出是 UTF-8,PowerShell 管道默认按 GBK(936) 解码会全屏乱码——根因在终端不在 CLI。消费端先 `chcp 65001` + `[Console]::OutputEncoding=[Text.Encoding]::UTF8` 再读 CLI 输出。
29
30
  9. 每个 section 读一次即可;同一 topic 不要反复读。
30
31
  10. **复杂流程先预检角色**:读 `org.role.list`/`org.user.search`,用真实可解析对象;缺独立角色就报告阻塞,不伪造。主子表条件按规则行 `schemaCode` 回所属子模型验证。
31
32
  11. **验收不只看回读**:配置后用 opt-in `npm run test:live`(`OMC_LIVE=1`),验设计器控件/条件 Set、门户字段/列表列/选项/流程;业务规则走 `data.save(workflowCode)`→`workitem.list`→`data.submit`→`data.load`。门户断言硬重载≥2次。