@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
@@ -387,114 +387,167 @@ export function registerFieldLane(tools) {
387
387
  },
388
388
  }),
389
389
  }));
390
- writeTool(tools, "field.create", FIELD_LANE, FIELD_TOOLS["field.create"].scope, FIELD_TOOLS["field.create"].description, {
391
- schemaCode: z.string().min(1),
392
- code: z.string().min(1),
393
- name: z.string().min(1),
394
- propertyType: propertyTypeArg,
395
- options: fieldOptionsArg.optional(),
396
- relativeCode: relativeCodeArg,
397
- choices: choicesArg,
398
- subFields: subFieldArg,
399
- skipExisting: z.boolean().optional().describe("幂等重入:code 已存在于本模型时跳过并返回 existed:true(批量脚本重跑不再被 schema-code-taken 打断);跨模型占用仍拒绝"),
400
- propertyEmpty: z.boolean().optional().describe("数据模型层是否声明该字段不可为空;默认 false(可为空)。必填/只读请在表单设计与流程节点权限配置,不要在模型层设置——平台会据此把流程各节点强制成可见+可编辑+必填"),
401
- }, async (session, args) => {
402
- const draft = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data);
403
- const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
404
- if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code)) {
405
- // r28b 账本(2026-10-28):批量脚本重入被重复拒绝打断 69 次——skipExisting
406
- // 让幂等重跑返回 existed:true 而不是拒(跨模型占用仍走 wire 层教学拒绝)。
407
- if (args.skipExisting === true)
408
- return { performed: { errcode: 0, skipped: true, reason: "duplicate-field" }, readback: { draftFieldPresent: true, existed: true, note: "skipExisting:true 命中已存在字段,幂等跳过(无写入)" } };
409
- throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST;批量重入加 skipExisting:true 幂等跳过`, nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true, code: args.code })}'` });
410
- }
411
- // One seam owns field-change normalization, every reproduced gate, and the
412
- // wire body (src/field-change.ts), so configure_model and intent share it.
413
- assertNoSelfSchemaAggregation(args, published);
414
- await assertNoChildSchemaAggregation(session, args.schemaCode, args.propertyType, args.options ?? {});
415
- // 目标版本决定 optionsSet 形状:8.2 以下要补 optionStructure/treeSelect
416
- // (cpdocs issue #8:缺键让设计器选项弹窗硬崩)。
417
- const targetVersion = session.detectedVersion();
418
- const prepared = prepareFieldChange({ ...args, ...(targetVersion ? { targetVersion } : {}) });
419
- let performed;
420
- try {
421
- performed = await wireCall(session, { path: ENDPOINTS.fieldCreate, method: "POST", body: fieldChangeWireBody(args, prepared) });
422
- }
423
- catch (error) {
424
- // r12 A5: the platform answers a bare 300005「数据模型已经存在!」when the
425
- // code collides. BizObjectSchemaManagerImpl#addBizSchema looks the code up
426
- // in the GLOBAL schema registry (`getBizObjectSchema(code)`), and child
427
- // tables are registered there too (`schema.isSubSchema()` →
428
- // `_addSubBizSchema`) — so a subtable code shares the model namespace and
429
- // collides across models, not just inside this one. The registry lookup is
430
- // not exposed over REST, so the occupier cannot be listed here; say that
431
- // instead of leaving a bare platform code.
432
- if (error instanceof WireError && error.kind === "business" && error.errcode === 300005 && args.skipExisting === true) {
433
- // r28b 账本(2026-10-28):批量重入 69 次 field.create 拒绝的大头是 code 已占——
434
- // 幂等跳过让批量脚本可重跑,只有跨模型占用才走拒绝教学。
435
- const existing = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
436
- const present = wireRows(existing.data).some((row) => (row.code ?? row.propertyCode) === args.code);
437
- if (present)
438
- return { performed: { errcode: 0, skipped: true, reason: "schema-code-taken" }, readback: { draftFieldPresent: true, existed: true, note: "skipExisting:true 命中已存在字段,幂等跳过(无写入)" } };
390
+ writeTool({
391
+ tools,
392
+ name: "field.create",
393
+ lane: FIELD_LANE,
394
+ scope: FIELD_TOOLS["field.create"].scope,
395
+ description: FIELD_TOOLS["field.create"].description,
396
+ inputSchema: {
397
+ schemaCode: z.string().min(1),
398
+ code: z.string().min(1),
399
+ name: z.string().min(1),
400
+ propertyType: propertyTypeArg,
401
+ options: fieldOptionsArg.optional(),
402
+ relativeCode: relativeCodeArg,
403
+ choices: choicesArg,
404
+ subFields: subFieldArg,
405
+ skipExisting: z.boolean().optional().describe("幂等重入:code 已存在于本模型时跳过并返回 existed:true(批量脚本重跑不再被 schema-code-taken 打断);跨模型占用仍拒绝"),
406
+ propertyEmpty: z.boolean().optional().describe("数据模型层是否声明该字段不可为空;默认 false(可为空)。必填/只读请在表单设计与流程节点权限配置,不要在模型层设置——平台会据此把流程各节点强制成可见+可编辑+必填"),
407
+ },
408
+ execute: async (session, args) => {
409
+ const draft = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data);
410
+ const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
411
+ if ([...draft, ...published].some((row) => (row.code ?? row.propertyCode) === args.code)) {
412
+ // r28b 账本(2026-10-28):批量脚本重入被重复拒绝打断 69 次——skipExisting
413
+ // 让幂等重跑返回 existed:true 而不是拒(跨模型占用仍走 wire 层教学拒绝)。
414
+ if (args.skipExisting === true)
415
+ return { performed: { errcode: 0, skipped: true, reason: "duplicate-field" }, readback: { draftFieldPresent: true, existed: true, note: "skipExisting:true 命中已存在字段,幂等跳过(无写入)" } };
416
+ throw new ToolRefusalError({ status: "refused", blocker: "duplicate-field", schemaCode: args.schemaCode, propertyCode: args.code, message: `字段 ${args.schemaCode}.${args.code} 已存在于草稿或发布面;拒绝重复 POST;批量重入加 skipExisting:true 幂等跳过`, nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: true, code: args.code })}'` });
439
417
  }
440
- if (error instanceof WireError && error.kind === "business" && error.errcode === 300005)
441
- throw new ToolRefusalError({
442
- status: "refused",
443
- blocker: "schema-code-taken",
444
- schemaCode: args.schemaCode,
445
- code: args.code,
446
- nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: false })}'`,
447
- message: `code ${args.code} 已被占用(300005 数据模型已经存在)。**模型与子表共用同一个全局 code 命名空间**——` +
448
- `反编译取证的判定点是 BizObjectSchemaManagerImpl#addBizSchema 的 getBizObjectSchema(code),子表走同一注册表(isSubSchema → _addSubBizSchema),` +
449
- `所以子表 code 与别的模型的子表/模型 code 撞了也会报这条,而不只是本模型内重复。` +
450
- `本模型内的重复本工具已提前拦住;剩下的是跨模型占用。平台没有「按 code 查占用者」的 REST 读面(只有 _addSubBizSchema 内部注册表),所以这里列不出占用者。` +
451
- `处理:换一个更具体的 code(建议 <模型前缀>_<语义>);如果确信是上一轮残留,用 model-purge 清掉那个模型后再试。`,
452
- });
453
- throw error;
454
- }
455
- const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
456
- const rows = wireRows(readback.data);
457
- return {
458
- performed: { errcode: performed.errcode },
459
- readback: { draftFieldPresent: rows.some((row) => (row.code ?? row.propertyCode) === args.code) },
460
- };
461
- }, undefined, undefined, fieldRollbackSpec("field.create"));
462
- writeTool(tools, "field.update", FIELD_LANE, FIELD_TOOLS["field.update"].scope, FIELD_TOOLS["field.update"].description, {
463
- schemaCode: z.string().min(1),
464
- code: z.string().min(1),
465
- name: z.string().min(1).optional().describe("new display name; also rewrites name_i18n to a valid JSON object string"),
466
- name_i18n: z.union([z.record(z.string(), z.unknown()), z.string()]).optional().describe("explicit i18n object (or JSON object string) to store verbatim; must parse to an object"),
467
- options: fieldOptionsArg.optional(),
468
- choices: choicesArg,
469
- relativeCode: relativeCodeArg,
470
- subFields: subFieldArg,
471
- propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)"),
472
- cascadeRepublish: z
473
- .boolean()
474
- .optional()
475
- .describe("true 时一调级联生效:field.update → field.publish → form.draft(rebuild) → form.publish(force)。表单按 published fields 全量重建默认表单脚手架(保留 non-structural 控件 options);无跨端点事务,中途失败按 partial steps(failedStep+envelope)如实报告,默认不级联"),
476
- }, async (session, args) => {
477
- const outcome = await runFieldUpdate(session, args);
478
- if (args.cascadeRepublish !== true)
479
- return outcome;
480
- return runFieldUpdateCascade(session, args, outcome);
481
- }, (args) => assertNameI18nArg(args.name_i18n), undefined, fieldRollbackSpec("field.update"));
482
- writeTool(tools, "field.publish", FIELD_LANE, FIELD_TOOLS["field.publish"].scope, FIELD_TOOLS["field.publish"].description, {
483
- schemaCode: z.string().min(1),
484
- code: z.string().min(1).optional().describe("optional single field to verify on the published face"),
485
- codes: z.array(z.string().min(1)).optional().describe("optional list of fields to verify; publishing is model-scoped, do NOT call once per field"),
486
- }, async (session, args) => executeFieldPublish(session, args));
487
- writeTool(tools, "field.delete", FIELD_LANE, FIELD_TOOLS["field.delete"].scope, FIELD_TOOLS["field.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
488
- const performed = await wireCall(session, {
489
- path: ENDPOINTS.fieldForceDelete,
490
- method: "POST",
491
- query: { bizSchemaCode: args.schemaCode, bizPropertyCode: args.code },
492
- });
493
- const draft = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
494
- const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
495
- const gone = (value) => !wireRows(value).some((row) => (row.code ?? row.propertyCode) === args.code);
496
- return { performed: { errcode: performed.errcode }, readback: { absentDraft: gone(draft.data), absentPublished: gone(published.data) } };
497
- }, undefined, undefined, fieldRollbackSpec("field.delete"));
418
+ // One seam owns field-change normalization, every reproduced gate, and the
419
+ // wire body (src/field-change.ts), so configure_model and intent share it.
420
+ assertNoSelfSchemaAggregation(args, published);
421
+ await assertNoChildSchemaAggregation(session, args.schemaCode, args.propertyType, args.options ?? {});
422
+ // 目标版本决定 optionsSet 形状:8.2 以下要补 optionStructure/treeSelect
423
+ // (cpdocs issue #8:缺键让设计器选项弹窗硬崩)。
424
+ const targetVersion = session.detectedVersion();
425
+ const prepared = prepareFieldChange({ ...args, ...(targetVersion ? { targetVersion } : {}) });
426
+ let performed;
427
+ try {
428
+ performed = await wireCall(session, { path: ENDPOINTS.fieldCreate, method: "POST", body: fieldChangeWireBody(args, prepared) });
429
+ }
430
+ catch (error) {
431
+ // r12 A5: the platform answers a bare 300005「数据模型已经存在!」when the
432
+ // code collides. BizObjectSchemaManagerImpl#addBizSchema looks the code up
433
+ // in the GLOBAL schema registry (`getBizObjectSchema(code)`), and child
434
+ // tables are registered there too (`schema.isSubSchema()` →
435
+ // `_addSubBizSchema`) — so a subtable code shares the model namespace and
436
+ // collides across models, not just inside this one. The registry lookup is
437
+ // not exposed over REST, so the occupier cannot be listed here; say that
438
+ // instead of leaving a bare platform code.
439
+ if (error instanceof WireError && error.kind === "business" && error.errcode === 300005 && args.skipExisting === true) {
440
+ // r28b 账本(2026-10-28):批量重入 69 次 field.create 拒绝的大头是 code 已占——
441
+ // 幂等跳过让批量脚本可重跑,只有跨模型占用才走拒绝教学。
442
+ const existing = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
443
+ const present = wireRows(existing.data).some((row) => (row.code ?? row.propertyCode) === args.code);
444
+ if (present)
445
+ return { performed: { errcode: 0, skipped: true, reason: "schema-code-taken" }, readback: { draftFieldPresent: true, existed: true, note: "skipExisting:true 命中已存在字段,幂等跳过(无写入)" } };
446
+ }
447
+ if (error instanceof WireError && error.kind === "business" && error.errcode === 300005)
448
+ throw new ToolRefusalError({
449
+ status: "refused",
450
+ blocker: "schema-code-taken",
451
+ schemaCode: args.schemaCode,
452
+ code: args.code,
453
+ nextCommand: `omc call field.list --args '${JSON.stringify({ schemaCode: args.schemaCode, published: false })}'`,
454
+ message: `code ${args.code} 已被占用(300005 数据模型已经存在)。**模型与子表共用同一个全局 code 命名空间**——` +
455
+ `反编译取证的判定点是 BizObjectSchemaManagerImpl#addBizSchema 的 getBizObjectSchema(code),子表走同一注册表(isSubSchema → _addSubBizSchema),` +
456
+ `所以子表 code 与别的模型的子表/模型 code 撞了也会报这条,而不只是本模型内重复。` +
457
+ `本模型内的重复本工具已提前拦住;剩下的是跨模型占用。平台没有「按 code 查占用者」的 REST 读面(只有 _addSubBizSchema 内部注册表),所以这里列不出占用者。` +
458
+ `处理:换一个更具体的 code(建议 <模型前缀>_<语义>);如果确信是上一轮残留,用 model-purge 清掉那个模型后再试。`,
459
+ });
460
+ throw error;
461
+ }
462
+ const readback = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
463
+ const rows = wireRows(readback.data);
464
+ // r28g F14:平台对 SHORT_TEXT/DROPDOWN 等文本族常自动落一条 type1
465
+ // checkType=4 maxLength=200 的 datarule;field.create 以前不提,list 多
466
+ // 出「我没建过」的行会让清理/对账猜。只作 advisory,不承诺平台一定写。
467
+ const textLike = String(args.propertyType) === "SHORT_TEXT" ||
468
+ String(args.propertyType) === "LONG_TEXT" ||
469
+ String(args.propertyType) === "DROPDOWN_BOX" ||
470
+ String(args.propertyType) === "DROPDOWN_MULTI_BOX" ||
471
+ String(args.propertyType) === "RADIO" ||
472
+ String(args.propertyType) === "CHECKBOX" ||
473
+ String(args.propertyType) === "SELECTION";
474
+ return {
475
+ performed: { errcode: performed.errcode },
476
+ readback: {
477
+ draftFieldPresent: rows.some((row) => (row.code ?? row.propertyCode) === args.code),
478
+ ...(textLike
479
+ ? {
480
+ sideEffectsNote: "平台可能为文本/下拉字段自动生成一条 datarule type1 checkType=4(maxLength≈200);本调用未显式创建。清理/对账请用 datarule.list 核对,不要把自动行当成重复写入。",
481
+ }
482
+ : {}),
483
+ },
484
+ };
485
+ },
486
+ rollback: fieldRollbackSpec("field.create"),
487
+ });
488
+ writeTool({
489
+ tools,
490
+ name: "field.update",
491
+ lane: FIELD_LANE,
492
+ scope: FIELD_TOOLS["field.update"].scope,
493
+ description: FIELD_TOOLS["field.update"].description,
494
+ inputSchema: {
495
+ schemaCode: z.string().min(1),
496
+ code: z.string().min(1),
497
+ name: z.string().min(1).optional().describe("new display name; also rewrites name_i18n to a valid JSON object string"),
498
+ name_i18n: z.union([z.record(z.string(), z.unknown()), z.string()]).optional().describe("explicit i18n object (or JSON object string) to store verbatim; must parse to an object"),
499
+ options: fieldOptionsArg.optional(),
500
+ choices: choicesArg,
501
+ relativeCode: relativeCodeArg,
502
+ subFields: subFieldArg,
503
+ propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)"),
504
+ cascadeRepublish: z
505
+ .boolean()
506
+ .optional()
507
+ .describe("true 时一调级联生效:field.update → field.publish → form.draft(rebuild) → form.publish(force)。表单按 published fields 全量重建默认表单脚手架(保留 non-structural 控件 options);无跨端点事务,中途失败按 partial steps(failedStep+envelope)如实报告,默认不级联"),
508
+ },
509
+ execute: async (session, args) => {
510
+ const outcome = await runFieldUpdate(session, args);
511
+ if (args.cascadeRepublish !== true)
512
+ return outcome;
513
+ return runFieldUpdateCascade(session, args, outcome);
514
+ },
515
+ preflight: (args) => assertNameI18nArg(args.name_i18n),
516
+ rollback: fieldRollbackSpec("field.update"),
517
+ });
518
+ writeTool({
519
+ tools,
520
+ name: "field.publish",
521
+ lane: FIELD_LANE,
522
+ scope: FIELD_TOOLS["field.publish"].scope,
523
+ description: FIELD_TOOLS["field.publish"].description,
524
+ inputSchema: {
525
+ schemaCode: z.string().min(1),
526
+ code: z.string().min(1).optional().describe("optional single field to verify on the published face"),
527
+ codes: z.array(z.string().min(1)).optional().describe("optional list of fields to verify; publishing is model-scoped, do NOT call once per field"),
528
+ },
529
+ execute: async (session, args) => executeFieldPublish(session, args),
530
+ });
531
+ writeTool({
532
+ tools,
533
+ name: "field.delete",
534
+ lane: FIELD_LANE,
535
+ scope: FIELD_TOOLS["field.delete"].scope,
536
+ description: FIELD_TOOLS["field.delete"].description,
537
+ inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1) },
538
+ execute: async (session, args) => {
539
+ const performed = await wireCall(session, {
540
+ path: ENDPOINTS.fieldForceDelete,
541
+ method: "POST",
542
+ query: { bizSchemaCode: args.schemaCode, bizPropertyCode: args.code },
543
+ });
544
+ const draft = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
545
+ const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
546
+ const gone = (value) => !wireRows(value).some((row) => (row.code ?? row.propertyCode) === args.code);
547
+ return { performed: { errcode: performed.errcode }, readback: { absentDraft: gone(draft.data), absentPublished: gone(published.data) } };
548
+ },
549
+ rollback: fieldRollbackSpec("field.delete"),
550
+ });
498
551
  }
