@shundoo-ai/dsh-cosmic 1.0.19 → 1.0.21

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 (245) hide show
  1. package/cordis.patch.yml +5 -5
  2. package/package.json +1 -1
  3. package/skills/cosmic-build/CHANGELOG.md +4 -0
  4. package/skills/cosmic-build/LOCAL-PATCHES.md +91 -40
  5. package/skills/cosmic-build/SKILL.md +46 -13
  6. package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +169 -0
  7. package/skills/cosmic-build/assets/platform-report-filters/platform.patch +98 -0
  8. package/skills/cosmic-build/assets/report-algo-benchmark/ReportAlgoBenchmark.java +48 -0
  9. package/skills/cosmic-build/assets/report-algo-benchmark/ReportBenchmarkLogFactory.java +7 -0
  10. package/skills/cosmic-build/examples/java-rules/CosmicEntryQuery.java +26 -0
  11. package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +35 -0
  12. package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +77 -0
  13. package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapter.java +25 -0
  14. package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapterCheck.java +26 -0
  15. package/skills/cosmic-build/knowledge/cosmic/platform/api/algo/GroupbyDataSet.md +47 -0
  16. package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterInfo.md +12 -12
  17. package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterItemInfo.md +2 -0
  18. package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +2 -4
  19. package/skills/cosmic-build/knowledge/cosmic/platform/java-type-contracts.md +11 -2
  20. package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +18 -14
  21. package/skills/cosmic-build/knowledge/cosmic/platform/operation-plugin.md +4 -4
  22. package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +17 -9
  23. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/cross-entity-aggregate-writeback.md +2 -0
  24. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/pre-post-operation.md +15 -7
  25. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +24 -68
  26. package/skills/cosmic-build/knowledge/cosmic/platform/workflow-plugin.md +2 -7
  27. package/skills/cosmic-build/references/application-context.md +35 -1
  28. package/skills/cosmic-build/references/common-reference.md +1 -1
  29. package/skills/cosmic-build/references/compile-guide.md +55 -68
  30. package/skills/cosmic-build/references/delivery.md +59 -7
  31. package/skills/cosmic-build/references/execution-contract.md +38 -0
  32. package/skills/cosmic-build/references/form-discovery.md +7 -1
  33. package/skills/cosmic-build/references/java-plugin-delivery.md +32 -162
  34. package/skills/cosmic-build/references/operation-dispatch.md +17 -0
  35. package/skills/cosmic-build/references/plugin-handoff-input.md +131 -0
  36. package/skills/cosmic-build/references/plugin-runtime-routing.md +1 -1
  37. package/skills/cosmic-build/references/report-filter-contract.md +78 -0
  38. package/skills/cosmic-build/references/sdk-inspection.md +23 -0
  39. package/skills/cosmic-build/schemas/data-model-authoring.schema.json +2 -3
  40. package/skills/cosmic-build/scripts/_http_client.py +55 -55
  41. package/skills/cosmic-build/scripts/appconfig_load.py +2 -2
  42. package/skills/cosmic-build/scripts/apply_report_filter_extension.py +39 -0
  43. package/skills/cosmic-build/scripts/bootstrap_cosmic_context.py +37 -2
  44. package/skills/cosmic-build/scripts/compile_plugin_source.py +279 -156
  45. package/skills/cosmic-build/scripts/execution_binding.py +40 -0
  46. package/skills/cosmic-build/scripts/inspect_platform_sdk.py +169 -0
  47. package/skills/cosmic-build/scripts/kd_api.py +23 -22
  48. package/skills/cosmic-build/scripts/kd_auth.py +1 -1
  49. package/skills/cosmic-build/scripts/kd_env_loader.py +19 -10
  50. package/skills/cosmic-build/scripts/kd_token_cache.py +2 -2
  51. package/skills/cosmic-build/scripts/kingscript_sdk_sync.py +350 -0
  52. package/skills/cosmic-build/scripts/kingscript_source_gate.py +11 -8
  53. package/skills/cosmic-build/scripts/platform_api_contract.py +246 -11
  54. package/skills/cosmic-build/scripts/plugin_corpus/build_index.py +403 -0
  55. package/skills/cosmic-build/scripts/plugin_corpus/jvm_classfile.py +281 -0
  56. package/skills/cosmic-build/scripts/plugin_corpus/measure_api_usage.py +446 -0
  57. package/skills/cosmic-build/scripts/plugin_source_gate.py +48 -7
  58. package/skills/cosmic-build/scripts/plugin_target_sdk.py +45 -0
  59. package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +493 -47
  60. package/skills/cosmic-build/scripts/preview_url.py +35 -6
  61. package/skills/cosmic-build/scripts/query_plugin_handoff.py +250 -0
  62. package/skills/cosmic-build/scripts/register_plugins.py +28 -2
  63. package/skills/cosmic-build/scripts/render_output_cards.py +42 -9
  64. package/skills/cosmic-build/scripts/report_algo_benchmark.py +46 -0
  65. package/skills/cosmic-build/scripts/run.py +2 -2
  66. package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +43 -0
  67. package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +64 -0
  68. package/skills/cosmic-build/scripts/test/fixtures/java_context_booking_native.json +541 -0
  69. package/skills/cosmic-build/scripts/test/test_add_rule_business_contract.py +85 -14
  70. package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +12 -3
  71. package/skills/cosmic-build/scripts/test/test_build_meta_order.py +0 -16
  72. package/skills/cosmic-build/scripts/test/test_cli_continuation.py +87 -0
  73. package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +224 -3
  74. package/skills/cosmic-build/scripts/test/test_control_authoring.py +123 -4
  75. package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +141 -0
  76. package/skills/cosmic-build/scripts/test/test_data_model_contract.py +30 -5
  77. package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +2 -10
  78. package/skills/cosmic-build/scripts/test/test_dym_sync_background.py +11 -0
  79. package/skills/cosmic-build/scripts/test/test_dynamic_form_metadata_only_contract.py +59 -1
  80. package/skills/cosmic-build/scripts/test/test_guide_skeleton_contract.py +87 -43
  81. package/skills/cosmic-build/scripts/test/test_inspect_platform_sdk.py +71 -0
  82. package/skills/cosmic-build/scripts/test/test_kd_api.py +67 -19
  83. package/skills/cosmic-build/scripts/test/test_kingscript_sdk_sync.py +221 -0
  84. package/skills/cosmic-build/scripts/test/test_kingscript_source_gate.py +10 -0
  85. package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +18 -0
  86. package/skills/cosmic-build/scripts/test/test_long_session_regressions.py +116 -0
  87. package/skills/cosmic-build/scripts/test/test_mcp_cli.py +524 -0
  88. package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +141 -0
  89. package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +36 -2
  90. package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +161 -1
  91. package/skills/cosmic-build/scripts/test/test_plugin_business_contract.py +91 -0
  92. package/skills/cosmic-build/scripts/test/test_plugin_corpus.py +195 -0
  93. package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +346 -1
  94. package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +70 -0
  95. package/skills/cosmic-build/scripts/test/test_preview_url.py +44 -1
  96. package/skills/cosmic-build/scripts/test/test_property_batch.py +22 -0
  97. package/skills/cosmic-build/scripts/test/test_property_input_discovery.py +13 -11
  98. package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +160 -0
  99. package/skills/cosmic-build/scripts/test/test_register_plugins.py +31 -1
  100. package/skills/cosmic-build/scripts/test/test_render_output_cards.py +50 -4
  101. package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -0
  102. package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +5 -4
  103. package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +36 -0
  104. package/skills/cosmic-build/scripts/test/test_rule_batch_evidence.py +1 -1
  105. package/skills/cosmic-build/scripts/test/test_rule_domain_contract.py +20 -0
  106. package/skills/cosmic-build/scripts/test/test_rule_function_formula.py +16 -6
  107. package/skills/cosmic-build/scripts/test/test_skill_entry_contract.py +1 -0
  108. package/skills/cosmic-build/scripts/test/test_test_environment.py +45 -0
  109. package/skills/cosmic-build/scripts/test/test_verify_plugin_loading.py +79 -0
  110. package/skills/cosmic-build/scripts/test_environment.py +49 -0
  111. package/skills/cosmic-build/scripts/verify_plugin_loading.py +71 -0
  112. package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +86 -162
  113. package/skills/cosmic-build/skills/cosmic-coding-script/ATTRIBUTION.md +1 -1
  114. package/skills/cosmic-build/skills/cosmic-coding-script/SKILL.md +45 -14
  115. package/skills/cosmic-build/skills/cosmic-coding-script/references/kwc-controller-authoring.md +6 -0
  116. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/README.md +29 -19
  117. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/docs//350/204/232/346/234/254SDK.md +3 -4
  118. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/indexes/package-index.md +1 -1
  119. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/strategy.md +27 -18
  120. package/skills/cosmic-build/skills/cosmic-meta-api/SKILL.md +11 -17
  121. package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +1 -1
  122. package/skills/cosmic-build/skills/cosmic-meta-api/apis/app_menu.md +1 -1
  123. package/skills/cosmic-build/skills/cosmic-meta-api/apis/meta_log.md +1 -1
  124. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/control-authoring.json +45 -4
  125. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/form-control-traits.json +1 -1
  126. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/layout-ui.json +18 -0
  127. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/menu-publish.json +8 -2
  128. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-batch.json +7 -0
  129. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-input-index.json +3 -3
  130. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +1 -0
  131. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +37 -5
  132. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-event-contract.json +24 -3
  133. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/run-build-meta.json +61 -12
  134. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/dropdown-menu.schema.json +69 -0
  135. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +2 -1
  136. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-set.schema.json +1 -0
  137. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-write.schema.json +14 -7
  138. package/skills/cosmic-build/skills/cosmic-meta-api/docs/api-summary.md +2 -2
  139. package/skills/cosmic-build/skills/cosmic-meta-api/modules/app-action/SKILL.md +6 -3
  140. package/skills/cosmic-build/skills/cosmic-meta-api/modules/develop/SKILL.md +1 -2
  141. package/skills/cosmic-build/skills/cosmic-meta-api/modules/export/SKILL.md +5 -6
  142. package/skills/cosmic-build/skills/cosmic-meta-api/modules/init/SKILL.md +13 -36
  143. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/SKILL.md +8 -9
  144. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/config.json.example +5 -15
  145. package/skills/cosmic-build/skills/cosmic-meta-api/modules/reset/SKILL.md +6 -6
  146. package/skills/cosmic-build/skills/cosmic-meta-api/modules/usage/SKILL.md +20 -148
  147. package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +1 -1
  148. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/build_receipts.py +171 -0
  149. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +25 -6
  150. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_controls.py +55 -21
  151. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_fields.py +10 -3
  152. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +63 -6
  153. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_forms.py +59 -2
  154. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +8 -0
  155. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +36 -1
  156. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_plugins.py +115 -90
  157. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +666 -4
  158. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +1415 -87
  159. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/commands.py +6 -1
  160. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +28 -15
  161. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +40 -0
  162. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +32 -5
  163. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_policy_contract.py +6 -0
  164. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +113 -17
  165. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/reset.py +3 -3
  166. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/rule_contract.py +107 -26
  167. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/build_meta_order.py +50 -49
  168. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +856 -150
  169. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +47 -14
  170. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +374 -0
  171. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_cli_failure_contract.py +24 -0
  172. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_creation_identity_boundary.py +4 -2
  173. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_dropdown_menu_contract.py +382 -0
  174. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_existing_form_ui.py +0 -25
  175. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_field_decorator.py +50 -6
  176. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_functional_group_resolution.py +83 -16
  177. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_identity_template_resolution.py +103 -0
  178. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +28 -2
  179. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +22 -0
  180. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +65 -0
  181. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +1041 -15
  182. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +65 -0
  183. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +17 -0
  184. package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/api-param-checklist.md +1 -1
  185. package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/rule-modeling-guide.md +7 -4
  186. package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +61 -13
  187. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/app-menu/SKILL.md +8 -0
  188. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/SKILL.md +5 -0
  189. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/dynamic-form-artifact-schema.md +5 -0
  190. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +21 -9
  191. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-entity-graphs.md +1 -1
  192. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-storage-constraints.md +5 -3
  193. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-ui-composition.md +6 -3
  194. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/creation-recovery.md +13 -0
  195. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/parse-core.md +3 -0
  196. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +6 -1
  197. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +135 -87
  198. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/field_type_registry.py +11 -1
  199. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/isv_resolver.py +1 -3
  200. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +29 -0
  201. package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +4 -2
  202. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/context/SKILL.md +2 -2
  203. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/execute/SKILL.md +1 -1
  204. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/SKILL.md +5 -4
  205. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/operation-write.md +6 -2
  206. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
  207. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +20 -5
  208. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/complex-apis.md +1 -1
  209. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/policy-grammar.md +4 -1
  210. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +9 -0
  211. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +61 -4
  212. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/raw-rule-authoring.md +3 -1
  213. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-actions.md +2 -2
  214. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-reference.md +6 -6
  215. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/state.md +1 -1
  216. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/unit/SKILL.md +6 -3
  217. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +4 -4
  218. package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +4 -1
  219. package/skills/cosmic-build/skills/cosmic-meta-ui/SKILL.md +9 -1
  220. package/skills/cosmic-build/skills/cosmic-meta-ui/modules/button/SKILL.md +3 -0
  221. package/skills/cosmic-build/skills/mcp-server/SKILL.md +210 -0
  222. package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +431 -0
  223. package/skills/cosmic-build/skills/openapi-gen/SKILL.md +3 -3
  224. package/skills/cosmic-build/skills/openapi-gen/scripts/api/gen_runner.py +2 -2
  225. package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +19 -9
  226. package/skills/cosmic-dev-best-practices/references/cosmic-meta-api.md +14 -11
  227. package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +1 -1
  228. package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +1 -1
  229. package/skills/cosmic-dev-best-practices/references/elements/AssistantField.md +1 -1
  230. package/skills/cosmic-dev-best-practices/references/elements/BasedataField.md +2 -2
  231. package/skills/cosmic-dev-best-practices/references/elements/BasedataPropField.md +3 -3
  232. package/skills/cosmic-dev-best-practices/references/elements/CreaterField.md +2 -2
  233. package/skills/cosmic-dev-best-practices/references/elements/CurrencyField.md +2 -2
  234. package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +2 -2
  235. package/skills/cosmic-dev-best-practices/references/elements/FlexField.md +1 -1
  236. package/skills/cosmic-dev-best-practices/references/elements/MaterielField.md +2 -2
  237. package/skills/cosmic-dev-best-practices/references/elements/OrgField.md +2 -2
  238. package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +2 -2
  239. package/skills/cosmic-dev-best-practices/references/elements/UnitField.md +1 -1
  240. package/skills/cosmic-dev-best-practices/references/elements/UserField.md +3 -3
  241. package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +16 -11
  242. package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +5 -4
  243. package/skills/erp-analysis/references/delivery-contract.md +4 -2
  244. package/skills/erp-analysis/templates/AGENTS.template.md +1 -1
  245. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/.dsh/cosmic/.kd/config.json +0 -21
