@stalfh233/omc-cli 0.6.13 → 0.6.14

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
@@ -883,6 +883,18 @@ function projectArtifactPath(value, path) {
883
883
  ? { missed: true, ...(fanout.at !== undefined ? { at: fanout.at } : {}), ...(fanout.available !== undefined ? { available: fanout.available } : {}) }
884
884
  : { missed: false, content: fanout.values, mapped: fanout.values.length, ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries } : {}), nullEntries: fanout.nullEntries };
885
885
  }
886
+ /** Preserve source completeness/count facts on every artifact projection. */
887
+ function artifactProjectionMetadata(value) {
888
+ if (value === null || typeof value !== "object" || Array.isArray(value))
889
+ return {};
890
+ const record = value;
891
+ const metadata = {};
892
+ for (const key of ["truncated", "truncatedKeys", "itemsCount", "total"]) {
893
+ if (Object.prototype.hasOwnProperty.call(record, key))
894
+ metadata[key] = record[key];
895
+ }
896
+ return metadata;
897
+ }
886
898
  /**
887
899
  * T6(GitLab #17):`mapped`/`nullEntries` 的读法要跟数据一起走,否则 `mapped:5`
888
900
  * 会被读成"取到 5 个值"。文案的单一真相源在 path-projection(与 `--extract`
@@ -938,15 +950,17 @@ async function cmdArtifactGet(id, transport, options = {}) {
938
950
  // 一层 `projection:{path,content}`——`artifact get --jq` 与 `--summary --jq`
939
951
  // 因此同形(顶层 path/content),换读面不用重新探根键。
940
952
  return emitTransportResult(projected.missed
941
- ? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
953
+ ? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, ...artifactProjectionMetadata(content), keys, path: options.jq, content: null, projections: [{ path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }], pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
942
954
  : {
943
955
  status: "ok",
944
956
  artifactId: id,
945
957
  sha256: artifact.sha256,
946
958
  bytes: artifact.content.length,
959
+ ...artifactProjectionMetadata(content),
947
960
  keys,
948
961
  path: options.jq,
949
962
  content: projected.content ?? null,
963
+ projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }],
950
964
  ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}),
951
965
  ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}),
952
966
  ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}),
@@ -992,12 +1006,12 @@ async function cmdArtifactGet(id, transport, options = {}) {
992
1006
  ? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
993
1007
  : { path, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) };
994
1008
  });
995
- return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint: partialPathMissHint() } : {}) }, transport);
1009
+ return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint: partialPathMissHint() } : {}) }, transport);
996
1010
  }
997
1011
  const projected = projectArtifactPath(artifactContent, options.jq);
998
1012
  return emitTransportResult(projected.missed
999
- ? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
1000
- : { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
1013
+ ? { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
1014
+ : { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: projected.content ?? null, projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }], ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
1001
1015
  }
1002
1016
  // r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
1003
1017
  // large ones (up to 55 KB) pulled whole workflow templates just to read one
@@ -1894,7 +1908,22 @@ function cmdHelpSearch(topic, query, transport, searchMode, limit) {
1894
1908
  }
1895
1909
  if (all) {
1896
1910
  console.error(`omc help ${document.name} --all(${Buffer.byteLength(document.content, "utf8")} B)`);
1897
- return emitTransportResult({ status: "ok", topic: document }, transport);
1911
+ // r32 F3(真机):三种模式三种形状——默认回 sections[]、--section 回顶层 content、
1912
+ // --all 却回 {topic:{name,description,content}}。按前两种写的解析(.content /
1913
+ // .result.topic 当字符串)在 --all 上全部取不到,而默认模式的 hint 恰恰把 --all
1914
+ // 指成"确需整篇"的下一步。
1915
+ // 现在 --all 与前两者同形:topic 是 topic 名(字符串),正文在顶层 content,
1916
+ // 并附 bytes/chars/complete。**不要再把 content 放进 topic**——那会让整篇在
1917
+ // 信封里出现两次,撑破 64KiB 预算后被外置成 artifact(r32 实测:help-write-hazards
1918
+ // 因此从"直接回正文"退化成"回一个每次不同的 artifactId",golden 永久漂移)。
1919
+ return emitTransportResult({
1920
+ status: "ok",
1921
+ topic: document.name,
1922
+ content: document.content,
1923
+ bytes: Buffer.byteLength(document.content, "utf8"),
1924
+ chars: document.content.length,
1925
+ complete: true,
1926
+ }, transport);
1898
1927
  }
1899
1928
  // Shape first. Round 10 paid 35,872 B per full read of one topic, seven times
1900
1929
  // in a single session, to use one or two headings. The index names every
package/dist/discovery.js CHANGED
@@ -485,6 +485,10 @@ export function describeTool(registry, name) {
485
485
  // High-frequency read faces: declare the RESULT keys so `--jq` does not
486
486
  // have to learn them from a pathMiss. Zero bytes when undeclared.
487
487
  ...(returnsFace(tool.name) ?? {}),
488
+ // r32 F1(真机):探针并发发两条 app.create,第二条回 write-lock-held。
489
+ // 撞上之前没有任何发现面说过"写是环境级串行"——describe 的 params 里没有 wait
490
+ // (它是 omc call 的 CLI 开关,不在工具 schema),capability notes 是空的。
491
+ ...(tool.kind === "write" ? { writeLock: "写锁=环境级:同一环境的并发写,后到的直接回 write-lock-held(不排队)。可带 --wait <ms> 让 CLI 内部排队(CLI 开关,不是本工具参数);不要写 shell 重试循环" } : {}),
488
492
  // Keep the cost salient at the decision point: agents reached for --full even
489
493
  // when --example/--param existed (sandbox22: 8 --full calls, one 19 KB).
490
494
  full: `omc describe ${tool.name} --full(5–20KB;先试 --example/--param)`,
package/dist/lanes/app.js CHANGED
@@ -80,7 +80,7 @@ export function registerAppLane(tools) {
80
80
  lane: APP_LANE,
81
81
  scope: APP_TOOLS["app.create"].scope,
82
82
  description: APP_TOOLS["app.create"].description,
83
- inputSchema: { code: z.string().min(1), name: z.string().min(1), groupId: z.string().optional() },
83
+ inputSchema: { code: z.string().min(1), name: z.string().min(1), groupId: z.string().describe("省略时使用环境配置的默认应用分组(回执 resolvedFrom=auto-configured-app-group),不需要先查 groupId").optional() },
84
84
  execute: async (session, args) => {
85
85
  const existing = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data).find((row) => (row.appCode ?? row.code) === args.code);
86
86
  if (existing)
@@ -190,7 +190,7 @@ export const FIELD_LANE_CONTRACT = Object.freeze({
190
190
  }),
191
191
  "field.publish": Object.freeze({
192
192
  scope: "create publish readback",
193
- description: "Publish the model's draft fields (bizproperty/publish). **Model-scoped**: one call publishes every draft field — never once per field. Optional `code`/`codes` verify published face; readback 回 `publishedCodes`。`codes` 短路须草稿/发布 options 一致,否则真发布。formBinding 可含 dictionaryStale / relation-mappings-empty。瞬态重试(ticket 79):异常小资源下 DDL 中间态自动重试≤3(退避 2s/5s);仍失败等≥30s 再发,勿重复建字段。",
193
+ description: "Publish the model's draft fields (bizproperty/publish). **Model-scoped**: one call publishes every draft field — never once per field. Optional `code`/`codes` verify published face; readback 回 `publishedCodes`。`codes` 短路须草稿/发布 options 一致,否则真发布。formBinding 可含 dictionaryStale / relation-mappings-empty。瞬态重试(ticket 79):异常小资源下 DDL 中间态自动重试≤3(退避 2s/5s);仍失败等≥30s 再发,勿重复建字段。**部分交付口径**:有请求的 code 不在发布面时 `allRequestedPresent:false` + `verified:false` + `readback.present:false`(共享写门据此把 deliveryState 降为 WRITE_UNVERIFIED)+ `unpublishedRequested`,并区分 `nonexistentCodes`(草稿面都没有——先 field.list 核对)与 `stillDraftCodes`(模型里有、发布后仍不在发布面)——已发布的字段仍然算发布了,但不要只看 ok/DELIVERED。",
194
194
  }),
195
195
  "field.delete": Object.freeze({
196
196
  scope: "create publish readback",
@@ -219,7 +219,7 @@ export function assertFieldContract() {
219
219
  throw new Error("field contract pins the publish scope to model");
220
220
  assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.create", ["bizproperty/create", "OCR/ELECTRONIC_SIGN", "`choices`", "options.optionsSet", "`relativeCode`", "searchFormula", "`options.formulaConfig`", "`options.relevanceField`", "code_reserved", "WORK_SHEET"]);
221
221
  assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.update", ["bizproperty/update", "`options`", "field.publish", "cascadeRepublish", "form.draft(rebuild)", "form.publish(force)", "published fields", "non-structural", "partial"]);
222
- assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.publish", ["bizproperty/publish", "published", "Model-scoped", "publishedCodes"]);
222
+ assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.publish", ["bizproperty/publish", "published", "Model-scoped", "publishedCodes", "unpublishedRequested", "nonexistentCodes", "stillDraftCodes"]);
223
223
  assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.delete", ["bizproperty/force_delete", "absence"]);
224
224
  }
225
225
  //# sourceMappingURL=field-contract.js.map
@@ -197,7 +197,7 @@ export function registerFieldLane(tools) {
197
197
  lane: FIELD_LANE,
198
198
  scope: FIELD_TOOLS["field.list"].scope,
199
199
  description: FIELD_TOOLS["field.list"].description,
200
- inputSchema: { environment: environmentArg, schemaCode: z.string(), published: z.boolean().default(false), code: z.string().optional(), fields: z.array(z.string()).optional(), full: z.boolean().optional().describe("行内联完整 options/subSchema;默认清单面只有 code/name/type/typeName(+conditions 信号/shapeWarning),验证 options 用 code/fields 选择器或 full:true"), all: z.boolean().optional().describe("默认只返业务字段(过滤 SYSTEM_FIELD_CODES 的 13 个平台字段:id/name/sequenceNo/createdTime/creater/sequenceStatus/…);true 连系统字段一起返"), capability: capabilityArg },
200
+ inputSchema: { environment: environmentArg, schemaCode: z.string(), published: z.boolean().default(false), code: z.string().optional(), fields: z.array(z.string()).optional(), full: z.boolean().optional().describe("行内联完整 options/subSchema;默认清单面只有 code/name/type/typeName(+conditions 信号/shapeWarning),验证 options 用 code/fields 选择器或 full:true;平台读面带回 propertyEmpty 时一并输出(不回则省略,不塞 false)"), all: z.boolean().optional().describe("默认只返业务字段(过滤 SYSTEM_FIELD_CODES 的 13 个平台字段:id/name/sequenceNo/createdTime/creater/sequenceStatus/…);true 连系统字段一起返"), capability: capabilityArg },
201
201
  handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
202
202
  const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: args.published ? "true" : "false" } })).data);
203
203
  const mapped = rows.map((row) => {
@@ -230,6 +230,11 @@ export function registerFieldLane(tools) {
230
230
  : {}),
231
231
  ...(row.relativeCode ? { relativeCode: row.relativeCode } : {}),
232
232
  ...(typeof row.relativePropertyCode === "string" && row.relativePropertyCode ? { relativePropertyCode: row.relativePropertyCode } : {}),
233
+ // r32:propertyEmpty 只在平台读面**确实**带回该值时才输出——缺键不给
234
+ // 每个字段塞 false(假读回 + 噪音)。真机 8.6.26 的 bizproperty/list
235
+ // 不带这个键,于是不会出现;write-only 的确认走 field.update 的
236
+ // readback.committed。
237
+ ...(Object.prototype.hasOwnProperty.call(row, "propertyEmpty") ? { propertyEmpty: row.propertyEmpty } : {}),
233
238
  ...(options !== undefined ? { options } : {}),
234
239
  ...(scaleWarning !== undefined ? { shapeWarning: scaleWarning } : {}),
235
240
  };
@@ -454,7 +459,7 @@ export function registerFieldLane(tools) {
454
459
  choices: choicesArg,
455
460
  relativeCode: relativeCodeArg,
456
461
  subFields: subFieldArg,
457
- propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)"),
462
+ propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)。readback.committed 列出本次实际提交到线缆的键,readback.propertyEmpty 只在草稿面回读到该值时才出现(r32 真机:bizproperty/list 行不带 propertyEmpty,读不到即不编造,用 committed 确认本次提交)"),
458
463
  syncDictionary: z
459
464
  .boolean()
460
465
  .optional()
@@ -1042,6 +1047,21 @@ async function runFieldUpdate(session, args) {
1042
1047
  // confirm the write landed — the readback only named the field. Field
1043
1048
  // options are the form-interaction contract (ADR 0005), so read them back.
1044
1049
  const storedOptions = after !== undefined ? parseFieldOptionsForReadback(after.options) : undefined;
1050
+ // r32(真机复现:field.update propertyEmpty:true 回 ok/DELIVERED,field.list --full
1051
+ // 与 model.inspect 都看不到该值):平台字段行读面(bizproperty/list 与
1052
+ // bizproperty/get)都不回 propertyEmpty,这是**只写属性**。不编造读回值:
1053
+ // - `propertyEmpty` 只在草稿行确实带该键时才输出(不与 null 比较);否则省略;
1054
+ // - `committed` 列出本次线缆体里实际提交的键,调用方据此确认「提交了什么」。
1055
+ const submittedPropertyEmpty = args.propertyEmpty;
1056
+ const hasReadBackPropertyEmpty = after !== undefined && Object.prototype.hasOwnProperty.call(after, "propertyEmpty");
1057
+ const committedKeys = [
1058
+ ...(args.name !== undefined ? ["name"] : []),
1059
+ ...(args.name_i18n !== undefined ? ["name_i18n"] : []),
1060
+ ...(effectiveOptions !== undefined || args.choices !== undefined ? ["options"] : []),
1061
+ ...(args.relativeCode !== undefined ? ["relativeCode"] : []),
1062
+ ...(args.propertyEmpty !== undefined ? ["propertyEmpty"] : []),
1063
+ ];
1064
+ const unverifiable = submittedPropertyEmpty !== undefined && !hasReadBackPropertyEmpty ? ["propertyEmpty"] : [];
1045
1065
  return {
1046
1066
  performed: { errcode: performed.errcode },
1047
1067
  ...(prepared.conditionCompiled !== undefined && prepared.conditionCompiled.length > 0
@@ -1053,6 +1073,14 @@ async function runFieldUpdate(session, args) {
1053
1073
  ...(after?.name !== undefined ? { name: after.name } : {}),
1054
1074
  ...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
1055
1075
  ...(after?.type !== undefined || after?.propertyType !== undefined ? { type: after?.type ?? after?.propertyType } : {}),
1076
+ ...(hasReadBackPropertyEmpty ? { propertyEmpty: after?.propertyEmpty } : {}),
1077
+ committed: committedKeys,
1078
+ ...(unverifiable.length > 0
1079
+ ? {
1080
+ unverifiable,
1081
+ unverifiableNote: "propertyEmpty 是只写属性:平台字段读面(bizproperty/list|get)不回该键,无法读回核对。本回执只证明它已随本次请求提交。",
1082
+ }
1083
+ : {}),
1056
1084
  ...(storedOptions !== undefined && storedOptions !== null ? { options: storedOptions } : {}),
1057
1085
  ...(dictionarySynced !== undefined ? { dictionarySynced } : {}),
1058
1086
  ...(formConditionSync !== undefined ? { formConditionSync } : {}),
@@ -1245,6 +1273,18 @@ function fieldTypeName(row) {
1245
1273
  const raw = row.typeName ?? fieldTypeLabel(row.type ?? row.propertyType);
1246
1274
  return String(raw ?? "");
1247
1275
  }
1276
+ /**
1277
+ * r32: 一句话说清「哪些 code 没到位、下一步怎么核对」。不存在的 code 指向
1278
+ * field.list(拼错/尚未创建),仍留在草稿面的 code 指向平台发布异常。
1279
+ */
1280
+ function signPartialDelivery(missing, nonexistent, stillDraft) {
1281
+ const parts = [];
1282
+ if (nonexistent.length > 0)
1283
+ parts.push(`${nonexistent.join("、")} 在模型里不存在(草稿面也没有):先 field.list 核对 code(omc call field.list --args '{"schemaCode":"<schemaCode>"}'),或先 field.create 建字段——本次发布不会建字段`);
1284
+ if (stillDraft.length > 0)
1285
+ parts.push(`${stillDraft.join("、")} 模型里有但发布后仍不在发布面:按发布失败排查(查看平台日志后重试本工具,模型级发布可重复执行)`);
1286
+ return `部分交付(unpublishedRequested: ${missing.join("、")}):已发布的字段确实已发布,但这些请求的 code 不在发布面。${parts.join(";")}`;
1287
+ }
1248
1288
  export async function executeFieldPublish(session, args) {
1249
1289
  // bizproperty/publish is MODEL-SCOPED: one call publishes every draft
1250
1290
  // field of the schema. `code`/`codes` only select which fields to verify
@@ -1289,6 +1329,10 @@ export async function executeFieldPublish(session, args) {
1289
1329
  const mismatches = checked.filter((entry) => entry.kind === "mismatch");
1290
1330
  return { mode: "detail", verified: true, checked: bounded.length, ok: mismatches.length === 0, ...(mismatches.length > 0 ? { mismatches } : {}) };
1291
1331
  };
1332
+ // r32: 请求里不存在的 code 不能靠「发布面缺它」推断——发布面缺也可能是这次
1333
+ // 发布才补上的。先记录 DRAFT 面(= 模型里真实存在的字段)的 code 集合,
1334
+ // 用来把「不存在」与「未发布」分开报。
1335
+ const draftCodesBefore = new Set(draftSnapshot.keys());
1292
1336
  const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
1293
1337
  const prePublishedRows = wireRows(preRead.data);
1294
1338
  const prePublished = prePublishedRows.map(codeOf).filter((code) => code.length > 0);
@@ -1375,8 +1419,22 @@ export async function executeFieldPublish(session, args) {
1375
1419
  const publishedCodes = wireRows(readback.data).map(codeOf).filter((code) => code.length > 0);
1376
1420
  const requested = [...(args.code ? [args.code] : []), ...(args.codes ?? [])];
1377
1421
  const present = requested.filter((code) => publishedCodes.includes(code));
1422
+ // r32(真机复现 0.6.13):`codes:["g1","notExist"]` 回 ok + DELIVERED +
1423
+ // allRequestedPresent:false + verification.ok:true —— 三个成功标记与「有请求的
1424
+ // 字段不在发布面」同真。不在发布面的 code 分两类,报法不同:
1425
+ // nonexistentCodes —— 草稿面都没有(模型里根本不存在,多半是拼错);
1426
+ // stillDraftCodes —— 模型里有、发布后仍不在发布面(真异常)。
1427
+ // 两者都让这次交付只算**部分成功**:已发布的字段是真发布了,所以不整单
1428
+ // 判失败;但既有的 verified/ok、allRequestedPresent、verification.ok 都不能
1429
+ // 再报 true(派生的 readback.present=false 会让共享写门把 deliveryState 降为
1430
+ // 既有枚举值 WRITE_UNVERIFIED——刻意不新增枚举外的值)。
1431
+ const missingRequested = requested.filter((code) => !present.includes(code));
1432
+ const nonexistentCodes = missingRequested.filter((code) => !draftCodesBefore.has(code));
1433
+ const stillDraftCodes = missingRequested.filter((code) => draftCodesBefore.has(code));
1434
+ const allRequestedPresent = missingRequested.length === 0;
1378
1435
  const targets = (requested.length > 0 ? requested : [...draftSnapshot.keys()]).filter((code) => publishedCodes.includes(code));
1379
- const verification = await verifyFieldDetails(targets);
1436
+ const verificationRun = await verifyFieldDetails(targets);
1437
+ const verification = allRequestedPresent ? verificationRun : { ...verificationRun, ok: false };
1380
1438
  const mismatches = verification.mismatches;
1381
1439
  if (mismatches !== undefined)
1382
1440
  throw new ToolRefusalError({
@@ -1392,7 +1450,23 @@ export async function executeFieldPublish(session, args) {
1392
1450
  publishedCodes,
1393
1451
  ...(args.code ? { publishedFieldPresent: publishedCodes.includes(args.code) } : {}),
1394
1452
  ...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, publishedCodes.includes(code)])) } : {}),