499
552
  /** The proven field.update write body — shared by the write tool and the cascade path. */
500
553
  async function runFieldUpdate(session, args) {
@@ -13,7 +13,7 @@ export const FOLDER_WIRE = Object.freeze({
13
13
  delete: "POST /api/api/app/folders/delete?folderId=",
14
14
  }),
15
15
  defaults: "createFolder forces type=Folder, defaults parentId to the app package id (root) and does NOT reject duplicate names; updateFolder refuses parentId===id and only writes parentId when non-empty; deleteFolder refuses a non-empty folder (DIRECTORY_NOT_EMPTY) — delete is NOT recursive",
16
- tree: "apppackage/trees?appCode= returns a NESTED tree (folders carry children[]); reading only the top-level array makes a freshly created child folder look absent — flatten recursively and report path/depth",
16
+ tree: "apppackage/trees?appCode= returns a NESTED tree (folders carry children[]); reading only the top-level array makes a freshly created child folder look absent — flatten recursively and report path/depth; missing app → applicationExists:false + empty nodes (still ok); default folder marked isDefault via name_i18n.en=defaultGroup; BizModel gets displayPath",
17
17
  live: "2026-10-23 customer HAR replay: folders/create answered errcode 0 成功创建目录; after the token was attached apppackage/trees returned the full tree",
18
18
  customPage: "CustomPageController(8.6.26, @RequestMapping \"/api/custom\"): POST /create takes AppFunctionVO and requires parentId + a non-null menuPointType (getMenuPointType().toString() NPEs to 50000 otherwise) and forces type=Page; POST /page/content/update writes draftContent (existDraft=true) and /page/content/publish writes publishedContent (published=true); BOTH first do getAppFunctionByCode(code) and throw BIZ_SCHEMA_NOT_EXIST when the tree node is missing — node first, then body",
19
19
  });
