@stalfh233/omc-cli 0.6.7 → 0.6.10

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.
Files changed (67) hide show
  1. package/README.md +2 -1
  2. package/dist/approval-flow.js +5 -3
  3. package/dist/bizservice-config.js +26 -10
  4. package/dist/call.js +47 -0
  5. package/dist/capability-overview.js +47 -24
  6. package/dist/cli-contract.js +73 -26
  7. package/dist/cli-output.js +41 -2
  8. package/dist/cli-stdout.js +74 -0
  9. package/dist/cli.js +135 -52
  10. package/dist/command-transport.js +8 -1
  11. package/dist/config-store.js +17 -2
  12. package/dist/discovery.js +62 -3
  13. package/dist/field-families.js +35 -6
  14. package/dist/goal-routes.js +118 -11
  15. package/dist/human-summary.js +11 -0
  16. package/dist/lanes/app-contract.js +12 -5
  17. package/dist/lanes/app.js +221 -134
  18. package/dist/lanes/apply-changes.js +2 -1
  19. package/dist/lanes/b2-registration-contract.js +3 -3
  20. package/dist/lanes/b2-registration.js +890 -693
  21. package/dist/lanes/businessrule.js +75 -51
  22. package/dist/lanes/custom-page.js +166 -115
  23. package/dist/lanes/datarule-contract.js +2 -2
  24. package/dist/lanes/datarule.js +238 -154
  25. package/dist/lanes/dictionary.js +253 -110
  26. package/dist/lanes/environment.js +57 -9
  27. package/dist/lanes/field.js +160 -107
  28. package/dist/lanes/folder-contract.js +7 -3
  29. package/dist/lanes/folder.js +249 -118
  30. package/dist/lanes/form-contract.js +1 -1
  31. package/dist/lanes/form.js +188 -64
  32. package/dist/lanes/list-view.js +405 -369
  33. package/dist/lanes/meta.js +31 -6
  34. package/dist/lanes/model-contract.js +1 -1
  35. package/dist/lanes/model.js +610 -512
  36. package/dist/lanes/online-js-contract.js +1 -1
  37. package/dist/lanes/online-js.js +160 -146
  38. package/dist/lanes/organization.js +905 -640
  39. package/dist/lanes/programming.js +274 -136
  40. package/dist/lanes/report-contract.js +2 -2
  41. package/dist/lanes/report.js +354 -198
  42. package/dist/lanes/rule-lifecycle.js +294 -140
  43. package/dist/lanes/runtime-data-contract.js +4 -4
  44. package/dist/lanes/runtime-data.js +869 -745
  45. package/dist/lanes/workflow-contract.js +3 -3
  46. package/dist/lanes/workflow.js +732 -605
  47. package/dist/model-scaffold.js +26 -5
  48. package/dist/path-projection.js +196 -14
  49. package/dist/plan.js +25 -0
  50. package/dist/programming-workflow.js +55 -3
  51. package/dist/read-shapes.js +146 -0
  52. package/dist/read.js +40 -9
  53. package/dist/recipes/businessrule.js +74 -24
  54. package/dist/recipes/dictionary.js +7 -4
  55. package/dist/recipes/form.js +46 -0
  56. package/dist/recipes/runtime-data.js +3 -2
  57. package/dist/refusal-guidance.js +29 -5
  58. package/dist/rule-support.js +14 -1
  59. package/dist/rule-when.js +2 -2
  60. package/dist/tool-types.js +9 -3
  61. package/dist/version-family-gaps.js +101 -0
  62. package/dist/write.js +2 -1
  63. package/package.json +6 -4
  64. package/skills/omc-glossary.md +13 -0
  65. package/skills/omc-refusals.md +13 -5
  66. package/skills/omc-tool-selection.md +2 -1
  67. package/skills/omc-write-hazards.md +5 -2
@@ -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: `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 + 立即发布整张表单(等同 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.`,
82
82
  }),
83
83
  }),
84
84
  });