@@ -18,13 +18,10 @@ from .config import _api_call
18
18
  from .formatters import _extract_error_message, _is_api_success, _has_api_failure
19
19
  from .mutation_contract import mutation_contract, classify_mutation_result, combine_mutation_evidence
20
20
  from .skill_assets import asset_candidates
21
+ from .property_policy_contract import PUBLISHED_SIMPLE_PROPERTIES as _PUBLISHED_SIMPLE_PROPERTIES
21
22
 
22
23
  # ─── 属性设置 API ──────────────────────────────────────────────────────────────
23
24
 
24
- _PUBLISHED_SIMPLE_PROPERTIES = frozenset({
25
- "mustinput", "defvalue", "maxlength", "precision", "scale", "invisible",
26
- "width", "height",
27
- })
28
25
 
29
26
  # Some field families render a scalar property through a dialog editor. The
30
27
  # domain model therefore labels the designer surface "complex", but the field
@@ -571,6 +568,16 @@ def _describe_one_property(declaration, indexed, verified, element_type=None):
571
568
  out["guidance"] = "the platform declares this property not editable; do not attempt a write"
572
569
  return out
573
570
 
571
+ if declaration.get("propertyName") == "DropdownItems":
572
+ out["inputFormat"] = "menuEditIntent"
573
+ out["guidance"] = (
574
+ "Use configureDropdownMenu with append/update/remove/order/moves intent. "
575
+ "It resolves the live menu and preserves existing identities; do not submit "
576
+ "the read projection as a replacement or clear/rebuild after failure. "
577
+ "Its command schema defines the input."
578
+ )
579
+ return out
580
+
574
581
  if declaration.get("propertyName") in _SCALAR_PROPERTIES_WITH_COMPLEX_EDITOR:
