@stalfh233/omc-cli 0.6.17 → 0.6.19

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.
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ENDPOINTS } from "../endpoints.js";
3
3
  import { fieldChangeWireBody, fieldUpdateWireBody, prepareFieldChange, prepareFieldUpdate } from "../field-change.js";
4
- import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, childColumnsOf, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, assertFieldFamilyAllowed, assertSelectionChoicesAllowed, } from "../field-families.js";
4
+ import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, childColumnsOf, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, assertFieldFamilyAllowed, assertSelectionChoicesAllowed, assertRelationTargetExists, relationPublishFailureMessage, } from "../field-families.js";
5
5
  import { capabilityArg, environmentArg } from "../args.js";
6
6
  import { governedRead } from "../read.js";
7
7
  import { writeTool } from "../write.js";
@@ -12,10 +12,35 @@ import { defineTool, ToolRefusalError } from "../tool-types.js";
12
12
  import { wireCall, wireRows, WireError } from "../wire.js";
13
13
  import { executeFormDraftWrite, executeFormPublishWrite, readFormOrNull } from "../form-support.js";
14
14
  import { FIELD_LANE_CONTRACT } from "./field-contract.js";
15
- import { mapBounded } from "../support.js";
15
+ import { flattenTreeRows, mapBounded } from "../support.js";
16
16
  import { extractBizObjectPage } from "./runtime-data.js";
17
17
  const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
18
18
  const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
19
+ /** Read actual model schema codes; bare modelList may expose internal node ids. */
20
+ export async function modelSchemaCodes(session) {
21
+ try {
22
+ // Relation codes are tenant-global on the platform, not application-scoped.
23
+ // Enumerate every application for this call only; deliberately do not cache
24
+ // this merged candidate set across calls because model writes can change it.
25
+ const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
26
+ .map((row) => row.appCode ?? row.code)
27
+ .filter((code) => typeof code === "string" && code.length > 0);
28
+ const trees = await mapBounded(apps, 4, async (appCode) => flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data)));
29
+ const codes = trees.flat()
30
+ .filter((row) => row.type === "BizModel")
31
+ .map((row) => String(row.code ?? row.schemaCode ?? row.bizSchemaCode ?? ""))
32
+ .filter((code) => code.length > 0);
33
+ if (codes.length > 0)
34
+ return codes;
35
+ }
36
+ catch {
37
+ // Fall through to the compatibility endpoint below when the tenant-wide
38
+ // discovery face is unavailable (older/fake transports).
39
+ }
40
+ return wireRows((await wireCall(session, { path: ENDPOINTS.modelList, query: {} })).data)
41
+ .map((row) => String(row.schemaCode ?? row.bizSchemaCode ?? row.code ?? ""))
42
+ .filter((code) => code.length > 0);
43
+ }
19
44
  async function readFieldRollback(session, args) {
20
45
  for (const published of [false, true]) {
21
46
  const row = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: String(args.schemaCode), isPublish: String(published) } })).data).find((entry) => String(entry.code ?? entry.propertyCode) === String(args.code));
