@stalfh233/omc-cli 0.4.22 → 0.4.24

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
@@ -738,7 +738,7 @@ function intentWarnings(intent) {
738
738
  warnings.push({
739
739
  resource: `rule:${model.code}.${rule.code}`,
740
740
  code: "rule-preset-name-ignored",
741
- message: `规则 ${rule.code} 是平台预置触发规则,其名称由平台维护(如 Create=数据新增);声明的 name "${rule.name}" 不会生效(只有自定义规则的名称与节点名可改)。要改的是节点名,请改 nodeGraph/actions 中节点的 nodeName。`,
741
+ message: `规则 ${rule.code} 是平台预置触发规则,其名称由平台维护(如 Create=数据新增);声明的 name "${rule.name}" 不会生效(只有自定义规则的名称与节点名可改)——预置行可直接省略 name 以消除本警告;要改显示名请改 nodeGraph/actions 中节点的 nodeName。`,
742
742
  });
743
743
  let graph;
744
744
  try {
@@ -1067,11 +1067,40 @@ async function cmdIntentApply(planPath, options, transport) {
1067
1067
  }
1068
1068
  }
1069
1069
  let parsed;
1070
+ let autoPlanned = false;
1070
1071
  try {
1071
1072
  parsed = JSON.parse(planText);
1072
1073
  }
1073
1074
  catch (error) {
1074
- return invalid(undefined, `plan 文件不是合法 JSON:${error instanceof Error ? error.message : String(error)}`);
1075
+ // r17 finding (two delegated agents, same wall): `intent apply <file>` read
1076
+ // as "apply this intent file", but only a compiled plan JSON was accepted —
1077
+ // each agent paid a help round-trip to discover plan --plan-out. Accept the
1078
+ // SOURCE intent YAML directly: compile it in-process (same pipeline as
1079
+ // `intent plan`) and apply the resulting plan.
1080
+ const read = readIntentOrInvalid(planPath, transport);
1081
+ if (read.kind !== "exit") {
1082
+ try {
1083
+ const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
1084
+ const session = await context.sessions.session(options.env);
1085
+ const baseline = await readBaseline(session, read.intent);
1086
+ const compiled = compileIntent(read.intent, baseline);
1087
+ parsed = buildPlan({
1088
+ environment: session.environmentName,
1089
+ environmentFingerprint: session.environmentFingerprint(),
1090
+ compiler: INTENT_COMPILER_VERSION,
1091
+ baseline,
1092
+ intent: read.intent,
1093
+ compiled,
1094
+ });
1095
+ autoPlanned = true;
1096
+ }
1097
+ catch (planError) {
1098
+ return invalid(undefined, `文件既不是合法 plan JSON,也不能编译为意图计划:${planError instanceof Error ? planError.message : String(planError)}(若想先看计划再应用:intent plan <yaml> --env <env> --plan-out <plan.json>)`);
1099
+ }
1100
+ }
1101
+ else {
1102
+ return invalid(undefined, `plan 文件不是合法 JSON:${error instanceof Error ? error.message : String(error)};意图 YAML 请先 intent plan <yaml> --plan-out <plan.json>,或直接 intent apply <yaml> --env <env>(自动编译)`);
1103
+ }
1075
1104
  }
1076
1105
  const verified = verifyPlan(parsed);
1077
1106
  if (!verified.ok)
@@ -1137,7 +1166,7 @@ async function cmdIntentApply(planPath, options, transport) {
1137
1166
  // incomplete or drifted and the agent must re-plan, not retry blindly.
1138
1167
  const confirmReadback = async (base) => {
1139
1168
  if (options.verify === false)
1140
- return base;
1169
+ return autoPlanned ? { ...base, plannedFrom: "intent-yaml" } : base;
1141
1170
  let verification;
1142
1171
  try {
1143
1172
  const report = await verifyIntentLive(session, plan.intent);
@@ -1148,7 +1177,7 @@ async function cmdIntentApply(planPath, options, transport) {
1148
1177
  }
1149
1178
  if (verification.equivalent === true) {
1150
1179
  console.error(`intent apply ${planId}:最终读回等价 ✓(意图 ${plan.intent.models.length} 个模型)`);
1151
- return { ...base, verification };
1180
+ return { ...base, verification, ...(autoPlanned ? { plannedFrom: "intent-yaml" } : {}) };
1152
1181
  }
1153
1182
  console.error(`intent apply ${planId}:最终读回不等价——${Array.isArray(verification.differences) ? `${verification.differences.length} 项差异` : String(verification.error ?? "读回失败")};不得当作成功`);
1154
1183
  return {
package/dist/discovery.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { zodToJsonSchema } from "zod-to-json-schema";
2
+ import { closestMatch } from "./closest-match.js";
3
+ import { RUNNABLE_EXAMPLES } from "./intent/schema.js";
4
+ const INTENT_EXAMPLE_IDS = RUNNABLE_EXAMPLES.map((example) => example.id);
2
5
  import { z } from "zod";
3
6
  import { LANE_CONTRACTS } from "./contract-resources.js";
4
7
  import { intentCapabilityBlock } from "./intent/cli.js";
@@ -213,12 +216,31 @@ export function describeExample(registry, name, exampleId) {
213
216
  // No id (or an unknown one) lists the runnable ids instead of a dead end, so
214
217
  // an agent that guessed never burns a retry (sandbox26: `--example` with no
215
218
  // id was a hard commander error; an unknown id already listed `available`).
219
+ // r18 retro: two more layers — a did-you-mean (the id was GUESSED, e.g.
220
+ // `scalar-numeric-radio` on configure_model) and a cross-tool reverse
221
+ // lookup (that id exists under ANOTHER tool's recipes — point there
222
+ // instead of a bare miss). Machine-recoverable status, copy-paste next.
223
+ const ids = recipes.map((entry) => entry.id);
224
+ const closest = exampleId !== undefined ? closestMatch(exampleId, ids) : undefined;
225
+ const otherTool = exampleId === undefined
226
+ ? undefined
227
+ : registry.find((entry) => entry !== tool && (entry.executionMetadata?.recipes ?? []).some((recipe2) => recipe2.id === exampleId));
228
+ // The intent examples (scalar-numeric-radio, relation-work-sheet, …) live
229
+ // on the `intent` CLI face, not a registry tool — r18 asked lane tools for
230
+ // them repeatedly. Redirect to the face that owns them.
231
+ const intentExample = exampleId !== undefined && otherTool === undefined ? INTENT_EXAMPLE_IDS.find((id) => id === exampleId) : undefined;
216
232
  return {
217
233
  name: tool.name,
234
+ // NOTE: no `status` key — the envelope classifier would turn a non-"ok"
235
+ // status into a non-zero exit; a discovery miss is a successful query
236
+ // with a miss payload (`found:false` is the machine signal).
218
237
  ...(exampleId !== undefined ? { example: exampleId } : {}),
219
238
  found: false,
220
- available: recipes.map((entry) => entry.id),
221
- ...(recipes[0] ? { hint: `本工具的例子 id 是 ${recipes.map((entry) => entry.id).join("、")};用 describe ${tool.name} --example ${recipes[0].id} 取可运行参数` } : {}),
239
+ available: ids,
240
+ ...(closest !== undefined ? { didYouMean: closest, next: `omc describe ${tool.name} --example ${closest}` } : recipes[0] ? { next: `omc describe ${tool.name} --example ${recipes[0].id}` } : {}),
241
+ ...(otherTool !== undefined ? { existsUnder: otherTool.name, crossToolNext: `omc describe ${otherTool.name} --example ${exampleId}` } : {}),
242
+ ...(intentExample !== undefined ? { existsUnder: "intent", crossToolNext: `omc intent schema --example ${exampleId}(整包任务用声明式形状)` } : {}),
243
+ ...(ids.length === 0 && !otherTool ? { hint: `本工具没有配方示例;整包任务用 omc intent schema --example <id> 取声明式形状` } : {}),
222
244
  };
223
245
  }
224
246
  return {
@@ -419,6 +419,26 @@ const workflowSave = Object.freeze({
419
419
  requiresEnvironmentDerivedIds: true,
420
420
  prerequisiteData: { source: "workflow.get_published; data.save authoritative readback", fields: ["workflow.code", "bizObjectId", "workflowInstanceId", "workItemId"] },
421
421
  });
422
+ const plainSubmit = Object.freeze({
423
+ id: "plain-data-submit",
424
+ title: "无流程模型:草稿直提(DRAFT→COMPLETED,触发 Create 规则)",
425
+ status: "requires-prior-readback",
426
+ tool: "data.submit",
427
+ args: { environment, schemaCode: model, bizObjectId: "<data.save.performed.bizObjectId>", formData: { name: "recipe draft" } },
428
+ preconditions: ["模型无 workflowCode(有流程走 workflow-backed-data-submit)", "data.save 已成功并读回 DRAFT 草稿", "bizObjectId 取 data.save 读回,不要手造"],
429
+ conditionalRequirements: ["无需 workflowInstanceId/workItemId——无流程模型 submit 自持久化(runtime/form/submit 就地置 COMPLETED)"],
430
+ priorToolReferences: [
431
+ { tool: "data.save", args: { environment, schemaCode: model, formData: { name: "recipe draft" } }, purpose: "建 DRAFT 草稿并读回 bizObjectId(其 draftRuleHint 会指回本链)" },
432
+ ],
433
+ dryRun: { supported: true, command: `omc call data.submit --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 验证参数关系,不推进状态" },
434
+ expectedReadback: { performed: { errcode: 0 }, readback: { sequenceStatus: "COMPLETED" } },
435
+ refusalRecovery: [
436
+ { code: "submit-biz-object-absent", action: "bizObjectId 在平台无行(304001)——回 data.save 重建草稿后再直提" },
437
+ { code: "invalid-input", action: "带了 workItemId/workflowInstanceId 时必须同时带 bizObjectId(fail-closed)" },
438
+ ],
439
+ notes: "规则验证路径:Create/Update 规则只在 COMPLETED 分发——plain save 是 DRAFT 不触发(DomainServiceImpl.java:240-248);要验证规则效果就用本链提交后 data.load 核对",
440
+ requiresEnvironmentDerivedIds: true,
441
+ });
422
442
  const workflowSubmit = Object.freeze({
423
443
  id: "workflow-backed-data-submit",
424
444
  title: "消费 data.save 读回身份并提交 workflow work item",
@@ -1080,7 +1100,7 @@ export const EXECUTION_METADATA = Object.freeze({
1080
1100
  "workflow.update": Object.freeze({ recipes: [workflowTemplateUpdate, workflowConditionalBranch], relatedTools: ["workflow.get_draft", "workflow.publish"] }),
1081
1101
  "workflow.bindRule": Object.freeze({ recipes: [workflowBinding], relatedTools: ["workflow.create", "workflow.get_draft", "workflow.get_published", "rule.list"] }),
1082
1102
  "data.save": Object.freeze({ recipes: [workflowSave], relatedTools: ["workflow.get_published", "data.submit", "workitem.list"] }),
1083
- "data.submit": Object.freeze({ recipes: [workflowSubmit], relatedTools: ["data.save", "workitem.list", "data.load"] }),
1103
+ "data.submit": Object.freeze({ recipes: [plainSubmit, workflowSubmit], relatedTools: ["data.save", "workitem.list", "data.load"] }),
1084
1104
  "service.register": Object.freeze({ recipes: [restfulQueryService], relatedTools: ["service.category.list", "service.adapter_config", "service.get", "method.get"] }),
1085
1105
  "method.update": Object.freeze({ recipes: [bizserviceSelfCallCorp], relatedTools: ["method.get", "method.create", "service.get"] }),
1086
1106
  "listview.configure": Object.freeze({ recipes: [listViewConfigure], relatedTools: ["listview.list", "listview.get", "field.list", "field.publish"] }),
@@ -44,7 +44,26 @@ function intentGuidance() {
44
44
  return {
45
45
  path: "declarative",
46
46
  why: "整包需求:用一份声明式意图文件描述 models/fields/form/listview/rules/workflow/onlinejs/services,由编译器合成平台原生结构并按依赖顺序执行;比逐能力拼装更省上下文、更不易漂移。",
47
+ starter: `intentVersion: 1
48
+ models:
49
+ - code: <模型code> # 如 omc_todo_task
50
+ name: <业务名>
51
+ fields:
52
+ - {code: title, name: 标题, type: SHORT_TEXT}
53
+ - {code: amount, name: 数量, type: NUMERICAL, options: {scale: 2}} # NUMERICAL 必带 scale
54
+ - {code: level, name: 等级, type: RADIO, choices: [高, 中, 低]} # 选择族用顶层 choices
55
+ form: {publish: true}
56
+ listview: {fieldCodes: [title, level]}
57
+ rules:
58
+ - code: Create # 平台预置触发行(name 省略)
59
+ name: 新增规则 # 仅自定义规则需要 name;预置行可省
60
+ enabled: true
61
+ actions:
62
+ - when: [{field: amount, gt: 10}]
63
+ set: [{field: title, value: "超限"}]
64
+ `,
47
65
  commands: [
66
+ "把上面的 starter 存成 <file>.yaml 按需求改字段后:omc intent validate <file>.yaml(离线)→ omc intent plan <file>.yaml --env <env> → omc intent apply <file>.yaml --env <env>(apply 也能直接吃 YAML)",
48
67
  "omc intent schema [--example <id>]:离线看 intent YAML 的键位/操作符/可运行示例(写 intent 前先看,省去猜键名与试错)",
49
68
  "omc intent plan <file> --env <env>:离线先校验(形体错在接触环境前返回,故 validate 可选);大 plan 自动外置,回 planExternalized:true + planFile,直接 `omc intent apply <planFile>`;小 plan 才内联在 result.plan",
50
69
  "omc intent apply <planFile|plan.json>(默认最终读回等价校验;--no-verify 关闭)",
@@ -22,7 +22,7 @@ export const INTENT_CONDITION_OPERATORS = Object.freeze({
22
22
  });
23
23
  /** A `when` gate leaf compares against a literal; `from` (field-to-field) is only for `filter`. */
24
24
  export const INTENT_WHEN_OPERATORS = Object.freeze(["equals", "notEquals", "gt", "gte", "lt", "lte"]);
25
- const RUNNABLE_EXAMPLES = Object.freeze([
25
+ export const RUNNABLE_EXAMPLES = Object.freeze([
26
26
  {
27
27
  id: "scalar-numeric-radio",
28
28
  title: "标量/数值/RADIO(choices)——type 用族名,NUMERICAL 必带 options.scale,选择族用顶层 choices",
@@ -126,7 +126,7 @@ export function intentSchemaBlock() {
126
126
  listview: { required: [], optional: ["code", "name", "fieldCodes", "absent"], note: "intent 只声明列;业务筛选/排序/按钮 apply 后用 omc call listview.configure 补" },
127
127
  datarules: { required: ["propertyCode", "dataRuleType"], optional: ["options", "checkType", "childRule", "name", "sortKey", "absent"], note: "type3/4/5 不驱动门户;门户条件走 fields[].options" },
128
128
  rules: {
129
- required: ["code", "name", "enabled"],
129
+ required: ["code", "name(预置触发行 Create/Update/Delete… 可省略,声明的 name 不会生效)", "enabled"],
130
130
  optional: ["bizRuleType", "customTrigger", "actions", "nodeGraph"],
131
131
  note: "actions(紧凑 UPDATE)与 nodeGraph 二选一;非 UPDATE 节点用 nodeGraph",
132
132
  actionLeaf: {
@@ -241,7 +241,11 @@ export function registerListViewLane(tools) {
241
241
  code,
242
242
  present: true,
243
243
  name: model.name ?? null,
244
- published: model.published === true,
244
+ // r18 retro (G5): the placeholder flag misleads ("is my view
245
+ // inactive?"). Views are ALWAYS-LIVE on save; keep the legacy key
246
+ // null-ed for shape compatibility and name the lifecycle instead.
247
+ published: null,
248
+ lifecycle: "always-live",
245
249
  presentationType: model.queryPresentationType ?? null,
246
250
  columnCount: columns.length,
247
251
  columns,
@@ -11,6 +11,7 @@ import { wireCall, wireRows, WireError } from "../wire.js";
11
11
  import { CHILD_TABLE_PROPERTY_TYPE, runtimeWriteWithAppMapping } from "../runtime-support.js";
12
12
  import { getReplayToken, searchWorkItems, wireString } from "../workflow-support.js";
13
13
  import { ENGINE_OWNED_KEYS, RUNTIME_DATA_LANE_CONTRACT, RUNTIME_DATA_WIRE, runtimeFormDataArg, } from "./runtime-data-contract.js";
14
+ import { SYSTEM_FIELD_CODES } from "../field-families.js";
14
15
  const RUNTIME_LANE = RUNTIME_DATA_LANE_CONTRACT.lane;
15
16
  const RUNTIME_TOOLS = RUNTIME_DATA_LANE_CONTRACT.tools;
16
17
  /**
@@ -462,6 +463,16 @@ export function registerRuntimeDataLane(tools) {
462
463
  // for EVERY selected column, and the published metadata confirms those
463
464
  // are real model fields, surface the recovery hint. `items` is never
464
465
  // reshaped — the hint is a sibling readback field.
466
+ // Candidate keys first (pure — zero wires); metadata only when there is
467
+ // at least one all-null column to classify (preserves the rawCondition
468
+ // path's zero-extra-wire contract when there is nothing to hint).
469
+ const businessNullColumns = async () => {
470
+ const candidates = Object.keys(items[0]?.fields ?? {}).filter((field) => !SYSTEM_FIELD_CODES.has(field) && items.every((item) => item.fields[field] === null));
471
+ if (candidates.length === 0)
472
+ return [];
473
+ const typeByCode = await publishedTypeByCode();
474
+ return candidates.filter((field) => typeByCode.has(field));
475
+ };
465
476
  if (fields &&
466
477
  fields.length > 0 &&
467
478
  items.length > 0 &&
@@ -470,6 +481,15 @@ export function registerRuntimeDataLane(tools) {
470
481
  if (fields.every((field) => typeByCode.has(field)))
471
482
  readback.projectionHint = RUNTIME_DATA_WIRE.projectionHint;
472
483
  }
484
+ else if (!fields && items.length > 0) {
485
+ // r17 finding: BOTH delegated agents tripped on implicit projection —
486
+ // business fields absent from the list view render as null in every
487
+ // row with no explanation. Name the null'd business columns so the
488
+ // agent reads data.load instead of misdiagnosing "data lost".
489
+ const nullColumns = await businessNullColumns();
490
+ if (nullColumns.length > 0)
491
+ readback.projectionHint = `这些列在默认视图未配置(值可能存在但列表投影为 null):${nullColumns.join("、")};先用 listview.configure 配置列,或用 data.load 读单条权威值`;
492
+ }
473
493
  return readback;
474
494
  }, { full: args.full === true, target: `data.list:${JSON.stringify({ schemaCode: args.schemaCode, queryCode: args.queryCode ?? args.schemaCode, filter: args.filter, filters: args.filters, filterGroup: args.filterGroup, rawCondition: args.rawCondition, displayFields: args.displayFields, columns: args.columns, page: args.page ?? 0, size: args.size ?? 10 })}` });
475
495
  },
@@ -528,6 +548,7 @@ export function registerRuntimeDataLane(tools) {
528
548
  sheetCode: z.string().optional(),
529
549
  formData: runtimeFormDataArg,
530
550
  workflowCode: z.string().optional().describe("portal draft-save shape: create the row AND a DRAFT workflow instance + start workItem (finishStart=FALSE); submit later via data.submit with the returned workItemId. formData must NOT carry an id — in-place workflow start has no REST surface in 8.6.26"),
551
+ submit: z.boolean().optional().describe("r18 retro:true=一调完成态——保存后立即直提(无流程模型 DRAFT→COMPLETED,触发 Create 业务规则);与 workflowCode 互斥(流程草稿须走 data.submit 两段式)。默认 false 仅存草稿"),
531
552
  ...readbackProjectionArgs,
532
553
  }, async (session, args) => {
533
554
  const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
@@ -610,18 +631,46 @@ export function registerRuntimeDataLane(tools) {
610
631
  if (typeof bizObjectId === "string" && bizObjectId) {
611
632
  readbackData = (await wireCall(session, { path: ENDPOINTS.runtimeFormLoad, query: { schemaCode: args.schemaCode, sheetCode, objectId: bizObjectId } })).data;
612
633
  }
634
+ // Ticket 111: a plain save lands as sequenceStatus DRAFT, and DRAFT
635
+ // rows SKIP the Create/Update rule dispatch entirely
636
+ // (DomainServiceImpl.java:240-248 buckets drafts before the rule
637
+ // branch) — business rules only run once data.submit advances the
638
+ // row. Saying so on every plain-save readback prevents the
639
+ // "rule didn't fire" misdiagnosis.
640
+ const plainReadback = {
641
+ authoritative: projectReadbackRecord(normalizeLoadedRecord((readbackData ?? {})), args),
642
+ draftRuleHint: "草稿态(DRAFT)保存不触发业务规则——DRAFT 行跳过 Create/Update 分发(DomainServiceImpl.java:240-248);要一调完成态本调用加 submit:true;已存草稿直提形状:data.submit {schemaCode, bizObjectId: 本读回的 bizObjectId, formData}(submit 自持久化)",
643
+ };
644
+ // r18 retro (G3): one-call COMPLETED creation — the agent goal is usually
645
+ // "record AND rule effect", not "a draft". Chain the no-flow submit in
646
+ // the same governed write (the same sibling execute apply_changes uses).
647
+ if (args.submit === true) {
648
+ const submitTool = tools.find((entry) => entry.name === "data.submit");
649
+ const submitOutcome = submitTool?.writeExecute
650
+ ? await submitTool.writeExecute(session, { schemaCode: args.schemaCode, bizObjectId, formData: args.formData })
651
+ : { status: "error", message: "data.submit 工具不可用" };
652
+ if ("status" in submitOutcome && typeof submitOutcome.status === "string" && submitOutcome.status !== "ok") {
653
+ // The save itself DELIVERED — report it honestly with the submit
654
+ // refusal attached, never as a plain refusal.
655
+ const envelope = "envelope" in submitOutcome ? submitOutcome.envelope : submitOutcome;
656
+ return {
657
+ performed: { errcode: performed.errcode, bizObjectId, sheetCode, resolvedFrom, submitted: false },
658
+ readback: { ...plainReadback, submitRefused: envelope },
659
+ };
660
+ }
661
+ const okOutcome = submitOutcome;
662
+ return {
663
+ performed: { errcode: performed.errcode, bizObjectId, sheetCode, resolvedFrom, submitted: true },
664
+ readback: {
665
+ ...okOutcome.readback,
666
+ submitted: true,
667
+ ruleHint: "已直提 COMPLETED——Create 业务规则本次已分发;规则效果以本读回字段值为准",
668
+ },
669
+ };
670
+ }
613
671
  return {
614
672
  performed: { errcode: performed.errcode, bizObjectId, sheetCode, resolvedFrom },
615
- readback: {
616
- authoritative: projectReadbackRecord(normalizeLoadedRecord((readbackData ?? {})), args),
617
- // Ticket 111: a plain save lands as sequenceStatus DRAFT, and DRAFT
618
- // rows SKIP the Create/Update rule dispatch entirely
619
- // (DomainServiceImpl.java:240-248 buckets drafts before the rule
620
- // branch) — business rules only run once data.submit advances the
621
- // row. Saying so on every plain-save readback prevents the
622
- // "rule didn't fire" misdiagnosis.
623
- draftRuleHint: "草稿态(DRAFT)保存不触发业务规则——DRAFT 行跳过 Create/Update 分发(DomainServiceImpl.java:240-248);data.submit 提交(DRAFT→COMPLETED)后才触发 Create 规则",
624
- },
673
+ readback: plainReadback,
625
674
  };
626
675
  });
627
676
  writeTool(tools, "data.update", RUNTIME_LANE, RUNTIME_TOOLS["data.update"].scope, RUNTIME_TOOLS["data.update"].description, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
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.22",
54
+ "omc-core": "0.4.24",
55
55
  "proper-lockfile": "4.1.2",
56
56
  "write-file-atomic": "6.0.0",
57
57
  "zod": "^3.23.8",
@@ -170,7 +170,8 @@ only the section for the capability you need —
170
170
  - **工具**:`data.list`、`data.load`、`data.save`、`data.update`、`data.delete`、`data.submit`
171
171
  - **recipe**:
172
172
  - `omc describe data.save --example workflow-backed-data-save`
173
- - `omc describe data.submit --example workflow-backed-data-submit`
173
+ - `omc describe data.submit --example workflow-backed-data-submit`(有流程)
174
+ - 无流程模型的规则验证路径:`data.save`(DRAFT,不触发规则)→ `data.submit {schemaCode, bizObjectId, formData}` 直提(COMPLETED,触发 Create 规则)——示例 `omc describe data.submit --example plain-data-submit`
174
175
  - **边界 / 陷阱**:读实际业务记录是 OMC 独有能力。**计算字段(FORMULA/子表计算)不由 API 回填**——写测试数据带预算值,公式正确性人工前台确认;带子表计算字段的模型 API 写可能 50000(服务端重算 NPE),前台正常。
175
176
 
176
177
  ---