575
582
  out.pop("complexType", None)
576
583
  out["inputFormat"] = "scalarViaSetProperties"
@@ -1035,6 +1042,12 @@ def _normalize_recorded_complex_properties(properties):
1035
1042
  normalized = {}
1036
1043
  for selector, value in properties.items():
1037
1044
  wanted = str(selector or "").strip().casefold()
1045
+ if wanted == "lock":
1046
+ raise ValueError(
1047
+ "PROPERTY_POLICY_LOCK_SHAPE_INVALID: Lock belongs in policy.set, "
1048
+ "for example {\"set\":{\"Lock\":\"new,edit,submit,audit\"}}; "
1049
+ "do not use policy.complex or infer support from the read-only designer schema"
1050
+ )
1038
1051
  matches = [
1039
1052
  name for name in _PROPERTY_POLICY_COMPLEX_TYPES
1040
1053
  if wanted in {
@@ -5145,6 +5158,12 @@ def do_set_complex_property(cfg, token, form_number, complex_type, data, element
5145
5158
  rejection = _assert_known_complex_type(complex_type)
5146
5159
  if rejection is not None:
5147
5160
  return rejection
5161
+ if complex_type == "dropdownItems":
5162
+ return {
5163
+ "status": False, "errorCode": "DROPDOWN_MENU_EDIT_REQUIRED",
5164
+ "message": "Use configureDropdownMenu with targeted edits so existing IDs and unmentioned items are retained.",
5165
+ "writeAttempted": False,
5166
+ }
5148
5167
  data = _normalize_complex_payload(complex_type, data)
5149
5168
  rejection = _assert_complex_payload_shape(complex_type, data)
5150
5169
  if rejection is not None:
@@ -5592,6 +5611,649 @@ def _set_candidate_backed_property(
5592
5611
  return result if verified else _mark_unknown_after_write(result)
5593
5612
 
5594
5613
 
5614
+ _DROPDOWN_ITEM_FIELDS = {
5615
+ "Key", "Title", "SubTitle", "OperationKey", "Background", "ImageKey",
5616
+ "IsSwitch", "IsTheme", "IsMerge", "Lock", "Visible", "Uploadable",
5617
+ "UploadConfig", "Id", "Index",
5618
+ }
5619
+ _DROPDOWN_ALIASES = {
5620
+ "key": "Key", "title": "Title", "subtitle": "SubTitle",
5621
+ "operationkey": "OperationKey", "operation": "OperationKey",
5622
+ "background": "Background", "imagekey": "ImageKey", "icon": "ImageKey",
5623
+ "isswitch": "IsSwitch", "switch": "IsSwitch", "istheme": "IsTheme",
5624
+ "theme": "IsTheme", "ismerge": "IsMerge", "merge": "IsMerge",
5625
+ "lock": "Lock", "visible": "Visible", "uploadable": "Uploadable",
5626
+ "uploadconfig": "UploadConfig", "id": "Id", "index": "Index",
5627
+ "isv": "Isv", "isvflag": "Isv",
5628
+ }
5629
+ _DROPDOWN_STATES = {
5630
+ "初始": "init", "新增": "new", "修改": "edit", "查看": "view",
5631
+ "提交": "submit", "审核": "audit",
5632
+ }
5633
+
5634
+
5635
+ def _dropdown_items_value(cfg, token, form_number, element_key):
5636
+ read = do_get_properties(cfg, token, form_number, element_key, ["DropdownItems"])
5637
+ if not _is_api_success(read):
5638
+ return None, read
5639
+ values = read.get("data") or {}
5640
+ actual = next((value for key, value in values.items()
5641
+ if str(key).casefold() == "dropdownitems"), None)
5642
+ if actual is None:
5643
+ return None, {
5644
+ "status": False, "errorCode": "DROPDOWN_MENU_READ_UNAVAILABLE",
5645
+ "message": "DropdownItems was not returned; missing data is not an empty menu",
5646
+ "writeAttempted": False,
5647
+ }
5648
+ actual = _decode_jsonish(actual)
5649
+ if actual in (None, ""):
5650
+ actual = []
5651
+ if not isinstance(actual, list) or not all(isinstance(item, dict) for item in actual):
5652
+ return None, {
5653
+ "status": False, "errorCode": "DROPDOWN_MENU_READ_INVALID",
5654
+ "message": "DropdownItems read-back is not an item array",
5655
+ "writeAttempted": False, "data": {"actual": actual},
5656
+ }
5657
+ try:
5658
+ items = [_dropdown_authored_item(item) for item in actual]
5659
+ keys = [item.get("Key") for item in items]
5660
+ if any(not key for key in keys) or len(keys) != len(set(keys)):
5661
+ raise ValueError("current menu identities are missing or duplicated")
5662
+ return items, None
5663
+ except ValueError as exc:
5664
+ return None, {
5665
+ "status": False, "errorCode": "DROPDOWN_MENU_PRESERVATION_UNAVAILABLE",
5666
+ "message": str(exc), "writeAttempted": False,
5667
+ }
5668
+
5669
+
5670
+ def _dropdown_authored_item(item):
5671
+ """Adapt MetaPropertyService JavaBeans to DropdownMenuEditPlugin's authored fields."""
5672
+ result = {}
5673
+ for raw_key, value in item.items():
5674
+ if raw_key == "_Type_" and value == "DropdownItem":
5675
+ continue
5676
+ if str(raw_key).casefold() == "visiblevalue":
5677
+ # Derived bitmask from getVisibleValue(), not an authored property.
5678
+ continue
5679
+ key = _DROPDOWN_ALIASES.get(str(raw_key).replace("_", "").casefold())
5680
+ # Read-back is a preservation baseline, not a write payload. Snapshot
5681
+ # editing retains the native objects by ID and patches only selected values.
5682
+ key = key or raw_key
5683
+ if value is not None:
5684
+ result[key] = value
5685
+ for key in ("Visible", "Lock"):
5686
+ if key in result:
5687
+ result[key] = ",".join(sorted(set(
5688
+ part.strip() for part in result[key].split(",") if part.strip())))
5689
+ if "UploadConfig" in result:
5690
+ result["UploadConfig"] = _dropdown_upload_config(result["UploadConfig"], readback=True)
5691
+ return result
5692
+
5693
+
5694
+ def _dropdown_mismatches(expected, actual, path="DropdownItems"):
5695
+ """Identity and authored text comparisons must remain case-sensitive."""
5696
+ if isinstance(expected, str) and isinstance(actual, dict) and path.rsplit(".", 1)[-1] in ("Title", "SubTitle"):
5697
+ if expected in [value for key, value in actual.items() if key != "resId"]:
5698
+ return []
5699
+ if isinstance(expected, dict) and isinstance(actual, dict):
5700
+ return [mismatch for key, value in expected.items()
5701
+ for mismatch in _dropdown_mismatches(value, actual.get(key), f"{path}.{key}")]
5702
+ if isinstance(expected, list) and isinstance(actual, list) and len(expected) == len(actual):
5703
+ return [mismatch for index, value in enumerate(expected)
5704
+ for mismatch in _dropdown_mismatches(value, actual[index], f"{path}[{index}]")]
5705
+ if path.endswith((".Visible", ".Lock")) and isinstance(expected, str) and isinstance(actual, str):
5706
+ expected = set(filter(None, expected.split(",")))
5707
+ actual = set(filter(None, actual.split(",")))
5708
+ if expected == actual:
5709
+ return []
5710
+ return [{"property": path, "expected": sorted(expected) if isinstance(expected, set) else expected,
5711
+ "actual": sorted(actual) if isinstance(actual, set) else actual}]
5712
+
5713
+
5714
+ def _dropdown_upload_config(value, *, readback=False):
5715
+ value = _decode_jsonish(value)
5716
+ if not isinstance(value, dict):
5717
+ raise ValueError("DROPDOWN_MENU_UPLOAD_CONFIG_INVALID: object required")
5718
+ aliases = {
5719
+ "multiple": "Multiple", "suffix": "Suffix", "accept": "Suffix",
5720
+ "limitcount": "LimitCount", "maxfiles": "LimitCount",
5721
+ "limitsize": "LimitSize", "maxfilesize": "LimitSize", "url": "Url",
5722
+ }
5723
+ result = {}
5724
+ for raw_key, field in value.items():
5725
+ if raw_key == "_Type_" and value[raw_key] == "UploadOption":
5726
+ continue
5727
+ key = aliases.get(str(raw_key).replace("_", "").casefold())
5728
+ if readback:
5729
+ # Native UploadOption has more fields than the published authoring
5730
+ # input. Keep that state, including unknown/default/null values.
5731
+ key = key or str(raw_key).casefold()
5732
+ if key in result and result[key] != field:
5733
+ raise ValueError(f"DROPDOWN_MENU_UPLOAD_CONFIG_AMBIGUOUS: {raw_key}")
5734
+ result[key] = copy.deepcopy(field)
5735
+ continue
5736
+ if key in result and result[key] == field:
5737
+ continue
5738
+ if key is None or key in result:
5739
+ raise ValueError(f"DROPDOWN_MENU_UPLOAD_CONFIG_UNKNOWN_OR_DUPLICATE: {raw_key}")
5740
+ if key == "Multiple" and not isinstance(field, bool):
5741
+ raise ValueError("DROPDOWN_MENU_UPLOAD_CONFIG_INVALID: Multiple requires boolean")
5742
+ if key in ("LimitCount", "LimitSize") and (
5743
+ isinstance(field, bool) or not isinstance(field, int) or field < 0):
5744
+ raise ValueError(f"DROPDOWN_MENU_UPLOAD_CONFIG_INVALID: {key} requires nonnegative integer")
5745
+ if key in ("Suffix", "Url") and not isinstance(field, str):
5746
+ raise ValueError(f"DROPDOWN_MENU_UPLOAD_CONFIG_INVALID: {key} requires string")
5747
+ result[key] = field
5748
+ return result
5749
+
5750
+
5751
+ def _normalize_dropdown_item(item, *, partial=False):
5752
+ if not isinstance(item, dict):
5753
+ raise ValueError("DROPDOWN_MENU_ITEM_INVALID: every item must be an object")
5754
+ normalized = {}
5755
+ for raw_key, value in item.items():
5756
+ key = _DROPDOWN_ALIASES.get(str(raw_key).replace("_", "").casefold())
5757
+ if key is None or key not in _DROPDOWN_ITEM_FIELDS:
5758
+ raise ValueError(f"DROPDOWN_MENU_ITEM_UNKNOWN: {raw_key}")
5759
+ if key in normalized:
5760
+ raise ValueError(f"DROPDOWN_MENU_ITEM_DUPLICATE: {raw_key}")
5761
+ if key in ("Id", "Index"):
5762
+ raise ValueError(f"DROPDOWN_MENU_ITEM_IDENTITY_OWNED: {raw_key}")
5763
+ normalized[key] = value
5764
+ if not partial and not str(normalized.get("Key") or "").strip():
5765
+ raise ValueError("DROPDOWN_MENU_ITEM_KEY_REQUIRED")
5766
+ for name in ("Key", "Title", "SubTitle", "OperationKey", "Background", "ImageKey"):
5767
+ if name in normalized and not isinstance(normalized[name], str):
5768
+ raise ValueError(f"DROPDOWN_MENU_ITEM_STRING_REQUIRED: {name}")
5769
+ for name in ("Visible", "Lock"):
5770
+ value = normalized.get(name)
5771
+ if isinstance(value, (list, str)):
5772
+ parts = value if isinstance(value, list) else value.split(",")
5773
+ normalized[name] = ",".join(sorted({
5774
+ _DROPDOWN_STATES.get(str(part).strip(), str(part).strip())
5775
+ for part in parts if str(part).strip()
5776
+ }))
5777
+ allowed = {"init", "new", "edit", "view", "submit", "audit"}
5778
+ if name == "Lock":
5779
+ allowed.discard("init")
5780
+ if set(normalized[name].split(",")) - allowed - {""}:
5781
+ raise ValueError(f"DROPDOWN_MENU_STATE_INVALID: {name}")
5782
+ for name in ("IsSwitch", "IsTheme", "IsMerge", "Uploadable"):
5783
+ if name in normalized and not isinstance(normalized[name], bool):
5784
+ raise ValueError(f"DROPDOWN_MENU_ITEM_BOOLEAN_REQUIRED: {name}")
5785
+ if "UploadConfig" in normalized:
5786
+ normalized["UploadConfig"] = _dropdown_upload_config(normalized["UploadConfig"])
5787
+ if normalized.get("Uploadable") is True and str(normalized.get("OperationKey") or ""):
5788
+ raise ValueError("DROPDOWN_MENU_UPLOAD_OPERATION_CONFLICT")
5789
+ return normalized
5790
+
5791
+
5792
+ def _dropdown_target(items, selector):
5793
+ """Resolve an exact key first, then one unique ID, title or operation key."""
5794
+ value = str(selector or "").strip()
5795
+ exact = [item for item in items if item.get("Key") == value]
5796
+ matches = exact or [item for item in items if value and (
5797
+ value in (item.get("Id"), item.get("Title"), item.get("OperationKey"))
5798
+ or isinstance(item.get("Title"), dict) and value in item["Title"].values())]
5799
+ if len(matches) != 1:
5800
+ raise ValueError(
5801
+ f"DROPDOWN_MENU_TARGET_{'AMBIGUOUS' if matches else 'NOT_FOUND'}: {value}")
5802
+ return matches[0]
5803
+
5804
+
5805
+
5806
+ def _dropdown_full_document(result):
5807
+ if not _is_api_success(result) or not isinstance(result.get("data"), dict):
5808
+ return None
5809
+ document = copy.deepcopy(result["data"])
5810
+ for key in ("formmeta", "entitymeta"):
5811
+ document[key] = _decode_jsonish(document.get(key))
5812
+ if not isinstance(document[key], dict):
5813
+ return None
5814
+ return document
5815
+
5816
+
5817
+ def _dropdown_apply_locale_value(document, item_id, property_name, value, requested_locale,
5818
+ *, create_prefix=None):
5819
+ """Update one existing locale resource and return its exact JSON pointer."""
5820
+ rows = ((document.get("localeItems") or {}).get("form")) or []
5821
+ suffix = f".DropdownItems.{item_id}.{property_name}"
5822
+ matches = []
5823
+ for index, row in enumerate(rows):
5824
+ if not isinstance(row, dict):
5825
+ continue
5826
+ for resource_key, resource in row.items():
5827
+ if not isinstance(resource, dict):
5828
+ continue
5829
+ identity = str(resource.get("resId") or resource_key)
5830
+ if identity.endswith(suffix):
5831
+ matches.append((index, resource_key, resource))
5832
+ if not matches and create_prefix:
5833
+ known = {name for row in rows if isinstance(row, dict)
5834
+ for resource in row.values() if isinstance(resource, dict)
5835
+ for name in resource if name != "resId"}
5836
+ selected = requested_locale or document["formmeta"].get("Language")
5837
+ if not selected and len(known) == 1:
5838
+ selected = next(iter(known))
5839
+ if not selected:
5840
+ return None, {
5841
+ "status": False, "errorCode": "DROPDOWN_MENU_LOCALE_REQUIRED",
5842
+ "message": "Choose a language key for the new menu text with --locale.",
5843
+ "writeAttempted": False, "retryable": False,
5844
+ "data": {"itemId": item_id, "property": property_name,
5845
+ "locales": sorted(known), "argument": "--locale"},
5846
+ }
5847
+ resource_key = f"{create_prefix}.DropdownItems.{item_id}.{property_name}"
5848
+ resource = {"resId": resource_key, selected: value}
5849
+ rows.append({resource_key: resource})
5850
+ matches = [(len(rows) - 1, resource_key, resource)]
5851
+ if len(matches) != 1:
5852
+ return None, {
5853
+ "status": False, "errorCode": "DROPDOWN_MENU_LOCALE_RESOURCE_UNAVAILABLE",
5854
+ "message": "The selected menu text has no unique locale resource; nothing was written.",
5855
+ "writeAttempted": False, "retryable": False,
5856
+ "data": {"itemId": item_id, "property": property_name,
5857
+ "resourceMatches": len(matches)},
5858
+ }
5859
+ index, resource_key, resource = matches[0]
5860
+ locales = [name for name in resource if name != "resId"]
5861
+ selected = requested_locale if requested_locale in locales else None
5862
+ if selected is None and len(locales) == 1:
5863
+ selected = locales[0]
5864
+ if selected is None:
5865
+ return None, {
5866
+ "status": False, "errorCode": "DROPDOWN_MENU_LOCALE_REQUIRED",
5867
+ "message": "Choose one returned language key and retry the same intent with --locale.",
5868
+ "writeAttempted": False, "retryable": False,
5869
+ "data": {"itemId": item_id, "property": property_name,
5870
+ "locales": locales, "argument": "--locale"},
5871
+ }
5872
+ resource[selected] = value
5873
+ escape = lambda token: str(token).replace("~", "~0").replace("/", "~1")
5874
+ return f"/localeItems/form/{index}/{escape(resource_key)}/{escape(selected)}", None
5875
+
5876
+ def _dropdown_snapshot_edit(cfg, token, form_number, element_key, current, desired, *, locale=None):
5877
+ """Preserve native objects through the existing full-snapshot API, scoped to one menu."""
5878
+ import pathlib
5879
+ import tempfile
5880
+ import uuid
5881
+ from .cmd_listui import do_load_full_metadata, do_save_full_metadata
5882
+
5883
+ def fail(code, message, **details):
5884
+ return {"status": False, "errorCode": code, "message": message,
5885
+ "writeAttempted": False, "retryable": False, "data": details}
5886
+
5887
+ schema = do_get_property_schema(cfg, token, form_number, element_key)
5888
+ if not _is_api_success(schema):
5889
+ return fail("DROPDOWN_MENU_SCHEMA_UNAVAILABLE", "Cannot verify the menu's editable surface.", cause=schema)
5890
+ declarations = [row for row in schema.get("data") or []
5891
+ if str(row.get("propertyName", "")).casefold() == "dropdownitems"]
5892
+ if len(declarations) != 1 or declarations[0].get("editable") is not True:
5893
+ return fail("DROPDOWN_MENU_NOT_EDITABLE", "The live designer does not declare this menu editable.",
5894
+ declarations=declarations)
5895
+
5896
+ loaded = do_load_full_metadata(cfg, token, form_number)
5897
+ before = _dropdown_full_document(loaded)
5898
+ if before is None:
5899
+ return fail("DROPDOWN_MENU_SNAPSHOT_UNAVAILABLE",
5900
+ "A complete form+entity snapshot is required for lossless menu editing. Other metadata commands remain available.",
5901
+ cause=loaded)
5902
+ if str(before["formmeta"].get("Key") or "") != form_number:
5903
+ return fail("DROPDOWN_MENU_SNAPSHOT_TARGET_MISMATCH", "Snapshot does not match the bound form.")
5904
+ matches = [(index, item) for index, item in enumerate(before["formmeta"].get("Items") or [])
5905
+ if isinstance(item, dict) and item.get("Key") == element_key]
5906
+ if len(matches) != 1:
5907
+ return fail("DROPDOWN_MENU_TARGET_AMBIGUOUS", "No unique button exists in the primary form layout.")
5908
+ position, button = matches[0]
5909
+ raw_menu = _decode_jsonish(button.get("DropdownItems", []))
5910
+ if not isinstance(raw_menu, list) or not all(isinstance(row, dict) for row in raw_menu):
5911
+ return fail("DROPDOWN_MENU_SNAPSHOT_INVALID", "The authored menu is not an object array.")
5912
+ pairs = [(row.get("Id"), row.get("Key")) for row in raw_menu]
5913
+ if pairs != [(row.get("Id"), row.get("Key")) for row in current]:
5914
+ return fail("DROPDOWN_MENU_BASELINE_CHANGED", "Menu identities or order changed after resolution; refresh this menu.")
5915
+ # Compare shared authored fields as well; full metadata omits default-valued fields.
5916
+ for raw, baseline in zip(raw_menu, current):
5917
+ shared = {key: value for key, value in _dropdown_authored_item(raw).items()
5918
+ if key in baseline and key != "Index"}
5919
+ if _dropdown_mismatches(shared, baseline):
5920
+ return fail("DROPDOWN_MENU_BASELINE_CHANGED", "Menu properties changed after resolution; refresh this menu.")
5921
+
5922
+ source_by_key = {row["Key"]: row for row in raw_menu}
5923
+ current_by_key = {row["Key"]: row for row in current}
5924
+ owner = before["entitymeta"].get("IsvFlag") or before["formmeta"].get("IsvFlag") or before.get("isv")
5925
+ edited = []
5926
+ locale_updates = []
5927
+ for item in desired:
5928
+ source = source_by_key.get(item["Key"])
5929
+ if source is None:
5930
+ if not owner:
5931
+ return fail("DROPDOWN_MENU_OWNER_UNRESOLVED", "Snapshot does not identify the developer for new menu items.")
5932
+ native = {"_Type_": "DropdownItem", "Id": uuid.uuid4().hex,
5933
+ "IsvFlag": owner, "Key": item["Key"]}
5934
+ changes = item
5935
+ else:
5936
+ native = copy.deepcopy(source)
5937
+ changes = {key: value for key, value in item.items()
5938
+ if key not in ("Id", "Isv") and _dropdown_mismatches(
5939
+ value, current_by_key[item["Key"]].get(key), key)}
5940
+ for key, value in changes.items():
5941
+ if key in ("Id", "Isv"):
5942
+ continue
5943
+ if key in ("Title", "SubTitle") and isinstance(value, str):
5944
+ selected_locale = locale or cfg.get("locale")
5945
+ if isinstance(native.get(key), dict):
5946
+ locales = [name for name in native[key] if name != "resId"]
5947
+ if selected_locale not in locales:
5948
+ if selected_locale or len(locales) != 1:
5949
+ return fail("DROPDOWN_MENU_LOCALE_REQUIRED",
5950
+ "Choose one returned language key and retry the same intent with --locale.",
5951
+ locales=locales, itemKey=item["Key"], argument="--locale")
5952
+ selected_locale = locales[0]
5953
+ native[key][selected_locale] = value
5954
+ else:
5955
+ native[key] = value
5956
+ locale_updates.append((
5957
+ native.get("Id"), key, value, selected_locale,
5958
+ ".".join(filter(None, (
5959
+ str(before.get("appNum") or ""), str(before["formmeta"].get("Id") or ""),
5960
+ str(button.get("Id") or "")))) or None,
5961
+ ))
5962
+ elif key == "UploadConfig":
5963
+ raw_config = native.get(key)
5964
+ merged = copy.deepcopy(_decode_jsonish(raw_config)) if raw_config is not None else {}
5965
+ if not isinstance(merged, dict):
5966
+ return fail("DROPDOWN_MENU_UPLOAD_CONFIG_INVALID", "Authored upload configuration is not an object.")
5967
+ baseline = current_by_key.get(item["Key"], {}).get("UploadConfig", {})
5968
+ for field, setting in value.items():
5969
+ if field in baseline and baseline[field] == setting:
5970
+ continue
5971
+ actual_key = next((name for name in merged
5972
+ if field in _dropdown_upload_config({name: merged[name]}, readback=True)), field)
5973
+ merged[actual_key] = copy.deepcopy(setting)
5974
+ native[key] = json.dumps(merged, ensure_ascii=False) if isinstance(raw_config, str) else merged
5975
+ else:
5976
+ native[key] = copy.deepcopy(value)
5977
+ native["Index"] = item["Index"]
5978
+ edited.append(native)
5979
+ after = copy.deepcopy(before)
5980
+ after["formmeta"]["Items"][position]["DropdownItems"] = edited
5981
+ pointer = f"/formmeta/Items/{position}/DropdownItems"
5982
+ locale_paths = set()
5983
+ for item_id, property_name, value, selected_locale, create_prefix in locale_updates:
5984
+ locale_path, locale_error = _dropdown_apply_locale_value(
5985
+ after, item_id, property_name, value, selected_locale,
5986
+ create_prefix=create_prefix)
5987
+ if locale_error is not None:
5988
+ return locale_error
5989
+ locale_paths.add(locale_path)
5990
+ changed = _instant_sync_changed_pointers(before, after)
5991
+ locale_list_changed = len(((before.get("localeItems") or {}).get("form")) or []) != len(
5992
+ ((after.get("localeItems") or {}).get("form")) or [])
5993
+ if any(path not in locale_paths
5994
+ and not (locale_list_changed and path == "/localeItems/form")
5995
+ and path != pointer and not path.startswith(pointer + "/") for path in changed):
5996
+ return fail("DROPDOWN_MENU_DIFF_SCOPE_MISMATCH", "Proposed changes exceed the selected menu.")
5997
+
5998
+ # Preserve source serialization for API versions returning JSON-encoded sections.
5999
+ payload = copy.deepcopy(loaded["data"])
6000
+ payload["formmeta"] = (json.dumps(after["formmeta"], ensure_ascii=False)
6001
+ if isinstance(payload["formmeta"], str) else after["formmeta"])
6002
+ if locale_paths:
6003
+ payload["localeItems"] = copy.deepcopy(after["localeItems"])
6004
+ directory = pathlib.Path(tempfile.mkdtemp(prefix="cosmic-dropdown-"))
6005
+ snapshot = directory / "before.json"
6006
+ snapshot.write_text(json.dumps(loaded["data"], ensure_ascii=False, indent=2), encoding="utf-8")
6007
+ fresh = do_load_full_metadata(cfg, token, form_number)
6008
+ if _dropdown_full_document(fresh) != before:
6009
+ return fail("DROPDOWN_MENU_BASELINE_CHANGED", "The form changed before saving; no write was sent.",
6010
+ beforeSnapshot=str(snapshot))
6011
+ result = do_save_full_metadata(cfg, token, form_number, payload, allow_full_replace=True)
6012
+ if not _is_api_success(result):
6013
+ result = dict(result)
6014
+ result.setdefault("data", {})
6015
+ if not isinstance(result["data"], dict):
6016
+ result["data"] = {}
6017
+ result["data"]["beforeSnapshot"] = str(snapshot)
6018
+ return result
6019
+ readback = do_load_full_metadata(cfg, token, form_number)
6020
+ actual = _dropdown_full_document(readback)
6021
+ differences = _instant_sync_changed_pointers(after, actual) if actual is not None else {"/"}
6022
+ # Only server-managed audit fields are exempt; layout, rules, operations and
6023
+ # other business metadata are all included in this independent comparison.
6024
+ differences -= {"/modifyDate", "/modifierId"}
6025
+ if actual is not None:
6026
+ (directory / "after.json").write_text(
6027
+ json.dumps(readback["data"], ensure_ascii=False, indent=2), encoding="utf-8")
6028
+ verified = not differences
6029
+ result.update(status=verified, errorCode="0" if verified else "DROPDOWN_MENU_SNAPSHOT_READBACK_MISMATCH",
6030
+ message="Menu edited and full snapshot verified." if verified
6031
+ else "The saved snapshot differs from the intended result; inspect before another write.",
6032
+ writeAttempted=True,
6033
+ verification={"verified": verified, "readPath": "loadFullMetadata",
6034
+ "changedMenuPath": pointer, "changedLocalePaths": sorted(locale_paths),
6035
+ "unexpectedPaths": sorted(differences)[:30]},
6036
+ data={"beforeSnapshot": str(snapshot), "changed": True if verified else None})
6037
+ return result if verified else _mark_unknown_after_write(result)
6038
+
6039
+ def _resolve_dropdown_parent(cfg, token, form_number, intent):
6040
+ from .cmd_listui import do_load_full_metadata
6041
+
6042
+ document = _dropdown_full_document(do_load_full_metadata(cfg, token, form_number))
6043
+ if document is None:
6044
+ return None, {"status": False, "errorCode": "DROPDOWN_MENU_SNAPSHOT_UNAVAILABLE",
6045
+ "message": "Cannot resolve a menu without a complete live form snapshot.",
6046
+ "writeAttempted": False}
6047
+ appended = {item.get("key") or item.get("Key") for item in intent.get("append", []) if isinstance(item, dict)}
6048
+ selectors = [item.get("key") or item.get("Key") for item in intent.get("update", []) if isinstance(item, dict)]
6049
+ selectors += [item.get("key") or item.get("Key") if isinstance(item, dict) else item
6050
+ for item in intent.get("remove", [])]
6051
+ selectors += list(intent.get("order", []))
6052
+ selectors += [item.get(key) for item in intent.get("moves", []) if isinstance(item, dict)
6053
+ for key in ("key", "before", "after") if item.get(key)]
6054
+ selectors = [selector for selector in selectors if selector and selector not in appended]
6055
+ candidates, matches = [], []
6056
+ for control in document["formmeta"].get("Items", []):
6057
+ if not isinstance(control, dict) or "DropdownItems" not in control:
6058
+ continue
6059
+ raw = _decode_jsonish(control["DropdownItems"])
6060
+ if not isinstance(raw, list):
6061
+ continue
6062
+ candidate = {"key": control.get("Key"), "name": control.get("Name")}
6063
+ candidates.append(candidate)
6064
+ if not selectors:
6065
+ continue
6066
+ try:
6067
+ items = [_dropdown_authored_item(item) for item in raw]
6068
+ for selector in selectors:
6069
+ _dropdown_target(items, selector)
6070
+ except ValueError:
6071
+ continue
6072
+ matches.append(candidate)
6073
+ if len(matches) == 1 and matches[0]["key"]:
6074
+ return matches[0]["key"], None
6075
+ return None, {
6076
+ "status": False, "errorCode": "DROPDOWN_MENU_PARENT_REQUIRED",
6077
+ "message": "Pass --element with the intended button; menu items do not identify one unique parent.",
6078
+ "writeAttempted": False, "data": {"candidates": matches or candidates, "argument": "--element"},
6079
+ }
6080
+
6081
+ def do_configure_dropdown_menu(cfg, token, form_number, element_key, intent, *, locale=None):
6082
+ """Stage edits on the current menu and save once, as the designer dialog does."""
6083
+ if not isinstance(intent, dict) or not intent:
6084
+ raise ValueError("DROPDOWN_MENU_INPUT_INVALID")
6085
+ allowed = {"append", "update", "remove", "order", "moves"}
6086
+ if set(intent) - allowed:
6087
+ return {
6088
+ "status": False, "errorCode": "DROPDOWN_MENU_INTENT_UNSUPPORTED",
6089
+ "message": "Use targeted edits. Clearing or replacing the menu is not an edit recovery strategy.",
6090
+ "writeAttempted": False,
6091
+ }
6092
+ if any(not isinstance(value, list) for value in intent.values()):
6093
+ raise ValueError("DROPDOWN_MENU_INPUT_INVALID: edit actions require arrays")
6094
+ if not element_key:
6095
+ element_key, error = _resolve_dropdown_parent(cfg, token, form_number, intent)
6096
+ if error is not None:
6097
+ return error
6098
+ current, error = _dropdown_items_value(cfg, token, form_number, element_key)
6099
+ if error is not None:
6100
+ return error
6101
+ desired = copy.deepcopy(current)
6102
+ changed_operations = set()
6103
+ try:
6104
+ appended = set()
6105
+ for raw in intent.get("append", []):
6106
+ item = _normalize_dropdown_item(raw)
6107
+ key = item["Key"]
6108
+ if any(row.get("Key") == key for row in desired):
6109
+ raise ValueError(f"DROPDOWN_MENU_APPEND_KEY_EXISTS: {key}; use update to edit it")
6110
+ desired.append(item)
6111
+ appended.add(key)
6112
+ if item.get("OperationKey"):
6113
+ changed_operations.add(item["OperationKey"])
6114
+ updated = set()
6115
+ for raw in intent.get("update", []):
6116
+ patch = _normalize_dropdown_item(raw)
6117
+ target = _dropdown_target(desired, patch.pop("Key"))
6118
+ key = target["Key"]
6119
+ if key in updated:
6120
+ raise ValueError(f"DROPDOWN_MENU_DUPLICATE_UPDATE: {key}")
6121
+ updated.add(key)
6122
+ if "UploadConfig" in patch:
6123
+ patch["UploadConfig"] = {
6124
+ **target.get("UploadConfig", {}), **patch["UploadConfig"],
6125
+ }
6126
+ if patch.get("OperationKey") and patch["OperationKey"] != target.get("OperationKey"):
6127
+ changed_operations.add(patch["OperationKey"])
6128
+ for name, value in patch.items():
6129
+ previous = target.get(name)
6130
+ if name in ("Title", "SubTitle") and isinstance(previous, dict):
6131
+ language = locale or cfg.get("locale")
6132
+ texts = {key: text for key, text in previous.items() if key != "resId"}
6133
+ if language and texts.get(language) == value:
6134
+ continue
6135
+ if not language and len(texts) == 1 and value in texts.values():
6136
+ continue
6137
+ elif not _dropdown_mismatches(value, previous, name):
6138
+ continue
6139
+ target[name] = value
6140
+ removed = set()
6141
+ for selector in intent.get("remove", []):
6142
+ if isinstance(selector, dict):
6143
+ selector = selector.get("key") or selector.get("Key")
6144
+ key = _dropdown_target(desired, selector)["Key"]
6145
+ if key in appended or key in updated:
6146
+ raise ValueError(f"DROPDOWN_MENU_CONFLICTING_EDIT: {key}")
6147
+ removed.add(key)
6148
+ desired = [item for item in desired if item["Key"] not in removed]
6149
+ if "order" in intent and "moves" in intent:
6150
+ raise ValueError("DROPDOWN_MENU_ORDER_MODE_CONFLICT")
6151
+ if "order" in intent:
6152
+ ordered = [_dropdown_target(desired, selector) for selector in intent["order"]]
6153
+ keys = {item["Key"] for item in ordered}
6154
+ if len(keys) != len(ordered):
6155
+ raise ValueError("DROPDOWN_MENU_ORDER_MISMATCH: each selected item must appear once")
6156
+ desired = ordered + [item for item in desired if item["Key"] not in keys]
6157
+ for move in intent.get("moves", []):
6158
+ if not isinstance(move, dict) or set(move) - {"key", "before", "after"}:
6159
+ raise ValueError("DROPDOWN_MENU_MOVE_INVALID")
6160
+ anchors = set(move) & {"before", "after"}
6161
+ if len(anchors) != 1:
6162
+ raise ValueError("DROPDOWN_MENU_MOVE_INVALID: supply one before/after anchor")
6163
+ anchor_name = next(iter(anchors))
6164
+ item = _dropdown_target(desired, move.get("key"))
6165
+ anchor = _dropdown_target(desired, move[anchor_name])
6166
+ if item is anchor:
6167
+ raise ValueError("DROPDOWN_MENU_MOVE_INVALID: item is its own anchor")
6168
+ desired.remove(item)
6169
+ desired.insert(desired.index(anchor) + (anchor_name == "after"), item)
6170
+ for item in desired:
6171
+ if item.get("Uploadable") and item.get("OperationKey"):
6172
+ raise ValueError("DROPDOWN_MENU_UPLOAD_OPERATION_CONFLICT")
6173
+ if desired == current:
6174
+ return {
6175
+ "status": True, "errorCode": "0", "message": "Menu already matches",
6176
+ "data": {"changed": False, "formNumber": form_number, "elementKey": element_key},
6177
+ "verification": {"verified": True, "readPath": "getProperties.DropdownItems"},
6178
+ }
6179
+ if any(not item.get("Id") for item in current):
6180
+ raise ValueError("DROPDOWN_MENU_ID_MISSING: cannot preserve original item identity")
6181
+ if len({item["Id"] for item in current}) != len(current):
6182
+ raise ValueError("DROPDOWN_MENU_ID_DUPLICATE: cannot preserve original item identity")
6183
+ except ValueError as exc:
6184
+ return {
6185
+ "status": False, "errorCode": str(exc).split(":")[0],
6186
+ "message": str(exc), "writeAttempted": False,
6187
+ "data": {"candidates": [
6188
+ {key: item.get(key) for key in ("Key", "Title", "OperationKey")}
6189
+ for item in current
6190
+ ]},
6191
+ }
6192
+ if changed_operations:
6193
+ from .cmd_operations import do_list_operations
6194
+ operation_result = do_list_operations(cfg, token, form_number)
6195
+ if not _is_api_success(operation_result):
6196
+ return {
6197
+ "status": False, "errorCode": "DROPDOWN_MENU_OPERATION_READ_FAILED",
6198
+ "message": "Cannot verify changed operation bindings; nothing was written",
6199
+ "writeAttempted": False, "data": {"cause": operation_result},
6200
+ }
6201
+ data = operation_result.get("data") or []
6202
+ rows = data.get("operations", []) if isinstance(data, dict) else data
6203
+ operations = {str(row.get("operationKey") or "") for row in rows if isinstance(row, dict)}
6204
+ missing = sorted(changed_operations - operations)
6205
+ if missing:
6206
+ return {
6207
+ "status": False, "errorCode": "DROPDOWN_MENU_OPERATION_NOT_FOUND",
6208
+ "message": "Bind an existing implemented operation; a placeholder does not implement menu behavior.",
6209
+ "writeAttempted": False, "data": {"missingOperationKeys": missing},
6210
+ }
6211
+ unresolved = [
6212
+ row for row in rows if row.get("operationKey") in changed_operations
6213
+ and (not row.get("operationType")
6214
+ or row.get("operationType") == "donothing" and not (
6215
+ row.get("pluginCount") or row.get("opBizRuleCount")))
6216
+ ]
6217
+ if unresolved:
6218
+ return {
6219
+ "status": False, "errorCode": "DROPDOWN_MENU_OPERATION_IMPLEMENTATION_REQUIRED",
6220
+ "message": "Menu binding exists but has no demonstrated executable implementation.",
6221
+ "writeAttempted": False, "retryable": False,
6222
+ "data": {"operations": [{
6223
+ "key": row.get("operationKey"), "type": row.get("operationType"),
6224
+ "pluginCount": row.get("pluginCount", 0),
6225
+ "businessRuleCount": row.get("opBizRuleCount", 0),
6226
+ } for row in unresolved]},
6227
+ "recovery": {"action": "implement_or_select_operation",
6228
+ "message": "Implement the requested behavior or select an existing implemented operation, then retry the same menu intent."},
6229
+ }
6230
+ for index, item in enumerate(desired, 1):
6231
+ item["Index"] = index
6232
+ result = _dropdown_snapshot_edit(cfg, token, form_number, element_key, current, desired, locale=locale)
6233
+ if isinstance(result, dict):
6234
+ if not _is_api_success(result):
6235
+ action = {
6236
+ "DROPDOWN_MENU_BASELINE_CHANGED": "refresh_menu_and_reapply_intent",
6237
+ "DROPDOWN_MENU_SNAPSHOT_UNAVAILABLE": "check_full_metadata_api_availability",
6238
+ "DROPDOWN_MENU_NOT_EDITABLE": "check_designer_editability",
6239
+ "DROPDOWN_MENU_LOCALE_REQUIRED": "select_title_locale",
6240
+ "DROPDOWN_MENU_SNAPSHOT_READBACK_MISMATCH": "compare_recovery_snapshots",
6241
+ }.get(result.get("errorCode"), "inspect_menu_edit_failure")
6242
+ result["recovery"] = {
6243
+ "automaticRetry": False,
6244
+ "action": action,
6245
+ "message": result.get("message"),
6246
+ "scope": {"formNumber": form_number, "elementKey": element_key},
6247
+ }
6248
+ if not isinstance(result.get("data"), dict):
6249
+ result["data"] = {}
6250
+ result["data"].update({
6251
+ "formNumber": form_number, "elementKey": element_key,
6252
+ "changed": True if _is_api_success(result) else None,
6253
+ })
6254
+ return result
6255
+
6256
+
5595
6257
  def do_set_org_funcs(cfg, token, form_number, element_key, data):
5596
6258
  """Resolve a live org-function name/code, write it, then independently read it back."""
5597
6259
  resolved, error = _resolve_org_func(