@@ -48,7 +48,7 @@ export const FOLDER_LANE_CONTRACT = Object.freeze({
48
48
  "issue/2026-10-23-omc-app-folder-create-and-move.md",
49
49
  ]),
50
50
  tools: Object.freeze({
51
- "folder.tree": Object.freeze({ scope: "app-tree folder/model tree (flattened with path/depth)", description: "读应用树(目录/分组 + 模型):平台 apppackage/trees 返回嵌套 children,本工具递归展平并给每节点 path/depth" }),
51
+ "folder.tree": Object.freeze({ scope: "app-tree folder/model tree (flattened with path/depth)", description: "读应用内目录树(folder 节点 + 模型,不是应用包):平台 apppackage/trees 返回嵌套 children,本工具递归展平并给每节点 path/depth;不存在应用回 applicationExists:false;默认分组带 isDefault" }),
52
52
  "folder.create": Object.freeze({ scope: "app-tree folder create (folders/create)", description: "新建目录:folders/create 省略 parentId 建顶层目录;同父同名默认拒绝(folder-duplicate)" }),
53
53
  "folder.update": Object.freeze({ scope: "app-tree folder rename/move (folders/update)", description: "重命名/挂载目录(不移动目录内模型,模型用 model.move);parentId 不可等于自身" }),
54
54
  "folder.delete": Object.freeze({ scope: "app-tree folder delete (folders/delete, non-recursive)", description: "删除空目录:非递归,有子节点回 DIRECTORY_NOT_EMPTY(工具先查子节点,拒 folder-not-empty)" }),
@@ -67,7 +67,7 @@ export const FOLDER_LANE_CONTRACT = Object.freeze({
67
67
  }),
68
68
  });
69
69
  export function assertFolderContract() {
70
- assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.tree", ["children", "path", "depth"]);
70
+ assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.tree", ["children", "path", "depth", "applicationExists", "isDefault"]);
71
71
  assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.create", ["parentId", "顶层", "folder-duplicate"]);
72
72
  assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.update", ["parentId", "model.move"]);
73
73
  assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.delete", ["非递归", "folder-not-empty"]);
@@ -75,6 +75,10 @@ export function assertFolderContract() {
75
75
  throw new Error("folder contract must pin the non-recursive delete refusal");
76
76
  if (!FOLDER_WIRE.tree.includes("NESTED"))
77
77
  throw new Error("folder contract must pin the nested tree shape");
78
+ if (!FOLDER_WIRE.tree.includes("applicationExists"))
79
+ throw new Error("folder contract must pin missing-app distinction");
80
+ if (!FOLDER_WIRE.tree.includes("defaultGroup"))
81
+ throw new Error("folder contract must pin default-folder isDefault token");
78
82
  // Custom pages: the two-row model (tree node + body row) and the NPE-on-null
79
83
  // menuPointType are the facts a future edit must not lose.
80
84
  if (!FOLDER_WIRE.customPage.includes("menuPointType"))