@@ -95,167 +95,181 @@ export function registerOnlineJsLane(tools) {
95
95
  compact: (result) => result,
96
96
  }),
97
97
  }));
98
- writeTool(tools, "onlinejs.compose", ONLINE_JS_LANE, ONLINE_JS_TOOLS["onlinejs.compose"].scope, ONLINE_JS_TOOLS["onlinejs.compose"].description, {
99
- schemaCode: z.string().min(1),
100
- sheetCode: z.string().min(1),
101
- source: z.string().min(1).describe("在线 JS 代码。默认 INCREMENTAL:并入平台 (function(form){…}) 模板的既有脚本,模板与既有注册全部保留;配 replace:true 才是整段替换。脚本不得使用 export/import;事件名必须是平台事件(onLoad/onRendered/onValidate/onPreAction/onCustomAction/onActionDone/onPrintFormDataHandle/onPrintWorkflowLogDataHandle)"),
102
- replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
103
- publish: z.boolean().optional(),
104
- summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
105
- }, async (session, args) => {
106
- const shouldPublish = args.publish !== false;
107
- // 1. Read the existing draftHtmlJson.
108
- const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode } });
109
- const existingSheet = (current.data ?? null);
110
- const existingRaw = typeof existingSheet?.draftHtmlJson === "string" ? existingSheet.draftHtmlJson : undefined;
111
- const existingCompat = validateHtmlRender(existingRaw);
112
- // A persisted legacy face may contain an object or malformed elements.
113
- // Do not pass any of it through: discard the unproven face and rebuild
114
- // only the canonical platform skeleton below.
115
- const existingElements = existingCompat.ok ? parseLayoutElements(existingRaw) : [];
116
- // 2. Script-level INCREMENTAL merge. The layout read-modify-write above
117
- // preserved the element skeleton, but the customScript innerHTML was
118
- // being replaced wholesale — that is how a bare `form.on(...)` (or an
119
- // `export function`) landed as the ENTIRE script, leaving `form`
120
- // undefined / a SyntaxError and rendering the form BLANK. Read the
121
- // existing script and merge the caller's code into it; the platform
122
- // default (an empty script) is seeded with the lifecycle scaffold.
123
- const existingScript = String(existingElements.find((element) => element.id === "customScript")?.innerHTML ?? "");
124
- const composedScript = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
125
- // 3. Preserve the platform template read-modify-write: keep the
126
- // style/beforeCustomScript/afterCustomScript skeleton and every
127
- // layout/control element; customScript carries the MERGED script.
128
- const composedElements = composeLayoutElements(existingElements, composedScript);
129
- assertHtmlRender(JSON.stringify(composedElements), { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "composedDraftHtmlJson" }, true);
130
- // 4. Stringify the COMPLETE element array (never partial).
131
- const composedDraftHtml = JSON.stringify(composedElements);
132
- // 5. bizsheet/update with the new draftHtmlJson (delegated to the
133
- // shared form.draft write body so view/attributes scaffolds stay
134
- // intact when present).
135
- await executeFormDraftWrite(session, {
136
- schemaCode: args.schemaCode,
137
- sheetCode: args.sheetCode,
138
- draftHtmlJson: composedDraftHtml,
139
- });
140
- // 6. publish promotes the FULL layout (including customScript). Use
141
- // force:true so compose re-promotes even when the form was
142
- // previously published — otherwise onlinejs.compose would
143
- // leave the stale publishedHtmlJson in place after the user
144
- // already published once.
145
- let published = false;
146
- if (shouldPublish) {
147
- const publishOutcome = await executeFormPublishWrite(session, {
98
+ writeTool({
99
+ tools,
100
+ name: "onlinejs.compose",
101
+ lane: ONLINE_JS_LANE,
102
+ scope: ONLINE_JS_TOOLS["onlinejs.compose"].scope,
103
+ description: ONLINE_JS_TOOLS["onlinejs.compose"].description,
104
+ inputSchema: {
105
+ schemaCode: z.string().min(1),
106
+ // r28e F9:本 lane 其它工具(form.draft/form.publish/form.get/onlinejs.get)
107
+ // 的 sheetCode 都是 optional(省略 = schemaCode)。此前只有这里必填,
108
+ // dry-run 与真写都拒——同一个 lane 两套约定,调用方要白撞一轮才知道。
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)"),
111
+ replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
112
+ publish: z.boolean().optional(),
113
+ summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
114
+ },
115
+ execute: async (session, args) => {
116
+ const shouldPublish = args.publish !== false;
117
+ // r28e F9:和同 lane 其它工具一致——省略 sheetCode 即模型码槽位。
118
+ const targetSheetCode = args.sheetCode ?? args.schemaCode;
119
+ // 1. Read the existing draftHtmlJson.
120
+ const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: targetSheetCode } });
121
+ const existingSheet = (current.data ?? null);
122
+ const existingRaw = typeof existingSheet?.draftHtmlJson === "string" ? existingSheet.draftHtmlJson : undefined;
123
+ const existingCompat = validateHtmlRender(existingRaw);
124
+ // A persisted legacy face may contain an object or malformed elements.
125
+ // Do not pass any of it through: discard the unproven face and rebuild
126
+ // only the canonical platform skeleton below.
127
+ const existingElements = existingCompat.ok ? parseLayoutElements(existingRaw) : [];
128
+ // 2. Script-level INCREMENTAL merge. The layout read-modify-write above
129
+ // preserved the element skeleton, but the customScript innerHTML was
130
+ // being replaced wholesale — that is how a bare `form.on(...)` (or an
131
+ // `export function`) landed as the ENTIRE script, leaving `form`
132
+ // undefined / a SyntaxError and rendering the form BLANK. Read the
133
+ // existing script and merge the caller's code into it; the platform
134
+ // default (an empty script) is seeded with the lifecycle scaffold.
135
+ const existingScript = String(existingElements.find((element) => element.id === "customScript")?.innerHTML ?? "");
136
+ const composedScript = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
137
+ // 3. Preserve the platform template read-modify-write: keep the
138
+ // style/beforeCustomScript/afterCustomScript skeleton and every
139
+ // layout/control element; customScript carries the MERGED script.
140
+ const composedElements = composeLayoutElements(existingElements, composedScript);
141
+ assertHtmlRender(JSON.stringify(composedElements), { schemaCode: args.schemaCode, sheetCode: targetSheetCode, face: "composedDraftHtmlJson" }, true);
142
+ // 4. Stringify the COMPLETE element array (never partial).
143
+ const composedDraftHtml = JSON.stringify(composedElements);
144
+ // 5. bizsheet/update with the new draftHtmlJson (delegated to the
145
+ // shared form.draft write body so view/attributes scaffolds stay
146
+ // intact when present).
147
+ await executeFormDraftWrite(session, {
148
148
  schemaCode: args.schemaCode,
149
- sheetCode: args.sheetCode,
150
- force: true,
149
+ sheetCode: targetSheetCode,
150
+ draftHtmlJson: composedDraftHtml,
151
151
  });
152
- published = publishOutcome.readback.published === true;
153
- }
154
- // 7. Readback publishedHtmlJson VERBATIM.
155
- const readback = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode } });
156
- const publishedSheet = (readback.data ?? {});
157
- const publishedRaw = typeof publishedSheet.publishedHtmlJson === "string" ? publishedSheet.publishedHtmlJson : "";
158
- if (publishedRaw !== "")
159
- assertHtmlRender(publishedRaw, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "publishedHtmlJson" }, true);
160
- const publishedElements = parseLayoutElements(publishedRaw);
161
- // 8. SHA-256 of the verbatim content.
162
- const publishedSha256 = createHash("sha256").update(publishedRaw).digest("hex");
163
- const draftSha256 = createHash("sha256").update(composedDraftHtml).digest("hex");
164
- const publishedScript = readCustomScriptSource(publishedRaw).script;
165
- const customScriptSha256 = createHash("sha256").update(composedScript, "utf8").digest("hex");
166
- const draftPublishedInSync = publishedScript !== null && publishedScript === composedScript;
167
- const htmlRenderCompat = { draft: validateHtmlRender(composedDraftHtml, true), published: validateHtmlRender(publishedRaw, true) };
168
- // Live 2026-10-21 (customer): this readback shipped the same ~10KB script
169
- // three times (composedDraftHtmlJson + publishedHtmlJson + customScript),
170
- // ~45KB for a call whose only question was "did publish land?" — the
171
- // session's single largest context amplification. Answer that question
172
- // directly and keep 原文 opt-in.
173
- if (args.summary === true) {
152
+ // 6. publish promotes the FULL layout (including customScript). Use
153
+ // force:true so compose re-promotes even when the form was
154
+ // previously published — otherwise onlinejs.compose would
155
+ // leave the stale publishedHtmlJson in place after the user
156
+ // already published once.
157
+ let published = false;
158
+ if (shouldPublish) {
159
+ const publishOutcome = await executeFormPublishWrite(session, {
160
+ schemaCode: args.schemaCode,
161
+ sheetCode: targetSheetCode,
162
+ force: true,
163
+ });
164
+ published = publishOutcome.readback.published === true;
165
+ }
166
+ // 7. Readback publishedHtmlJson VERBATIM.
167
+ const readback = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: targetSheetCode } });
168
+ const publishedSheet = (readback.data ?? {});
169
+ const publishedRaw = typeof publishedSheet.publishedHtmlJson === "string" ? publishedSheet.publishedHtmlJson : "";
170
+ if (publishedRaw !== "")
171
+ assertHtmlRender(publishedRaw, { schemaCode: args.schemaCode, sheetCode: targetSheetCode, face: "publishedHtmlJson" }, true);
172
+ const publishedElements = parseLayoutElements(publishedRaw);
173
+ // 8. SHA-256 of the verbatim content.
174
+ const publishedSha256 = createHash("sha256").update(publishedRaw).digest("hex");
175
+ const draftSha256 = createHash("sha256").update(composedDraftHtml).digest("hex");
176
+ const publishedScript = readCustomScriptSource(publishedRaw).script;
177
+ const customScriptSha256 = createHash("sha256").update(composedScript, "utf8").digest("hex");
178
+ const draftPublishedInSync = publishedScript !== null && publishedScript === composedScript;
179
+ const htmlRenderCompat = { draft: validateHtmlRender(composedDraftHtml, true), published: validateHtmlRender(publishedRaw, true) };
180
+ // Live 2026-10-21 (customer): this readback shipped the same ~10KB script
181
+ // three times (composedDraftHtmlJson + publishedHtmlJson + customScript),
182
+ // ~45KB for a call whose only question was "did publish land?" — the
183
+ // session's single largest context amplification. Answer that question
184
+ // directly and keep 原文 opt-in.
185
+ if (args.summary === true) {
186
+ return {
187
+ performed: { errcode: 0, published },
188
+ readback: {
189
+ publishedSha256,
190
+ draftSha256,
191
+ draftPublishedInSync,
192
+ publishedElementCount: publishedElements.length,
193
+ scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
194
+ customScriptMerged: args.replace !== true,
195
+ customScriptSha256,
196
+ customScriptBytes: Buffer.byteLength(composedScript, "utf8"),
197
+ composedDraftBytes: Buffer.byteLength(composedDraftHtml, "utf8"),
198
+ publishedHtmlBytes: Buffer.byteLength(publishedRaw, "utf8"),
199
+ htmlRenderCompat,
200
+ hint: "summary 模式不回原文;要脚本原文用 onlinejs.get,要完整布局用 artifact 或 form.get --full",
201
+ },
202
+ };
203
+ }
174
204
  return {
175
205
  performed: { errcode: 0, published },
176
206
  readback: {
207
+ // Dedupe: after a successful publish the draft and the published
208
+ // face are the same bytes, and the script also rides inside them.
209
+ // Return the composed payload ONCE (composedDraftHtmlJson keeps the
210
+ // field every consumer already reads) and state the equality instead
211
+ // of repeating ~11KB + ~10KB.
212
+ ...(composedDraftHtml === publishedRaw
213
+ ? { draftEqualsPublished: true }
214
+ : { publishedHtmlJson: publishedRaw }),
215
+ composedDraftHtmlJson: composedDraftHtml,
177
216
  publishedSha256,
178
217
  draftSha256,
179
218
  draftPublishedInSync,
180
219
  publishedElementCount: publishedElements.length,
181
220
  scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
221
+ customScript: composedScript,
222
+ customScriptMatchesPublished: draftPublishedInSync,
182
223
  customScriptMerged: args.replace !== true,
183
- customScriptSha256,
184
- customScriptBytes: Buffer.byteLength(composedScript, "utf8"),
185
- composedDraftBytes: Buffer.byteLength(composedDraftHtml, "utf8"),
186
- publishedHtmlBytes: Buffer.byteLength(publishedRaw, "utf8"),
187
224
  htmlRenderCompat,
188
- hint: "summary 模式不回原文;要脚本原文用 onlinejs.get,要完整布局用 artifact 或 form.get --full",
189
225
  },
190
226
  };