1395
- ...(requested.length > 0 ? { allRequestedPresent: present.length === requested.length } : {}),
1453
+ ...(requested.length > 0
1454
+ ? {
1455
+ allRequestedPresent,
1456
+ verified: allRequestedPresent,
1457
+ // 共享写门(write.ts deliveryStateFor)读 readback.present:部分成功不
1458
+ // 能盖 DELIVERED。全部就位时不写这个键(只增语义,不改变成功回执)。
1459
+ ...(allRequestedPresent ? {} : { present: false }),
1460
+ ...(allRequestedPresent
1461
+ ? {}
1462
+ : {
1463
+ unpublishedRequested: missingRequested,
1464
+ ...(nonexistentCodes.length > 0 ? { nonexistentCodes } : {}),
1465
+ ...(stillDraftCodes.length > 0 ? { stillDraftCodes } : {}),
1466
+ note: signPartialDelivery(missingRequested, nonexistentCodes, stillDraftCodes),
1467
+ }),
1468
+ }
1469
+ : {}),
1396
1470
  verification,
1397
1471
  // issue #24 主诉:publishedCodes 只证明「字段可用」,不等于「页面可见」。
1398
1472
  formBinding: await formBindingAdvisory(session, args.schemaCode, wireRows(readback.data)),
@@ -12,7 +12,7 @@ export const LIST_VIEW_WIRE = Object.freeze({
12
12
  configureDefault: "code omitted means the default view (code=schemaCode)",
13
13
  configureColumns: "query/get then query/update: for each fieldCodes entry, append a missing column as visible, or repair an existing column's name/propertyType/displayFormat/visible in place (never skip an already-present column), then query/get readback; propertyType comes from published field metadata; NUMERICAL displayFormat is derived from the field's 0-based `options.scale` (0=integer,1=1dp,2=2dp,3=3dp…8=8dp) through the platform parseNumber table ({0:1,1:2,2:3,3:11,4:12,23:…, percent 0..:4/5/6/13/14/27..30, currency ¥ 2dp..:7/15/16/31..34} — verified live: scale 2 → displayFormat 3 = 2 decimals) and DATE displayFormat from options.format (1=yyyy-MM-dd,2=datetime,3=minute,4=yyyy-MM); the readback columns array carries {propertyCode,propertyType,displayFormat,visible,sumType} and added/updated name the fieldCodes touched",
14
14
  configureSorts: "sorts -> querySorts [{propertyCode,propertyType,direction,sortKey,clientType}]; direction SortDirectionType numeric ASC(1)/DESC(2); omitted leaves the existing sorts untouched, provided replaces them",
15
- configureConditions: "conditions -> queryConditions [{propertyCode,propertyType,displayType,filterType,defaultValue,options,startValue,endValue,shortcutFilter,choiceType,dateType,accurateSearch,showNull,includeSubData,buildTreeBy,visible}]; filterType is the comparison OPERATOR (QueryFilterType STRING enum: Eq/NotEq/Gt/Lt/Gte/Lte/Like/NotLike/IsNull/IsNotNull/In/NotIn/Between/NotBetween/Of/NotOf/…; omitted → the platform default for the propertyType; aliases operator/op); displayType INPUT(0)/RADIO(1)/CHECKBOX(2)/SELECT(3)/RANGE(4); choiceType SELECT(1)/POPUP(2)/TREE_LEFT(3); options is a JSON string (decoded on readback)",
15
+ configureConditions: "conditions -> queryConditions [{propertyCode,propertyType,displayType,filterType,defaultValue,options,startValue,endValue,shortcutFilter,choiceType,dateType,accurateSearch,showNull,includeSubData,buildTreeBy,visible}]; filterType is the comparison OPERATOR (QueryFilterType STRING enum: Eq/NotEq/Gt/Lt/Gte/Lte/Like/NotLike/IsNull/IsNotNull/In/NotIn/Between/NotBetween/Of/NotOf/…; omitted → the platform default for the propertyType; aliases operator/op and legacy options.op); displayType INPUT(0)/RADIO(1)/CHECKBOX(2)/SELECT(3)/RANGE(4); choiceType SELECT(1)/POPUP(2)/TREE_LEFT(3); options is a JSON string (decoded on readback); conditionReadback is equivalent only for field-by-field equality, otherwise coerced with from/to details; viewConditionsAppliedRest:false records that REST data.list --queryCode does not apply fixed conditions",
16
16
  configureConditionMap: "queryConditionMap {<schemaCode>: queryConditions[]} is read-derived (query/get with a formCode child-table filter); BizQueryDetailConfig persists queryConditions/querySorts only, so the map is not a configure input",
17
17
  configureActions: "actions -> queryActions [{actionCode,name,icon,queryActionType,systemAction,serviceCode,serviceMethod,customService,associationCode,associationType,sortKey}]; queryActionType QueryActionType numeric QUERY(1)/ADD(2)/.../BATCH_SUBMIT(23); associationType QueryActionRelativeType numeric BIZSHEET(0)/WORKFLOW(1); associationCode/associationType are the JSON names of the DTO fields relativeCode/queryActionRelativeType (@JsonProperty); omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (live 2026-09-17: query/update replaces-then-re-adds the submitted list, so a merge keeps the platform's built-in 新增/删除/导入/导出 and updates a matching actionCode — replaceActions:true restores the authoritative full set, defaultActions:true re-seeds the platform default set); query/get auto-injects default actions (qr_code/batch_print/batch_update/import_attachment/export_attachment/batch_submit) and query/update resets sortKey to i+1",
18
18
  configurePresentation: "presentation -> queryPresentation {clientType,htmlJson,actionsJson,columnsJson,queryViewDataSource,schemaRelations,options}; htmlJson/actionsJson/columnsJson JSON strings are decoded once before render-shape validation (html element tagName+attrs, action actionCode/name/icon, column propertyCode/propertyType); invalid input refuses with listview-presentation-render-invalid before wire and is never double-encoded; clientType is forced to PC by query/update; schemaRelations is platform-derived from the queryColumns tree (resolveRelations) and overwrites your input; a non-empty htmlJson updates the environment record, which query/delete cleans up; omitted leaves the existing presentation untouched (the read-face object must be echoed, because saveBizQueryDetail removes every presentation whose clientType matches before re-adding), provided replaces it",
@@ -28,6 +28,24 @@ function fieldOptions(options) {
28
28
  }
29
29
  return options && typeof options === "object" ? options : {};
30
30
  }
31
+ function conditionOptionsWithOperator(condition) {
32
+ const options = condition.options;
33
+ if (typeof options !== "object" || options === null || Array.isArray(options))
34
+ return condition;
35
+ const optionRecord = options;
36
+ if (optionRecord.op === undefined)
37
+ return condition;
38
+ const aliases = { eq: "Eq", ne: "NotEq", neq: "NotEq", gt: "Gt", gte: "Gte", lt: "Lt", lte: "Lte", like: "Like" };
39
+ const raw = String(optionRecord.op);
40
+ return {
41
+ ...condition,
42
+ ...(condition.filterType === undefined && condition.operator === undefined ? { filterType: aliases[raw.toLowerCase()] ?? raw } : {}),
43
+ options: Object.fromEntries(Object.entries(optionRecord).filter(([key]) => key !== "op")),
44
+ };
45
+ }
46
+ function conditionValueEqual(left, right) {
47
+ return JSON.stringify(left) === JSON.stringify(right);
48
+ }
31
49
  /**
32
50
  * QueryRuntimeController.parseNumber keys its formatting off displayFormat with
33
51
  * no case 0 (valid values start at 1): 1=integer, 2=1dp, 3=2dp, 11=3dp,
@@ -650,10 +668,15 @@ export function registerListViewLane(tools) {
650
668
  let requestedConditionKeys;
651
669
  let requestedConditionSpec;
652
670
  if (args.conditions !== undefined) {
653
- const normalizedConditions = normalizeConditions(args.conditions, fieldRows, args.schemaCode);
671
+ const inputConditions = args.conditions.map(conditionOptionsWithOperator);
672
+ const normalizedConditions = normalizeConditions(inputConditions, fieldRows, args.schemaCode);
654
673
  body.queryConditions = normalizedConditions;
655
674
  requestedConditionKeys = normalizedConditions.map((row) => String(row.propertyCode ?? ""));
656
- requestedConditionSpec = normalizedConditions.map((row) => ({ propertyCode: String(row.propertyCode ?? ""), filterType: row.filterType }));
675
+ requestedConditionSpec = inputConditions.map((requested, index) => ({
676
+ propertyCode: String(normalizedConditions[index]?.propertyCode ?? requested.propertyCode ?? ""),
677
+ requested,
678
+ normalized: normalizedConditions[index] ?? {},
679
+ }));
657
680
  }
658
681
  // saveBizQueryDetail removes every action whose clientType matches the
659
682
  // update and re-adds the submitted list, so an omitted `actions` must echo
@@ -807,6 +830,8 @@ export function registerListViewLane(tools) {
807
830
  if (conditions.length > 0)
808
831
  readback.conditionRuntimeNote =
809
832
  "视图条件的运行时过滤语义未取证:REST `data.list --queryCode <本视图>` 实测不应用固定条件(r35:读回 Gte/5000 正确、返回未过滤 4 条)。别用 queryCode 断言过滤生效;REST 侧过滤用 `data.list` 显式 `filter`。";
833
+ if (conditions.length > 0)
834
+ readback.viewConditionsAppliedRest = false;
810
835
  // Post-write semantic readback (dogfood R2): the platform MODELS a list
811
836
  // condition as a filter CONTROL definition, so a requested condition whose
812
837
  // propertyCode does not survive the round-trip was lost. Surface that as
@@ -819,25 +844,35 @@ export function registerListViewLane(tools) {
819
844
  // lands with the WRONG comparison is just as wrong as one that is
820
845
  // dropped (dogfood R2 follow-up).
821
846
  const storedByCode = new Map(conditions.map((condition) => [String(condition.propertyCode ?? ""), condition]));
822
- const filterTypeMismatches = (requestedConditionSpec ?? [])
823
- .filter((spec) => storedByCode.has(spec.propertyCode))
824
- .filter((spec) => String(storedByCode.get(spec.propertyCode)?.filterType ?? "") !== String(spec.filterType ?? ""))
825
- .map((spec) => ({ propertyCode: spec.propertyCode, requested: spec.filterType ?? null, stored: storedByCode.get(spec.propertyCode)?.filterType ?? null }));
826
- const equivalent = missingConditions.length === 0 && filterTypeMismatches.length === 0;
847
+ const coercions = (requestedConditionSpec ?? []).flatMap((spec) => {
848
+ const stored = storedByCode.get(spec.propertyCode);
849
+ if (!stored)
850
+ return [{ propertyCode: spec.propertyCode, field: "condition", from: spec.requested, to: null }];
851
+ const fields = [
852
+ ["filterType", spec.requested.filterType ?? spec.requested.operator ?? spec.requested.op ?? spec.normalized.filterType, stored.filterType],
853
+ ["displayType", spec.requested.displayType, stored.displayType],
854
+ ["defaultValue", spec.requested.defaultValue, stored.defaultValue],
855
+ ["options", spec.requested.options, stored.options],
856
+ ["startValue", spec.requested.startValue, stored.startValue],
857
+ ["endValue", spec.requested.endValue, stored.endValue],
858
+ ];
859
+ return fields.filter(([, from]) => from !== undefined).filter(([, from, to]) => !conditionValueEqual(from, to)).map(([field, from, to]) => ({ propertyCode: spec.propertyCode, field, from, to }));
860
+ });
861
+ const equivalent = missingConditions.length === 0 && coercions.length === 0;
827
862
  readback.conditionReadback = {
828
- status: equivalent ? "equivalent" : "listview-condition-readback-mismatch",
863
+ status: equivalent ? "equivalent" : "coerced",
829
864
  requested: requestedConditionKeys,
830
865
  stored: [...storedKeys],
831
866
  // Operator-level summary so "operator + value" acceptance needs no
832
867
  // `--full`/artifact fetch (dogfood R2 round17b).
833
868
  operators: (requestedConditionSpec ?? []).map((spec) => ({
834
869
  propertyCode: spec.propertyCode,
835
- requested: spec.filterType ?? null,
870
+ requested: spec.requested.filterType ?? spec.requested.operator ?? spec.requested.op ?? spec.normalized.filterType ?? null,
836
871
  stored: storedByCode.get(spec.propertyCode)?.filterType ?? null,
837
872
  defaultValue: storedByCode.get(spec.propertyCode)?.defaultValue ?? null,
838
873
  })),
839
874
  ...(missingConditions.length > 0 ? { missing: missingConditions } : {}),
840
- ...(filterTypeMismatches.length > 0 ? { filterTypeMismatches } : {}),
875
+ ...(coercions.length > 0 ? { coercions } : {}),
841
876
  };
842
877
  }
843
878
  readback.columnsRenderCompat = columnsRenderCompat(readArray(afterModel, "queryColumns"));
@@ -330,7 +330,7 @@ function isReceiptDerivedKey(key) {
330
330
  // `detailFile` 故意**不在此列**:它是本地文件系统路径(进程内才有效),
331
331
  // 合进投影视图会让 `--keys` 把本机路径当"结果键"吐出来(2026-11-05 回归:
332
332
  // readpath-roundtrip 1.2 的 keys 断言多了 detailFile)。对外只认 artifactId。
333
- return /(?:Count|Omitted)$/.test(key) || key === "detailArtifactId" || key === "detailSha256" || key === "from" || key === "truncated" || key === "truncatedKeys" || key === "fetch" || key === "tablesHint";
333
+ return /(?:Count|Omitted)$/.test(key) || key === "total" || key === "detailArtifactId" || key === "detailSha256" || key === "from" || key === "truncated" || key === "truncatedKeys" || key === "fetch" || key === "tablesHint";
334
334
  }
335
335
  function mergeReceiptDerivedKeys(artifactSource, result) {
336
336
  if (artifactSource === null || typeof artifactSource !== "object" || Array.isArray(artifactSource))
@@ -438,6 +438,16 @@ export function projectResultExtract(result, paths, keysOnly) {
438
438
  ...(artifactId !== undefined ? { artifactId, sha256: artifact?.sha256 } : {}),
439
439
  from: readFromArtifact ? "artifact" : "result",
440
440
  };
441
+ // 投影只改变取值路径,不改变源结果的完整性事实。尤其是外置 artifact
442
+ // 场景,调用方仍必须能判断 items 是否被折叠,以及源侧 total/itemsCount。
443
+ // 只透传源中实际存在的键;未截断结果不要凭空写 truncated:false。
444
+ const sourceRecord = source && typeof source === "object" && !Array.isArray(source)
445
+ ? source
446
+ : undefined;
447
+ for (const key of ["truncated", "truncatedKeys", "itemsCount", "total"]) {
448
+ if (sourceRecord !== undefined && Object.prototype.hasOwnProperty.call(sourceRecord, key))
449
+ out[key] = sourceRecord[key];
450
+ }
441
451
  // T6:列表投影里「未配置的列」也是 null 键——工具自己已经写了 projectionHint
442
452
  // (#15 A6),但投影层此前把它丢了,于是 `--jq items[].fields.x` 全 null 时
443
453
  // 调用方看不到任何线索。把源结果的提示原样透传(有才带,零额外体积)。
@@ -570,6 +580,9 @@ export function projectResultExtract(result, paths, keysOnly) {
570
580
  out.rowCount = rowCount;
571
581
  // Deliberately NO top-level `content` on multi-path — that was the duplicate.
572
582
  }
583
+ // Keep one machine-readable shape for artifact/call consumers. `content` on
584
+ // the single-path branch remains as a backwards-compatible convenience.
585
+ out.projections = extracts;
573
586
  // A projection that missed is not an answer. `status` stays `ok` (adding a
574
587
  // new status value would fall through classifyResult to error/exit 15 and
575
588
  // change the exit contract), but the envelope now says so out loud.
@@ -54,7 +54,7 @@ const bizserviceSelfCallCorp = Object.freeze({
54
54
  environment,
55
55
  serviceCode: "<service_code>",
56
56
  methodCode: "<method_code>",
57
- inputParameters: [
57
+ methodInputParameters: [
58
58
  { code: "X-LowCode-Corpid", name: "corpId头", required: false, codeType: 0, bizCode: "<corpId 常量值>", configJson: { position: "REQUEST_HEADER" } },
59
59
  ],
60
60
  },
@@ -1,10 +1,10 @@
1
1
  import { environment, model, commonRecovery } from "./shared.js";
2
2
  const configureFormRecipe = Object.freeze({
3
3
  id: "publish-default-form",
4
- title: "把已发布字段绑进默认表单并发布(高层编排)",
4
+ title: "把已发布字段绑进默认表单(发布请随后调用 form.publish)",
5
5
  status: "offline-valid",
6
6
  tool: "configure_form",
7
- args: { environment, schemaCode: model, publish: true },
7
+ args: { environment, schemaCode: model },
8
8
  preconditions: ["字段已 field.publish"],
9
9
  conditionalRequirements: ["表单层条件必填/只读请用 field.update 的 options(form-conditional-required / form-always-readonly recipe),不是本编排器"],
10
10
  priorToolReferences: [{ tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认字段已发布" }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "commander": "14.0.3",
58
- "omc-core": "0.6.13",
58
+ "omc-core": "0.6.14",
59
59
  "proper-lockfile": "4.1.2",
60
60
  "write-file-atomic": "6.0.0",
61
61
  "zod": "^3.23.8",
@@ -73,6 +73,6 @@
73
73
  "directory": "packages/omc-cli"
74
74
  },
75
75
  "optionalDependencies": {
76
- "@stalfh233/omc-fake-platform": "0.6.13"
76
+ "@stalfh233/omc-fake-platform": "0.6.14"
77
77
  }
78
78
  }