@@ -377,6 +402,8 @@ export function registerFieldLane(tools) {
377
402
  // wire body (src/field-change.ts), so configure_model and intent share it.
378
403
  assertNoSelfSchemaAggregation(args, published);
379
404
  await assertNoChildSchemaAggregation(session, args.schemaCode, args.propertyType, args.options ?? {});
405
+ const models = await modelSchemaCodes(session);
406
+ assertRelationTargetExists({ ...args }, models);
380
407
  // 目标版本决定 optionsSet 形状:8.2 以下要补 optionStructure/treeSelect
381
408
  // (cpdocs issue #8:缺键让设计器选项弹窗硬崩)。
382
409
  const targetVersion = session.detectedVersion();
@@ -434,6 +461,7 @@ export function registerFieldLane(tools) {
434
461
  readback: {
435
462
  face: "draft",
436
463
  draftFieldPresent: rows.some((row) => (row.code ?? row.propertyCode) === args.code),
464
+ ...(prepared.normalized ? { normalized: prepared.normalized } : {}),
437
465
  ...(textLike
438
466
  ? {
439
467
  sideEffectsNote: "平台可能为文本/下拉字段自动生成一条 datarule type1 checkType=4(maxLength≈200);本调用未显式创建。清理/对账请用 datarule.list 核对,不要把自动行当成重复写入。",
@@ -637,6 +665,15 @@ async function loadFieldRowForUpdate(session, schemaCode, code) {
637
665
  const record = rows.find((row) => (row.code ?? row.propertyCode) === code);
638
666
  if (!record)
639
667
  throw new WireError("business", `field ${code} not found on ${schemaCode}`, 301001);
668
+ // Some platform list faces are diet projections and omit the display name;
669
+ // update readback must still preserve the existing name/name_i18n.
670
+ try {
671
+ const detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode, propertyCode: code } })).data;
672
+ return { ...detail, ...record, name: record.name ?? detail.name, name_i18n: record.name_i18n ?? detail.name_i18n };
673
+ }
674
+ catch {
675
+ // The detail face is optional; retain the list row.
676
+ }
640
677
  return record;
641
678
  }
642
679
  async function countModelDataRows(session, schemaCode) {
@@ -1031,6 +1068,58 @@ async function runFieldUpdate(session, args) {
1031
1068
  ...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
1032
1069
  };
1033
1070
  const prepared = prepareFieldUpdate(update);
1071
+ const currentOptions = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
1072
+ const requestedChanges = [];
1073
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
1074
+ if (args.name !== undefined && !same(record.name ?? null, prepared.name ?? args.name))
1075
+ requestedChanges.push("name");
1076
+ if (args.name_i18n !== undefined && !same(record.name_i18n ?? null, prepared.nameI18n ?? args.name_i18n))
1077
+ requestedChanges.push("name_i18n");
1078
+ if (args.relativeCode !== undefined && !same(record.relativeCode ?? null, args.relativeCode))
1079
+ requestedChanges.push("relativeCode");
1080
+ if (args.propertyEmpty !== undefined && !same(record.propertyEmpty ?? null, args.propertyEmpty))
1081
+ requestedChanges.push("propertyEmpty");
1082
+ if (args.options !== undefined) {
1083
+ for (const key of new Set([...Object.keys(currentOptions), ...Object.keys(prepared.options)]))
1084
+ if (!same(currentOptions[key] ?? null, prepared.options[key] ?? null))
1085
+ requestedChanges.push(`options.${key}`);
1086
+ }
1087
+ const mutableOptionKeys = new Set([
1088
+ // These are established field-row update surfaces (tickets 60/61 and the
1089
+ // condition pairing round). Everything else stays fail-closed: the
1090
+ // platform accepts many keys and silently drops them.
1091
+ "dictionaryData", "optionsSet", "mappings", "schemaCode", "queryCode", "displayField", "displayFieldType", "displayFieldFormat",
1092
+ "relativePropertyMatchCode", "relativePropertyCode", "linkMode", "showStyle", "searchFormula", "conditions",
1093
+ "requiredFormula", "readonlyCondition", "displayFormula", "readonlyFormula", "scale",
1094
+ ]);
1095
+ const changedOptionKeys = requestedChanges.filter((field) => field.startsWith("options.")).map((field) => field.slice("options.".length));
1096
+ const immutableOptionKeys = changedOptionKeys.filter((key) => !mutableOptionKeys.has(key));
1097
+ if (immutableOptionKeys.length > 0) {
1098
+ throw new ToolRefusalError({
1099
+ status: "refused",
1100
+ blocker: "field-options-immutable",
1101
+ schemaCode: args.schemaCode,
1102
+ code: args.code,
1103
+ fields: immutableOptionKeys.map((key) => `options.${key}`),
1104
+ mutableFields: [...mutableOptionKeys].map((key) => `options.${key}`),
1105
+ message: `field.update 不允许经此面修改 options.${immutableOptionKeys.join("、")};这类字段配置会被平台静默丢弃。可改白名单:${[...mutableOptionKeys].map((key) => `options.${key}`).join("、")}。关联结构(schemaCode/queryCode/displayField 等)请 field.delete + field.create 重建;条件请用字段条件配置。`,
1106
+ });
1107
+ }
1108
+ if (args.choices !== undefined && !same(choiceCodesOf(currentOptions) ?? [], [...args.choices]))
1109
+ requestedChanges.push("choices");
1110
+ if (requestedChanges.length === 0) {
1111
+ return {
1112
+ performed: { errcode: 0, unchanged: true },
1113
+ readback: {
1114
+ face: "draft",
1115
+ unchanged: true,
1116
+ changedFields: [],
1117
+ ...(record.name !== undefined ? { name: record.name } : {}),
1118
+ ...(record.name_i18n !== undefined ? { name_i18n: record.name_i18n } : {}),
1119
+ availableFields: ["name", "name_i18n", "options", "choices", "relativeCode", "propertyEmpty"],
1120
+ },
1121
+ };
1122
+ }
1034
1123
  const performed = await wireCall(session, { path: ENDPOINTS.fieldUpdate, method: "POST", body: fieldUpdateWireBody(update, prepared) });
1035
1124
  const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
1036
1125
  const after = wireRows(readback.data).find((row) => (row.code ?? row.propertyCode) === args.code);
@@ -1042,7 +1131,9 @@ async function runFieldUpdate(session, args) {
1042
1131
  // failure never hides the field write (it is surfaced as formConditionSync.error).
1043
1132
  let formConditionSync;
1044
1133
  let formConditionSyncError;
1045
- if (effectiveOptions !== undefined && !prepared.isChildTable) {
1134
+ const conditionKeys = new Set(Object.keys(FORM_CONDITION_OPTION_KEYS));
1135
+ const conditionChanged = effectiveOptions !== undefined && [...conditionKeys].some((key) => !fieldValuesEqual((decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {})[key], prepared.options[key]));
1136
+ if (conditionChanged && !prepared.isChildTable) {
1046
1137
  const previous = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
1047
1138
  try {
1048
1139
  const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, previous, prepared.options);
@@ -1058,6 +1149,19 @@ async function runFieldUpdate(session, args) {
1058
1149
  // confirm the write landed — the readback only named the field. Field
1059
1150
  // options are the form-interaction contract (ADR 0005), so read them back.
1060
1151
  const storedOptions = after !== undefined ? parseFieldOptionsForReadback(after.options) : undefined;
1152
+ if (args.options !== undefined && storedOptions && typeof storedOptions === "object" && !Array.isArray(storedOptions)) {
1153
+ const missed = Object.keys(args.options).filter((key) => !same(storedOptions[key] ?? null, prepared.options[key] ?? null));
1154
+ if (missed.length > 0) {
1155
+ throw new ToolRefusalError({
1156
+ status: "refused",
1157
+ blocker: "field-options-immutable",
1158
+ schemaCode: args.schemaCode,
1159
+ code: args.code,
1160
+ fields: missed.map((key) => `options.${key}`),
1161
+ message: `field.update 的 options.${missed.join("、")} 经此面提交后未生效;options 属于不可经此面修改的字段配置。请按字段族契约使用 field.delete + field.create 重建,或走对应的表单/数据规则工具(不要把 DELIVERED 当成已生效)。`,
1162
+ });
1163
+ }
1164
+ }
1061
1165
  // r32(真机复现:field.update propertyEmpty:true 回 ok/DELIVERED,field.list --full
1062
1166
  // 与 model.inspect 都看不到该值):平台字段行读面(bizproperty/list 与
1063
1167
  // bizproperty/get)都不回 propertyEmpty,这是**只写属性**。不编造读回值:
@@ -1083,8 +1187,12 @@ async function runFieldUpdate(session, args) {
1083
1187
  face: "draft",
1084
1188
  impact,
1085
1189
  fieldPresent: after !== undefined,
1086
- ...(after?.name !== undefined ? { name: after.name } : {}),
1087
- ...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
1190
+ ...(after?.name !== undefined || prepared.name !== undefined || record.name !== undefined || record.propertyName !== undefined
1191
+ ? { name: after?.name ?? prepared.name ?? record.name ?? record.propertyName }
1192
+ : {}),
1193
+ ...(typeof after?.name_i18n === "string" || typeof prepared.nameI18n === "string" || typeof record.name_i18n === "string"
1194
+ ? { name_i18n: after?.name_i18n ?? prepared.nameI18n ?? record.name_i18n }
1195
+ : {}),
1088
1196
  ...(after?.type !== undefined || after?.propertyType !== undefined ? { type: after?.type ?? after?.propertyType } : {}),
1089
1197
  ...(hasReadBackPropertyEmpty ? { propertyEmpty: after?.propertyEmpty } : {}),
1090
1198
  committed: committedKeys,
@@ -1349,6 +1457,13 @@ export async function executeFieldPublish(session, args) {
1349
1457
  const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
1350
1458
  const prePublishedRows = wireRows(preRead.data);
1351
1459
  const prePublished = prePublishedRows.map(codeOf).filter((code) => code.length > 0);
1460
+ // Older servers accept a dangling relation row and only reveal it as the
1461
+ // misleading 300010 at model publish. Inspect the draft before POST so an
1462
+ // existing poisoned model gets the same field-specific diagnosis as create.
1463
+ const relationCandidates = await modelSchemaCodes(session);
1464
+ const relationFailure = relationPublishFailureMessage(wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data), relationCandidates);
1465
+ if (relationFailure)
1466
+ throw new ToolRefusalError({ status: "refused", blocker: "relation-target-not-found", schemaCode: args.schemaCode, message: relationFailure, candidates: relationCandidates.slice(0, 5) });
1352
1467
  // r29e F10/F11:旧短路只看「code 在发布面存在」,改了 options 仍报
1353
1468
  // already-published + verification.ok(假绿)。再比草稿/发布 options 指纹;
1354
1469
  // 不一致就真发布。name/type 仍由 verifyFieldDetails 核对。
@@ -5,7 +5,7 @@ import { writeTool } from "../write.js";
5
5
  import { wireCall, wireRows } from "../wire.js";
6
6
  import { ENDPOINTS } from "../endpoints.js";
7
7
  import { capabilityArg, environmentArg } from "../args.js";
8
- import { resolveApplicationCode } from "../support.js";
8
+ import { jsonBytes, resolveApplicationCode } from "../support.js";
9
9
  import { LANES } from "./lane-ids.js";
10
10
  import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
11
11
  /**
@@ -38,6 +38,33 @@ import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
38
38
  */
39
39
  const LANE = LANES.folder;
40
40
  const T = FOLDER_LANE_CONTRACT.tools;
41
+ /**
42
+ * folder.tree is a node inventory, so its useful inline budget is node-count
43
+ * first rather than the generic read compact threshold. Keep the existing
44
+ * byte guard as a second gate: a pathological 50-node tree still belongs in
45
+ * the artifact when the node payload itself is too large.
46
+ */
47
+ export const FOLDER_TREE_INLINE_NODE_LIMIT = 50;
48
+ export const FOLDER_TREE_NODE_INLINE_CEILING_BYTES = 8192;
49
+ export function compactFolderTree(result) {
50
+ const nodes = Array.isArray(result.nodes) ? result.nodes : undefined;
51
+ if (nodes === undefined)
52
+ return { ...result };
53
+ if (nodes.length <= FOLDER_TREE_INLINE_NODE_LIMIT && jsonBytes(nodes) <= FOLDER_TREE_NODE_INLINE_CEILING_BYTES) {
54
+ return { ...result, nodes };
55
+ }
56
+ const countGate = nodes.length > FOLDER_TREE_INLINE_NODE_LIMIT;
57
+ const { nodes: _nodes, ...withoutNodes } = result;
58
+ return {
59
+ ...withoutNodes,
60
+ nodesPreview: nodes.slice(0, 5),
61
+ note: `${String(result.note ?? "")} ${countGate
62
+ ? `nodes 超过 ${FOLDER_TREE_INLINE_NODE_LIMIT} 条,完整节点已外置到 detailArtifactId;nodesPreview 仅展示前 5 条。`
63
+ : `nodes 正文超过 ${FOLDER_TREE_NODE_INLINE_CEILING_BYTES}B 字节闸,完整节点已外置到 detailArtifactId;这是字节闸,不是节点数限制。`}`.trim(),
64
+ // Deliberately omit nodes so governedRead adds the standard empty
65
+ // placeholder/truncatedKeys while the preview remains available.
66
+ };
67
+ }
41
68
  /**
42
69
  * Live 1.1.25 (r28d F2): platform `apppackage/create` seeds one top-level Folder
43
70
  * whose `name_i18n.en` is the stable token `defaultGroup` (zh display is「默认分组」).
@@ -202,7 +229,7 @@ function folderTools(tools) {
202
229
  : " applicationExists:false——应用不在 apppackage/list;空树不等于「空应用」。") +
203
230
  " nodeCount/folderCount/modelCount 是全应用聚合,共享环境含其它会话资源;核对自己的资源请按 code 前缀过滤 nodes[],不要只比计数。",
204
231
  };
205
- }),
232
+ }, { compact: compactFolderTree }),
206
233
  }));
207
234
  writeTool({
208
235
  tools,
@@ -116,7 +116,7 @@ export const MODEL_LANE_CONTRACT = Object.freeze({
116
116
  }),
117
117
  configure_model: Object.freeze({
118
118
  scope: MODEL_SCOPE,
119
- description: "High-level: create a model with one field (fieldCode/fieldName/propertyType, legacy) or N fields (fields:[{code,name,propertyType,choices?,relativeCode?,options?}]) and drive it to the structurally usable state (model.create → field.create×N → model-scoped field.publish), optionally drafting and publishing the default form in the same package (publishForm:true → form.draft → form.publish) and configuring the default list view (`listview:{fieldCodes,...}` → listview.configure). Resumable: if the model already exists in the application tree, the model.create step is SKIPPED (reported as `skipped:\"model-exists\"`) and the field/publish steps still run — it does not abort on a pre-existing model. Family gates run per field BEFORE the write permission gate; for the form-only path use configure_form.",
119
+ description: "High-level: create a model with one field (fieldCode/fieldName/propertyType, legacy) or N fields (fields:[{code,name,propertyType,choices?,relativeCode?,options?}]) and drive it to the structurally usable state (model.create → field.create×N → model-scoped field.publish), optionally drafting and publishing the default form in the same package (publishForm:true → form.draft → form.publish) and configuring the default list view (`listview:{fieldCodes,...}` → listview.configure). Resumable: if the model already exists, the readback declares `created:false` and `mergedChanges`; `createOnly:true` refuses with `model-already-exists`. Model code is tenant-global and cannot be reused across applications. Family gates run per field BEFORE the write permission gate; for the form-only path use configure_form.",
120
120
  }),
121
121
  "model.deleteMany": Object.freeze({
122
122
  scope: MODEL_SCOPE,
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ENDPOINTS } from "../endpoints.js";
3
- import { assertFieldFamilyAllowed, assertSelectionChoicesAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, } from "../field-families.js";
3
+ import { assertFieldFamilyAllowed, assertSelectionChoicesAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, assertRelationTargetExists, } from "../field-families.js";
4
+ import { modelSchemaCodes } from "./field.js";
4
5
  import { fieldChangeWireBody, prepareFieldChange, sameFieldChange } from "../field-change.js";
5
6
  import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
6
7
  import { mapBounded } from "../support.js";
@@ -9,7 +10,8 @@ import { writeSummary } from "../human-summary.js";
9
10
  import { capabilityArg, environmentArg } from "../args.js";
10
11
  import { governedRead, withoutReadbackCache } from "../read.js";
11
12
  import { parseToolInput } from "../tool-types.js";
12
- import { applicationResolutionOf, applicationScopeHint, applicationScopeNotice, flattenTreeRows, resolveApplicationCode, resolveApplicationScope } from "../support.js";
13
+ import { applicationResolutionOf, applicationScopeHint, applicationScopeNotice, flattenTreeRows, resolveApplicationCode } from "../support.js";
14
+ import { resolveValidatedApplicationScope } from "./app.js";
13
15
  import { writeTool } from "../write.js";
14
16
  import { registerPackageTool } from "../package-tool.js";
15
17
  import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
@@ -251,28 +253,11 @@ export function registerModelLane(tools) {
251
253
  capability: capabilityArg,
252
254
  },
253
255
  handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
254
- const scope = await resolveApplicationScope(session, args.applicationCode);
256
+ const scope = await resolveValidatedApplicationScope(session, args.applicationCode);
255
257
  const applicationCode = scope.code;
256
258
  // r29f F6:显式/配置的 applicationCode 若不在 app.list 里,旧路径会把拼写错误
257
259
  // 伪装成「该应用没有模型」。先核对租户应用表,未命中即拒(与 discover 的
258
260
  // applicationResolution.status 同构)。
259
- const availableApps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
260
- .map((row) => String(row.appCode ?? row.code ?? ""))
261
- .filter((code) => code !== "");
262
- if (availableApps.length > 0 && !availableApps.includes(applicationCode)) {
263
- throw new ToolRefusalError({
264
- status: "refused",
265
- blocker: "app-not-found",
266
- applicationCode,
267
- applicationResolution: {
268
- status: "unresolved",
269
- source: scope.source,
270
- requested: scope.requested ?? applicationCode,
271
- available: availableApps.slice(0, 40),
272
- },
273
- message: `应用「${applicationCode}」不在租户 app.list 里(不是「该应用没有模型」)。可用:${availableApps.slice(0, 12).join("、")}${availableApps.length > 12 ? " 等" : ""};先 app.list 取正确 applicationCode。`,
274
- });
275
- }
276
261
  const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
277
262
  const all = tree
278
263
  .filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code)
@@ -361,14 +346,15 @@ export function registerModelLane(tools) {
361
346
  // 节点本来就带 name,与六面同一批并发读,零额外顺序往返。
362
347
  const treeFace = async () => {
363
348
  try {
364
- return { node: (await resolveModelTreeNode(session, schemaCode)).node };
349
+ const resolved = await resolveModelTreeNode(session, schemaCode);
350
+ return { node: resolved.node, applicationCode: resolved.applicationCode };
365
351
  }
366
352
  catch {
367
353
  return undefined;
368
354
  }
369
355
  };
370
356
  const [fieldFace, formFace, listviewFace, ruleFace, dataruleFace, draftFace, treeRead] = await mapBounded([
371
- () => face("field.list", { schemaCode, published: true, ...(all ? { all: true } : {}) }, faceErrcodes),
357
+ () => face("field.list", { schemaCode, published: true, ...(all ? { all: true, full: true } : {}) }, faceErrcodes),
372
358
  () => face("form.get", { schemaCode }),
373
359
  () => face("listview.get", { schemaCode }),
374
360
  () => face("rule.list", { schemaCode }),
@@ -378,10 +364,21 @@ export function registerModelLane(tools) {
378
364
  ], 7, (task) => task());
379
365
  const treeNode = treeRead?.node;
380
366
  const modelName = typeof treeNode?.name === "string" && treeNode.name ? treeNode.name : typeof treeNode?.text === "string" && treeNode.text ? treeNode.text : undefined;
381
- const fieldRows = Array.isArray(fieldFace?.fields) ? fieldFace.fields : [];
367
+ let fieldRows = Array.isArray(fieldFace?.fields) ? fieldFace.fields : [];
368
+ if (all && typeof fieldFace?.detailFile === "string" && !fieldRows.some((field) => field.options !== undefined)) {
369
+ try {
370
+ const { readFileSync } = await import("node:fs");
371
+ const artifact = JSON.parse(readFileSync(fieldFace.detailFile, "utf8"));
372
+ if (Array.isArray(artifact.fields))
373
+ fieldRows = artifact.fields;
374
+ }
375
+ catch {
376
+ // Keep the compact field inventory if the nested artifact is unavailable.
377
+ }
378
+ }
382
379
  const fields = fieldRows
383
380
  .filter((field) => all || !SYSTEM_FIELD_CODES.has(String(field.code)))
384
- .map((field) => ({ code: field.code, type: field.type, ...(field.typeName !== undefined ? { typeName: field.typeName } : {}), name: field.name, ...(field.conditions ? { conditions: field.conditions } : {}) }));
381
+ .map((field) => ({ code: field.code, type: field.type, ...(field.typeName !== undefined ? { typeName: field.typeName } : {}), name: field.name, ...(field.conditions ? { conditions: field.conditions } : {}), ...(all && field.options !== undefined ? { options: field.options } : {}) }));
385
382
  const form = formFace && formFace.present !== undefined
386
383
  ? { present: formFace.present === true, published: formFace.published === true, controlCount: Array.isArray(formFace.draftControlCodes) ? formFace.draftControlCodes.length : 0 }
387
384
  : undefined;
@@ -454,6 +451,7 @@ export function registerModelLane(tools) {
454
451
  return {
455
452
  code: schemaCode,
456
453
  schemaCode,
454
+ ...(typeof treeRead?.applicationCode === "string" && treeRead.applicationCode ? { applicationCode: treeRead.applicationCode } : {}),
457
455
  ...(modelName !== undefined ? { name: modelName } : {}),
458
456
  ...(modelPresent !== undefined ? { modelPresent } : {}),
459
457
  ...(modelPresent === false
@@ -488,10 +486,14 @@ export function registerModelLane(tools) {
488
486
  return result;
489
487
  return {
490
488
  schemaCode: result.schemaCode,
489
+ ...(result.applicationCode !== undefined ? { applicationCode: result.applicationCode } : {}),
491
490
  // r35a-F3:compact 投影必须保留 code(model.list 有 code,inspect 此前
492
491
  // 只剩 schemaCode,`--jq '[].code'` 必 pathMiss 白跑一轮)。
493
492
  ...(result.code !== undefined ? { code: result.code } : {}),
494
493
  fieldCodes: (result.fields ?? []).map((field) => field.code),
494
+ ...(Array.isArray(result.fields) && result.fields.some((field) => field.options !== undefined)
495
+ ? { fields: result.fields }
496
+ : {}),
495
497
  form: result.form,
496
498
  listview: result.listview ? { code: result.listview.code, columnCount: result.listview.columnCount } : undefined,
497
499
  ruleCodes: (result.rules ?? []).map((rule) => rule.code),
@@ -533,15 +535,17 @@ export function registerModelLane(tools) {
533
535
  // r29a F6:dry-run 必须做与实跑同样的 duplicate 探测,否则会假绿灯。
534
536
  dryRunPayload: modelCreateDryRun,
535
537
  execute: async (session, args) => {
536
- const applicationCode = await resolveApplicationCode(session, args.applicationCode);
538
+ const applicationScope = await resolveValidatedApplicationScope(session, args.applicationCode);
539
+ const applicationCode = applicationScope.code;
537
540
  const existing = await resolveModelTreeNode(session, args.code, args.applicationCode);
538
541
  if (existing.node) {
539
542
  throw new ToolRefusalError({
540
543
  status: "refused",
541
544
  blocker: "model-create-duplicate",
542
545
  schemaCode: args.code,
543
- applicationCode: existing.applicationCode,
544
- message: `模型 ${args.code} 已存在于应用 ${existing.applicationCode} 的应用树中;create 拒绝重复 POST,不能把既有模型证明为本次创建结果`,
546
+ requestedApplicationCode: applicationCode,
547
+ conflictInApplicationCode: existing.applicationCode,
548
+ message: `模型 code 在租户内全局唯一,跨应用同名即冲突:模型 ${args.code} 已存在于应用 ${existing.applicationCode};create 拒绝重复 POST`,
545
549
  });
546
550
  }
547
551
  // A tree-miss does NOT prove absence: an orphan mount (row exists in
@@ -1305,6 +1309,7 @@ export function registerModelLane(tools) {
1305
1309
  .optional()
1306
1310
  .describe("也可在本调用内配置默认列表视图(内部走 listview.configure);一次到「模型+字段+表单+列表」"),
1307
1311
  applicationCode: z.string().optional(),
1312
+ createOnly: z.boolean().optional().describe("true=模型已存在时拒绝,不执行字段合并"),
1308
1313
  },
1309
1314
  // CFG-naming/CFG-02 parity: refuse an invalid code before a plan
1310
1315
  // is minted (the executor's model.create/field.create would otherwise
@@ -1358,6 +1363,12 @@ export function registerModelLane(tools) {
1358
1363
  execute: async (session, args) => {
1359
1364
  const results = [];
1360
1365
  const entries = fieldEntriesOf(args);
1366
+ // r37 跨路接线(同扇门):field.create 单独调用已对关联字段目标写前拒
1367
+ // (relation-target-not-found),configure_model 的字段包不能是例外——
1368
+ // 否则混合包仍会落到裸 300010 把整个模型毒住。
1369
+ const relationCandidates = await modelSchemaCodes(session);
1370
+ for (const entry of entries)
1371
+ assertRelationTargetExists(entry, relationCandidates);
1361
1372
  const stop = (index, tool, error) => {
1362
1373
  const envelope = error instanceof ToolRefusalError
1363
1374
  ? Object.fromEntries(Object.entries(error.payload).filter(([key]) => key !== "status"))
@@ -1380,7 +1391,8 @@ export function registerModelLane(tools) {
1380
1391
  return stop(index, tool, error);
1381
1392
  }
1382
1393
  };
1383
- const applicationCode = await resolveApplicationCode(session, args.applicationCode);
1394
+ const applicationScope = await resolveValidatedApplicationScope(session, args.applicationCode);
1395
+ const applicationCode = applicationScope.code;
1384
1396
  const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
1385
1397
  const folder = args.folderId ? tree.find((row) => row.id === args.folderId) : tree.find((row) => row.type !== "BizModel" && typeof row.id === "string");
1386
1398
  if (!folder?.id)
@@ -1391,6 +1403,10 @@ export function registerModelLane(tools) {
1391
1403
  // or form step. The application tree is already in hand, so an existing
1392
1404
  // model is detected with no extra call: skip the create and continue.
1393
1405
  const existingModel = tree.find((row) => row.type === "BizModel" && row.code === args.code);
1406
+ if (existingModel && args.createOnly === true) {
1407
+ throw new ToolRefusalError({ status: "refused", blocker: "model-already-exists", requestedApplicationCode: applicationCode, conflictInApplicationCode: applicationCode, schemaCode: args.code, message: `模型 code 在租户内全局唯一,跨应用同名即冲突:${args.code} 已存在;createOnly=true 拒绝合并` });
1408
+ }
1409
+ const created = !existingModel;
1394
1410
  let failed;
1395
1411
  if (existingModel) {
1396
1412
  results.push({ step: "model.create", skipped: "model-exists", folderId: String(folder.id), readback: { present: true } });
@@ -1407,6 +1423,7 @@ export function registerModelLane(tools) {
1407
1423
  return failed;
1408
1424
  }
1409
1425
  const existingFields = new Map();
1426
+ const mergedFieldCodes = [];
1410
1427
  for (const isPublish of ["false", "true"]) {
1411
1428
  for (const row of wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.code, isPublish } })).data))
1412
1429
  existingFields.set(String(row.code ?? row.propertyCode ?? ""), row);
@@ -1417,8 +1434,10 @@ export function registerModelLane(tools) {
1417
1434
  failed = await run(results.length, "field.create", async () => {
1418
1435
  const existing = existingFields.get(entry.code);
1419
1436
  if (existing !== undefined) {
1420
- if (sameFieldChange(existing, input))
1437
+ if (sameFieldChange(existing, input)) {
1438
+ mergedFieldCodes.push(entry.code);
1421
1439
  return { step: "field.create", skipped: "field-exists", code: entry.code, readback: { present: true } };
1440
+ }
1422
1441
  throw new ToolRefusalError({
1423
1442
  status: "refused",
1424
1443
  blocker: "field-conflict",
@@ -1432,6 +1451,7 @@ export function registerModelLane(tools) {
1432
1451
  const after = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.code, isPublish: "false" } })).data);
1433
1452
  if (!after.some((row) => String(row.code ?? row.propertyCode) === entry.code))
1434
1453
  throw new ToolRefusalError({ status: "refused", blocker: "field-create-readback-failed", message: `field.create 后权威草稿字段未读到 ${entry.code}` });
1454
+ mergedFieldCodes.push(entry.code);
1435
1455
  return { step: "field.create", errcode: performed.errcode, code: entry.code, readback: { present: true } };
1436
1456
  });
1437
1457
  if (failed)
@@ -1484,6 +1504,8 @@ export function registerModelLane(tools) {
1484
1504
  const rows = wireRows(published.data);
1485
1505
  const publishedCodes = rows.map((row) => String(row.code ?? row.propertyCode ?? ""));
1486
1506
  const readback = {
1507
+ created,
1508
+ ...(created ? {} : { mergedChanges: { fields: mergedFieldCodes, nameIgnored: args.name !== String(existingModel?.name ?? args.name) } }),
1487
1509
  publishedCodes,
1488
1510
  ...(entries.length === 1
1489
1511
  ? { publishedFieldPresent: publishedCodes.includes(entries[0].code) }
@@ -78,7 +78,7 @@ export const ONLINE_JS_LANE_CONTRACT = Object.freeze({
78
78
  }),
79
79
  "onlinejs.compose": Object.freeze({
80
80
  scope: ONLINE_JS_SCOPE,
81
- description: `**本调用 = 改 JS + 立即发布整张表单(等同 form.publish,含布局与按钮)**:它把 source 写进 customScript,然后无条件下一次 bizsheet/publish 把整份草稿提拔成发布面(内部 force:true,即使表单此前已发布也会重推)。所以改完 JS **不需要**再单独 form.publish;反过来,form.publish 只提拔布局草稿、**不碰**在线 JS。要只改不发布必须显式传 publish:false。Compose the Online JS source as the customScript element into the form's draftHtmlJson layout array (then publish, promoting it verbatim to publishedHtmlJson). Core contract: preserve the platform template and only extend customScript.innerHTML — the six-element skeleton includes the ${PLATFORM_LIFECYCLE_WRAPPER_MARKER} lifecycle wrapper; the style, toolbar, template, beforeCustomScript, customScript, and afterCustomScript elements are kept or synthesized with platform defaults, while every layout/control element is preserved. customScript is rebuilt canonically as {tagName:'script', id:'customScript', innerHTML:<platform lifecycle wrapper containing source>, attrs:[]} because the designer requires an ARRAY attrs (e.attrs.some) and tagName, else the Online JS editor fails with t.some is not a function. Drives bizsheet/update + bizsheet/publish; publishedHtmlJson is read back verbatim and SHA-256-fingerprinted so the caller can verify byte equality. Read the EXISTING script first with onlinejs.get (decide replace vs INCREMENTAL from what is actually stored). Never overwrites the layout with a standalone script object or a partial [customScript]-only array (regression for issue 11's root cause). summary:true answers "did it land?" without any HTML/JS body (sha + byte counts + draftPublishedInSync); the default readback dedupes the composed draft and the script when they equal the published face, so the same script is never returned three times.`,
81
+ description: `**本调用 = 改 JS + 立即发布整张表单**(force:true 重推,不需要再 form.publish——它只提拔布局、不碰 JS)。publish:false 只写草稿;remove:true 或 source:"" 移除脚本。Compose the Online JS source as the customScript element into the form's draftHtmlJson layout array (then publish, promoting it verbatim to publishedHtmlJson). Core contract: preserve the platform template and only extend customScript.innerHTML — the six-element skeleton includes the ${PLATFORM_LIFECYCLE_WRAPPER_MARKER} lifecycle wrapper; the style, toolbar, template, beforeCustomScript, customScript, and afterCustomScript elements are kept or synthesized with platform defaults, while every layout/control element is preserved. customScript is rebuilt canonically as {tagName:'script', id:'customScript', innerHTML:<platform lifecycle wrapper containing source>, attrs:[]} because the designer requires an ARRAY attrs (e.attrs.some) and tagName, else the Online JS editor fails with t.some is not a function. Drives bizsheet/update + bizsheet/publish; publishedHtmlJson is read back verbatim and SHA-256-fingerprinted so the caller can verify byte equality. Read the EXISTING script first with onlinejs.get (decide replace vs INCREMENTAL from what is actually stored). Never overwrites the layout with a standalone script object or a partial [customScript]-only array (regression for issue 11's root cause). summary:true answers "did it land?" without any HTML/JS body (sha + byte counts + draftPublishedInSync); the default readback dedupes the composed draft and the script when they equal the published face, so the same script is never returned three times.`,
82
82
  }),
83
83
  }),
84
84
  });
@@ -77,7 +77,7 @@ export function registerOnlineJsLane(tools) {
77
77
  return {
78
78
  schemaCode: args.schemaCode,
79
79
  sheetCode,
80
- present: true,
80
+ present: draft.present || published.present,
81
81
  sheetPublished: sheet.published === true,
82
82
  draft: { ...draft, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
83
83
  published: { ...published, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
@@ -107,13 +107,19 @@ export function registerOnlineJsLane(tools) {
107
107
  // 的 sheetCode 都是 optional(省略 = schemaCode)。此前只有这里必填,
108
108
  // dry-run 与真写都拒——同一个 lane 两套约定,调用方要白撞一轮才知道。
109
109
  sheetCode: z.string().min(1).optional().describe("表单槽位码;省略时默认 = schemaCode(与 form.draft/form.publish 同口径)"),
110
- source: z.string().min(1).describe("在线 JS 代码。默认 INCREMENTAL:并入平台 (function(form){…}) 模板的既有脚本,模板与既有注册全部保留;配 replace:true 才是整段替换。脚本不得使用 export/import;事件名必须是平台事件(onLoad/onRendered/onValidate/onPreAction/onCustomAction/onActionDone/onPrintFormDataHandle/onPrintWorkflowLogDataHandle)"),
110
+ source: z.string().optional().describe("在线 JS 代码。默认 INCREMENTAL:并入平台 (function(form){…}) 模板的既有脚本,模板与既有注册全部保留;配 replace:true 才是整段替换。脚本不得使用 export/import;事件名必须是平台事件(onLoad/onRendered/onValidate/onPreAction/onCustomAction/onActionDone/onPrintFormDataHandle/onPrintWorkflowLogDataHandle)。与 remove:true 二选一;source 为空字符串也表示移除 customScript"),
111
111
  replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
112
- publish: z.boolean().optional(),
112
+ remove: z.boolean().optional().describe("true = 显式移除 customScript,使 onlinejs.get 返回 present:false;会写入并按 publish 设置发布。与 source 二选一"),
113
+ publish: z.boolean().optional().describe("默认 true:写草稿并推发布面;false = 只写草稿不推发布面"),
113
114
  summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
114
115
  },
115
116
  execute: async (session, args) => {
116
117
  const shouldPublish = args.publish !== false;
118
+ const removing = args.remove === true || args.source === "";
119
+ if (!removing && args.source === undefined)
120
+ throw new Error("必须提供 source,或显式传 remove:true");
121
+ if (args.remove === true && args.source !== undefined && args.source !== "")
122
+ throw new Error("remove:true 不能与非空 source 同时传入");
117
123
  // r28e F9:和同 lane 其它工具一致——省略 sheetCode 即模型码槽位。
118
124
  const targetSheetCode = args.sheetCode ?? args.schemaCode;
119
125
  // 1. Read the existing draftHtmlJson.
@@ -133,10 +139,13 @@ export function registerOnlineJsLane(tools) {
133
139
  // existing script and merge the caller's code into it; the platform
134
140
  // default (an empty script) is seeded with the lifecycle scaffold.
135
141
  const existingScript = String(existingElements.find((element) => element.id === "customScript")?.innerHTML ?? "");
136
- const composedScript = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
142
+ const composedScript = removing ? "" : composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
137
143
  // 3. Preserve the platform template read-modify-write: keep the
138
144
  // style/beforeCustomScript/afterCustomScript skeleton and every
139
145
  // layout/control element; customScript carries the MERGED script.
146
+ // Keep the platform's required six-element skeleton, but an empty
147
+ // customScript element is semantically absent (onlinejs.get reports
148
+ // present:false) and remains accepted by the renderer contract.
140
149
  const composedElements = composeLayoutElements(existingElements, composedScript);
141
150
  assertHtmlRender(JSON.stringify(composedElements), { schemaCode: args.schemaCode, sheetCode: targetSheetCode, face: "composedDraftHtmlJson" }, true);
142
151
  // 4. Stringify the COMPLETE element array (never partial).
@@ -176,7 +185,10 @@ export function registerOnlineJsLane(tools) {
176
185
  const publishedScript = readCustomScriptSource(publishedRaw).script;
177
186
  const customScriptSha256 = createHash("sha256").update(composedScript, "utf8").digest("hex");
178
187
  const draftPublishedInSync = publishedScript !== null && publishedScript === composedScript;
179
- const htmlRenderCompat = { draft: validateHtmlRender(composedDraftHtml, true), published: validateHtmlRender(publishedRaw, true) };
188
+ const htmlRenderCompat = {
189
+ draft: { ...validateHtmlRender(composedDraftHtml, true), note: "仅 HTML 结构不含 JS 运行" },
190
+ published: { ...validateHtmlRender(publishedRaw, true), note: "仅 HTML 结构不含 JS 运行" },
191
+ };
180
192
  // Live 2026-10-21 (customer): this readback shipped the same ~10KB script
181
193
  // three times (composedDraftHtmlJson + publishedHtmlJson + customScript),
182
194
  // ~45KB for a call whose only question was "did publish land?" — the
@@ -222,13 +234,18 @@ export function registerOnlineJsLane(tools) {
222
234
  customScriptMatchesPublished: draftPublishedInSync,
223
235
  customScriptMerged: args.replace !== true,
224
236
  htmlRenderCompat,
237
+ portalVerificationPlan: ["在门户打开该表单", "触发已绑定的在线 JS 事件", "检查事件产生的副作用字段或校验提示"],
225
238
  },
226
239
  };
227
240
  },
228
241
  preflight: // Refuse a source that cannot run in the script slot (export/import, unknown
229
242
  // form.on event) BEFORE the write permission gate. This is the exact
230
243
  // class of mistake that blanked a live form page with no harness signal.
231
- (args) => lintOnlineJsSource(args.source),
244
+ (args) => {
245
+ if (args.remove === true || args.source === "" || args.source === undefined)
246
+ return;
247
+ lintOnlineJsSource(args.source);
248
+ },
232
249
  dryRunPayload: // r23 F4:dry-run 必须走与真写相同的 compose 校验路径(含 replace/merge 的
233
250
  // wrapper 判定),否则 dry-run 过、真写拒——探针按 dry-run 绿灯写正式调用被拒。
234
251
  async (session, args) => {
@@ -238,12 +255,27 @@ export function registerOnlineJsLane(tools) {
238
255
  const compat = validateHtmlRender(raw);
239
256
  const elements = compat.ok ? parseLayoutElements(raw) : [];
240
257
  const existingScript = String(elements.find((element) => element.id === "customScript")?.innerHTML ?? "");
241
- const composed = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
258
+ const removing = args.remove === true || args.source === "";
259
+ if (!removing && args.source === undefined)
260
+ throw new Error("必须提供 source,或显式传 remove:true");
261
+ const composed = removing ? "" : composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
262
+ const composedBytes = Buffer.byteLength(composed, "utf8");
263
+ const previewSize = 200;
264
+ const composedPreview = composed.length <= previewSize * 2
265
+ ? composed
266
+ : `${composed.slice(0, previewSize)}…${composed.slice(-previewSize)}`;
242
267
  return {
243
268
  mode: args.replace === true ? "replace" : "merge",
269
+ publish: args.publish !== false,
244
270
  wouldChange: composed !== existingScript,
245
271
  composedSha256: createHash("sha256").update(composed, "utf8").digest("hex"),
246
- composedBytes: Buffer.byteLength(composed, "utf8"),
272
+ composedBytes,
273
+ composedPreview,
274
+ composedPreviewHeadBytes: Buffer.byteLength(composed.slice(0, previewSize), "utf8"),
275
+ composedPreviewTailBytes: Buffer.byteLength(composed.slice(-previewSize), "utf8"),
276
+ composedPreviewTotalBytes: composedBytes,
277
+ composedPreviewNote: "预览为头/尾各 200 个字符(短文本不截断);字节数以 UTF-8 提交串对账,未自动补换行或外壳",
278
+ summary: args.publish === false ? "只写草稿,不推发布面" : "写草稿并推发布面",
247
279
  hint: "dry-run 已执行与真写一致的 compose 校验;该结果即正式调用将落库的脚本形状",
248
280
  };
249
281
  },
@@ -267,7 +299,7 @@ export function registerOnlineJsLane(tools) {
267
299
  inverse: ({ before, args }) => {
268
300
  const source = before?.customScript;
269
301
  if (typeof source !== "string")
270
- return { kind: "irreversible", reason: "before 表单没有可识别的 customScript element,无法构造清空脚本的等价 compose 调用" };
302
+ return { kind: "calls", calls: [{ tool: "onlinejs.compose", args: { schemaCode: String(args.schemaCode), sheetCode: String(args.sheetCode ?? args.schemaCode), remove: true, publish: args.publish } }] };
271
303
  return { kind: "calls", calls: [{ tool: "onlinejs.compose", args: { schemaCode: String(args.schemaCode), sheetCode: String(args.sheetCode ?? args.schemaCode), source, replace: true, publish: args.publish } }] };
272
304
  },
273
305
  },