@stalfh233/omc-cli 0.6.7 → 0.6.11

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
@@ -289,10 +289,13 @@ export function registerListViewLane(tools) {
289
289
  code,
290
290
  present: true,
291
291
  name: model.name ?? null,
292
- // r18 retro (G5): the placeholder flag misleads ("is my view
293
- // inactive?"). Views are ALWAYS-LIVE on save; keep the legacy key
294
- // null-ed for shape compatibility and name the lifecycle instead.
295
- published: null,
292
+ // r27 F5 / r18 G5: list views have no draft/publish lifecycle —
293
+ // save IS live. Previously `published:null` meant both "unpublished"
294
+ // and "N/A", so probes had to infer from `lifecycle`. Give an
295
+ // explicit published:true (save == effective; no separate publish
296
+ // step) plus effective:true so "does this view open?" is one field.
297
+ published: true, // 视图保存即生效,无需单独发布;不再用 null 兼表「未发布/不适用」
298
+ effective: true,
296
299
  lifecycle: "always-live",
297
300
  presentationType: model.queryPresentationType ?? null,
298
301
  columnCount: columns.length,
@@ -445,381 +448,414 @@ export function registerListViewLane(tools) {
445
448
  };
446
449
  }),
447
450
  }));
448
- writeTool(tools, "listview.create", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.create"].scope, LIST_VIEW_TOOLS["listview.create"].description, { schemaCode: z.string().min(1), name: z.string().min(1), code: z.string().optional() }, async (session, args) => {
449
- // Ticket 81: read the header FIRST — a same-code retry short-circuits
450
- // with the existing view and sends ZERO create wires (the live
451
- // post-timeout retry otherwise answers 303002 编码重复).
452
- const existing = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
453
- if (existing.data != null) {
454
- return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existing.data } };
455
- }
456
- try {
457
- const performed = await wireCall(session, {
458
- path: ENDPOINTS.listQueryCreate,
459
- method: "POST",
460
- body: { schemaCode: args.schemaCode, name: args.name, code: args.code, type: "LIST", showOnPc: false, published: false },
461
- });
462
- const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
463
- return { performed: { errcode: performed.errcode }, readback: { header: readback.data } };
464
- }
465
- catch (error) {
466
- // Race path: another writer created the view between the pre-check and
467
- // the create — the platform's 303002 is absence of work, not failure.
468
- if (!(error instanceof WireError && error.kind === "business" && error.errcode === 303002))
469
- throw error;
470
- const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
471
- if (!header.data)
472
- throw error;
473
- return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: header.data } };
474
- }
475
- }, undefined, undefined, listviewRollbackSpec("listview.create"));
476
- writeTool(tools, "listview.configure", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.configure"].scope, LIST_VIEW_TOOLS["listview.configure"].description, {
477
- schemaCode: z.string().min(1),
478
- code: z.string().optional(),
479
- fieldCodes: z.array(z.string()).min(1).describe("要展示的字段:可给 propertyCode 或字段显示名(唯一匹配时自动解析,歧义或不存在则拒绝,不静默跳过)"),
480
- hideDefaultSystemFields: z.boolean().optional().describe("是否隐藏平台默认系统列(createdTime/creater/name/sequenceNo 等);平台 update 按提交列集合决定,不能靠 visible:false"),
481
- replaceColumns: z.boolean().optional().describe("把 fieldCodes 当作列集合的权威全集:未列出的已有列(含系统列)全部移除,最终列 = fieldCodes(按给定顺序)。默认 false 为增量(保留已有列)。系统列与业务列一视同仁——想要 createdTime/sequenceNo 就把它写进 fieldCodes"),
482
- sorts: z.array(listViewSortArg).optional().describe("querySorts; omitted leaves the existing sorts untouched, provided replaces them"),
483
- conditions: z.array(listViewConditionArg).optional().describe("queryConditions; omitted leaves the existing conditions untouched, provided replaces them"),
484
- actions: z.array(listViewActionArg).optional().describe("queryActions; omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (existing platform defaults like 新增/删除/导入/导出 are preserved, a matching actionCode is updated). Use replaceActions:true for the authoritative full set"),
485
- replaceActions: z.boolean().optional().describe("把 actions 当作权威全集:未列出的已有动作(含平台内置的 add/删除/导入/导出/修改拥有者)全部移除。默认 false 为合并(保留内置动作,按 actionCode 更新/追加)"),
486
- defaultActions: z.boolean().optional().describe("先并入平台对 PC LIST 视图的默认动作集(add/delete/batch_update/batch_submit/import/export/batch_print/qr_code/editowner/import_attachment/export_attachment,常用于把被替换掉的内置按钮找回),再叠加 actions"),
487
- presentation: listViewPresentationArg.optional(),
488
- buttons: z
489
- .array(listViewButtonArg)
490
- .optional()
491
- .describe("customButtons; sent as the customButtonJson JSON array string; omitted leaves the existing buttons untouched, provided replaces every button under the QUERY trigger (schema + child tables)"),
492
- buttonScripts: z
493
- .array(z.object({ code: z.string().min(1), script: z.string() }))
494
- .optional()
495
- .describe("直接改按钮脚本:按 code 把 actionConfig.scriptData 换成 script(不改其它按钮/字段)。内部读回现有按钮再回写,因此不必先 listview.get 再整表重发;与 buttons 互斥"),
496
- options: listViewOptionsArg.optional(),
497
- presentationType: presentationTypeArg.optional(),
498
- gantt: listViewGanttArg.optional(),
499
- queryLayer: listViewQueryLayerArg.optional(),
500
- multiTableHeaderConfig: listViewMultiTableHeaderArg.optional(),
501
- }, async (session, args) => {
502
- const code = args.code ?? args.schemaCode;
503
- const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
504
- const model = (got.data ?? {});
505
- if (!model.id)
506
- throw new WireError("business", `list view ${code} not found on ${args.schemaCode}`, 302025);
507
- const columns = Array.isArray(model.queryColumns) ? ([...model.queryColumns]) : [];
508
- const fieldRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
509
- let sortKey = columns.reduce((max, c) => Math.max(max, Number(c.sortKey) || 0), 0);
510
- const added = [];
511
- const updated = [];
512
- // A `fieldCodes` entry may be a propertyCode OR a display name (cpdocs
513
- // base3.0 parity); an unresolvable reference is REFUSED, never silently
514
- // skipped — a silently dropped column reported as success is the
515
- // "declared a column, nothing appeared" trap.
516
- const unknownRefs = args.fieldCodes.filter((ref) => resolveFieldRef(fieldRows, ref) === undefined);
517
- if (unknownRefs.length > 0)
518
- throw new ToolRefusalError({
519
- status: "refused",
520
- blocker: "listview-columns-unknown-field",
521
- unknown: unknownRefs,
522
- message: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段(可给 propertyCode 或字段名):${unknownRefs.join("、")};先 field.create + field.publish,或修正引用`,
523
- });
524
- // replaceColumns: the caller's fieldCodes ARE the view. Start from an
525
- // empty set so system columns fall away unless listed — a system field
526
- // can still be shown by naming its code in fieldCodes (createdTime etc.
527
- // are published fields too), so the caller controls the exact set.
528
- if (args.replaceColumns === true)
529
- columns.length = 0;
530
- for (const ref of args.fieldCodes) {
531
- const fieldCode = resolveFieldRef(fieldRows, ref).code;
532
- const field = fieldRows.find((row) => (row.code ?? row.propertyCode) === fieldCode);
533
- const type = Number(field.type ?? field.propertyType);
534
- const displayFormat = columnDisplayFormat(type, field.options);
535
- const existing = columns.find((c) => c.propertyCode === fieldCode);
536
- if (existing) {
537
- existing.name = field.name;
538
- existing.propertyType = type;
539
- existing.displayFormat = displayFormat;
540
- existing.visible = true;
541
- updated.push(fieldCode);
542
- continue;
451
+ writeTool({
452
+ tools,
453
+ name: "listview.create",
454
+ lane: LIST_VIEW_LANE,
455
+ scope: LIST_VIEW_TOOLS["listview.create"].scope,
456
+ description: LIST_VIEW_TOOLS["listview.create"].description,
457
+ inputSchema: { schemaCode: z.string().min(1), name: z.string().min(1), code: z.string().optional().describe("视图 code;省略时**默认取 schemaCode**(与 listview.get / listview.list / rollback 的默认视图口径一致)。平台侧 code 必填,本工具会替你填,不要再依赖不回执的猜测(r26 真机:不传直接被 errcode 10005「编码不能为空」拒绝,白跑一轮线上往返)") },
458
+ execute: async (session, args) => {
459
+ // r26 真机:`code` 在 schema 里可选,但三处读面/rollback 都用
460
+ // `args.code ?? schemaCode`,唯独 create 把 undefined 原样发给平台 →
461
+ // `errcode 10005 编码不能为空`,探针白跑一轮。默认视图 code 就是
462
+ // schemaCode(listview.list 的 hint 也这么写),这里对齐。
463
+ const code = args.code ?? args.schemaCode;
464
+ // Ticket 81: read the header FIRST — a same-code retry short-circuits
465
+ // with the existing view and sends ZERO create wires (the live
466
+ // post-timeout retry otherwise answers 303002 编码重复).
467
+ const existing = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
468
+ if (existing.data != null) {
469
+ return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existing.data } };
543
470
  }
544
- sortKey += 1;
545
- columns.push({ name: field.name, propertyCode: fieldCode, propertyType: type, isDefaultProperty: false, displayFormat, width: "160", sortKey, sumType: null, relativeSchemaCode: null, childColumns: null, clientType: "PC", schemaAlias: "a_1", propertyAlias: fieldCode, syncDefaultFormat: false, displayFields: null, queryLevel: 1, options: null, visible: true, virtualField: false, enableEncrypt: false, autoFilled: false });
546
- added.push(fieldCode);
547
- }
548
- // replaceColumns already reduced the set to fieldCodes; hideDefaultSystemFields
549
- // additionally drops any system column that slipped in (an echoed system
550
- // column in incremental mode, or one the caller listed only to name its
551
- // display). Both act by OMITTING columns from the submitted PC collection:
552
- // the platform forces every submitted column visible=true
553
- // (BizQueryController.reSetQueryColumns:1000-1027), so visible:false is
554
- // not an option.
555
- const configuredColumns = filterDefaultSystemColumns(columns, args.hideDefaultSystemFields === true);
556
- const body = { ...model, queryColumns: configuredColumns };
557
- if (args.presentation !== undefined)
558
- body.queryPresentation = normalizePresentation(args.presentation);
559
- if (args.sorts !== undefined)
560
- body.querySorts = normalizeSorts(args.sorts, fieldRows, args.schemaCode);
561
- // Keep the normalized requested conditions so the post-write readback can
562
- // compare what was ASKED for against what the platform stored, rather than
563
- // reporting a write-side `ok` that silently lost fields (dogfood R2).
564
- let requestedConditionKeys;
565
- let requestedConditionSpec;
566
- if (args.conditions !== undefined) {
567
- const normalizedConditions = normalizeConditions(args.conditions, fieldRows, args.schemaCode);
568
- body.queryConditions = normalizedConditions;
569
- requestedConditionKeys = normalizedConditions.map((row) => String(row.propertyCode ?? ""));
570
- requestedConditionSpec = normalizedConditions.map((row) => ({ propertyCode: String(row.propertyCode ?? ""), filterType: row.filterType }));
571
- }
572
- // saveBizQueryDetail removes every action whose clientType matches the
573
- // update and re-adds the submitted list, so an omitted `actions` must echo
574
- // the read-face effective set (stored + platform-injected) rather than
575
- // sending an empty list, which would delete the view's actions.
576
- //
577
- // Live 2026-09-17: because it is replace-then-re-add, passing ONE action
578
- // used to delete the built-in 新增/删除/导入/导出 and the list rendered
579
- // with no Add button. `actions` now MERGES into the effective set by
580
- // actionCode; replaceActions:true restores the authoritative-set
581
- // behaviour, and defaultActions:true re-seeds the platform's own default
582
- // set (the repair path for a view that already lost its buttons).
583
- if (args.actions === undefined) {
584
- body.queryActions = readArray(model, "queryActions");
585
- }
586
- else {
587
- const normalized = normalizeActions(args.actions, args.schemaCode);
588
- if (args.replaceActions === true) {
589
- body.queryActions = normalized;
471
+ try {
472
+ const performed = await wireCall(session, {
473
+ path: ENDPOINTS.listQueryCreate,
474
+ method: "POST",
475
+ body: { schemaCode: args.schemaCode, name: args.name, code, type: "LIST", showOnPc: false, published: false },
476
+ });
477
+ const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
478
+ return { performed: { errcode: performed.errcode }, readback: { header: readback.data } };
590
479
  }
591
- else {
592
- const seeded = args.defaultActions === true
593
- ? normalizeActions(PLATFORM_DEFAULT_LIST_ACTIONS.map((action) => ({ ...action })), args.schemaCode)
594
- : [];
595
- body.queryActions = mergeActions([...seeded, ...readArray(model, "queryActions")], normalized);
480
+ catch (error) {
481
+ // Race path: another writer created the view between the pre-check and
482
+ // the create — the platform's 303002 is absence of work, not failure.
483
+ if (!(error instanceof WireError && error.kind === "business" && error.errcode === 303002))
484
+ throw error;
485
+ const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
486
+ if (!header.data)
487
+ throw error;
488
+ return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: header.data } };
596
489
  }
597
- }
598
- // BizButtonFacadeImpl.addBizButtonList runs only when the update body
599
- // carries a JSON array in customButtonJson; it then deletes every button
600
- // under the QUERY trigger for the schema (+ its child tables) and re-adds
601
- // the submitted list. An omitted `buttons` therefore leaves the stored
602
- // buttons untouched (no customButtonJson key), while `buttons: []` clears.
603
- // Ticket 75: high-level targetModel/targetForm/configureList params are
604
- // expanded into the authoritative actionConfig shape from readback
605
- // BEFORE normalizeButtons (targetCode/targetObjCode never emitted for
606
- // ADD_DATA/FORM — measured ineffective, see dry-lint).
607
- if (args.buttons !== undefined && args.buttonScripts !== undefined)
608
- throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-ambiguous", message: "buttons 与 buttonScripts 互斥:整表重配用 buttons,只改脚本用 buttonScripts" });
609
- if (args.buttonScripts !== undefined) {
610
- // Patch ONE field of ONE button in place: read the stored rows, rewrite
611
- // actionConfig.scriptData by code, and send the full list back. This is
612
- // the write counterpart of `listview.get --buttons`.
613
- const existing = readArray(model, "customButtons");
614
- if (existing.length === 0)
615
- throw new ToolRefusalError({ status: "refused", blocker: "listview-button-absent", schemaCode: args.schemaCode, message: `视图 ${code} 没有自定义按钮——先用 listview.configure 的 buttons 建按钮(bindAction:SCRIPT)` });
616
- const byCode = new Map(existing.map((button) => [String(button.code ?? ""), button]));
617
- const missing = args.buttonScripts.map((entry) => entry.code).filter((buttonCode) => !byCode.has(buttonCode));
618
- if (missing.length > 0)
490
+ },
491
+ rollback: listviewRollbackSpec("listview.create"),
492
+ });
493
+ writeTool({
494
+ tools,
495
+ name: "listview.configure",
496
+ lane: LIST_VIEW_LANE,
497
+ scope: LIST_VIEW_TOOLS["listview.configure"].scope,
498
+ description: LIST_VIEW_TOOLS["listview.configure"].description,
499
+ inputSchema: {
500
+ schemaCode: z.string().min(1),
501
+ code: z.string().optional(),
502
+ fieldCodes: z.array(z.string()).min(1).describe("要展示的字段:可给 propertyCode 或字段显示名(唯一匹配时自动解析,歧义或不存在则拒绝,不静默跳过)"),
503
+ hideDefaultSystemFields: z.boolean().optional().describe("是否隐藏平台默认系统列(createdTime/creater/name/sequenceNo 等);平台 update 按提交列集合决定,不能靠 visible:false"),
504
+ replaceColumns: z.boolean().optional().describe("把 fieldCodes 当作列集合的权威全集:未列出的已有列(含系统列)全部移除,最终列 = fieldCodes(按给定顺序)。默认 false 为增量(保留已有列)。系统列与业务列一视同仁——想要 createdTime/sequenceNo 就把它写进 fieldCodes"),
505
+ sorts: z.array(listViewSortArg).optional().describe("querySorts; omitted leaves the existing sorts untouched, provided replaces them"),
506
+ conditions: z.array(listViewConditionArg).optional().describe("queryConditions; omitted leaves the existing conditions untouched, provided replaces them"),
507
+ actions: z.array(listViewActionArg).optional().describe("queryActions; omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (existing platform defaults like 新增/删除/导入/导出 are preserved, a matching actionCode is updated). Use replaceActions:true for the authoritative full set"),
508
+ replaceActions: z.boolean().optional().describe("把 actions 当作权威全集:未列出的已有动作(含平台内置的 add/删除/导入/导出/修改拥有者)全部移除。默认 false 为合并(保留内置动作,按 actionCode 更新/追加)"),
509
+ defaultActions: z.boolean().optional().describe("先并入平台对 PC LIST 视图的默认动作集(add/delete/batch_update/batch_submit/import/export/batch_print/qr_code/editowner/import_attachment/export_attachment,常用于把被替换掉的内置按钮找回),再叠加 actions"),
510
+ presentation: listViewPresentationArg.optional(),
511
+ buttons: z
512
+ .array(listViewButtonArg)
513
+ .optional()
514
+ .describe("customButtons; sent as the customButtonJson JSON array string; omitted leaves the existing buttons untouched, provided replaces every button under the QUERY trigger (schema + child tables)"),
515
+ buttonScripts: z
516
+ .array(z.object({ code: z.string().min(1), script: z.string() }))
517
+ .optional()
518
+ .describe("直接改按钮脚本:按 code 把 actionConfig.scriptData 换成 script(不改其它按钮/字段)。内部读回现有按钮再回写,因此不必先 listview.get 再整表重发;与 buttons 互斥"),
519
+ options: listViewOptionsArg.optional(),
520
+ presentationType: presentationTypeArg.optional(),
521
+ gantt: listViewGanttArg.optional(),
522
+ queryLayer: listViewQueryLayerArg.optional(),
523
+ multiTableHeaderConfig: listViewMultiTableHeaderArg.optional(),
524
+ },
525
+ execute: async (session, args) => {
526
+ const code = args.code ?? args.schemaCode;
527
+ const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
528
+ const model = (got.data ?? {});
529
+ if (!model.id)
530
+ throw new WireError("business", `list view ${code} not found on ${args.schemaCode}`, 302025);
531
+ const columns = Array.isArray(model.queryColumns) ? ([...model.queryColumns]) : [];
532
+ const fieldRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
533
+ let sortKey = columns.reduce((max, c) => Math.max(max, Number(c.sortKey) || 0), 0);
534
+ const added = [];
535
+ const updated = [];
536
+ // A `fieldCodes` entry may be a propertyCode OR a display name (cpdocs
537
+ // base3.0 parity); an unresolvable reference is REFUSED, never silently
538
+ // skipped — a silently dropped column reported as success is the
539
+ // "declared a column, nothing appeared" trap.
540
+ const unknownRefs = args.fieldCodes.filter((ref) => resolveFieldRef(fieldRows, ref) === undefined);
541
+ if (unknownRefs.length > 0)
619
542
  throw new ToolRefusalError({
620
543
  status: "refused",
621
- blocker: "listview-button-absent",
622
- schemaCode: args.schemaCode,
623
- buttonCodes: missing,
624
- available: existing.map((button) => String(button.code ?? "")),
625
- message: `视图 ${code} 没有按钮 ${missing.join("、")};可用:${existing.map((button) => String(button.code ?? "")).join("、")}`,
544
+ blocker: "listview-columns-unknown-field",
545
+ unknown: unknownRefs,
546
+ message: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段(可给 propertyCode 或字段名):${unknownRefs.join("、")};先 field.create + field.publish,或修正引用`,
626
547
  });
627
- const patched = existing.map((button) => {
628
- const entry = args.buttonScripts.find((candidate) => candidate.code === String(button.code ?? ""));
629
- if (!entry)
630
- return button;
631
- const rawConfig = button.actionConfig;
632
- let config = {};
633
- if (typeof rawConfig === "string" && rawConfig.trim() !== "") {
634
- try {
635
- const parsed = JSON.parse(rawConfig);
636
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
637
- config = parsed;
638
- }
639
- catch {
640
- config = {};
641
- }
548
+ // replaceColumns: the caller's fieldCodes ARE the view. Start from an
549
+ // empty set so system columns fall away unless listed — a system field
550
+ // can still be shown by naming its code in fieldCodes (createdTime etc.
551
+ // are published fields too), so the caller controls the exact set.
552
+ if (args.replaceColumns === true)
553
+ columns.length = 0;
554
+ for (const ref of args.fieldCodes) {
555
+ const fieldCode = resolveFieldRef(fieldRows, ref).code;
556
+ const field = fieldRows.find((row) => (row.code ?? row.propertyCode) === fieldCode);
557
+ const type = Number(field.type ?? field.propertyType);
558
+ const displayFormat = columnDisplayFormat(type, field.options);
559
+ const existing = columns.find((c) => c.propertyCode === fieldCode);
560
+ if (existing) {
561
+ existing.name = field.name;
562
+ existing.propertyType = type;
563
+ existing.displayFormat = displayFormat;
564
+ existing.visible = true;
565
+ updated.push(fieldCode);
566
+ continue;
642
567
  }
643
- else if (rawConfig !== null && typeof rawConfig === "object" && !Array.isArray(rawConfig)) {
644
- config = { ...rawConfig };
568
+ sortKey += 1;
569
+ columns.push({ name: field.name, propertyCode: fieldCode, propertyType: type, isDefaultProperty: false, displayFormat, width: "160", sortKey, sumType: null, relativeSchemaCode: null, childColumns: null, clientType: "PC", schemaAlias: "a_1", propertyAlias: fieldCode, syncDefaultFormat: false, displayFields: null, queryLevel: 1, options: null, visible: true, virtualField: false, enableEncrypt: false, autoFilled: false });
570
+ added.push(fieldCode);
571
+ }
572
+ // replaceColumns already reduced the set to fieldCodes; hideDefaultSystemFields
573
+ // additionally drops any system column that slipped in (an echoed system
574
+ // column in incremental mode, or one the caller listed only to name its
575
+ // display). Both act by OMITTING columns from the submitted PC collection:
576
+ // the platform forces every submitted column visible=true
577
+ // (BizQueryController.reSetQueryColumns:1000-1027), so visible:false is
578
+ // not an option.
579
+ const configuredColumns = filterDefaultSystemColumns(columns, args.hideDefaultSystemFields === true);
580
+ const body = { ...model, queryColumns: configuredColumns };
581
+ if (args.presentation !== undefined)
582
+ body.queryPresentation = normalizePresentation(args.presentation);
583
+ if (args.sorts !== undefined)
584
+ body.querySorts = normalizeSorts(args.sorts, fieldRows, args.schemaCode);
585
+ // Keep the normalized requested conditions so the post-write readback can
586
+ // compare what was ASKED for against what the platform stored, rather than
587
+ // reporting a write-side `ok` that silently lost fields (dogfood R2).
588
+ let requestedConditionKeys;
589
+ let requestedConditionSpec;
590
+ if (args.conditions !== undefined) {
591
+ const normalizedConditions = normalizeConditions(args.conditions, fieldRows, args.schemaCode);
592
+ body.queryConditions = normalizedConditions;
593
+ requestedConditionKeys = normalizedConditions.map((row) => String(row.propertyCode ?? ""));
594
+ requestedConditionSpec = normalizedConditions.map((row) => ({ propertyCode: String(row.propertyCode ?? ""), filterType: row.filterType }));
595
+ }
596
+ // saveBizQueryDetail removes every action whose clientType matches the
597
+ // update and re-adds the submitted list, so an omitted `actions` must echo
598
+ // the read-face effective set (stored + platform-injected) rather than
599
+ // sending an empty list, which would delete the view's actions.
600
+ //
601
+ // Live 2026-09-17: because it is replace-then-re-add, passing ONE action
602
+ // used to delete the built-in 新增/删除/导入/导出 and the list rendered
603
+ // with no Add button. `actions` now MERGES into the effective set by
604
+ // actionCode; replaceActions:true restores the authoritative-set
605
+ // behaviour, and defaultActions:true re-seeds the platform's own default
606
+ // set (the repair path for a view that already lost its buttons).
607
+ if (args.actions === undefined) {
608
+ body.queryActions = readArray(model, "queryActions");
609
+ }
610
+ else {
611
+ const normalized = normalizeActions(args.actions, args.schemaCode);
612
+ if (args.replaceActions === true) {
613
+ body.queryActions = normalized;
645
614
  }
646
- return { ...button, actionConfig: encodeJsonColumn({ ...config, scriptData: entry.script }) };
647
- });
648
- body.customButtonJson = JSON.stringify(normalizeButtons(patched, args.schemaCode, code));
649
- }
650
- else if (args.buttons !== undefined) {
651
- const expandedButtons = await expandButtonActionConfigs(session, args.schemaCode, args.buttons);
652
- body.customButtonJson = JSON.stringify(normalizeButtons(expandedButtons, args.schemaCode, code));
653
- }
654
- else {
655
- delete body.customButtonJson;
656
- }
657
- // BizQueryModel.options is a single String column. BizQueryController.update
658
- // sets publish=TRUE before updateBizQuery, so after the first configure the
659
- // store's publish is TRUE and only the name/options-diff branch
660
- // (ModelManagementFacadeImpl:1966-1973) persists options — an omitted body
661
- // key (null) would overwrite a non-null stored value. `{...model}` already
662
- // echoes the read-face options, so only a provided value replaces it.
663
- if (args.options !== undefined)
664
- body.options = encodeJsonColumn(args.options);
665
- // QueryPresentationType is a header enum always assignable by
666
- // updateBizQuery (:1955, not gated), so an omitted body key (null) would
667
- // overwrite a stored type; `{...model}` echoes the read-face value. GANTT
668
- // requires queryGanttModel (controller validateNotEmptyGantt :1121-1136).
669
- if (args.presentationType !== undefined)
670
- body.queryPresentationType = args.presentationType;
671
- // saveBizQueryDetail only swaps queryGantt when non-null (:178-183), so an
672
- // omitted gantt leaves the stored one untouched.
673
- if (args.gantt !== undefined)
674
- body.queryGanttModel = normalizeGantt(args.gantt, args.schemaCode);
675
- // saveBizQueryDetail assigns queryLayer and multiTableHeaderConfig
676
- // unconditionally (:184-186); the read face always carries them
677
- // (BizQueryFacadeImpl:344-345), so an omitted arg relies on the
678
- // `{...model}` echo while a provided value replaces it.
679
- if (args.queryLayer !== undefined)
680
- body.queryLayer = args.queryLayer;
681
- if (args.multiTableHeaderConfig !== undefined) {
682
- body.multiTableHeaderConfig = encodeJsonColumn(args.multiTableHeaderConfig);
683
- }
684
- const performed = await wireCall(session, { path: ENDPOINTS.listQueryUpdate, method: "POST", body });
685
- // BUG-11 (live, docker+source verified): the controller's /query/update
686
- // forces publish=TRUE and the engine's updateBizQuery SKIPS the entity
687
- // update block once the STORED view is published
688
- // (ModelManagementFacadeImpl:1998-2010), so a body-driven
689
- // queryPresentationType is silently dropped for a published view (the
690
- // default view is published). The designer switches view type through
691
- // /query/update_header (a plain header update, no publish gate) — drive
692
- // the same path whenever presentationType is provided.
693
- if (args.presentationType !== undefined) {
694
- const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
695
- const headerModel = (header.data ?? {});
696
- await wireCall(session, {
697
- path: ENDPOINTS.listQueryUpdateHeader,
698
- method: "POST",
699
- body: { ...headerModel, code, schemaCode: args.schemaCode, queryPresentationType: args.presentationType },
700
- });
701
- }
702
- const after = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
703
- const afterModel = (after.data ?? {});
704
- const afterCols = readArray(afterModel, "queryColumns").map((c) => ({
705
- propertyCode: c.propertyCode,
706
- propertyType: c.propertyType,
707
- displayFormat: c.displayFormat ?? null,
708
- visible: c.visible ?? null,
709
- sumType: c.sumType ?? null,
710
- }));
711
- const sorts = readArray(afterModel, "querySorts")
712
- .map(decodeSortRow)
713
- .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
714
- const conditions = readArray(afterModel, "queryConditions")
715
- .map(decodeConditionRow)
716
- .sort((a, b) => String(a.propertyCode ?? "").localeCompare(String(b.propertyCode ?? "")));
717
- const readback = { code, added, updated, columns: afterCols, sorts, conditions };
718
- // r35(2026-10-31 真机):视图固定条件读回正确,但 `data.list --queryCode <视图>`
719
- // 不应用该过滤(4 条未过滤返回)——视图条件的运行时过滤语义未取证(门户 vs
720
- // REST queryCode),coverage 已声明 "filtering … not probed"。前置声明省 3 轮排查。
721
- if (conditions.length > 0)
722
- readback.conditionRuntimeNote =
723
- "视图条件的运行时过滤语义未取证:REST `data.list --queryCode <本视图>` 实测不应用固定条件(r35:读回 Gte/5000 正确、返回未过滤 4 条)。别用 queryCode 断言过滤生效;REST 侧过滤用 `data.list` 显式 `filter`。";
724
- // Post-write semantic readback (dogfood R2): the platform MODELS a list
725
- // condition as a filter CONTROL definition, so a requested condition whose
726
- // propertyCode does not survive the round-trip was lost. Surface that as
727
- // `conditionReadbackStatus` (not a business `ok`) so a caller never
728
- // mistakes the success envelope for the filter having landed.
729
- if (requestedConditionKeys !== undefined) {
730
- const storedKeys = new Set(conditions.map((condition) => String(condition.propertyCode ?? "")));
731
- const missingConditions = requestedConditionKeys.filter((key) => !storedKeys.has(key));
732
- // Also verify the OPERATOR (filterType) survived — a propertyCode that
733
- // lands with the WRONG comparison is just as wrong as one that is
734
- // dropped (dogfood R2 follow-up).
735
- const storedByCode = new Map(conditions.map((condition) => [String(condition.propertyCode ?? ""), condition]));
736
- const filterTypeMismatches = (requestedConditionSpec ?? [])
737
- .filter((spec) => storedByCode.has(spec.propertyCode))
738
- .filter((spec) => String(storedByCode.get(spec.propertyCode)?.filterType ?? "") !== String(spec.filterType ?? ""))
739
- .map((spec) => ({ propertyCode: spec.propertyCode, requested: spec.filterType ?? null, stored: storedByCode.get(spec.propertyCode)?.filterType ?? null }));
740
- const equivalent = missingConditions.length === 0 && filterTypeMismatches.length === 0;
741
- readback.conditionReadback = {
742
- status: equivalent ? "equivalent" : "listview-condition-readback-mismatch",
743
- requested: requestedConditionKeys,
744
- stored: [...storedKeys],
745
- // Operator-level summary so "operator + value" acceptance needs no
746
- // `--full`/artifact fetch (dogfood R2 round17b).
747
- operators: (requestedConditionSpec ?? []).map((spec) => ({
748
- propertyCode: spec.propertyCode,
749
- requested: spec.filterType ?? null,
750
- stored: storedByCode.get(spec.propertyCode)?.filterType ?? null,
751
- defaultValue: storedByCode.get(spec.propertyCode)?.defaultValue ?? null,
752
- })),
753
- ...(missingConditions.length > 0 ? { missing: missingConditions } : {}),
754
- ...(filterTypeMismatches.length > 0 ? { filterTypeMismatches } : {}),
755
- };
756
- }
757
- readback.columnsRenderCompat = columnsRenderCompat(readArray(afterModel, "queryColumns"));
758
- if (args.actions !== undefined) {
759
- readback.actions = readArray(afterModel, "queryActions")
760
- .map(decodeActionRow)
761
- .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
762
- }
763
- if (afterModel.queryPresentation && typeof afterModel.queryPresentation === "object") {
764
- readback.presentation = decodePresentation(afterModel.queryPresentation);
765
- }
766
- if (Array.isArray(afterModel.customButtons)) {
767
- const buttons = readArray(afterModel, "customButtons");
768
- readback.buttons = buttons
769
- .map(decodeButtonRow)
770
- .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
615
+ else {
616
+ const seeded = args.defaultActions === true
617
+ ? normalizeActions(PLATFORM_DEFAULT_LIST_ACTIONS.map((action) => ({ ...action })), args.schemaCode)
618
+ : [];
619
+ body.queryActions = mergeActions([...seeded, ...readArray(model, "queryActions")], normalized);
620
+ }
621
+ }
622
+ // BizButtonFacadeImpl.addBizButtonList runs only when the update body
623
+ // carries a JSON array in customButtonJson; it then deletes every button
624
+ // under the QUERY trigger for the schema (+ its child tables) and re-adds
625
+ // the submitted list. An omitted `buttons` therefore leaves the stored
626
+ // buttons untouched (no customButtonJson key), while `buttons: []` clears.
627
+ // Ticket 75: high-level targetModel/targetForm/configureList params are
628
+ // expanded into the authoritative actionConfig shape from readback
629
+ // BEFORE normalizeButtons (targetCode/targetObjCode never emitted for
630
+ // ADD_DATA/FORM — measured ineffective, see dry-lint).
631
+ if (args.buttons !== undefined && args.buttonScripts !== undefined)
632
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-ambiguous", message: "buttons 与 buttonScripts 互斥:整表重配用 buttons,只改脚本用 buttonScripts" });
771
633
  if (args.buttonScripts !== undefined) {
772
- const decoded = buttons.map((button) => decodeButtonScript(button));
773
- readback.buttonScripts = decoded
774
- .filter((button) => (args.buttonScripts ?? []).some((entry) => entry.code === button.code))
775
- .map((button) => ({ code: button.code, bindAction: button.bindAction, scriptBytes: button.scriptBytes ?? 0, scriptSha256: button.scriptSha256 ?? null, scriptData: button.scriptData ?? null }));
634
+ // Patch ONE field of ONE button in place: read the stored rows, rewrite
635
+ // actionConfig.scriptData by code, and send the full list back. This is
636
+ // the write counterpart of `listview.get --buttons`.
637
+ const existing = readArray(model, "customButtons");
638
+ if (existing.length === 0)
639
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-button-absent", schemaCode: args.schemaCode, message: `视图 ${code} 没有自定义按钮——先用 listview.configure 的 buttons 建按钮(bindAction:SCRIPT)` });
640
+ const byCode = new Map(existing.map((button) => [String(button.code ?? ""), button]));
641
+ const missing = args.buttonScripts.map((entry) => entry.code).filter((buttonCode) => !byCode.has(buttonCode));
642
+ if (missing.length > 0)
643
+ throw new ToolRefusalError({
644
+ status: "refused",
645
+ blocker: "listview-button-absent",
646
+ schemaCode: args.schemaCode,
647
+ buttonCodes: missing,
648
+ available: existing.map((button) => String(button.code ?? "")),
649
+ message: `视图 ${code} 没有按钮 ${missing.join("、")};可用:${existing.map((button) => String(button.code ?? "")).join("、")}`,
650
+ });
651
+ const patched = existing.map((button) => {
652
+ const entry = args.buttonScripts.find((candidate) => candidate.code === String(button.code ?? ""));
653
+ if (!entry)
654
+ return button;
655
+ const rawConfig = button.actionConfig;
656
+ let config = {};
657
+ if (typeof rawConfig === "string" && rawConfig.trim() !== "") {
658
+ try {
659
+ const parsed = JSON.parse(rawConfig);
660
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
661
+ config = parsed;
662
+ }
663
+ catch {
664
+ config = {};
665
+ }
666
+ }
667
+ else if (rawConfig !== null && typeof rawConfig === "object" && !Array.isArray(rawConfig)) {
668
+ config = { ...rawConfig };
669
+ }
670
+ return { ...button, actionConfig: encodeJsonColumn({ ...config, scriptData: entry.script }) };
671
+ });
672
+ body.customButtonJson = JSON.stringify(normalizeButtons(patched, args.schemaCode, code));
776
673
  }
777
- readback.buttonsRenderCompat = buttonsRenderCompat(buttons);
778
- }
779
- if (afterModel.options !== undefined && afterModel.options !== null) {
780
- readback.options = parseConditionOptions(afterModel.options);
781
- }
782
- if (afterModel.queryPresentationType !== undefined && afterModel.queryPresentationType !== null) {
783
- readback.presentationType = afterModel.queryPresentationType;
784
- }
785
- if (afterModel.queryGanttModel && typeof afterModel.queryGanttModel === "object") {
786
- readback.gantt = decodeGantt(afterModel.queryGanttModel);
787
- }
788
- if (afterModel.queryLayer !== undefined && afterModel.queryLayer !== null) {
789
- readback.queryLayer = afterModel.queryLayer;
790
- }
791
- if (afterModel.multiTableHeaderConfig !== undefined && afterModel.multiTableHeaderConfig !== null && afterModel.multiTableHeaderConfig !== "") {
792
- readback.multiTableHeaderConfig = parseConditionOptions(afterModel.multiTableHeaderConfig);
793
- }
794
- return { performed: { errcode: performed.errcode }, readback };
795
- }, assertListViewConfigureShape, undefined, listviewRollbackSpec("listview.configure"));
796
- writeTool(tools, "listview.delete", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.delete"].scope, LIST_VIEW_TOOLS["listview.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
797
- const before = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
798
- if (before.data == null)
799
- return { performed: { skipped: "already-absent" }, readback: { absent: true, header: null } };
800
- let performed;
801
- try {
802
- performed = await wireCall(session, { path: ENDPOINTS.listQueryDelete, method: "POST", query: { schemaCode: args.schemaCode, code: args.code } });
803
- }
804
- catch (error) {
805
- // 306103 删除后将无可用视图 — the platform requires at least one visible
806
- // list view per model. r15's `nextCommand` fell back to the generic
807
- // refusal router; name the constraint and the only real deletion path.
808
- if (error instanceof WireError && error.kind === "business" && (error.errcode === 306103 || /无可用视图/.test(error.message)))
809
- throw new ToolRefusalError({
810
- status: "refused",
811
- blocker: "listview-last-visible",
812
- schemaCode: args.schemaCode,
813
- code: args.code,
814
- nextCommand: `omc model-purge --code ${args.schemaCode}`,
815
- message: `不能删除视图 ${args.code}:平台要求每个模型至少保留一条可见列表视图(306103)。` +
816
- `单独删最后一条视图没有意义——视图会随模型一起级联删除。要继续用这个模型就**就地改**(listview.configure 省略 code 即改默认视图),` +
817
- `要整体清理就用 model-purge(默认 dry-run,先看计划)。`,
674
+ else if (args.buttons !== undefined) {
675
+ const expandedButtons = await expandButtonActionConfigs(session, args.schemaCode, args.buttons);
676
+ body.customButtonJson = JSON.stringify(normalizeButtons(expandedButtons, args.schemaCode, code));
677
+ }
678
+ else {
679
+ delete body.customButtonJson;
680
+ }
681
+ // BizQueryModel.options is a single String column. BizQueryController.update
682
+ // sets publish=TRUE before updateBizQuery, so after the first configure the
683
+ // store's publish is TRUE and only the name/options-diff branch
684
+ // (ModelManagementFacadeImpl:1966-1973) persists options — an omitted body
685
+ // key (null) would overwrite a non-null stored value. `{...model}` already
686
+ // echoes the read-face options, so only a provided value replaces it.
687
+ if (args.options !== undefined)
688
+ body.options = encodeJsonColumn(args.options);
689
+ // QueryPresentationType is a header enum always assignable by
690
+ // updateBizQuery (:1955, not gated), so an omitted body key (null) would
691
+ // overwrite a stored type; `{...model}` echoes the read-face value. GANTT
692
+ // requires queryGanttModel (controller validateNotEmptyGantt :1121-1136).
693
+ if (args.presentationType !== undefined)
694
+ body.queryPresentationType = args.presentationType;
695
+ // saveBizQueryDetail only swaps queryGantt when non-null (:178-183), so an
696
+ // omitted gantt leaves the stored one untouched.
697
+ if (args.gantt !== undefined)
698
+ body.queryGanttModel = normalizeGantt(args.gantt, args.schemaCode);
699
+ // saveBizQueryDetail assigns queryLayer and multiTableHeaderConfig
700
+ // unconditionally (:184-186); the read face always carries them
701
+ // (BizQueryFacadeImpl:344-345), so an omitted arg relies on the
702
+ // `{...model}` echo while a provided value replaces it.
703
+ if (args.queryLayer !== undefined)
704
+ body.queryLayer = args.queryLayer;
705
+ if (args.multiTableHeaderConfig !== undefined) {
706
+ body.multiTableHeaderConfig = encodeJsonColumn(args.multiTableHeaderConfig);
707
+ }
708
+ const performed = await wireCall(session, { path: ENDPOINTS.listQueryUpdate, method: "POST", body });
709
+ // BUG-11 (live, docker+source verified): the controller's /query/update
710
+ // forces publish=TRUE and the engine's updateBizQuery SKIPS the entity
711
+ // update block once the STORED view is published
712
+ // (ModelManagementFacadeImpl:1998-2010), so a body-driven
713
+ // queryPresentationType is silently dropped for a published view (the
714
+ // default view is published). The designer switches view type through
715
+ // /query/update_header (a plain header update, no publish gate) — drive
716
+ // the same path whenever presentationType is provided.
717
+ if (args.presentationType !== undefined) {
718
+ const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
719
+ const headerModel = (header.data ?? {});
720
+ await wireCall(session, {
721
+ path: ENDPOINTS.listQueryUpdateHeader,
722
+ method: "POST",
723
+ body: { ...headerModel, code, schemaCode: args.schemaCode, queryPresentationType: args.presentationType },
818
724
  });
819
- throw error;
820
- }
821
- const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
822
- return { performed: { errcode: performed.errcode }, readback: { absent: readback.data == null, header: readback.data ?? null } };
823
- }, undefined, undefined, listviewRollbackSpec("listview.delete"));
725
+ }
726
+ const after = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
727
+ const afterModel = (after.data ?? {});
728
+ const afterCols = readArray(afterModel, "queryColumns").map((c) => ({
729
+ propertyCode: c.propertyCode,
730
+ propertyType: c.propertyType,
731
+ displayFormat: c.displayFormat ?? null,
732
+ visible: c.visible ?? null,
733
+ sumType: c.sumType ?? null,
734
+ }));
735
+ const sorts = readArray(afterModel, "querySorts")
736
+ .map(decodeSortRow)
737
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
738
+ const conditions = readArray(afterModel, "queryConditions")
739
+ .map(decodeConditionRow)
740
+ .sort((a, b) => String(a.propertyCode ?? "").localeCompare(String(b.propertyCode ?? "")));
741
+ const readback = { code, added, updated, columns: afterCols, sorts, conditions };
742
+ // r35(2026-10-31 真机):视图固定条件读回正确,但 `data.list --queryCode <视图>`
743
+ // 不应用该过滤(4 条未过滤返回)——视图条件的运行时过滤语义未取证(门户 vs
744
+ // REST queryCode),coverage 已声明 "filtering … not probed"。前置声明省 3 轮排查。
745
+ if (conditions.length > 0)
746
+ readback.conditionRuntimeNote =
747
+ "视图条件的运行时过滤语义未取证:REST `data.list --queryCode <本视图>` 实测不应用固定条件(r35:读回 Gte/5000 正确、返回未过滤 4 条)。别用 queryCode 断言过滤生效;REST 侧过滤用 `data.list` 显式 `filter`。";
748
+ // Post-write semantic readback (dogfood R2): the platform MODELS a list
749
+ // condition as a filter CONTROL definition, so a requested condition whose
750
+ // propertyCode does not survive the round-trip was lost. Surface that as
751
+ // `conditionReadbackStatus` (not a business `ok`) so a caller never
752
+ // mistakes the success envelope for the filter having landed.
753
+ if (requestedConditionKeys !== undefined) {
754
+ const storedKeys = new Set(conditions.map((condition) => String(condition.propertyCode ?? "")));
755
+ const missingConditions = requestedConditionKeys.filter((key) => !storedKeys.has(key));
756
+ // Also verify the OPERATOR (filterType) survived — a propertyCode that
757
+ // lands with the WRONG comparison is just as wrong as one that is
758
+ // dropped (dogfood R2 follow-up).
759
+ const storedByCode = new Map(conditions.map((condition) => [String(condition.propertyCode ?? ""), condition]));
760
+ const filterTypeMismatches = (requestedConditionSpec ?? [])
761
+ .filter((spec) => storedByCode.has(spec.propertyCode))
762
+ .filter((spec) => String(storedByCode.get(spec.propertyCode)?.filterType ?? "") !== String(spec.filterType ?? ""))
763
+ .map((spec) => ({ propertyCode: spec.propertyCode, requested: spec.filterType ?? null, stored: storedByCode.get(spec.propertyCode)?.filterType ?? null }));
764
+ const equivalent = missingConditions.length === 0 && filterTypeMismatches.length === 0;
765
+ readback.conditionReadback = {
766
+ status: equivalent ? "equivalent" : "listview-condition-readback-mismatch",
767
+ requested: requestedConditionKeys,
768
+ stored: [...storedKeys],
769
+ // Operator-level summary so "operator + value" acceptance needs no
770
+ // `--full`/artifact fetch (dogfood R2 round17b).
771
+ operators: (requestedConditionSpec ?? []).map((spec) => ({
772
+ propertyCode: spec.propertyCode,
773
+ requested: spec.filterType ?? null,
774
+ stored: storedByCode.get(spec.propertyCode)?.filterType ?? null,
775
+ defaultValue: storedByCode.get(spec.propertyCode)?.defaultValue ?? null,
776
+ })),
777
+ ...(missingConditions.length > 0 ? { missing: missingConditions } : {}),
778
+ ...(filterTypeMismatches.length > 0 ? { filterTypeMismatches } : {}),
779
+ };
780
+ }
781
+ readback.columnsRenderCompat = columnsRenderCompat(readArray(afterModel, "queryColumns"));
782
+ if (args.actions !== undefined) {
783
+ readback.actions = readArray(afterModel, "queryActions")
784
+ .map(decodeActionRow)
785
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
786
+ }
787
+ if (afterModel.queryPresentation && typeof afterModel.queryPresentation === "object") {
788
+ readback.presentation = decodePresentation(afterModel.queryPresentation);
789
+ }
790
+ if (Array.isArray(afterModel.customButtons)) {
791
+ const buttons = readArray(afterModel, "customButtons");
792
+ readback.buttons = buttons
793
+ .map(decodeButtonRow)
794
+ .sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
795
+ if (args.buttonScripts !== undefined) {
796
+ const decoded = buttons.map((button) => decodeButtonScript(button));
797
+ readback.buttonScripts = decoded
798
+ .filter((button) => (args.buttonScripts ?? []).some((entry) => entry.code === button.code))
799
+ .map((button) => ({ code: button.code, bindAction: button.bindAction, scriptBytes: button.scriptBytes ?? 0, scriptSha256: button.scriptSha256 ?? null, scriptData: button.scriptData ?? null }));
800
+ }
801
+ readback.buttonsRenderCompat = buttonsRenderCompat(buttons);
802
+ }
803
+ if (afterModel.options !== undefined && afterModel.options !== null) {
804
+ readback.options = parseConditionOptions(afterModel.options);
805
+ }
806
+ if (afterModel.queryPresentationType !== undefined && afterModel.queryPresentationType !== null) {
807
+ readback.presentationType = afterModel.queryPresentationType;
808
+ }
809
+ if (afterModel.queryGanttModel && typeof afterModel.queryGanttModel === "object") {
810
+ readback.gantt = decodeGantt(afterModel.queryGanttModel);
811
+ }
812
+ if (afterModel.queryLayer !== undefined && afterModel.queryLayer !== null) {
813
+ readback.queryLayer = afterModel.queryLayer;
814
+ }
815
+ if (afterModel.multiTableHeaderConfig !== undefined && afterModel.multiTableHeaderConfig !== null && afterModel.multiTableHeaderConfig !== "") {
816
+ readback.multiTableHeaderConfig = parseConditionOptions(afterModel.multiTableHeaderConfig);
817
+ }
818
+ return { performed: { errcode: performed.errcode }, readback };
819
+ },
820
+ preflight: assertListViewConfigureShape,
821
+ rollback: listviewRollbackSpec("listview.configure"),
822
+ });
823
+ writeTool({
824
+ tools,
825
+ name: "listview.delete",
826
+ lane: LIST_VIEW_LANE,
827
+ scope: LIST_VIEW_TOOLS["listview.delete"].scope,
828
+ description: LIST_VIEW_TOOLS["listview.delete"].description,
829
+ inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1) },
830
+ execute: async (session, args) => {
831
+ const before = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
832
+ if (before.data == null)
833
+ return { performed: { skipped: "already-absent" }, readback: { absent: true, header: null } };
834
+ let performed;
835
+ try {
836
+ performed = await wireCall(session, { path: ENDPOINTS.listQueryDelete, method: "POST", query: { schemaCode: args.schemaCode, code: args.code } });
837
+ }
838
+ catch (error) {
839
+ // 306103 删除后将无可用视图 — the platform requires at least one visible
840
+ // list view per model. r15's `nextCommand` fell back to the generic
841
+ // refusal router; name the constraint and the only real deletion path.
842
+ if (error instanceof WireError && error.kind === "business" && (error.errcode === 306103 || /无可用视图/.test(error.message)))
843
+ throw new ToolRefusalError({
844
+ status: "refused",
845
+ blocker: "listview-last-visible",
846
+ schemaCode: args.schemaCode,
847
+ code: args.code,
848
+ nextCommand: `omc model-purge --code ${args.schemaCode}`,
849
+ message: `不能删除视图 ${args.code}:平台要求每个模型至少保留一条可见列表视图(306103)。` +
850
+ `单独删最后一条视图没有意义——视图会随模型一起级联删除。要继续用这个模型就**就地改**(listview.configure 省略 code 即改默认视图),` +
851
+ `要整体清理就用 model-purge(默认 dry-run,先看计划)。`,
852
+ });
853
+ throw error;
854
+ }
855
+ const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
856
+ return { performed: { errcode: performed.errcode }, readback: { absent: readback.data == null, header: readback.data ?? null } };
857
+ },
858
+ rollback: listviewRollbackSpec("listview.delete"),
859
+ });
824
860
  }
825
861
  //# sourceMappingURL=list-view.js.map