191
- }
192
- return {
193
- performed: { errcode: 0, published },
194
- readback: {
195
- // Dedupe: after a successful publish the draft and the published
196
- // face are the same bytes, and the script also rides inside them.
197
- // Return the composed payload ONCE (composedDraftHtmlJson keeps the
198
- // field every consumer already reads) and state the equality instead
199
- // of repeating ~11KB + ~10KB.
200
- ...(composedDraftHtml === publishedRaw
201
- ? { draftEqualsPublished: true }
202
- : { publishedHtmlJson: publishedRaw }),
203
- composedDraftHtmlJson: composedDraftHtml,
204
- publishedSha256,
205
- draftSha256,
206
- draftPublishedInSync,
207
- publishedElementCount: publishedElements.length,
208
- scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
209
- customScript: composedScript,
210
- customScriptMatchesPublished: draftPublishedInSync,
211
- customScriptMerged: args.replace !== true,
212
- htmlRenderCompat,
213
- },
214
- };
215
- },
216
- // Refuse a source that cannot run in the script slot (export/import, unknown
217
- // form.on event) BEFORE the write permission gate. This is the exact
218
- // class of mistake that blanked a live form page with no harness signal.
219
- (args) => lintOnlineJsSource(args.source),
220
- // r23 F4:dry-run 必须走与真写相同的 compose 校验路径(含 replace/merge 的
221
- // wrapper 判定),否则 dry-run 过、真写拒——探针按 dry-run 绿灯写正式调用被拒。
222
- async (session, args) => {
223
- const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode ?? args.schemaCode } });
224
- const sheet = (current.data ?? null);
225
- const raw = typeof sheet?.draftHtmlJson === "string" ? sheet.draftHtmlJson : "";
226
- const compat = validateHtmlRender(raw);
227
- const elements = compat.ok ? parseLayoutElements(raw) : [];
228
- const existingScript = String(elements.find((element) => element.id === "customScript")?.innerHTML ?? "");
229
- const composed = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
230
- return {
231
- mode: args.replace === true ? "replace" : "merge",
232
- wouldChange: composed !== existingScript,
233
- composedSha256: createHash("sha256").update(composed, "utf8").digest("hex"),
234
- composedBytes: Buffer.byteLength(composed, "utf8"),
235
- hint: "dry-run 已执行与真写一致的 compose 校验;该结果即正式调用将落库的脚本形状",
236
- };
237
- }, {
238
- target: (args) => `onlinejs:${String(args.schemaCode ?? "")}/${String(args.sheetCode ?? args.schemaCode ?? "")}`,
239
- read: async (session, args) => {
240
- const sheet = await readFormOrNull(session, String(args.schemaCode), String(args.sheetCode ?? args.schemaCode));
241
- if (!sheet)
242
- return null;
243
- const raw = typeof sheet.draftHtmlJson === "string" ? sheet.draftHtmlJson : "";
244
- let elements = [];
245
- try {
246
- const parsed = JSON.parse(raw);
247
- if (Array.isArray(parsed))
248
- elements = parsed;
249
- }
250
- catch { }
251
- const script = elements.find((element) => element && typeof element === "object" && element.id === "customScript");
252
- return { customScript: typeof script?.innerHTML === "string" ? script.innerHTML : null };
253
227
  },
254
- inverse: ({ before, args }) => {
255
- const source = before?.customScript;
256
- if (typeof source !== "string")
257
- return { kind: "irreversible", reason: "before 表单没有可识别的 customScript element,无法构造清空脚本的等价 compose 调用" };
258
- return { kind: "calls", calls: [{ tool: "onlinejs.compose", args: { schemaCode: String(args.schemaCode), sheetCode: String(args.sheetCode ?? args.schemaCode), source, replace: true, publish: args.publish } }] };
228
+ preflight: // Refuse a source that cannot run in the script slot (export/import, unknown
229
+ // form.on event) BEFORE the write permission gate. This is the exact
230
+ // class of mistake that blanked a live form page with no harness signal.
231
+ (args) => lintOnlineJsSource(args.source),
232
+ dryRunPayload: // r23 F4:dry-run 必须走与真写相同的 compose 校验路径(含 replace/merge 的
233
+ // wrapper 判定),否则 dry-run 过、真写拒——探针按 dry-run 绿灯写正式调用被拒。
234
+ async (session, args) => {
235
+ const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode ?? args.schemaCode } });
236
+ const sheet = (current.data ?? null);
237
+ const raw = typeof sheet?.draftHtmlJson === "string" ? sheet.draftHtmlJson : "";
238
+ const compat = validateHtmlRender(raw);
239
+ const elements = compat.ok ? parseLayoutElements(raw) : [];
240
+ const existingScript = String(elements.find((element) => element.id === "customScript")?.innerHTML ?? "");
241
+ const composed = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
242
+ return {
243
+ mode: args.replace === true ? "replace" : "merge",
244
+ wouldChange: composed !== existingScript,
245
+ composedSha256: createHash("sha256").update(composed, "utf8").digest("hex"),
246
+ composedBytes: Buffer.byteLength(composed, "utf8"),
247
+ hint: "dry-run 已执行与真写一致的 compose 校验;该结果即正式调用将落库的脚本形状",
248
+ };
249
+ },
250
+ rollback: {
251
+ target: (args) => `onlinejs:${String(args.schemaCode ?? "")}/${String(args.sheetCode ?? args.schemaCode ?? "")}`,
252
+ read: async (session, args) => {
253
+ const sheet = await readFormOrNull(session, String(args.schemaCode), String(args.sheetCode ?? args.schemaCode));
254
+ if (!sheet)
255
+ return null;
256
+ const raw = typeof sheet.draftHtmlJson === "string" ? sheet.draftHtmlJson : "";
257
+ let elements = [];
258
+ try {
259
+ const parsed = JSON.parse(raw);
260
+ if (Array.isArray(parsed))
261
+ elements = parsed;
262
+ }
263
+ catch { }
264
+ const script = elements.find((element) => element && typeof element === "object" && element.id === "customScript");
265
+ return { customScript: typeof script?.innerHTML === "string" ? script.innerHTML : null };
266
+ },
267
+ inverse: ({ before, args }) => {
268
+ const source = before?.customScript;
269
+ if (typeof source !== "string")
270
+ return { kind: "irreversible", reason: "before 表单没有可识别的 customScript element,无法构造清空脚本的等价 compose 调用" };
271
+ 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
+ },
259
273
  },
260
274
  });
261
275
  }